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

13 KiB
Raw Permalink Blame History

Синхронизация статусов 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. Если профиль offPATCH материала не отправляем, локальный статус не меняем.
  • Этот гард уже частично реализован в 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 по дизайну — пользователь должен включить его вручную.