12 KiB
Доставка и синхронизация личных сообщений
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. Обычная отправка
- Клиент отправляет
SendMessagePairна один свой access-сервер. - Сервер проверяет формат, пользователей, подписи и согласованность пары.
- Сервер атомарно сохраняет входящую и исходящую копии.
- В том же request-процессе сервер читает до двух актуальных маршрутов получателя.
- Оба вызова
ReceiveIncomingMessageзапускаются параллельно. - Если хотя бы один вызов успешен, устанавливается
delivered. - После этой попытки сервер передаёт полную пару своему второму access-серверу старой операцией
ReceiveOutcomingMessage. 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 и не изменяет данные.