Replace obsolete media relay description with current attempt and buffer contracts

This commit is contained in:
evgeny committed 2026-10-22 19:43:59 +02:00
1 parent 3b24e7e207
commit bd0260040c
2 files changed
+54 -58

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
/* file_io — синхронные файловые операции для workers: UTF-8 пути, идентичность и публикация через соседний temp.
/* file_io — синхронные файловые операции: UTF-8 пути, идентичность и публикация через соседний temp.
* inspect: 0 найден, 1 отсутствует, -1 ошибка; остальные операции 0/-1, paths также 1 для одного файла.
* Temp обнулить → open → писать в file → finish → publish; discard обязателен также при ошибке.
* Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов.
+53 -57
View File
@@ -181,71 +181,67 @@ status INTEGER — 0=processing (качается), 1=completed (скач
Уникальный индекс: `(block_uuid, node_id)` — предотвращает две completed-записи для одного блока.
## 12. Relay-стриминг: передача блока в процессе загрузки
## 12. Попытки передачи и relay
### Принцип
Если узел C хочет скачать блок, а узел B уже качает этот блок от узла A:
1. C видит B в QUERY_RESP (благодаря BLOCK_PROCESSING, status=0)
2. C отправляет BLOCK_REQ к B
3. B принимает запрос, добавляет C в `relay_downstream` для этого блока
4. B получает BLOCK_CHUNK от A → пишет в файл → форвардит C
5. B получает BLOCK_DONE от A → форвардит C → удаляет relay-контекст
Актуальные структуры пакетов находятся в `media_delivery_proto.h`, владение загрузкой —
в `media_download.h`, источником байтов и выдачами — в `media_delivery.h`.
Реализация приёма, проверки, сборки и выдачи блоков работает синхронно в uasync.
### Flow control
- Relay-узел всегда отстаёт от апстрима: он не может слать быстрее, чем получает
- Каждый downstream хранит `sent_offset` — сколько байт уже отправлено
- При получении нового чанка: читается файл, отправляется отставание (до 1KB за раз)
- При ошибке отправки — чанк пропускается, ретрай на следующем чанке или в BLOCK_DONE
- Бэкпрессур через `etcp_router_on_send_ready` для асинхронной досылки
Каждый 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 до файловых операций.
Поздние ответы прежней попытки не меняют таймеры, файл и счётчики новой.
### Переполнение relay (RELAY_FULL)
- Максимум 5 downstream-узлов на блок (`MD_MAX_RELAY_DOWNSTREAM`)
- При переполнении B отправляет C пакет `RELAY_FULL` (subcmd 0x11)
со списком node_id текущих downstream-узлов
- C извлекает список и пробует запросить блок у одного из этих узлов
CHUNK содержит ровно объявленное число байтов, от 1 до 1024. Принимается только
непрерывный префикс в пределах ожидаемого размера блока. Полный дубль игнорируется
без обновления прогресса; разрыв, частичное перекрытие и повреждённая текущая попытка
вызывают смену держателя. Ошибка локального чтения/записи/close завершает загрузку.
Прогресс публикуется после успешного закрытия записанного файла.
### Структуры данных
Каждая попытка создаёт уникальный temp и `media_block_source`. Producer загрузки
удерживает одну ссылку, каждая выдача — ещё одну. Источник индексирован по block_id;
группа, media_id и chunk дополнительно проверяются при выборе. Доступны только
успешно записанные байты `available`. Источник готового файла использует тот же
объект, но задаёт диапазон `base/size` и не владеет удалением файла.
**relay_block_ctx** — контекст relay для одного блока (индекс по block_id в `md->relay_blocks`):
- `block_id[16]`, `media_id[16]`, `chunk` — идентификаторы блока
- `chunk_file[2048]` — путь к `.chunk_N` файлу (источник данных для форвардинга)
- `file_offset` — всего получено байт от апстрима (размер файла)
- `downstream[5]` — массив relay_downstream
Один `stream_ctx` на получателя хранит свой UUID, offset, waiter, отложенный pump и
таймер простоя. Единственный pump выдаёт готовые диапазоны и растущие temp.
Waiter только планирует pump: синхронное срабатывание не создаёт рекурсивную отправку.
DATA и DONE соблюдают backpressure router. DONE со статусом OK отправляется, только
когда блок проверен и весь его хвост принят в очередь router.
**relay_downstream** — один пир, запросивший relay:
- `node_id`, `group_id` — кому слать
- `sent_offset` — сколько байт уже отправлено этому пиру
- `waiter` — handle для бэкпрессур-досылки
DONE не передаёт подпись: ожидаемая Ed25519-подпись взята из метаданных сообщения,
ровно 32 байта ключа автора копируются из nodes при старте. Проверяются размер файла
и подпись фактических байтов. Затем все блоки собираются в соседний temp; полный
SHA-256 проверяется всегда. После проверенных файловых операций имя назначения
заменяется атомарно, затем регистрируется в SQLite. Между ФС и SQLite общей транзакции
нет: ошибка регистрации оставляет опубликованный файл в учёте подготовки для очистки.
### Диаграмма relay-потока
До трёх выдач индексированных файлов допускаются одновременно; действует также
настраиваемый лимит на файл. У растущего блока до пяти relay-читателей. При перегрузке
RELAY_FULL содержит уникальных получателей именно этого блока своей группы, без
самого запросившего узла; если таких нет, возвращается OVERLOADED с задержкой.
CANCEL закрывает только точную выдачу данного узла/группы/UUID, снимая её счётчики один раз.
```
[Узел A: источник] [Узел B: relay] [Узел C: req]
| | |
| |<── BLOCK_REQ ────────────|
| |── relay downstream add |
| |── catchup send (если есть)|
| | |
|── BLOCK_CHUNK ──────────>| |
| |── fwrite (в файл) |
| |── fread → BLOCK_CHUNK ──>|
| | |
|── BLOCK_DONE ───────────>| |
| |── fwd BLOCK_DONE ───────>|
| |── md_relay_remove |
```
После завершения producer проверенный temp остаётся до окончания последнего читателя.
Отмена непроверенного producer помечает источник ошибочным и будит выдачи для ответа
UNAVAILABLE; при недоступной очереди выдача освобождается по таймауту.
Удаление канала отменяет его загрузки и выдачи. Waiter, pump и timer снимаются до free.
### Жизненный цикл relay_block_ctx
Отмена из progress callback немедленно отсоединяет загрузку, но обработчик удерживает
её память до возврата из callback. Только после снятия последней ссылки вызывается
done. Повторный start того же media_id независим от старого контекста и его пакетов.
Входящие и исходящие писатели используют общую резервацию нормализованного назначения.
1. **Создание** — в `md_dl_start_block` при отправке первого BLOCK_REQ
2. **Обновление file_offset** — в `media_download_handle_chunk` при каждом чанке
3. **Форвардинг downstream** — в `media_download_handle_chunk` после записи
4. **Завершение** — в `media_download_handle_done` после валидации: форвард BLOCK_DONE + удаление
5. **Отмена** — при `media_download_cancel`: контекст удаляется вместе с `md->relay_blocks`
| Структура | Размер заголовка/пакета, байт |
|-----------|-------------------------------|
| BLOCK_REQ / CANCEL | 53 |
| BLOCK_CHUNK | 59 + data_len |
| BLOCK_DONE | 58 |
| BLOCK_OVERLOADED | 57 |
| BLOCK_RELAY_FULL | 55 + num_relay_nodes × 8 |
### Константы relay
| Константа | Значение | Описание |
|-----------|----------|----------|
| `MD_MAX_RELAY_DOWNSTREAM` | 5 | Макс число downstream-узлов на блок |
| `MEDIA_RELAY_FULL_HDR_SIZE` | 39 | Размер заголовка RELAY_FULL (1+16+16+4+2) |
Регрессии: `tests/test_media_blocks.c` проверяет I/O, реальные подписи с первым байтом
ключа 00, отмену/повторный start, поздние пакеты, лимиты и две relay-очереди разной скорости.
`tests/test_media_delivery_full.c` проверяет сетевую загрузку усечённого последнего блока.