150 lines
9.7 KiB
Markdown
150 lines
9.7 KiB
Markdown
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](src/routing_layer/etcp_router_doc.md).
|
||
|
||
# План: эпоха (reset_id) для ETCP Router
|
||
|
||
Аналог фикса `reset_id` из ETCP (см. `61c572c`), применённый к сервисному слою
|
||
маршрутизации `etcp_router` (src/routing_layer/etcp_router.c).
|
||
|
||
## 1. Контекст
|
||
|
||
`etcp_router` — «упрощённый TCP поверх ETCP»: своя seq-нумерация (`tx_seq`/`rx_seq`),
|
||
reorder (`recv_q`), inflight-контроль (`inflight_q`), ретрансмиты, START/RST handshake.
|
||
|
||
Текущий механизм детекции рестарта пира:
|
||
|
||
- `SVC_ROUTE_HDR.flags & ROUTER_FLAG_START` — флаг на первом data-пакете сеанса
|
||
(`seq==0 && !start_sent && pl_len>0`, `etcp_router.c:207`).
|
||
- `router_check_peer_restart` (`etcp_router.c:1055`) — если пришёл START на уже
|
||
синхронизированное соединение (`peer_sync_done==1`), то это рестарт пира;
|
||
**дедуп ретрансмитов** — побайтовое сравнение с `last_start_dgram`
|
||
(`etcp_router.c:1063`). Совпало → это ретрансмит (не рестарт). Не совпало →
|
||
`etcp_router_conn_restart` (полный сброс + `sess_id++`).
|
||
- `ROUTER_FLAG_RST` + `router_restart_send` (`etcp_router.c:285`) — сброс только
|
||
TX-стороны; повторные RST гасятся через `send_restart_pending`.
|
||
- `sess_id` (`etcp_router.h:121`) — **локальный счётчик переустановок, на wire
|
||
не пишется**. Используется только в `DEBUG_INFO`.
|
||
|
||
## 2. Проблема (аналог ETCP seq-коллизии)
|
||
|
||
Дедуп рестарта строится на **побайтовой идентичности** `last_start_dgram`. Это тот
|
||
же класс хрупкости, что убрали в ETCP:
|
||
|
||
1. **Байт-идентичный START** может быть и ретрансмитом, и (теоретически) новым
|
||
сеансом — если у нового START совпали `seq==0`, `svc_id`, `timestamp` (16-битные
|
||
часы 0.1ms заворачиваются каждые ~6.5 сек) и payload. Тогда рестарт **не
|
||
детектится**.
|
||
2. Если рестарт не распознан: у пира `tx_seq` ушёл в 0, а у нас `rx_seq` остался
|
||
«высоким». В `router_handle_data_packet` (`etcp_router.c:1104`) ветка
|
||
`(rx_seq - seq) > 0` / `queue_find_data_by_index` считает пакеты новой сессии
|
||
**дубликатами** → молчаливый `c_dup_dropped` + пере-ACK (как `RX dup` в ETCP) →
|
||
потеря сервисных пакетов.
|
||
3. `sess_id` не на wire → пир вообще не может отличить «мы перезапустились» от
|
||
«продолжаем сеанс» иначе как по байтам START.
|
||
|
||
То есть нужен надёжный маркер **эпохи сеанса** на wire — аналог `reset_id`.
|
||
|
||
## 3. Решение (аналог reset_id)
|
||
|
||
Семантика та же, что в ETCP:
|
||
|
||
- эпоха генерится **один раз** при создании `ETCP_ROUTER_CONN`;
|
||
- меняется **только** при (фатальном) рестарте соединения;
|
||
- при приёме START/RST с **другой** эпохой → рестарт (adopt);
|
||
- **первая** увиденная эпоха принимается без рестарта (свежие при создании);
|
||
- дедуп ретрансмитов START — по эпохе, а не по байтам.
|
||
|
||
Два варианта источника эпохи:
|
||
|
||
- **(A) Перенести `sess_id` на wire** — он уже есть как `uint32` счётчик
|
||
(0 при создании, `++` при рестарте). Минимально инвазивно.
|
||
- **(B) Новый `reset_id` (uint64, случайный)** — симметрично ETCP; случайный
|
||
надёжнее счётчика (нет wrap-around и предсказуемости), но чуть больше поле.
|
||
|
||
Рекомендация — **(A)** как минимальный шаг (поле уже есть), при желании апгрейд
|
||
до (B).
|
||
|
||
### Разрыв цикла (master/slave)
|
||
|
||
Как и в ETCP, симметричное «увидел другую эпоху → рестарт» зацикливается при
|
||
одновременном рестарте обеих сторон (A++ → B++ → A видит B → A++ → …).
|
||
|
||
Ти-брейкер по `node_id` (как в ETCP):
|
||
|
||
- на приёме START/RST с чужой эпохой:
|
||
- **master** (меньший `node_id`) — держит свою эпоху, шлёт RST пиру;
|
||
- **slave** (больший `node_id`) — принимает эпоху пира и рестартится.
|
||
|
||
Существующие предохранители (`send_restart_pending`, `router_check_peer_restart`
|
||
на стороне master) сохраняются.
|
||
|
||
## 4. Wire-формат
|
||
|
||
`SVC_ROUTE_HDR` (`etcp_router.h:26`) сейчас 33 байта:
|
||
|
||
```
|
||
cmd(1) group_id(8) dst(8) src(8) seq(4) svc_id(1) flags(1) timestamp(2)
|
||
```
|
||
|
||
Добавить поле эпохи (только START/RST, в обычных data/ACK — 0):
|
||
|
||
- вариант (A): `uint32_t sess_id` → заголовок 33→37 байт (упакован);
|
||
- вариант (B): `uint8_t reset_id[8]` → 33→41 байт.
|
||
|
||
Заполняется в `router_build_packet` (START) и `router_send_rst`; читается в
|
||
`router_check_peer_restart` / обработчике RST.
|
||
|
||
## 5. Шаги реализации
|
||
|
||
Файлы: `src/routing_layer/etcp_router.h`, `src/routing_layer/etcp_router.c`,
|
||
(опц.) `tests/`.
|
||
|
||
1. **Поле в заголовке**: добавить `sess_id`/`reset_id` в `struct SVC_ROUTE_HDR`;
|
||
обновить `SVC_ROUTE_HDR_SIZE`.
|
||
2. **Локальное состояние** (`struct ETCP_ROUTER_CONN`):
|
||
- `uint32_t sess_id` (уже есть) — оставить как «моя эпоха»;
|
||
- добавить `uint32_t peer_sess_id` — последняя увиденная эпоха пира.
|
||
- `router_conn_reset` (`etcp_router.c:380`) НЕ сбрасывает `sess_id`
|
||
(комментарий на :379 уже это гарантирует).
|
||
3. **Запись эпохи**:
|
||
- `router_build_packet` (`:193`) — при `ROUTER_FLAG_START` писать `sess_id`;
|
||
- `router_send_rst` (`:251`) — писать `sess_id`.
|
||
4. **Детект рестарта по эпохе** — переписать `router_check_peer_restart` (`:1055`):
|
||
- если `peer_sess_id==0` → первая эпоха, принять без рестарта (сохранить);
|
||
- если `hdr->sess_id == peer_sess_id` → ретрансмит (не рестарт);
|
||
- если отличается → master/slave: slave → `etcp_router_conn_restart` + adopt;
|
||
master → держать свою, при необходимости RST.
|
||
- `last_start_dgram`-байт-дедуп заменить на это сравнение (поле можно удалить).
|
||
5. **RST-обработчик** (`:1151`) — при RST также сверять `sess_id` (дедуп RST по
|
||
эпохе вместо/в дополнение к `send_restart_pending`).
|
||
6. **Инициализация** — `peer_sess_id = 0` в `router_conn_reset`/`etcp_router_conn_get`.
|
||
|
||
## 6. Тест
|
||
|
||
`tests/test_etcp_router_*` (существующие `test_etcp_router`, `test_etcp_router_reconnect`,
|
||
`test_etcp_router_unit`) — прогнать после правки заголовка (37/41 байт).
|
||
|
||
Добавить (по возможности) регрессию, аналогичную `test_etcp_seq_collision`:
|
||
|
||
- два инстанса, роутер-коннект, отправка N пакетов;
|
||
- принудительный рестарт одной стороны (смена эпохи);
|
||
- assert: пакеты новой эпохи доставлены (не `c_dup_dropped`), обе стороны сходятся
|
||
к одной эпохе.
|
||
|
||
## 7. Решения (зафиксировано)
|
||
|
||
1. **Вариант (B)** — `uint64_t reset_id`, случайный (`random_bytes`). Причина: вариант (A)
|
||
`sess_id` стартует с 0 и конфликтует с сентинелом «эпоха не задана». На wire — native
|
||
order (как остальные uint64-поля заголовка, SVC_ROUTE не использует htobe*).
|
||
2. **master/slave по node_id** — нужен: `etcp_router_conn_restart` делает полный
|
||
двунаправленный сброс, без ти-брейкера симметричный рестарт зациклится. master
|
||
(меньший node_id) держит эпоху и шлёт RST; slave принимает и ресетится.
|
||
3. **route_crypto** — совместим: заголовок не шифруется (только payload), подписывается
|
||
только в SIGN-режиме; `SVC_ROUTE_HDR_SIZE = sizeof(...)` авто-адаптируется. START
|
||
идёт через `route_crypto_encode`, RST/ACK/CLOSE — plain header-only.
|
||
|
||
## 8. Статус
|
||
|
||
Реализовано и проверено: `etcp_router.h/.c` + `tests/test_etcp_router_unit.c`.
|
||
Сборка `make clean && make` без ошибок; `./check.sh` → 74 passed / 0 failed / 1 skipped;
|
||
`test_etcp_router_unit` 34/34, `test_etcp_router` и `test_etcp_router_reconnect` — PASS.
|