Feat: simplify docker compose

Feat: use docker healthcheck
This commit is contained in:
Alexander Pankratov 2024-04-16 19:17:27 +02:00
parent 8e93934e75
commit dd9803e079
13 changed files with 150 additions and 246 deletions

View File

@ -5,6 +5,8 @@ VERSION=1
# See "ports" in docker-compose.yml.
SERVER_ADDRESS=0.0.0.0
SERVER_PORT=9503
# If you use nginx, then provide here name of client header. for example x-real-ip or x-forwarded-for
REAL_IP_HEADER=
MEMORY_LIMIT=256M
TIMEZONE=UTC
@ -65,11 +67,4 @@ DB_SERIALIZER=serialize
# Enable to add cache info about users to database. Disable if you only read data from channels.
DB_ENABLE_MIN_DATABASE=0
# Enable file metadata cache
DB_ENABLE_FILE_REFERENCE_DATABASE=0
# HEALTHCHECK
# If server stops responding to requests it will be stoped
# Requests made each 30 seconds by default.
HEALTHCHECK_ENABLED=0
HEALTHCHECK_INTERVAL=30
HEALTHCHECK_REQUEST_TIMEOUT=10
DB_ENABLE_FILE_REFERENCE_DATABASE=0

View File

@ -6,6 +6,8 @@ VERSION=1
# To recieve requests from the Internet change to 0.0.0.0 and add rule to your firewall (THIS WILL MAKE API UNSECURE!)
SERVER_ADDRESS=127.0.0.1
SERVER_PORT=9503
# If you use nginx, then provide here name of client header. for example x-real-ip or x-forwarded-for
REAL_IP_HEADER=
MEMORY_LIMIT=256M
TIMEZONE=UTC
@ -63,11 +65,4 @@ DB_SERIALIZER=serialize
# Enable to add cache info about users to database. Disable if you only read data from channels.
DB_ENABLE_MIN_DATABASE=0
# Enable file metadata cache
DB_ENABLE_FILE_REFERENCE_DATABASE=0
# HEALTHCHECK
# If server stops responding to requests it will be stoped
# Requests made each 30 seconds by default.
HEALTHCHECK_ENABLED=0
HEALTHCHECK_INTERVAL=30
HEALTHCHECK_REQUEST_TIMEOUT=10
DB_ENABLE_FILE_REFERENCE_DATABASE=0

View File

