SHA256
137 lines
11 KiB
Markdown
137 lines
11 KiB
Markdown
# Доставка и синхронизация личных сообщений
|
||
|
||
## 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 и не изменяет данные.
|