4 changed files with 297 additions and 53 deletions
@ -0,0 +1,147 @@
|
||||
# План: эпоха (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. |
||||
Loading…
Reference in new issue