From 426fc2f4e84e8303acb489b461c8abd038631c26 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 1 Oct 2026 01:06:56 +0300 Subject: [PATCH] conn_mgr: verify route loss and document handle lifetime recovery --- src/routing_layer/conn_mgr_doc.md | 42 +++++++++++-- tests/test_conn_mgr_phases.c | 97 +++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 5 deletions(-) diff --git a/src/routing_layer/conn_mgr_doc.md b/src/routing_layer/conn_mgr_doc.md index 6ea2b67d..8dcadf6b 100644 --- a/src/routing_layer/conn_mgr_doc.md +++ b/src/routing_layer/conn_mgr_doc.md @@ -61,8 +61,36 @@ DIRECT/REVERSE/INDIRECT/NONE. Событие `CHAT_EVT_CALL_CONNECTION` соде После готовности прямого пути новые дополнительные пробы не запускаются. Уже начатое согласование INDIRECT может завершиться и сохранить резерв. -После начального бюджета поздние NCD/READY всё ещё могут улучшить путь. Бесконечные -повторные поиски посредников и гарантированное восстановление не предусмотрены. + +## Восстановление + +Потребность в доставке живёт столько же, сколько сервисные handles. Начальный +TIMEOUT и срок очередного цикла проб независимы: начальный TIMEOUT приходит один +раз, если до начального дедлайна ни один путь не заработал. Он не прекращает поиск. + +При потере прямого пути сразу используется пригодный подтверждённый резерв. +Если потеряны все пути, сервис получает DOWN. Затем новый цикл запускает DIRECT +и LOCAL сразу, REVERSE через 0/500 мс; при отсутствии пригодного резерва INDIRECT +запускается сразу независимо от NAT. Рабочий посредник сохраняется во время проб. +Каждый цикл ограничен 15 секундами. Между неудачными циклами пауза растёт: +1 → 2 → 4 → 8 → 15 секунд, далее остаётся 15 секунд. Подтверждённый прямой READY +останавливает поиск и сбрасывает паузу. + +Свежая версия записи пира или изменение локальных сокетов обходят паузу и запускают +новую пробу. Изменения сокетов передаются через `conn_mgr_network_changed()`. +Работающие прямые соединения дополнительных проб не требуют. NCD сохраняется +между циклами; новый handle создаётся только после CLOSED. Существующие UDP-пробы +и TCP reconnect продолжаются; отсутствующие адресные линки добавляются без дублей. + +Каждый исходящий цикл имеет новые request_id. Ответы старых/завершённых циклов +игнорируются. Входящие запросы имеют отдельный срок обработки: новый request_id +начинает новый срок, повтор того же запроса его не продлевает. Входящая запись +без сервисного владельца обслуживает запрос, затем освобождается и сама постоянное +восстановление не запускает. Закрытие последнего сервисного handle отменяет таймеры +и отложенные callbacks; групповые владельцы транспорта продолжают жить независимо. + +Восстановление зависит от доступных адресов, маршрутов и READY-сессий. Оно не меняет +порог обнаружения транспортного обрыва ETCP и таймаут завершения звонка без медиа. ## INDIRECT @@ -97,7 +125,8 @@ INTERM_SELECTED и INTERM_ACK содержат request_id и до трёх `{nod - UP означает появление пригодного пути. Улучшение пути не повторяет UP. - DOWN означает потерю рабочих путей; при живом резерве DOWN не доставляется. - TIMEOUT означает, что начальное подключение не получило ни одного рабочего пути. - Ошибка отдельной пробы при живом посреднике не становится TIMEOUT для сервиса. + При живом handle поиск продолжается. Ошибка отдельной пробы при живом посреднике + не становится TIMEOUT для сервиса; после первого UP начального TIMEOUT уже не будет. - DOWN/TIMEOUT не освобождают handle. Владелец обязан вызвать `conn_mgr_close()`. - Закрытие последнего handle отменяет CM timer и callbacks и закрывает только NCD-владение CM. Группа и другие NCD-владельцы сохраняют транспорт. @@ -105,10 +134,13 @@ INTERM_SELECTED и INTERM_ACK содержат request_id и до трёх `{nod при INDIRECT/отсоединённом handle. Данные следует отправлять через router. Диагностика CM — категория `general`: INFO показывает расписание, выбор и смену -пути с временем от старта; DEBUG — события NCD, RTT и обработку ответов; WARN/ERROR — +пути с временем от старта, новые циклы и паузы; DEBUG — события NCD, RTT и обработку ответов; WARN/ERROR — ошибки, недоступные пути и некорректные пакеты. Создание адресных линков NCD — категория `connection`. Проверки: `test_conn_mgr`, `test_conn_mgr_handles`, `test_conn_mgr_already_connected`, `test_conn_mgr_phases`. Последний проверяет расписание, LAN, односторонний INDIRECT, -поздний DIRECT после NCD TIMEOUT, резерв, реальные данные и обработку кандидатов. +поздний DIRECT после NCD TIMEOUT, резерв, реальные данные и обработку кандидатов, +обрыв после начального срока, повторные циклы с реальными паузами 1/2/4с, смену адресов +и локальных сокетов, потерю маршрута посредника, устаревший ACK, начальный TIMEOUT +с последующим UP и закрытие последнего handle из TIMEOUT callback. diff --git a/tests/test_conn_mgr_phases.c b/tests/test_conn_mgr_phases.c index a4bf3242..933790b3 100644 --- a/tests/test_conn_mgr_phases.c +++ b/tests/test_conn_mgr_phases.c @@ -52,6 +52,7 @@ static struct CONN_MGR_HANDLE *h_a = NULL, *h_b = NULL; static int g_failed = 0; static int up_count, down_count, timeout_count, data_count, expected_marker; +static int close_on_timeout; static uint64_t data_hop; #define TEST_CM_SERVICE 200 @@ -109,6 +110,7 @@ static void conn_cb_a(struct CONN_MGR_HANDLE* h, uint64_t node, uint64_t grp, en if (ev == CONN_EVENT_UP) up_count++; else if (ev == CONN_EVENT_DOWN) down_count++; else timeout_count++; + if (ev == CONN_EVENT_TIMEOUT && close_on_timeout) { conn_mgr_close(h); h_a = NULL; } } static void conn_cb_b(struct CONN_MGR_HANDLE* h, uint64_t node, uint64_t grp, enum conn_mgr_event ev, void* arg) { (void)h; (void)node; (void)grp; (void)arg; ev_b = (int)ev; @@ -258,6 +260,7 @@ static void scenario_teardown(void) { if (inst_c) { inst_c->running = 0; utun_instance_destroy(inst_c); inst_c = NULL; } cleanup_configs(); up_count = down_count = timeout_count = data_count = expected_marker = 0; + close_on_timeout = 0; ev_a = ev_b = -1; } @@ -627,6 +630,96 @@ static int run_recovery(void) { return 0; } +/* WITHDRAW последнего маршрута убирает резерв; новый NODEINFO восстанавливает доставку. */ +static int run_relay_recovery(void) { + fprintf(stderr, "\n=== Scenario 9: lost relay route and new exchange ===\n"); + if (create_configs("nat", "nat") || scenario_setup() || !poll_until(candidates_a_and_b, 8000)) return -1; + mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 1000)); + mangle_addrs(inst_b, nid_a, (uint16_t)(base_port + 1000)); + if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a) || !poll_until(ev_a_up, 3000)) return -1; + struct CONN_MGR_ENTRY* entry = h_a->entry; + uint32_t old_request = entry->exchange_request_id; + if (etcp_router_bind(inst_b, TEST_CM_SERVICE, data_recv) < 0 || send_data(1, nid_c)) return -1; + // Входящий обмен уже истёк, а собственная попытка ещё в backoff. + struct CONN_MGR_ENTRY* inbound = cm_find_entry(defgrp(inst_b)->conn_mgr, nid_a); + if (!inbound) { fail("RELAY RECOVERY: missing inbound exchange"); return -1; } + inbound->peer_exchange_deadline_tb = get_time_tb() - 1; + entry->deadline_tb = get_time_tb() + 100; cm_arm_timer(entry); poll_ms(30); + struct TOPO_GROUP_NODE* target = topo_node_find_by_id(defgrp(inst_a), nid_b); + while (target && target->paths && target->paths->head) + topo_group_remove_path(target, ((struct TOPO_NODEPATH*)target->paths->head)->conn); + conn_mgr_routes_changed(defgrp(inst_a)->conn_mgr); poll_ms(20); + if (down_count != 1 || up_count != 1 || timeout_count || entry->conn_type != CONN_TYPE_NONE || + !entry->attempt_active || !entry->exchange_request_id || entry->exchange_request_id == old_request) { + fail("RELAY RECOVERY: lost route did not start a fresh exchange with DOWN"); return -1; + } + struct CM_INTERM_SEL stale = {0}; stale.request_id = old_request; stale.count = 1; + stale.selected[0] = (struct CONN_MGR_CANDIDATE){nid_c, 1}; + cm_handle_interm_ack(defgrp(inst_a)->conn_mgr, nid_b, (const uint8_t*)&stale, sizeof(stale)); + if (entry->indirect_confirmed || entry->intermediariy_count != 1 || up_count != 1) { + fail("RELAY RECOVERY: old ACK confirmed a new attempt"); return -1; + } + struct ETCP_CONN* to_a = instance_find_conn(inst_c, nid_a); + struct TOPO_GROUP_NODE* b_on_c = topo_node_find_by_id(defgrp(inst_c), nid_b); + if (!to_a || !b_on_c || topo_group_send_nodeinfo(defgrp(inst_c), b_on_c, to_a) < 0) return -1; + ev_a = -1; + if (!poll_until(ev_a_up, 3000) || up_count != 2 || down_count != 1 || timeout_count || + entry->conn_type != CONN_TYPE_INDIRECT || inbound->peer_exchange_request_id != entry->exchange_request_id || + inbound->peer_exchange_deadline_tb <= get_time_tb()) { + fail("RELAY RECOVERY: fresh exchange did not restore UP"); return -1; + } + if (send_data(2, nid_c)) return -1; + fprintf(stderr, " OK: one DOWN/UP, immediate exchange, stale ACK ignored and expired incoming deadline renewed\n"); + return 0; +} + +/* Начальный TIMEOUT не снимает потребность; callback может безопасно закрыть последний handle. */ +static int run_initial_timeout(void) { + fprintf(stderr, "\n=== Scenario 10: initial TIMEOUT keeps demand and callback close ===\n"); + if (create_configs("nat", "nat") || scenario_setup() || !poll_until(candidates_a_and_b, 8000)) return -1; + mangle_addrs(inst_a, nid_b, (uint16_t)(base_port + 1000)); + mangle_addrs(inst_b, nid_a, (uint16_t)(base_port + 1000)); + struct TOPO_GROUP_NODE* target = topo_node_find_by_id(defgrp(inst_a), nid_b); + while (target && target->paths && target->paths->head) + topo_group_remove_path(target, ((struct TOPO_NODEPATH*)target->paths->head)->conn); + if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a)) return -1; + struct CONN_MGR_ENTRY* entry = h_a->entry; + entry->initial_deadline_tb = entry->deadline_tb = get_time_tb() + 100; + cm_arm_timer(entry); poll_ms(30); + if (timeout_count != 1 || up_count || down_count || h_a->entry != entry || entry->attempt_active || !entry->retry_tb) { + fail("INITIAL TIMEOUT: demand was lost or wrong events"); return -1; + } + uint64_t start = entry->start_tb; + entry->retry_tb = get_time_tb() + 100; cm_arm_timer(entry); poll_ms(30); + if (!entry->attempt_active || entry->start_tb == start || timeout_count != 1) { + fail("INITIAL TIMEOUT: no subsequent retry"); return -1; + } + struct ETCP_CONN* to_a = instance_find_conn(inst_c, nid_a); + struct TOPO_GROUP_NODE* b_on_c = topo_node_find_by_id(defgrp(inst_c), nid_b); + if (!to_a || !b_on_c || topo_group_send_nodeinfo(defgrp(inst_c), b_on_c, to_a) < 0) return -1; + ev_a = -1; + if (!poll_until(ev_a_up, 3000) || up_count != 1 || timeout_count != 1 || down_count) { + fail("INITIAL TIMEOUT: late success did not deliver UP"); return -1; + } + if (etcp_router_bind(inst_b, TEST_CM_SERVICE, data_recv) < 0 || send_data(1, nid_c)) return -1; + conn_mgr_close(h_a); h_a = NULL; poll_ms(30); + target = topo_node_find_by_id(defgrp(inst_a), nid_b); + while (target && target->paths && target->paths->head) + topo_group_remove_path(target, ((struct TOPO_NODEPATH*)target->paths->head)->conn); + close_on_timeout = 1; + if (conn_mgr_open(inst_a, TOPO_GROUP_UTUN, nid_b, conn_cb_a, NULL, &h_a)) return -1; + entry = h_a->entry; + entry->initial_deadline_tb = entry->deadline_tb = get_time_tb() + 100; + cm_arm_timer(entry); poll_ms(30); + if (h_a || cm_find_entry(defgrp(inst_a)->conn_mgr, nid_b) || timeout_count != 2) { + fail("INITIAL TIMEOUT: last close inside callback did not cancel recovery"); return -1; + } + poll_ms(1100); + if (up_count != 1 || down_count || timeout_count != 2) { fail("INITIAL TIMEOUT: callback after close"); return -1; } + fprintf(stderr, " OK: one initial TIMEOUT, continuing retries, late UP and safe close inside TIMEOUT callback\n"); + return 0; +} + /* ─── main ─── */ int main(void) { @@ -664,6 +757,10 @@ int main(void) { scenario_teardown(); if (run_recovery() != 0) g_failed = 1; scenario_teardown(); + if (run_relay_recovery() != 0) g_failed = 1; + scenario_teardown(); + if (run_initial_timeout() != 0) g_failed = 1; + scenario_teardown(); if (ua) { uasync_poll(ua, 0); uasync_destroy(ua, 0); ua = NULL; }