Files
SHiNE-server/docs/Personal_Messages/Доставка_и_синхронизация_DM.md
T

12 KiB
Raw Blame History

Доставка и синхронизация личных сообщений

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-й минутах сервер сначала вызывает у второго сервера отправителя:

{
  "op": "GetDmDeliveryStatus",
  "payload": {
    "messageKey": "alice|bob|1774700000123|123456789|2"
  }
}

Ответ:

{
  "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, локально сразу считается синхронизированным, чтобы не образовалась петля.

sourceServerLogin является единственным признаком межсерверного вызова для этих операций: если поле пустое, запрос считается клиентским и его запись не должна сразу переводиться в synced=true.

Синхронизация настроек и 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 и не изменяет данные.