You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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:

  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.