Убрали старый sync и обновили bundle

Что сделано: вычистили неиспользуемый user-settings sync/DM sync хвост, сохранили сборку, обновили bundle.sh так, чтобы gradle-wrapper.jar всегда попадал в архив.

Проверено: compileJava и deploy на t2 (server + UI).
Не проверяли: полные интеграционные сценарии, ручные UI-флоу и продовый деплой.
This commit is contained in:
AidarKC
2026-08-28 14:51:47 +04:00
parent ef1fd9f579
commit 8a0275d962
56 changed files with 747 additions and 2428 deletions
@@ -1,138 +1,84 @@
# Доставка и синхронизация личных сообщений
# Доставка личных сообщений
## 1. Главный принцип
## 1. Инвариант
DM считается доставленным, когда signed-входящую копию сохранил хотя бы один актуальный access-сервер получателя. Доставка на оба сервера не требуется: сервер получателя самостоятельно синхронизирует входящее сообщение со своим вторым сервером.
У каждого пользователя действует ровно один сервер доступа: первый элемент
access_servers[0] в его Solana PDA.
Клиентский `status=200` от `SendMessagePair` означает, что собственный сервер сохранил пару. Это отдельный факт от доставки получателю.
Остальные элементы старой PDA, если они существуют, не участвуют в
маршрутизации и не являются fallback. UI позволяет только заменить текущий
сервер другим.
Формат подписанного контейнера `SHiNE_DM` не меняется. Состояние доставки и флаг синхронизации — изменяемые серверные метаданные.
Межсерверной репликации личной переписки между серверами одного пользователя
нет. При смене сервера история автоматически не переносится.
## 2. Идентификаторы
## 2. Подписанные копии
- `baseKey` связывает входящую и исходящую копии одного логического сообщения;
- `incomingKey` идентифицирует копию получателя;
- `outgoingKey`/`messageKey` идентифицирует копию отправителя и используется для проверки доставки;
- дополнительный публичный `eventId` для доставки не создаётся;
- `syncId` используется только внутри `DmSyncBatch` как ACK конкретной ревизии, потому что редактирование сохраняет прежний `messageKey`.
Формат SHiNE_DM не меняется. Для сообщения клиент создаёт:
## 3. Состояния доставки
- входящую копию для получателя;
- исходящую копию для отправителя.
| Состояние | Смысл | Повторные попытки |
|---|---|---|
| `accepted` | пара сохранена сервером отправителя, но ни один сервер получателя ещё не подтвердил запись | да |
| `delivered` | хотя бы один сервер получателя подтвердил запись | нет |
| `failed` | последняя попытка через час завершилась без доставки | никогда |
Сервер отправителя принимает пару через SendMessagePair, атомарно сохраняет её
и создаёт изменяемое состояние доставки.
Состояние `delivered` терминальное. Сервер отправителя не пытается отдельно добиться второго ACK получателя.
## 3. Основной маршрут
## 4. Обычная отправка
1. UI отправителя вызывает SendMessagePair на своём access-сервере.
2. Сервер сохраняет исходящую копию отправителя и входящую копию, необходимую
для доставки и повторных попыток.
3. Сервер читает единственный маршрут получателя из
user_access_servers_current.
4. Если это другой физический сервер, он вызывает внутреннюю операцию
ReceiveIncomingMessage и передаёт только входящую копию.
5. Сервер получателя проверяет пользовательскую подпись, идемпотентно сохраняет
входящую копию и уведомляет активные сессии.
6. Успешное сохранение означает delivered.
1. Клиент отправляет `SendMessagePair` на один свой access-сервер.
2. Сервер проверяет формат, пользователей, подписи и согласованность пары.
3. Сервер атомарно сохраняет входящую и исходящую копии.
4. В том же request-процессе сервер читает до двух актуальных маршрутов получателя.
5. Оба вызова `ReceiveIncomingMessage` запускаются параллельно.
6. Если хотя бы один вызов успешен, устанавливается `delivered`.
7. После этой попытки сервер передаёт полную пару своему второму access-серверу старой операцией `ReceiveOutcomingMessage`.
8. `SendMessagePair` возвращает существующие ключи и единственное новое поле `deliveryState`.
ReceiveIncomingMessage не является синхронизацией двух серверов одного
пользователя. Это основная доставка между серверами разных пользователей.
Успешный повтор уже сохранённого signed-блока считается ACK. Все операции должны быть идемпотентными.
## 4. Повторные попытки
## 5. Расписание повторов
Попытки привязаны ко времени первоначального принятия:
Воркер запускается каждые 5 секунд и выбирает только due-строки по индексу. Он не сканирует всю таблицу сообщений.
- сразу;
- через 30 секунд;
- через 5 минут;
- через 25 минут;
- через 1 час.
Попытки привязаны к времени первоначального принятия:
Перед каждой попыткой заново читается первый актуальный access-сервер
получателя. После финальной неудачи состояние становится failed.
| Номер | Время от старта | Сначала спросить второй сервер отправителя |
|---:|---:|---|
| 1 | сразу | нет |
| 2 | 30 секунд | нет |
| 3 | 5 минут | да |
| 4 | 25 минут | да |
| 5 | 1 час | да |
Второй сервер отправителя не опрашивается, полная пара ему не передаётся.
Из-за шага воркера повтор может начаться на несколько секунд позже указанного времени. Первая попытка выполняется немедленно и от воркера не зависит.
## 5. Состояния
На 5-й, 25-й и 60-й минутах сервер сначала вызывает у второго сервера отправителя:
- accepted — пара сохранена сервером отправителя;
- delivered — входящая копия сохранена единственным сервером получателя;
- failed — финальная попытка завершилась неудачей;
- read-receipt — отдельное подписанное DM-событие.
```json
{
"op": "GetDmDeliveryStatus",
"payload": {
"messageKey": "alice|bob|1774700000123|123456789|2"
}
}
```
Подписанные DM-контейнеры остаются криптографическими фактами. Сетевое
deliveryState остаётся изменяемой серверной метаданной.
Ответ:
## 6. Удаления
```json
{
"messageKey": "alice|bob|1774700000123|123456789|2",
"known": true,
"delivered": true
}
```
DeleteMessage и DeleteConversation доставляют подписанный tombstone только на
единственные access-серверы обеих сторон. Повторное применение идемпотентно.
Операция read-only. Если peer отвечает `delivered=true`, локальный сервер устанавливает `delivered` и не обращается к серверам получателя. Ошибка или отсутствие операции на старом peer не блокирует собственную попытку.
## 7. Удалённая репликация
Перед каждой попыткой маршруты получателя заново читаются из `user_access_servers_current`. Изменение серверов учитывается только пока сообщение находится в `accepted`. После `delivered` или `failed` состояние больше не открывается.
После перехода на один сервер доступа удалены:
Если последняя проверка и попытка через час не дали ACK, устанавливается `failed`, `next_attempt_at_ms` очищается и сообщение больше никогда автоматически не отправляется.
- ReceiveOutcomingMessage;
- DmSyncBatch;
- GetDmDeliveryStatus;
- dm_sync_outbox;
- dm_sync_peer_state;
- ACK-флаги synced;
- периодический DM-backfill между access-серверами.
## 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 и не изменяет данные.
Блокчейн-синхронизация через server-PDA sync_servers остаётся отдельным
механизмом и продолжает работать.