Browse Source

conn_mgr: verify route loss and document handle lifetime recovery

master
evgeny 2 days ago
parent
commit
426fc2f4e8
  1. 42
      src/routing_layer/conn_mgr_doc.md
  2. 97
      tests/test_conn_mgr_phases.c

42
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.

97
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; }

Loading…
Cancel
Save