Пул CF worker: закрытие при остановке, OPTI, сборка, доки (#1411)

This commit is contained in:
holdingq0 authored and GitHub committed 2026-10-10 22:36:55 +03:00
1 parent bf07b42f88
commit c317406b52
11 files changed
+78 -20

No files matched your search

+2 -1
View File
@@ -55,11 +55,12 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
| `--no-secure` | `false` | Use 80 port for CF-proxy and CF-worker connections | | `--no-secure` | `false` | Use 80 port for CF-proxy and CF-worker connections |
| `--fake-tls-domain` | | Enable Fake TLS masquerading (ee-secret) with specified SNI domain | | `--fake-tls-domain` | | Enable Fake TLS masquerading (ee-secret) with specified SNI domain |
| `--proxy-protocol` | disabled | Accept HAProxy PROXY protocol v1 (for use behind nginx/haproxy with `proxy_protocol on`) | | `--proxy-protocol` | disabled | Accept HAProxy PROXY protocol v1 (for use behind nginx/haproxy with `proxy_protocol on`) |
| `--force-test-dc` | `false` | Route all traffic to Telegram test DCs [Learn more](./TestDc.md) |
| `--buf-kb` | `256` | Buffer size in KB | | `--buf-kb` | `256` | Buffer size in KB |
| `--pool-size` | `4` | Number of ready WS connections per DC. `0` disables the direct DC→IP WS route | | `--pool-size` | `4` | Number of ready WS connections per DC. `0` disables the direct DC→IP WS route |
| `--log-file` | disabled | Path to file for saving logs | | `--log-file` | disabled | Path to file for saving logs |
| `--log-max-mb` | `5` | Maximum log file size in MB (afterwards overwrites) | | `--log-max-mb` | `5` | Maximum log file size in MB (afterwards overwrites) |
| `--log-backups` | `0` | Number of log backups after overwrite | | `--log-backups` | `1` | Number of rotated log files to keep (minimum 1) |
| `-v`, `--verbose` | disabled | Verbose logging (DEBUG) | | `-v`, `--verbose` | disabled | Verbose logging (DEBUG) |
**Examples:** **Examples:**
+9 -4
View File
@@ -18,13 +18,18 @@ The tray application stores data in:
"verbose": false, "verbose": false,
"buf_kb": 256, "buf_kb": 256,
"pool_size": 4, "pool_size": 4,
"log_max_mb": 5.0, "log_max_mb": 5,
"check_updates": true, "check_updates": true,
"cfproxy": true, "cfproxy": true,
"cfproxy_user_domain": "", "h2": true,
"cfproxy_worker_domain": "", "cfproxy_user_domain_enabled": false,
"cfproxy_user_domain": [],
"cfproxy_worker_enabled": false,
"cfproxy_worker_domain": [],
"force_test_dc": false, "force_test_dc": false,
"appearance": "auto" "no_secure": false,
"appearance": "auto",
"language": "en"
} }
``` ```
+2 -1
View File
@@ -55,11 +55,12 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
| `--no-secure` | `false` | Принудительно использует 80 порт для CF-proxy и CF-worker | | `--no-secure` | `false` | Принудительно использует 80 порт для CF-proxy и CF-worker |
| `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом | | `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом |
| `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) | | `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) |
| `--force-test-dc` | `false` | Направлять весь трафик на тестовые DC Telegram [Подробнее](./TestDc.md) |
| `--buf-kb` | `256` | Размер буфера в КБ | | `--buf-kb` | `256` | Размер буфера в КБ |
| `--pool-size` | `4` | Количество заготовленных WS-соединений на каждый DC. `0` отключает прямой WS-маршрут DC→IP | | `--pool-size` | `4` | Количество заготовленных WS-соединений на каждый DC. `0` отключает прямой WS-маршрут DC→IP |
| `--log-file` | выкл. | Путь к файлу, в который будут сохраняться логи | | `--log-file` | выкл. | Путь к файлу, в который будут сохраняться логи |
| `--log-max-mb` | `5` | Максимальный размер файла логов в МБ (после этого начинается перезапись) | | `--log-max-mb` | `5` | Максимальный размер файла логов в МБ (после этого начинается перезапись) |
| `--log-backups` | `0` | Количество сохранений логов после перезаписи | | `--log-backups` | `1` | Сколько старых файлов логов хранить при ротации (минимум 1) |
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) | | `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) |
**Примеры:** **Примеры:**
+9 -4
View File
@@ -18,13 +18,18 @@ Tray-приложение хранит данные в:
"verbose": false, "verbose": false,
"buf_kb": 256, "buf_kb": 256,
"pool_size": 4, "pool_size": 4,
"log_max_mb": 5.0, "log_max_mb": 5,
"check_updates": true, "check_updates": true,
"cfproxy": true, "cfproxy": true,
"cfproxy_user_domain": "", "h2": true,
"cfproxy_worker_domain": "", "cfproxy_user_domain_enabled": false,
"cfproxy_user_domain": [],
"cfproxy_worker_enabled": false,
"cfproxy_worker_domain": [],
"force_test_dc": false, "force_test_dc": false,
"appearance": "auto" "no_secure": false,
"appearance": "auto",
"language": "ru"
} }
``` ```
+1 -1
View File
@@ -24,7 +24,7 @@ a = Analysis(
[os.path.join(os.path.dirname(SPEC), os.pardir, 'linux.py')], [os.path.join(os.path.dirname(SPEC), os.pardir, 'linux.py')],
pathex=[], pathex=[],
binaries=appindicator_binaries, binaries=appindicator_binaries,
datas=[(ctk_path, 'customtkinter/'), (_i18n_path, 'ui/i18n')], datas=[(os.path.join(ctk_path, 'assets'), 'customtkinter/assets'), (_i18n_path, 'ui/i18n')],
hiddenimports=[ hiddenimports=[
'httpx', 'httpx',
'h2', 'h2',
+1 -1
View File
@@ -15,7 +15,7 @@ a = Analysis(
[os.path.join(os.path.dirname(SPEC), os.pardir, 'macos.py')], [os.path.join(os.path.dirname(SPEC), os.pardir, 'macos.py')],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[(ctk_path, 'customtkinter/'), (_i18n_path, 'ui/i18n')] + certifi_datas, datas=[(os.path.join(ctk_path, 'assets'), 'customtkinter/assets'), (_i18n_path, 'ui/i18n')] + certifi_datas,
hiddenimports=[ hiddenimports=[
'httpx', 'httpx',
'h2', 'h2',
+1 -1
View File
@@ -18,7 +18,7 @@ a = Analysis(
[os.path.join(os.path.dirname(SPEC), os.pardir, 'windows.py')], [os.path.join(os.path.dirname(SPEC), os.pardir, 'windows.py')],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[(ctk_path, 'customtkinter/'), (_i18n_path, 'ui/i18n')] + certifi_datas, datas=[(os.path.join(ctk_path, 'assets'), 'customtkinter/assets'), (_i18n_path, 'ui/i18n')] + certifi_datas,
hiddenimports=[ hiddenimports=[
'httpx', 'httpx',
'h2', 'h2',
+23 -5
View File
@@ -5,7 +5,7 @@ import time
from collections import deque from collections import deque
from urllib.parse import urlencode from urllib.parse import urlencode
from typing import Dict, List, Optional, Tuple, Set from typing import Dict, List, Optional, Tuple
from .raw_websocket import RawWebSocket, WsHandshakeError from .raw_websocket import RawWebSocket, WsHandshakeError
from .stats import stats from .stats import stats
@@ -237,7 +237,7 @@ class _CfWorkerPool:
def __init__(self): def __init__(self):
self._idle: Dict[int, deque] = {} self._idle: Dict[int, deque] = {}
self._refilling: Set[int] = set() self._refilling: Dict[int, asyncio.Task] = {}
self._exhausted_until: Dict[str, float] = {} self._exhausted_until: Dict[str, float] = {}
async def get(self, dc: int, fallback_dst: str, async def get(self, dc: int, fallback_dst: str,
@@ -269,8 +269,7 @@ class _CfWorkerPool:
def _schedule_refill(self, dc, fallback_dst, worker_domains): def _schedule_refill(self, dc, fallback_dst, worker_domains):
if dc in self._refilling: if dc in self._refilling:
return return
self._refilling.add(dc) self._refilling[dc] = asyncio.create_task(self._refill(
asyncio.create_task(self._refill(
dc, fallback_dst, list(worker_domains))) dc, fallback_dst, list(worker_domains)))
async def _refill(self, dc, fallback_dst, worker_domains): async def _refill(self, dc, fallback_dst, worker_domains):
@@ -287,11 +286,15 @@ class _CfWorkerPool:
if connected is None: if connected is None:
break break
ws, worker_domain = connected ws, worker_domain = connected
if self._refilling.get(dc) is not asyncio.current_task():
await self._quiet_close(ws)
return
bucket.append((ws, time.monotonic(), worker_domain)) bucket.append((ws, time.monotonic(), worker_domain))
log.debug("CF worker pool refilled DC%d: %d ready", log.debug("CF worker pool refilled DC%d: %d ready",
dc, len(bucket)) dc, len(bucket))
finally: finally:
self._refilling.discard(dc) if self._refilling.get(dc) is asyncio.current_task():
self._refilling.pop(dc, None)
async def _connect_one(self, worker_domains, fallback_dst, dc): async def _connect_one(self, worker_domains, fallback_dst, dc):
query = urlencode({ query = urlencode({
@@ -359,10 +362,25 @@ class _CfWorkerPool:
log.info("CF worker pool warmup started for %d DC(s)", len(cf_fallbacks)) log.info("CF worker pool warmup started for %d DC(s)", len(cf_fallbacks))
def reset(self): def reset(self):
loop = asyncio.get_running_loop()
for task in self._refilling.values():
if not task.done() and task.get_loop() is loop:
task.cancel()
for bucket in self._idle.values():
for ws, _, _ in bucket:
try:
ws.writer.close()
except Exception as exc:
log.debug("CF worker pool close failed: %r", exc)
self._idle.clear() self._idle.clear()
self._refilling.clear() self._refilling.clear()
self._exhausted_until.clear() self._exhausted_until.clear()
async def close(self):
tasks = list(self._refilling.values())
self.reset()
await asyncio.gather(*tasks, return_exceptions=True)
ws_pool = _WsPool() ws_pool = _WsPool()
cf_worker_pool = _CfWorkerPool() cf_worker_pool = _CfWorkerPool()
+1
View File
@@ -536,6 +536,7 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
await _quiet_cancel(task) await _quiet_cancel(task)
await server.wait_closed() await server.wait_closed()
await ws_pool.close() await ws_pool.close()
await cf_worker_pool.close()
if cf_h2_pool is not None: if cf_h2_pool is not None:
await cf_h2_pool.close() await cf_h2_pool.close()
cf_h2_pool = None cf_h2_pool = None
+2 -1
View File
@@ -33,7 +33,8 @@ PROTO_PADDED_INTERMEDIATE_INT = 0xDDDDDDDD
RESERVED_FIRST_BYTES = {0xEF} RESERVED_FIRST_BYTES = {0xEF}
RESERVED_STARTS = {b'\x48\x45\x41\x44', b'\x50\x4F\x53\x54', RESERVED_STARTS = {b'\x48\x45\x41\x44', b'\x50\x4F\x53\x54',
b'\x47\x45\x54\x20', b'\xee\xee\xee\xee', b'\x47\x45\x54\x20', b'\xee\xee\xee\xee',
b'\xdd\xdd\xdd\xdd', b'\x16\x03\x01\x02'} b'\xdd\xdd\xdd\xdd', b'\x16\x03\x01\x02',
b'\x4F\x50\x54\x49'}
RESERVED_CONTINUE = b'\x00\x00\x00\x00' RESERVED_CONTINUE = b'\x00\x00\x00\x00'
_GITHUB_IPS: Dict[str, str] = { _GITHUB_IPS: Dict[str, str] = {
+27 -1
View File
@@ -7,7 +7,7 @@ from types import SimpleNamespace
from unittest import mock from unittest import mock
from proxy.config import proxy_config from proxy.config import proxy_config
from proxy.pool import _WsPool from proxy.pool import _CfWorkerPool, _WsPool
from proxy.raw_websocket import RawWebSocket, WsHandshakeError from proxy.raw_websocket import RawWebSocket, WsHandshakeError
from proxy.utils import ws_domains from proxy.utils import ws_domains
@@ -241,5 +241,31 @@ class WsPoolTest(unittest.IsolatedAsyncioTestCase):
idle.writer.close.assert_called_once() idle.writer.close.assert_called_once()
class CfWorkerPoolTest(unittest.IsolatedAsyncioTestCase):
async def test_close_cancels_refill_and_closes_unclaimed_connections(self):
pool = _CfWorkerPool()
ready = _open_ws()
idle = _open_ws()
started = asyncio.Event()
pool._idle[2] = deque([(idle, time.monotonic(), 'a.example')])
async def connect(*args):
started.set()
try:
await asyncio.Future()
except asyncio.CancelledError:
return ready, 'b.example'
with mock.patch.object(proxy_config, 'pool_size', 1), \
mock.patch.object(pool, '_connect_one', side_effect=connect):
pool._schedule_refill(4, '192.0.2.4', ['b.example'])
await asyncio.wait_for(started.wait(), 1)
await pool.close()
self.assertFalse(pool._idle)
self.assertFalse(pool._refilling)
ready.writer.close.assert_called_once()
idle.writer.close.assert_called_once()
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()