18 KiB
Репликация block_availability между суперузлами
Каталог и курсоры
Нормативные wire-структуры находятся в media_delivery_proto.h, состояние и владение
ресурсами — в media_delivery.h. Все обработчики выполняются в uasync-потоке ядра.
block_availability хранит положительные объявления PROCESSING/COMPLETE:
id AUTOINCREMENT, block_uuid[16], group_id, node_id, chunk, timestamp, status.
Уникальный ключ — (group_id, block_uuid, node_id), индекс страниц — (group_id,id).
Это каталог кандидатов для загрузки, не подтверждённый снимок удалённой файловой системы.
Протокола удалённых tombstone здесь нет; загрузчик проверяет полученные блоки и меняет
держателя при недоступности. Пересоздание источника не удаляет ранее полученные объявления.
super_sync хранит нашу позицию приёма от источника:
(group_id, peer_node_id) PRIMARY KEY, source_epoch[16], last_recv_id.
Позиции отправки в БД и ACK нет. ID входящей строки не становится локальным ID.
media_replication_meta хранит версию схемы и случайную эпоху локального каталога.
Обычный stop/start сохраняет эпоху, каталог и RX-курсоры. При переходе со старой схемы
или отсутствии одной из таблиц вспомогательный каталог пересоздаётся одной транзакцией
с новой эпохой. media_files и данные чата при этом сохраняются.
Роли и соединения
Пир индексируется по паре (node_id,group_id). Обе стороны репликации должны быть
актуальными суперузлами одной CHAT-группы: adm_tags.supernode=yes, без deleted=yes.
Производный node_type и общий диагностический флаг md.is_supernode не дают полномочий.
Группа и отправитель берутся только из router; группа не дублируется в SYNC payload.
Перечитывание ролей выполняется при init, событиях member_sync и каждые 30 секунд. Так обнаруживаются и группы, созданные после init. Chat core и media_index должны быть готовы до media_delivery_init; отказ подписки на роли завершает init ошибкой.
Каждый контекст владеет отдельным CM handle. CM может предоставить прямой или непрямой путь; handle сохраняется при DOWN, восстановлением занимается CM. После TIMEOUT свой handle закрывается, повтор открытия выполняется с задержкой. Raw ETCP DOWN и потеря BGP-маршрута не удаляют объявления или RX-курсоры.
При потере локальной роли закрываются только peers этой группы; чужие объявления и все её RX-курсоры удаляются атомарно, собственные объявления сохраняются. Удаление канала отменяет его работу и удаляет весь его каталог вместе с курсорами. Обычный destroy сохраняет БД; до free отменяются CM callbacks, waiter, pump и таймеры.
Запросы страниц
| Команда | Подкоманда | Размер |
|---|---|---|
| SYNC_REQ | 0x08 | 41 байт |
| SYNC_RESP | 0x09 | 53 + N × 45 байт, N ≤ 32 |
| CHANGED | 0x0E | 1 байт |
Все числа SYNC кодируются little endian. UUID запроса и эпоха — непрозрачные 16 байт. ID строк/курсоры ограничены INT64_MAX. Максимум ответа — 1493 байта.
После CM UP получатель создаёт запрос с новым UUID и сохранёнными эпохой/курcором источника. На пару «группа–пир» приходится один ожидающий запрос. При таймауте повторяются его прежние UUID и параметры, с задержками 2, 4, 8, 16, 30 секунд. Успешный commit сбрасывает задержку до 2 секунд. DOWN отменяет RPC; UP создаёт новый UUID.
Источник строит страницу внутри read transaction:
- При совпадении эпох читает после запрошенного курсора, иначе после нуля.
- Фиксирует верхнюю границу H из
sqlite_sequence, которая не уменьшается при DELETE. - Выбирает только строки router-группы с
after < id ≤ H, ORDER BY id, LIMIT 33. - Передаёт первые 32. Если есть 33-я,
more=1,next_idравен ID последней переданной. - В последней странице
more=0,next_id=H, даже при отсутствии строк.
Так курсор проходит удалённые ID и промежутки, принадлежащие другим группам. Повторный запрос может получить более свежий согласованный снимок: принимается только первый корректный ответ актуального UUID, следующие игнорируются.
Ошибка чтения/завершения snapshot даёт DB_ERROR без записей; получатель сохраняет RPC и повторяет его по исходному дедлайну. Если при совпавшей эпохе курсор выше H, CURSOR_INVALID сбрасывает только эту RX-пару, оставляя объявления; новый запрос с нуля будет создан после задержки 30 секунд. Эпоха источника от чужого курсора не меняется.
Атомарный приём и сходимость
До SQL проверяются весь пакет и его точная длина, группа/пир/UUID, эпоха, диапазон from/next, количество, порядок ID, статусы, дубли ключей и согласованность chunk. Неподходящий ответ не меняет запрос, курсор или дедлайн.
Все объявления и RX-эпоха/курсор применяются одной собственной транзакцией. Внешняя незавершённая транзакция приводит к отказу этой попытки: вложенный savepoint не гарантировал бы окончательный commit. При ошибке строки, курсора или commit прогресс не публикуется; учитывается и автоматический rollback SQLite.
Объединение выбирает максимум (status,timestamp): COMPLETE выше PROCESSING,
при равном статусе выигрывает более свежий timestamp. Chunk одного блока в группе
неизменяем. Только реальное изменение выполняет REPLACE с новым локальным ID;
повтор/старое объявление не меняет строку и sqlite_sequence. Это останавливает эхо
при распространении одной записи через несколько суперузлов.
HAVE_BLOCK и BLOCK_PROCESSING проходят тот же merge; несовпадение router-группы с полем объявления отклоняется. ACK HAVE: 0 — изменение, 1 — no-op, 2 — ошибка БД. Собственные зарегистрированные блоки добавляются непосредственно из media_files, в том числе на единственном суперузле; повторный анонс использует стабильное время.
После commit изменений отправляется CHANGED остальным peers этой группы.
CHANGED во время RPC ставит dirty, и после commit выполняется ещё один запрос.
more=1 также сразу запускает следующую страницу. Потеря CHANGED восстанавливается
периодическим запросом каждые 30 секунд; дубликаты не создают новые локальные ID.
Очереди, диагностика и тесты
Отправка репликации использует force=0 и общий router backpressure. Контекст хранит один отложенный запрос ответа, один свой RPC и признак CHANGED; очередь ответов не растёт. Более поздний входящий запрос заменяет отложенный. Если это старый RPC, его ответ будет проигнорирован, а актуальный запрос повторится.
Waiter только планирует отменяемый pump; один вызов отправляет максимум три пакета. Watchdog каждые 200ms проверяет дедлайны, повторяет неудачные отправки и восстанавливает пробуждение после reset router-очереди. Репликация не перезапускает общий MEDIA-канал, который также используется загрузками и выдачами блоков.
Категория media на DEBUG показывает group/peer, UUID/epoch, from/next, count/more, результат merge и commit. Ошибки SQL показывают фазу и коды, таймауты — UUID и задержку.
test_media_replication использует настоящие handlers/SQLite/router send_q с
управляемыми CM и доставкой: страницы до 10000 записей, ID выше 2^32, усечения,
дубли/чужие ответы, SQL/commit faults, retry, смена эпохи, две группы, смена роли,
сходимость трёх узлов и backpressure/reset. test_media_delivery_sql проверяет
настоящую схему, restart/reset и сохранность media_files. Full/chat/integration
проверяют реальные транспорт, CHAT-членство и подписанные роли.
12. Попытки передачи и relay
Актуальные структуры пакетов находятся в media_delivery_proto.h, владение загрузкой —
в media_download.h, источником байтов и выдачами — в media_delivery.h.
Реализация приёма, проверки, сборки и выдачи блоков работает синхронно в uasync.
Каждый BLOCK_REQ имеет новый случайный transfer_id[16]. BLOCK_CHUNK, BLOCK_DONE,
BLOCK_OVERLOADED, BLOCK_RELAY_FULL и CANCEL повторяют его. Группа и end-to-end
отправитель берутся из router. Получатель проверяет группу, текущего держателя,
media_id, block_id, chunk, состояние REQ/RECV и UUID до файловых операций.
Поздние ответы прежней попытки не меняют таймеры, файл и счётчики новой.
CHUNK содержит ровно объявленное число байтов, от 1 до 1024. Принимается только непрерывный префикс в пределах ожидаемого размера блока. Полный дубль игнорируется без обновления прогресса; разрыв, частичное перекрытие и повреждённая текущая попытка вызывают смену держателя. Ошибка локального чтения/записи/close завершает загрузку. Прогресс публикуется после успешного закрытия записанного файла.
Каждая попытка создаёт уникальный temp и media_block_source. Producer загрузки
удерживает одну ссылку, каждая выдача — ещё одну. Источник индексирован по block_id;
группа, media_id и chunk дополнительно проверяются при выборе. Доступны только
успешно записанные байты available. Источник готового файла использует тот же
объект, но задаёт диапазон base/size и не владеет удалением файла.
Один stream_ctx на получателя хранит свой UUID, offset, waiter, отложенный pump и
таймер простоя. Единственный pump выдаёт готовые диапазоны и растущие temp.
Waiter только планирует pump: синхронное срабатывание не создаёт рекурсивную отправку.
DATA и DONE соблюдают backpressure router. DONE со статусом OK отправляется, только
когда блок проверен и весь его хвост принят в очередь router.
DONE не передаёт подпись: ожидаемая Ed25519-подпись взята из метаданных сообщения, ровно 32 байта ключа автора копируются из nodes при старте. Проверяются размер файла и подпись фактических байтов. Затем все блоки собираются в соседний temp; полный SHA-256 проверяется всегда. После проверенных файловых операций имя назначения заменяется атомарно, затем регистрируется в SQLite. Между ФС и SQLite общей транзакции нет: ошибка регистрации оставляет опубликованный файл в учёте подготовки для очистки.
До трёх выдач индексированных файлов допускаются одновременно; действует также настраиваемый лимит на файл. У растущего блока до пяти relay-читателей. При перегрузке RELAY_FULL содержит уникальных получателей именно этого блока своей группы, без самого запросившего узла; если таких нет, возвращается OVERLOADED с задержкой. CANCEL закрывает только точную выдачу данного узла/группы/UUID, снимая её счётчики один раз.
После завершения producer проверенный temp остаётся до окончания последнего читателя. Отмена непроверенного producer помечает источник ошибочным и будит выдачи для ответа UNAVAILABLE; при недоступной очереди выдача освобождается по таймауту. Удаление канала отменяет его загрузки и выдачи. Waiter, pump и timer снимаются до free.
Отмена из progress callback немедленно отсоединяет загрузку, но обработчик удерживает её память до возврата из callback. Только после снятия последней ссылки вызывается done. Повторный start того же media_id независим от старого контекста и его пакетов. Входящие и исходящие писатели используют общую резервацию нормализованного назначения.
| Структура | Размер заголовка/пакета, байт |
|---|---|
| BLOCK_REQ / CANCEL | 53 |
| BLOCK_CHUNK | 59 + data_len |
| BLOCK_DONE | 58 |
| BLOCK_OVERLOADED | 57 |
| BLOCK_RELAY_FULL | 55 + num_relay_nodes × 8 |
Регрессии: tests/test_media_blocks.c проверяет I/O, реальные подписи с первым байтом
ключа 00, отмену/повторный start, поздние пакеты, лимиты и две relay-очереди разной скорости.
tests/test_media_delivery_full.c проверяет сетевую загрузку усечённого последнего блока.