122 lines
15 KiB
Markdown
122 lines
15 KiB
Markdown
# Task 08: Жизненный цикл соединения, фон и сеть (hermes-android)
|
||
|
||
**Repo:** `ochenstarik-ui/hermes-android`
|
||
**Assigned to:** Antigravity (режим оркестратора, два кодера)
|
||
**Priority:** HIGH (ложные статусы, бесконечные реконнекты, потеря работы при сворачивании)
|
||
**Date:** 2026-08-24
|
||
**Base SHA:** результат задания 07 — указать фактический SHA при выдаче
|
||
**Зависимость:** задание 01 принято — идентификация сокета и порядок событий уже исправлены; без этого поведение реконнекта не воспроизводимо.
|
||
|
||
## Роли и протокол
|
||
|
||
| Роль | Модель | Что делает |
|
||
|---|---|---|
|
||
| Оркестратор | Antigravity | Разбивает работу, маршрутизирует, принимает результат |
|
||
| Кодер 1 | Gemini Flash 3.7 high | Реализация §Scope пунктов 1–5 |
|
||
| Кодер 2 | Gemini Pro high | Независимая проверка, доводка, **и пункт 6** (фоновая работа) |
|
||
|
||
Пункт 6 отдан кодеру 2: foreground service затрагивает политику Play Store и разрешения, решение не механическое.
|
||
|
||
**Раунд 1 (кодер 1).** Тесты из §Required tests, фиксация падения на base SHA, затем код по пунктам 1–5.
|
||
**Раунд 2 (кодер 2).** Независимое воспроизведение, проход §Anti-checklist с явными отметками, доводка пунктов 1–5, затем пункт 6. Findings по шкале.
|
||
**Раунд 3 (оркестратор).** Приёмка при фактическом выводе команд и подтверждении поведения на устройстве для пунктов 5–6.
|
||
|
||
## Проблема
|
||
|
||
**1. Состояние реконнекта — незащищённые поля (`NET-05`).** `HermesHostRuntime.kt:52-54,231-245` — `autoReconnectEnabled`, `reconnectAttempt`, `reconnectJob` читаются и пишутся из нескольких корутин на `Dispatchers.Default` без `@Volatile` и мьютекса. Между проверкой `reconnectJob?.isActive` и присваиванием есть окно, в котором заводятся два параллельных цикла переподключения к одному хосту. Счётчик попыток растёт неограниченно, попытки не прекращаются никогда — даже когда хост заведомо недоступен.
|
||
|
||
**2. `connect()` рапортует об успехе преждевременно (`NET-06`).** `HermesHostRuntime.kt:218-224` возвращает `Result.success(Unit)` сразу после `gatewayClient.connect()`, который лишь создаёт сокет. `HostsViewModel.kt:80-91` показывает пользователю «подключено» для соединения, которое может отвалиться через секунду. Ожидание `gateway.ready` есть только в `sendPrompt`.
|
||
|
||
**3. `disconnect()` обрывает собственное закрытие (`NET-07`).** `JsonRpcGatewayClient.kt:154-166` — `close(1000, …)` и сразу `cancel()`: рукопожатие закрытия не успевает завершиться, сервер видит аварийный разрыв и дольше держит рантайм-сессию.
|
||
|
||
**4. Гонка вокруг `gatewayReadyDeferred` (`NET-09`).** `JsonRpcGatewayClient.kt:73,93,139-144` — `connect()` заменяет объект целиком, а `awaitGatewayReady()` мог захватить ссылку на предыдущий и будет ждать его до таймаута при уже готовом соединении.
|
||
|
||
**5. Служебные гонки менеджера (`DATA-11`, `DATA-12`).** `HermesConnectionManager.kt:91-110` запускает корутины внутри `ConcurrentHashMap.computeIfAbsent` — работа под блокировкой бина, любой повторный вход в карту даёт `IllegalStateException` или взаимоблокировку; `:150-153` — `refreshAllHosts` перезаписывает `_hosts`, не синхронизируя рантаймы. `MigrationHelper.kt:15-41` читает legacy DataStore при каждом холодном старте, флага завершения нет, legacy-блоб не удаляется, и миграция стартует параллельно менеджеру, который уже подписан на список хостов.
|
||
|
||
**6. Нет реакции на сеть и фон (`UI-11`).** `ACCESS_NETWORK_STATE` запрошено в манифесте и не используется нигде. Foreground service и WorkManager отсутствуют: сворачивание приложения рвёт все WebSocket-соединения, длительная задача на хосте остаётся без клиента — при том что README обещает фоновое завершение работы и коммит результата в общий таймлайн.
|
||
|
||
## Scope
|
||
|
||
**1. Управление реконнектом (`NET-05`).**
|
||
Свести управление в один последовательный путь — `actor`/`Channel` либо `Mutex`; счётчик в `AtomicInteger`. Гарантировать: не более одного активного цикла реконнекта на хост.
|
||
Ввести предел: после N неудачных попыток подряд хост переходит в состояние, требующее ручного действия, вместо вечного цикла. Значение N и поведение обосновать.
|
||
Реконнект не запускать, когда сети нет вообще, — см. пункт 5.
|
||
|
||
**2. Честный результат подключения (`NET-06`).**
|
||
`connect()` возвращает успех после `awaitGatewayReady()`; таймаут — ошибка с различимой причиной. UI показывает «подключение» до готовности, а не «подключено».
|
||
|
||
**3. Корректное закрытие (`NET-07`).**
|
||
`close()`, ожидание `onClosed` с таймаутом, `cancel()` только если закрытие не завершилось. Проверить, что состояние по завершении — `Disconnected`, а не `Failed`.
|
||
|
||
**4. Устранение гонки готовности (`NET-09`).**
|
||
`gatewayReadyDeferred` читается один раз в локальную переменную под тем же примитивом, что и `activeWebSocket` (задание 01). Проверить сценарий: `awaitGatewayReady` вызван, следом `connect()` заменил соединение.
|
||
|
||
**5. Служебные гонки и сеть (`DATA-11`, `DATA-12`).**
|
||
Создание рантайма — внутри `computeIfAbsent`, подписки — после возврата из него. `refreshAllHosts` синхронизирует рантаймы тем же путём, что и основной сборщик в `init`.
|
||
`MigrationHelper`: флаг завершения, очистка legacy-ключа, запуск до создания менеджера соединений.
|
||
Подписка на `ConnectivityManager`: при потере сети соединения переводятся в понятное состояние и реконнект приостанавливается; при появлении — возобновляется немедленно, не дожидаясь очередного шага backoff.
|
||
|
||
**6. Фоновая работа (`UI-11`) — кодер 2.**
|
||
Сначала решение в отчёте: нужен ли foreground service, при каких условиях он стартует и когда останавливается, какой тип объявляется, что видит пользователь в уведомлении, как это соотносится с политикой Play Store по foreground-типам. Только затем код.
|
||
Минимальное требование: пока хост выполняет задачу, инициированную пользователем, соединение живёт при свёрнутом приложении, и результат попадает в таймлайн. Когда активной задачи нет — сервис не держится.
|
||
README привести в соответствие с фактическим поведением.
|
||
|
||
## Do not change
|
||
|
||
- Идентификацию сокета и конвейер событий — задание 01, принято.
|
||
- Схему БД и батчинг записи — задание 02; флаг миграции добавляется по правилам задания 02.
|
||
- ViewModel и экраны — задание 03; здесь только состояние подключения, отображаемое в существующем UI.
|
||
- Авторизацию и обновление токенов — задание 06.
|
||
- Кэши в памяти и производительность списка — задание 09.
|
||
|
||
## Anti-checklist
|
||
|
||
1. Мьютекс добавлен, но `autoReconnectEnabled` по-прежнему обычный `var`, читаемый вне него.
|
||
2. Предел попыток введён, а сбрасывается он только при успешном подключении — после суток офлайна хост не восстановится сам даже при вернувшейся сети. Проверить сброс при появлении сети.
|
||
3. `connect()` ждёт готовности, но `HostsViewModel` по-прежнему показывает «подключено» по старому пути. Проверить UI, а не только рантайм.
|
||
4. `cancel()` убран совсем — при зависшем закрытии соединение не освобождается никогда.
|
||
5. Гонка `gatewayReadyDeferred` «починена» увеличением таймаута.
|
||
6. Подписки вынесены из `computeIfAbsent`, но появилось окно, в котором рантайм уже в карте, а события ещё не собираются — первые события теряются. Проверить именно этот интервал.
|
||
7. Флаг миграции ставится до фактического завершения — прерванная миграция больше не повторится.
|
||
8. `ConnectivityManager` подписан, но callback не отписывается — утечка на каждом пересоздании.
|
||
9. Foreground service держится постоянно, «чтобы не переподключаться» — расход батареи и риск для публикации.
|
||
10. Пункт 6 сделан кодером 1 в обход §Роли, без решения в отчёте.
|
||
11. В отчёте `green` для незапущенной команды (`AGENTS.md §3`).
|
||
|
||
## Definition of Done
|
||
|
||
- Не более одного цикла реконнекта на хост при любом порядке событий подключения и отключения.
|
||
- После предела неудачных попыток хост переходит в требующее действия состояние; появление сети восстанавливает работу.
|
||
- «Подключено» показывается только после `gateway.ready`.
|
||
- Штатное отключение завершается закрывающим рукопожатием; зависшее — освобождается по таймауту.
|
||
- `awaitGatewayReady` не ждёт устаревший deferred.
|
||
- Первые события после создания рантайма не теряются.
|
||
- Миграция legacy выполняется однократно, до старта менеджера соединений.
|
||
- Потеря и восстановление сети отражаются в состоянии хостов без вечного цикла попыток.
|
||
- Задача, запущенная пользователем, доживает до конца при свёрнутом приложении, и результат попадает в таймлайн — подтверждено на устройстве.
|
||
- Все существующие тесты зелёные, ни один не удалён и не ослаблен.
|
||
|
||
## Required tests
|
||
|
||
`core/runtime/ReconnectSingleFlightTest.kt` — параллельные `Failed`/`Disconnected` дают один цикл реконнекта. Обязан падать на base SHA.
|
||
`core/runtime/ReconnectLimitTest.kt` — предел попыток и сброс при появлении сети.
|
||
`core/runtime/ConnectReadinessTest.kt` — `connect()` не возвращает успех до `gateway.ready`. Обязан падать на base SHA.
|
||
`core/network/GracefulCloseTest.kt` — закрытие проходит рукопожатием; при отсутствии `onClosed` срабатывает таймаут.
|
||
`core/network/ReadyDeferredRaceTest.kt` — `awaitGatewayReady` не залипает на устаревшем deferred. Обязан падать на base SHA.
|
||
`core/runtime/RuntimeSubscriptionGapTest.kt` — событие, пришедшее сразу после создания рантайма, доставлено.
|
||
`core/storage/MigrationOnceTest.kt` — повторный старт не повторяет миграцию; прерванная повторяется.
|
||
|
||
Фоновая работа и поведение при переключении сети — на устройстве (`adb shell svc wifi disable/enable`, сворачивание, `dumpsys activity services`). Без устройства — `UNVERIFIED`.
|
||
|
||
## Required verification
|
||
|
||
```text
|
||
./gradlew --no-daemon testDebugUnitTest
|
||
./gradlew --no-daemon connectedDebugAndroidTest
|
||
./gradlew --no-daemon lint
|
||
./gradlew --no-daemon assembleDebug
|
||
```
|
||
|
||
## Result
|
||
|
||
`agents/antigravity/done/TASK-2026-08-24-08-connection-lifecycle.md` — разделы `## Кодер 1`, `## Кодер 2 (review + доработка + пункт 6)`, `## Решение по фоновой работе`, `## Вердикт оркестратора`.
|