9.5 KiB
План: эпоха (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:
- Байт-идентичный START может быть и ретрансмитом, и (теоретически) новым
сеансом — если у нового START совпали
seq==0,svc_id,timestamp(16-битные часы 0.1ms заворачиваются каждые ~6.5 сек) и payload. Тогда рестарт не детектится. - Если рестарт не распознан: у пира
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) → потеря сервисных пакетов. 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) — принимает эпоху пира и рестартится.
- master (меньший
Существующие предохранители (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/.
- Поле в заголовке: добавить
sess_id/reset_idвstruct SVC_ROUTE_HDR; обновитьSVC_ROUTE_HDR_SIZE. - Локальное состояние (
struct ETCP_ROUTER_CONN):uint32_t sess_id(уже есть) — оставить как «моя эпоха»;- добавить
uint32_t peer_sess_id— последняя увиденная эпоха пира. router_conn_reset(etcp_router.c:380) НЕ сбрасываетsess_id(комментарий на :379 уже это гарантирует).
- Запись эпохи:
router_build_packet(:193) — приROUTER_FLAG_STARTписатьsess_id;router_send_rst(:251) — писатьsess_id.
- Детект рестарта по эпохе — переписать
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-байт-дедуп заменить на это сравнение (поле можно удалить).
- если
- RST-обработчик (
:1151) — при RST также сверятьsess_id(дедуп RST по эпохе вместо/в дополнение кsend_restart_pending). - Инициализация —
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. Решения (зафиксировано)
- Вариант (B) —
uint64_t reset_id, случайный (random_bytes). Причина: вариант (A)sess_idстартует с 0 и конфликтует с сентинелом «эпоха не задана». На wire — native order (как остальные uint64-поля заголовка, SVC_ROUTE не использует htobe*). - master/slave по node_id — нужен:
etcp_router_conn_restartделает полный двунаправленный сброс, без ти-брейкера симметричный рестарт зациклится. master (меньший node_id) держит эпоху и шлёт RST; slave принимает и ресетится. - 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.