# Доставка и синхронизация личных сообщений ## 1. Главный принцип DM считается доставленным, когда signed-входящую копию сохранил хотя бы один актуальный access-сервер получателя. Доставка на оба сервера не требуется: сервер получателя самостоятельно синхронизирует входящее сообщение со своим вторым сервером. Клиентский `status=200` от `SendMessagePair` означает, что собственный сервер сохранил пару. Это отдельный факт от доставки получателю. Формат подписанного контейнера `SHiNE_DM` не меняется. Состояние доставки и флаг синхронизации — изменяемые серверные метаданные. ## 2. Идентификаторы - `baseKey` связывает входящую и исходящую копии одного логического сообщения; - `incomingKey` идентифицирует копию получателя; - `outgoingKey`/`messageKey` идентифицирует копию отправителя и используется для проверки доставки; - дополнительный публичный `eventId` для доставки не создаётся; - `syncId` используется только внутри `DmSyncBatch` как ACK конкретной ревизии, потому что редактирование сохраняет прежний `messageKey`. ## 3. Состояния доставки | Состояние | Смысл | Повторные попытки | |---|---|---| | `accepted` | пара сохранена сервером отправителя, но ни один сервер получателя ещё не подтвердил запись | да | | `delivered` | хотя бы один сервер получателя подтвердил запись | нет | | `failed` | последняя попытка через час завершилась без доставки | никогда | Состояние `delivered` терминальное. Сервер отправителя не пытается отдельно добиться второго ACK получателя. ## 4. Обычная отправка 1. Клиент отправляет `SendMessagePair` на один свой access-сервер. 2. Сервер проверяет формат, пользователей, подписи и согласованность пары. 3. Сервер атомарно сохраняет входящую и исходящую копии. 4. В том же request-процессе сервер читает до двух актуальных маршрутов получателя. 5. Оба вызова `ReceiveIncomingMessage` запускаются параллельно. 6. Если хотя бы один вызов успешен, устанавливается `delivered`. 7. После этой попытки сервер передаёт полную пару своему второму access-серверу старой операцией `ReceiveOutcomingMessage`. 8. `SendMessagePair` возвращает существующие ключи и единственное новое поле `deliveryState`. Успешный повтор уже сохранённого signed-блока считается ACK. Все операции должны быть идемпотентными. ## 5. Расписание повторов Воркер запускается каждые 5 секунд и выбирает только due-строки по индексу. Он не сканирует всю таблицу сообщений. Попытки привязаны к времени первоначального принятия: | Номер | Время от старта | Сначала спросить второй сервер отправителя | |---:|---:|---| | 1 | сразу | нет | | 2 | 30 секунд | нет | | 3 | 5 минут | да | | 4 | 25 минут | да | | 5 | 1 час | да | Из-за шага воркера повтор может начаться на несколько секунд позже указанного времени. Первая попытка выполняется немедленно и от воркера не зависит. На 5-й, 25-й и 60-й минутах сервер сначала вызывает у второго сервера отправителя: ```json { "op": "GetDmDeliveryStatus", "payload": { "messageKey": "alice|bob|1774700000123|123456789|2" } } ``` Ответ: ```json { "messageKey": "alice|bob|1774700000123|123456789|2", "known": true, "delivered": true } ``` Операция read-only. Если peer отвечает `delivered=true`, локальный сервер устанавливает `delivered` и не обращается к серверам получателя. Ошибка или отсутствие операции на старом peer не блокирует собственную попытку. Перед каждой попыткой маршруты получателя заново читаются из `user_access_servers_current`. Изменение серверов учитывается только пока сообщение находится в `accepted`. После `delivered` или `failed` состояние больше не открывается. Если последняя проверка и попытка через час не дали ACK, устанавливается `failed`, `next_attempt_at_ms` очищается и сообщение больше никогда автоматически не отправляется. ## 6. Старая межсерверная доставка Форматы существующих операций не расширяются данными о результате доставки: - `ReceiveOutcomingMessage` получает прежнюю пару `incomingBlobB64` + `outgoingBlobB64` и необязательный `sourceServerLogin`; - `ReceiveIncomingMessage` получает один `incomingBlobB64` и необязательный `sourceServerLogin`. Второй сервер отправителя после получения пары создаёт собственное локальное состояние доставки и самостоятельно пробует маршруты получателя. Серверы обмениваются результатом только через read-only `GetDmDeliveryStatus`. ## 7. Догоняющая синхронизация двух серверов пользователя Для каждого владельца сервер ведёт outbox с флагом `synced`: - исходящая пара отправителя — один элемент с двумя blob в порядке incoming/outgoing; - входящая копия получателя — один элемент с одним blob; - read-receipt и tombstone применяются теми же проверенными обработчиками. `DmSyncBatch` возвращает только элементы с `synced=false`. Получатель проверяет signed-блоки, сохраняет их идемпотентно и в следующем запросе подтверждает `syncId` через `ackSyncIds`. Источник ставит `synced=true` только после ACK. При обрыве соединения неподтверждённый элемент остаётся `synced=false` и безопасно приходит повторно. Элемент, полученный от peer, локально сразу считается синхронизированным, чтобы не образовалась петля. Синхронизация настроек и DM проходит последовательно через один WS-сеанс: сначала настройки, затем все страницы DM. Если второй сервер был выключен, после включения он сам догружает пропущенные элементы. Существующий `MarkAllUserSettingsUnsynced` также сбрасывает DM-флаги. После сброса история повторно передаётся как синхронизация, но старые сообщения получателю заново не отправляются: delivery-состояние создаётся с учётом их возраста и не открывает завершённую часовую очередь. ## 8. UI | Вид | Значение | |---|---| | одна серая галочка | собственный сервер принял сообщение (`accepted`) | | одна светлая галочка | хотя бы один сервер получателя принял сообщение (`delivered`) | | две светлые галочки | пришёл существующий read-receipt | | красный `!` и «Сообщение не доставлено» | окончательное состояние `failed` | Read-receipt имеет приоритет над delivery-индикатором: если сообщение прочитано, оно заведомо было доставлено. Сервер сообщает поздние изменения через `DmDeliveryStateChanged` с полями `outgoingKey`, `baseKey`, `deliveryState`. При повторном открытии чата то же состояние приходит в `GetDirectMessages`. ## 9. Нагрузка и отказоустойчивость - воркер выбирает только due-записи по частичному индексу; - терминальные строки не попадают в рабочую выборку; - два маршрута первой попытки выполняются параллельно; - ограниченный пул потоков и очередь защищают сервер при всплеске отправок; - сетевой lease и сравнение версии строки предотвращают одновременную обработку одной задачи несколькими worker-потоками; - повторная запись одного signed-блока безопасна; - отсутствие второго сервера отправителя не мешает собственной доставке; - отсутствие обоих серверов получателя завершает задачу через час. ## 10. Граница доверия Межсерверная авторизация пока отложена. `sourceServerLogin` временно принимается на доверии, но каждый контейнер `SHiNE_DM` всё равно проходит проверку пользовательской подписи. `GetDmDeliveryStatus` сообщает только факт локального delivery-state и не изменяет данные.