@ -115,11 +115,8 @@ It's recommended to use http_build_query, when using GET requests.
### Get events/updates
Telegram is event driven platform. For example: every time your account receives a message you immediately get an update.
There are multiple ways of [getting updates](https://docs.madelineproto.xyz/docs/UPDATES.html) in TelegramApiServer / MadelineProto:
1. [Websocket](#eventhandler-updates-webhooks)
2. Long Polling:
send request to getUpdates endpoint
`curl "127.0.0.1:9503/api/getUpdates?data[limit]=3&data[offset]=0&data[timeout]=10.0" -g`
3. Webhook:
1. [Websocket](#eventhandler-updates-webhooks)
2. Webhook:
Redirect all updates to your endpoint, just like bot api!
`curl "127.0.0.1:9503/api/setWebhook?url=http%3A%2F%2Fexample.com%2Fsome_webhook" -g `
Example uses urlencoded url in query.
@ -199,9 +196,35 @@ curl --location --request POST '127.0.0.1:9503/api/downloadToResponse' \
Also see: https://docs.madelineproto.xyz/docs/FILES.html#downloading-files
### Multiple sessions support
**WARNING: running multiple sessions in one instance is unstable.**
Its recommended to run every session in separate container.
To add more containers create `docker-compose.override.yml` file.
Docker will [automatically merge](https://docs.docker.com/compose/multiple-compose-files/merge/) it with default docker-compose file.
Example of `docker-compose.override.yml` with two additional containers/sessions (3 in total):
```yaml
services:
api-2:
extends:
file: docker-compose.base.yml
service: base-api
ports:
- "127.0.0.1:9512:9503"
command:
- "-s=session-2"
api-3:
extends:
file: docker-compose.base.yml
service: base-api
ports:
- "127.0.0.1:9513:9503"
command:
- "-s=session-3"
```
### Multiple sessions in one container (deprecated)
**WARNING: running multiple sessions in one instance/container is unstable.**
Crash/error in one session will crash all of them.
Correct way: override docker-compose.yml and add containers with different ports and session names for each session.
When running multiple sessions, need to define which session to use for request.
Each session stored in `sessions/{$session}.madeline`. Nested folders supported.
@ -259,10 +282,10 @@ Each session stored in `sessions/{$session}.madeline`. Nested folders supported.
**Examples:**
* Session list: `http://127.0.0.1:9503/system/getSessionList`
* Adding session: `http://127.0.0.1:9503/system/addSession?session=users/xtrime`
* Removing session (session file will remain): `http://127.0.0.1:9503/system/removeSession?session=users/xtrime`
Due to madelineProto issue its instance still might be in memory and continue working even after the remove.
* Remove session file: `http://127.0.0.1:9503/system/unlinkSessionFile?session=users/xtrime`
Don`t forget to logout and call removeSession first!
* ~~Removing session (session file will remain): `http://127.0.0.1:9503/system/removeSession?session=users/xtrime`
Due to madelineProto issue its instance still might be in memory and continue working even after the remove.~~
* ~~Remove session file: `http://127.0.0.1:9503/system/unlinkSessionFile?session=users/xtrime`
Don`t forget to logout and call removeSession first!~~
* Close TelegramApiServer (end process): `http://127.0.0.1:9503/system/exit`
Full list of system methods available in [SystemApiExtensions class](https://github.com/xtrime-ru/TelegramApiServer/blob/master/src/MadelineProtoExtensions/SystemApiExtensions.php)

View File

@ -7,6 +7,7 @@ $settings = [
'server' => [
'address' => (string)getenv('SERVER_ADDRESS'),
'port' => (int)getenv('SERVER_PORT'),
'real_ip_header' => (string)(getenv('REAL_IP_HEADER') ?? ''),
],
'telegram' => [
'app_info' => [ // obtained in https://my.telegram.org
@ -67,11 +68,6 @@ $settings = [
'passwords' => (array)json_decode((string)getenv('PASSWORDS'), true),
'bulk_interval' => (float)getenv('REQUESTS_BULK_INTERVAL')
],
'health_check' => [
'enabled' => (bool)filter_var((string)getenv('HEALTHCHECK_ENABLED'), FILTER_VALIDATE_BOOL),
'interval' => ((int)getenv('HEALTHCHECK_INTERVAL') ?: 30),
'timeout' => ((int)getenv('HEALTHCHECK_REQUEST_TIMEOUT') ?: 60),
]
];
if (empty($settings['telegram']['connection']['proxies']['\danog\MadelineProto\Stream\Proxy\SocksProxy'][0]['address'])) {

37
docker-compose.base.yml Normal file
View File

@ -0,0 +1,37 @@
services:
base-api:
image: xtrime/telegram-api-server:latest
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
volumes:
- ./:/app-host-link
working_dir: /app-host-link
depends_on:
- mysql
environment:
WAIT_HOSTS: mysql:3306
logging:
driver: "json-file"
options:
max-size: "1024k"
max-file: "2"
healthcheck:
test: curl -f http://localhost:9503/system/healthcheck || exit 1
interval: 60s
timeout: 10s
retries: 2
start_period: 60s
base-mysql:
image: mariadb:11.1
restart: unless-stopped
volumes:
- ./.mysql:/var/lib/mysql
environment:
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 'yes'
MARIADB_AUTO_UPGRADE: 'yes'
command:
- --skip-grant-tables
- --innodb-buffer-pool-size=128M
- --wait_timeout=65

View File

@ -1,5 +1,8 @@
services:
api:
extends:
file: docker-compose.base.yml
service: base-api
image: xtrime/telegram-api-server:dev
build:
context: .
@ -8,4 +11,13 @@ services:
- "127.0.0.1:9503:9503"
- "9003"
environment:
PHP_IDE_CONFIG: "serverName=Docker"
PHP_IDE_CONFIG: "serverName=Docker"
mysql:
extends:
file: docker-compose.base.yml
service: base-mysql
ports:
- "127.0.0.1:9507:3306"
networks:
default:
name: telegram-api-server

View File

@ -1,41 +1,18 @@
services:
api:
image: xtrime/telegram-api-server:latest
build:
context: .
dockerfile: Dockerfile
init: true
restart: unless-stopped
extends:
file: docker-compose.base.yml
service: base-api
ports:
- "127.0.0.1:9503:9503"
volumes:
- ./:/app-host-link
working_dir: /app-host-link
depends_on:
- mysql
environment:
WAIT_HOSTS: mysql:3306
logging:
driver: "json-file"
options:
max-size: "1024k"
max-file: "2"
command:
- "-s=session"
mysql:
image: mariadb:11.1
restart: unless-stopped
extends:
file: docker-compose.base.yml
service: base-mysql
ports:
- "127.0.0.1:9507:3306"
volumes:
- ./.mysql:/var/lib/mysql
environment:
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: 'yes'
MARIADB_AUTO_UPGRADE: 'yes'
command:
- --skip-grant-tables
- --innodb-buffer-pool-size=128M
- --wait_timeout=65
networks:
default:
name: telegram-api-server

View File

@ -1,12 +1,7 @@
<?php
use TelegramApiServer\Config;
use TelegramApiServer\Files;
use TelegramApiServer\Migrations\SessionsMigration;
use TelegramApiServer\Migrations\StartUpFixes;
use TelegramApiServer\Migrations\SwooleToAmpMigration;
use TelegramApiServer\Server\Fork;
use TelegramApiServer\Server\HealthCheck;
if (PHP_SAPI !== 'cli') {
throw new RuntimeException('Start in CLI');
@ -68,18 +63,6 @@ Example:
require_once __DIR__ . '/bootstrap.php';
$mainProcessPid = getmypid();
if (Config::getInstance()->get('health_check.enabled')) {
if (!defined('SIGINT')) {
throw new RuntimeException('pcintl required for healthcheck. Use docker.');
}
Fork::run(static function () use ($mainProcessPid) {
HealthCheck::start($mainProcessPid);
});
}
$sessions = [];
foreach ($options['session'] as $session) {
$session = trim($session);

View File

@ -22,6 +22,22 @@ class SystemApiExtensions
$this->client = $client;
}
/**
* @return array<string, array>
*/
public function healthcheck(): array
{
$results = [];
['sessions' => $sessions] = $this->getSessionList();
foreach ($sessions as $sessionKey => $session) {
$instance = $this->client->instances[$sessionKey];
if ($instance->getAuthorization() === API::LOGGED_IN) {
$results[$sessionKey] = $instance->fullGetSelf();
}
}
return $results;
}
public function addSession(string $session, array $settings = []): array
{
if (!empty($settings['app_info']['api_id'])) {

View File

@ -30,7 +30,11 @@ class Authorization implements Middleware
public function handleRequest(Request $request, RequestHandler $requestHandler): Response
{
[$host] = explode(':', $request->getClient()->getRemoteAddress()->toString(), 2);
$host = Server::getClientIp($request);
if ($this->isLocal($host)) {
return $requestHandler->handleRequest($request);
}
if ($this->passwords) {
$header = (string)$request->getHeader('Authorization');
@ -54,6 +58,19 @@ class Authorization implements Middleware
private function isIpAllowed(string $host): bool
{
if ($this->ipWhitelist && !in_array($host, $this->ipWhitelist, true)) {
return false;
}
return true;
}
private function isLocal(string $host): bool {
if ($host === '127.0.0.1' || $host === 'localhost') {
return true;
}
global $options;
if ($options['docker']) {
$isSameNetwork = abs(ip2long($host) - $this->selfIp) < 256;
@ -61,10 +78,6 @@ class Authorization implements Middleware
return true;
}
}
if ($this->ipWhitelist && !in_array($host, $this->ipWhitelist, true)) {
return false;
}
return true;
return false;
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace TelegramApiServer\Server;
use RuntimeException;
class Fork
{
public static function run(callable $callback)
{
$pid = pcntl_fork();
if ($pid === -1) {
throw new RuntimeException('Could not fork');
}
if ($pid !== 0) {
return;
}
$callback();
exit;
}
}

View File

@ -1,134 +0,0 @@
<?php
namespace TelegramApiServer\Server;
use Amp\Future;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use Revolt\EventLoop;
use RuntimeException;
use TelegramApiServer\Config;
use TelegramApiServer\Logger;
use Throwable;
use UnexpectedValueException;
use function Amp\async;
use function Amp\Future\awaitAll;
use function Amp\trapSignal;
class HealthCheck
{
private static string $host = '127.0.0.1';
private static int $port = 9503;
private static int $checkInterval = 30;
private static int $requestTimeout = 60;
/**
* Sends requests to /system and /api
* In case of failure will shut down main process.
*
* @param int $parentPid
* Pid of process to shut down in case of failure.
*/
public static function start(int $parentPid): void
{
static::$host = (string)Config::getInstance()->get('server.address');
if (static::$host === '0.0.0.0') {
static::$host = '127.0.0.1';
}
static::$port = (int)Config::getInstance()->get('server.port');
static::$checkInterval = (int)Config::getInstance()->get('health_check.interval');
static::$requestTimeout = (int)Config::getInstance()->get('health_check.timeout');
EventLoop::repeat(static::$checkInterval, static function () use ($parentPid) {
try {
Logger::getInstance()->info('Start health check');
if (!self::isProcessAlive($parentPid)) {
throw new RuntimeException('Parent process died');
}
$sessions = static::getSessionList();
$sessionsForCheck = static::getLoggedInSessions($sessions);
$futures = [];
foreach ($sessionsForCheck as $session) {
$futures[] = static::checkSession($session);
}
awaitAll($futures);
Logger::getInstance()->info('Health check ok. Sessions checked: ' . count($sessionsForCheck));
} catch (Throwable $e) {
Logger::getInstance()->error($e->getMessage());
Logger::getInstance()->critical('Health check failed');
if (self::isProcessAlive($parentPid)) {
Logger::getInstance()->critical('Killing parent process');
exec("kill -2 $parentPid");
if (self::isProcessAlive($parentPid)) {
exec("kill -9 $parentPid");
}
}
exit(1);
}
});
trapSignal([SIGINT, SIGTERM]);
Logger::getInstance()->critical('Health check process exit');
}
private static function getSessionList(): array
{
$url = sprintf("http://%s:%s/system/getSessionList", static::$host, static::$port);
$response = static::sendRequest($url);
if ($response === false) {
throw new UnexpectedValueException('No response from /system');
}
return json_decode($response, true, 10, JSON_THROW_ON_ERROR)['response']['sessions'];
}
private static function getLoggedInSessions(array $sessions): array
{
$loggedInSessions = [];
foreach ($sessions as $sessionName => $session) {
if ($session['status'] === 'LOGGED_IN') {
$loggedInSessions[] = $sessionName;
}
}
return $loggedInSessions;
}
private static function checkSession(string $sessionName): Future
{
return async(function () use ($sessionName) {
$url = sprintf("http://%s:%s/api/%s/getSelf", static::$host, static::$port, $sessionName);
$response = static::sendRequest($url);
$response = json_decode($response, true, 10, JSON_THROW_ON_ERROR);
if (empty($response['response'])) {
Logger::getInstance()->error('Health check response: ', $response);
throw new RuntimeException("Failed health check: $url");
}
return $response;
});
}
private static function sendRequest(string $url): string
{
$client = (new HttpClientBuilder)::buildDefault();
$request = new Request($url);
$request->setInactivityTimeout(static::$requestTimeout);
$request->setTransferTimeout(static::$requestTimeout);
$response = $client->request($request);
return $response->getBody()->buffer();
}
private static function isProcessAlive(int $pid): bool
{
$result = exec("ps -p $pid | grep $pid");
return !empty($result);
}
}

View File

@ -92,13 +92,25 @@ class Server
public static function getClientIp(Request $request): string
{
$remote = $request->getClient()->getRemoteAddress()->toString();
$hostArray = explode(':', $remote);
if (count($hostArray) >= 2) {
$port = (int)array_pop($hostArray);
if ($port > 0 && $port <= 65353) {
$remote = implode(':', $hostArray);
$realIpHeader = Config::getInstance()->get('server.real_ip_header');
if ($realIpHeader) {
$remote = $request->getHeader($realIpHeader);
if (!$remote) {
GOTO DIRECT;
}
$tmp = explode(',', $remote);
$remote = trim(end($tmp));
} else {
DIRECT:
$remote = $request->getClient()->getRemoteAddress()->toString();
$hostArray = explode(':', $remote);
if (count($hostArray) >= 2) {
$port = (int)array_pop($hostArray);
if ($port > 0 && $port <= 65353) {
$remote = implode(':', $hostArray);
}
}
}
return $remote;