fix(proxy): загрузка останавливала отправку, туннель шёл в одну сторону (#42, #32) (#51)

Оба направления туннеля обслуживал один `select!` с пометкой `biased`.
`biased` опрашивает ветки строго по порядку: пока в первой — «Telegram →
клиент» — есть данные, до второй очередь не доходит вообще. При непрерывном
потоке вниз, то есть при первичной синхронизации телефона или загрузке медиа,
исходящие пакеты клиента не читались.

Второй дефект в том же цикле: одна задача на оба направления. `tcp_w.write_all`
ждёт, пока клиент разберёт присланное, и всё это время не опрашивается чтение
от клиента. Телефон по Wi-Fi разбирает поток медленнее, чем Telegram Desktop на
той же машине через loopback, — отсюда асимметрия «на компьютере работает, на
телефоне нет».

Для MTProto это фатально: клиент обязан слать подтверждения, а за каждым
следующим куском файла — свой `upload.getFile`. Первый запрос уходит, дальше
идёт поток вниз, и следующие запросы наверх не попадают. Снаружи это выглядит
как «Подключено» при живом туннеле, нулевых сбоях и нулевых отклонениях: чаты
на месте, иконки не грузятся, отправка виснет с часиками.

Направления разделены на две независимые половины: `ws.split()` плюс
`CryptoContext::split()`, потому что шифры направлений независимы — два потока
AES-CTR со своими ключами. Ping приходит в читающую половину, а отвечает на
него пишущая, через канал на четыре слота: владелец отправляющей половины
должен оставаться ровно один.

Два теста, падающие на beta.11: за пять секунд непрерывной загрузки наверх не
уходит ни одного байта, и клиент, не успевающий читать, замораживает
собственную отправку. Снятие одного `biased` чинит только первый — это и
показывает, что дефекта два.

Co-authored-by: by-sonic <171230345+by-sonic@users.noreply.github.com>
This commit is contained in:
Никита Sonic
2026-08-26 16:28:43 +03:00
committed by GitHub
parent 8ce3c368a3
commit 923c22f9b4
3 changed files with 320 additions and 28 deletions
+32
View File
@@ -116,6 +116,38 @@ LAN-режим превращал бы машину в открытый прок
Счётчик `ws_failures` от них отличается тем, что растёт после успешного
рукопожатия с клиентом: там договорились с клиентом, но не смогли с Telegram.
## Туннель: два независимых направления
Каждое клиентское соединение получает свой WebSocket-туннель, и внутри него
данные идут в обе стороны сразу. До 2.0.0-beta.12 оба направления обслуживал
один `select!` с пометкой `biased`, и это давало два дефекта, снаружи
выглядевших одинаково: «Подключено», а ничего не идёт.
`biased` опрашивает ветки строго по порядку. Пока в первой — «Telegram →
клиент» — есть данные, до второй очередь не доходит вообще. То есть при
непрерывном потоке вниз (первичная синхронизация телефона, загрузка медиа)
исходящие пакеты клиента не читались.
Второй дефект — одна задача на оба направления. `tcp_w.write_all` ждёт, пока
клиент разберёт присланное, и всё это время не опрашивается чтение от клиента.
Телефон по Wi-Fi разбирает поток медленнее, чем Telegram Desktop на той же
машине через loopback, — отсюда асимметрия «на компьютере работает, на телефоне
нет» из #42.
Для MTProto это фатально: клиент обязан слать подтверждения, а за каждым
следующим куском файла — свой `upload.getFile`. Первый запрос уходит, дальше
идёт поток вниз, и следующие запросы наверх не попадают. Загрузка встаёт при
живом туннеле, нулевых сбоях и нулевых отклонениях — ровно картина из #32.
Теперь это две независимые половины: `ws.split()` плюс `CryptoContext::split()`,
потому что шифры направлений независимы — два потока AES-CTR со своими ключами.
Ping приходит в читающую половину, а отвечает на него пишущая, через канал на
четыре слота: владелец отправляющей половины должен оставаться ровно один.
Оба дефекта закрыты тестами, которые падают на beta.11. Первый: за пять секунд
непрерывной загрузки наверх не уходит ни одного байта. Второй: клиент, не
успевающий читать, замораживает собственную отправку.
## Учёт состояния
`Stats::ws` считает **установленные** туннели: счётчик поднимается после