1
0
forked from BRT/arc
arc/docs/status-sync.md

112 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Синхронизация статусов bank_profile и material
## Определения
### Локальные статусы (SQLite)
| Таблица | Поле | Значения |
|---|---|---|
| `bank_profiles` | `status` | `"active"` \| `"off"` |
| `materials` | `status` | `"active"` \| `"off"` (enum `MaterialStatus::Active` / `MaterialStatus::Off`) |
### Серверные статусы (REST API)
| Сущность | Поле | Значения |
|---|---|---|
| bank_profile | `state` | `"enabled"` \| `"disabled"` |
| material | `state` | `"enabled"` \| `"disabled"` |
### Маппинг
| Локально | Сервер |
|---|---|
| `active` | `enabled` |
| `off` | `disabled` |
---
## Инвариант
**Локальное состояние — единственный источник истины.** Сервер всегда отражает локаль. При любых расхождениях побеждает локальная БД, и сервер дотягивается до неё (а не наоборот).
Pull с сервера (`syncBankProfilesFromServer`) используется только для **создания новых записей** (новые bank_profile / новые материалы) и привязки серверных UUID. Статусы существующих записей с сервера в локаль никогда не пишутся.
---
## Основные правила
### 1. Смена онлайн-статуса устройства
- **Device → OFFLINE:** bank_profile этого устройства выключается **только на сервере** (`PATCH bank_profile/{id}/false`). Локальный статус **не трогаем** — это намерение пользователя; его меняет лишь ручной тумблер или выключение приложения (см. правило 1-А). Дисконнект — не «выключение пользователем». Материалы локально не трогаем (см. правило 4-А ниже).
- **Device → ONLINE:** локальный статус **не меняется**. На сервер пушится текущее локальное состояние bank_profile и всех его материалов, если оно расходится с серверным. Т.к. при дисконнекте локаль осталась `active`, профиль снова включается на сервере (`PATCH .../true`).
> Концептуально: серверное состояние bank_profile = `(локально active) AND (устройство online)`. Локаль хранит намерение пользователя, сервер дополнительно отражает достижимость устройства.
### 1-А. Выключение приложения (graceful shutdown)
- При штатном выключении приложения (`NetworkService::stop`) профили гасятся **и на сервере, и локально** (`BankProfileDAO::deactivateAllApps`). Это единственный, кроме ручного тумблера, путь, который ставит локальный `off`.
- Отличие от паузы обновления: пауза (`disableAllProfilesOnServer`) гасит **только на сервере**, локаль остаётся `active`, чтобы профили воскресли после апдейта.
### 2. Периодическая синхронизация
- Каждые **60 секунд** запускается фоновая задача, которая сравнивает **желаемое** серверное состояние с фактическим и отправляет `PATCH` только для расходящихся.
- Желаемое серверное состояние bank_profile = `(локально active) AND (устройство online)`: профиль офлайн-устройства держится `disabled` и **не реактивируется**, пока устройство не вернётся онлайн. Материалы при этом не пушатся (гард, правило 4-B).
- Локаль не трогается. Цель — компенсировать сетевые сбои, перезапуски сервера, непрошедшие PATCH и т.п.
### 3. Обработка ошибок при включении
- Когда пользователь включает bank_profile или material и `PATCH .../true` падает (network error / HTTP не-2xx / `success:false`):
- локально оставляем `off`;
- показываем пользователю сообщение об ошибке (см. правило 6 про UI).
- При неудачном **выключении** локально оставляем `off`; рассинхрон с сервером устранит минутный таймер из правила 2.
### 4. Реконсиляция материалов после включения профиля
- Срабатывает **только при включении** профиля (`PATCH bank_profile/{id}/true`), при выключении — нет (см. правило 4-B про гард ниже).
- Отдельной реконсиляции после PATCH не делаем — расхождения материалов подхватит 60s-таймер из правила 2 (в худшем случае — через минуту).
- Допустима оптимизация: «дёрнуть» периодический sync сразу после успешного включения профиля, чтобы не ждать.
---
## Дополнительные правила и инварианты
### 4-A. Материалы при device → OFFLINE
- При переходе device → OFFLINE правило 1 выключает только профиль. Материалы локально не трогаем — после повторного включения профиля они вернутся к работе без лишних кликов пользователя.
- В UI материалы при `bank_profile.status == "off"` отображаются **приглушённо** или с пометкой «профиль выключен», но их `status` в БД остаётся как есть.
### 4-B. Гард на пуш материала при выключенном профиле
- На сервере действует инвариант: `material.state ≤ bank_profile.state`. Если профиль `disabled`, сервер принудительно держит материалы в `disabled`.
- Чтобы не получить бесконечный diff-цикл, **при любом пуше статуса материала** (toggle в UI, периодический sync, follow-up после toggle профиля) проверяем локальный `bank_profile.status`. Если профиль `off``PATCH` материала **не отправляем**, локальный статус не меняем.
- Этот гард уже частично реализован в `AccountSettingsWindow.cpp:556`; нужно поднять его на уровень `NetworkService`, чтобы покрыть все пути пуша.
### 5. Дебаунс ADB-флапа
- ADB может мерцать (USB-хаб, спящий экран, Doze). Без дебаунса каждое мерцание сработает как device → OFFLINE.
- **Online — мгновенно** (0 секунд). Как только устройство видно — сразу триггерим логику device → ONLINE.
- **Offline — с задержкой 30 секунд.** Устройство считается офлайн только после того, как ADB **не** показывает его как `device` непрерывно в течение 30 секунд. Если за этот период оно вернулось — считаем что флапа не было.
### 6. Стартовый порядок (избегание гонки)
- `DeviceScreener` эмитит сигналы `deviceWentOnline(deviceId)` / `deviceWentOffline(deviceId)` **только при смене** статуса. На «никогда не видел в этой сессии» никаких событий нет.
- `NetworkService` стартует 60s-таймер только **после первого успешного прохода** `DeviceScreener` (сигнал `firstPassDone`). До этого никаких offline-эвентов не обрабатывает.
- Это исключает ситуацию «на старте все устройства выглядят офлайн → массово выключаем профили».
### 7. Сериализация PATCH-ов
- Mutex на пару `(deviceId, bankProfileId)`: внутри одного устройства+профиля PATCH-операции последовательны, между разными — могут идти параллельно.
- Защищает от гонки: одновременно прилетели device-event, юзер-toggle и периодический sync для одного профиля → они выстраиваются в очередь.
### 8. UI-обработка ошибок (overlay-pattern)
- При любом PATCH статуса (профиля или материала) UI показывает оверлей «Сохранение…» поверх тоггла.
- Тоггл **двигается только после ответа сервера**:
- успех → переходит в новое положение, оверлей скрывается;
- ошибка → остаётся в исходном положении, оверлей сменяется сообщением «Ошибка: …», локальный статус не меняется.
- Сейчас частично реализовано через `hideOverlay` в `AccountSettingsWindow.cpp:559`. Применить тот же pattern в `DeviceWidget`, `BankSetupWizard` и всех других местах с toggle.
### 9. Индикация неработающей синхронизации
- В `NetworkService` ведём счётчик `m_consecutiveSyncFailures`, инкрементируем при каждом проваленном тике (правило 2) или провале `GET /current_profile`. Сбрасываем при первом успехе.
- При превышении порога (например, **3 тика подряд**) эмитим сигнал в `MainWindow`, который показывает индикатор «Нет связи с сервером, последняя успешная синхронизация: HH:MM:SS».
- При следующем успехе индикатор скрывается.
### 10. Параллелизм и ограничения нагрузки
- В одном тике периодического sync может образоваться большой объём расхождений (5 устройств × 6 профилей × 4 материала = до 120 PATCH).
- Параллелизм с **лимитом 4-8 одновременных PATCH**; mutex из правила 7 соблюдается.
- Флаг `m_syncInProgress`: если предыдущий тик ещё не завершился, новый тик **пропускается**. Это предотвращает накопление очереди.
### 11. Поведение при провале `GET /current_profile`
- Если периодический `GET /current_profile` упал (таймаут/сеть/5xx) — тик **пропускается тихо**, локаль не трогаем, никакие PATCH не шлём.
- Сам факт неудачи учитывается в счётчике из правила 9.
---
## Связанные существующие правила (контекст)
- Toggle материала, когда `bank_profile.status == "off"`, **не идёт на сервер** — сохраняется только локально (см. правило 4-B). Серверная синхронизация произойдёт после включения профиля и следующего 60s-тика.
- После `BankSetupWizard` `bank_profile` остаётся `off` по дизайну — пользователь должен включить его вручную.