1742 lines
101 KiB
C
1742 lines
101 KiB
C
/* Настоящие очереди/таймеры ETCP; управляемые часы, перехват только передачи и отказа аллокации. */
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <limits.h>
|
|
#include "etcp.h"
|
|
#include "etcp_api.h"
|
|
#include "etcp_bbr.h"
|
|
#include "etcp_loadbalancer.h"
|
|
#include "etcp_session.h"
|
|
#include "config_parser.h"
|
|
#include "utun_instance.h"
|
|
#include "test_clock.h"
|
|
#include "../lib/mem.h"
|
|
|
|
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "FAIL %s:%d: %s", __func__, __LINE__, #x); exit(1); } } while (0)
|
|
static struct UASYNC* ua;
|
|
static struct UTUN_INSTANCE inst;
|
|
static struct utun_config cfg;
|
|
static struct ETCP_CONN* conn;
|
|
static struct ETCP_LINK *a, *b;
|
|
static unsigned sends, fail_alloc, fail_send, fail_tx_timer, bbr_calls, saw_cwnd_limited, saw_app_limited;
|
|
static unsigned fail_shaper_timer;
|
|
static unsigned pacer_clock_active, capture_shaper, shaper_arm_count, pacer_ready_count, pacer_ready_action;
|
|
static uint64_t pacer_now_tb;
|
|
static int captured_shaper_delay;
|
|
static timeout_callback_t captured_shaper_callback;
|
|
static void* captured_shaper_arg;
|
|
static unsigned fail_rx_data, fail_rx_fragment, fail_ack_entry;
|
|
static struct ETCP_LINK* last_sent_link;
|
|
static unsigned last_sent_request, last_sent_payload;
|
|
static struct bbr_rate_sample last_sample;
|
|
static uint32_t last_mss;
|
|
static unsigned fail_rx_message_at, fail_tx_inflight, fail_normalizer_timer, fail_normalizer_waiter, fail_session_timer_at;
|
|
static unsigned no_alloc, unexpected_alloc, fail_rng, session_sends, received, receive_action;
|
|
static uint8_t received_values[8];
|
|
static unsigned ack_clock_active, capture_ack_timer, fail_ack_timer;
|
|
static uint64_t ack_now_us;
|
|
static timeout_callback_t captured_ack_callback;
|
|
static void* captured_ack_arg;
|
|
|
|
/* Фиксируем только TB на время синхронной проверки pacer; uasync при этих часах не опрашивается. */
|
|
uint64_t __real_get_time_tb(void);
|
|
uint64_t __wrap_get_time_tb(void) {
|
|
if (ack_clock_active) return ack_now_us / 100;
|
|
return pacer_clock_active ? pacer_now_tb : __real_get_time_tb();
|
|
}
|
|
|
|
uint64_t __real_get_time_us(void);
|
|
uint64_t __wrap_get_time_us(void) { return ack_clock_active ? ack_now_us : __real_get_time_us(); }
|
|
|
|
void* __real_uasync_call_soon(struct UASYNC*, void*, timeout_callback_t);
|
|
void* __wrap_uasync_call_soon(struct UASYNC* loop, void* arg, timeout_callback_t callback) {
|
|
if (fail_normalizer_timer && conn && arg == conn->normalizer) { fail_normalizer_timer--; return NULL; }
|
|
if (fail_normalizer_waiter && conn && arg == &conn->normalizer->input_waiter_handle) {
|
|
fail_normalizer_waiter--; return NULL;
|
|
}
|
|
if (fail_tx_timer && arg == conn) { fail_tx_timer--; return NULL; }
|
|
return __real_uasync_call_soon(loop, arg, callback);
|
|
}
|
|
|
|
void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*);
|
|
void* __wrap_uasync_set_timeout(struct UASYNC* loop, int delay, void* arg, timeout_callback_t callback, const char* name) {
|
|
if (capture_ack_timer && !strcmp(name, "etcp_ack_resp")) { captured_ack_callback = callback; captured_ack_arg = arg; }
|
|
if (fail_ack_timer && !strcmp(name, "etcp_ack_resp")) { fail_ack_timer--; return NULL; }
|
|
if (capture_shaper && name && !strcmp(name, "etcp_shaper")) {
|
|
shaper_arm_count++; captured_shaper_delay = delay;
|
|
captured_shaper_callback = callback; captured_shaper_arg = arg;
|
|
}
|
|
if (fail_session_timer_at && !strcmp(name, "etcp_session") && !--fail_session_timer_at) return NULL;
|
|
if (fail_shaper_timer && !strcmp(name, "etcp_shaper")) { fail_shaper_timer--; return NULL; }
|
|
if (fail_tx_timer && !strcmp(name, "etcp_tx")) { fail_tx_timer--; return NULL; }
|
|
return __real_uasync_set_timeout(loop, delay, arg, callback, name);
|
|
}
|
|
|
|
int __wrap_etcp_encrypt_send(struct ETCP_DGRAM* pkt) {
|
|
if (pkt->data_len == ETCP_SESSION_CONTROL_SIZE && pkt->data[0] >= ETCP_SESSION_HELLO &&
|
|
pkt->data[0] <= ETCP_SESSION_CONFIRM) { session_sends++; return pkt->data_len; }
|
|
CHECK(conn->tx_running && pkt->link && pkt->link->initialized && pkt->link->link_status);
|
|
sends++;
|
|
last_sent_link = pkt->link; last_sent_request = pkt->ack_request; last_sent_payload = pkt->payload_len;
|
|
if (fail_send) { fail_send--; return -1; }
|
|
return pkt->data_len;
|
|
}
|
|
|
|
void* __real_memory_pool_alloc_impl(struct memory_pool* pool, const char* location);
|
|
void* __wrap_memory_pool_alloc_impl(struct memory_pool* pool, const char* location) {
|
|
if (no_alloc) { unexpected_alloc++; return NULL; }
|
|
if (fail_tx_inflight && conn && pool == conn->inflight_pool) { fail_tx_inflight--; return NULL; }
|
|
if (pool == inst.data_pool && fail_rx_data) { fail_rx_data--; return NULL; }
|
|
if (fail_rx_fragment && conn && pool == conn->io_pool) { fail_rx_fragment--; return NULL; }
|
|
if (pool == inst.ack_pool && fail_ack_entry) { fail_ack_entry--; return NULL; }
|
|
if (pool == inst.pkt_pool && fail_alloc) { fail_alloc--; return NULL; }
|
|
return __real_memory_pool_alloc_impl(pool, location);
|
|
}
|
|
|
|
struct ll_entry* __real_ll_alloc_lldgram(uint16_t);
|
|
struct ll_entry* __wrap_ll_alloc_lldgram(uint16_t len) {
|
|
if (fail_rx_message_at && !--fail_rx_message_at) return NULL;
|
|
return __real_ll_alloc_lldgram(len);
|
|
}
|
|
void* __real_u_malloc_impl(uint32_t, const char*);
|
|
void* __wrap_u_malloc_impl(uint32_t size, const char* where) {
|
|
if (no_alloc) { unexpected_alloc++; return NULL; }
|
|
if (fail_normalizer_waiter && strstr(where, "ll_queue.c:")) { fail_normalizer_waiter--; return NULL; }
|
|
return __real_u_malloc_impl(size, where);
|
|
}
|
|
void* __real_u_calloc_impl(uint32_t, uint32_t, const char*);
|
|
void* __wrap_u_calloc_impl(uint32_t count, uint32_t size, const char* where) {
|
|
if (no_alloc) { unexpected_alloc++; return NULL; }
|
|
return __real_u_calloc_impl(count, size, where);
|
|
}
|
|
void* __real_u_realloc_impl(void*, uint32_t, const char*);
|
|
void* __wrap_u_realloc_impl(void* ptr, uint32_t size, const char* where) {
|
|
if (no_alloc) { unexpected_alloc++; return NULL; }
|
|
return __real_u_realloc_impl(ptr, size, where);
|
|
}
|
|
int __real_random_bytes(uint8_t*, size_t);
|
|
int __wrap_random_bytes(uint8_t* data, size_t len) {
|
|
if (fail_rng) { fail_rng--; return -1; }
|
|
return __real_random_bytes(data, len);
|
|
}
|
|
|
|
void __real_bbr_main(struct bbr*, const struct bbr_rate_sample*, uint32_t*, uint32_t*, uint32_t, uint32_t, int);
|
|
void __wrap_bbr_main(struct bbr* model, const struct bbr_rate_sample* sample, uint32_t* cwnd,
|
|
uint32_t* pacing, uint32_t mss, uint32_t inflight, int limited) {
|
|
bbr_calls++; saw_cwnd_limited |= !!limited; saw_app_limited |= !!sample->is_app_limited;
|
|
last_sample = *sample;
|
|
last_mss = mss;
|
|
__real_bbr_main(model, sample, cwnd, pacing, mss, inflight, limited);
|
|
}
|
|
|
|
/* Счётчики сверяются с содержимым очередей, а не с ожидаемыми результатами самого алгоритма. */
|
|
static void check_accounting(void) {
|
|
uint32_t attempts = 0;
|
|
for (struct ETCP_LINK* l = conn->links; l; l = l->next) {
|
|
uint32_t bytes = 0, packets = 0;
|
|
for (struct ll_entry* e = l->attempts->head; e; e = e->next) { bytes += e->len; packets++; }
|
|
CHECK(l->inflight_bytes == bytes && l->inflight_packets == packets);
|
|
attempts += packets;
|
|
}
|
|
CHECK(conn->active_attempts == attempts);
|
|
struct ll_queue* logical_queues[] = {conn->input_send_q, conn->input_wait_ack};
|
|
for (unsigned i = 0; i < 2; i++) for (struct ll_entry* e = logical_queues[i]->head; e; e = e->next) {
|
|
struct INFLIGHT_PACKET* packet = (struct INFLIGHT_PACKET*)e;
|
|
unsigned active = 0;
|
|
for (struct ETCP_LINK* link = conn->links; link; link = link->next)
|
|
for (struct ll_entry* entry = link->attempts->head; entry; entry = entry->next)
|
|
active += ((struct ETCP_TX_ATTEMPT*)entry)->seq == packet->seq;
|
|
CHECK(packet->active_attempts == active);
|
|
CHECK(packet->state == (i ? INFLIGHT_STATE_WAIT_ACK : INFLIGHT_STATE_WAIT_SEND));
|
|
}
|
|
CHECK(conn->unacked_bytes == conn->input_send_q->total_bytes + conn->input_wait_ack->total_bytes);
|
|
}
|
|
|
|
static void setup(void) {
|
|
memset(&inst, 0, sizeof(inst)); memset(&cfg, 0, sizeof(cfg));
|
|
sends = fail_alloc = fail_send = fail_tx_timer = bbr_calls = saw_cwnd_limited = saw_app_limited = 0;
|
|
fail_shaper_timer = 0;
|
|
pacer_clock_active = capture_shaper = shaper_arm_count = pacer_ready_count = pacer_ready_action = 0;
|
|
captured_shaper_delay = 0; captured_shaper_callback = NULL; captured_shaper_arg = NULL;
|
|
fail_rx_data = fail_rx_fragment = fail_ack_entry = 0;
|
|
fail_rx_message_at = fail_tx_inflight = fail_normalizer_timer = fail_normalizer_waiter = fail_session_timer_at = 0;
|
|
no_alloc = unexpected_alloc = fail_rng = session_sends = received = receive_action = 0;
|
|
memset(received_values, 0, sizeof(received_values));
|
|
ack_clock_active = capture_ack_timer = fail_ack_timer = 0;
|
|
captured_ack_callback = NULL; captured_ack_arg = NULL;
|
|
last_sent_link = NULL; last_sent_request = last_sent_payload = 0;
|
|
ua = uasync_create(); CHECK(ua);
|
|
inst.ua = ua; inst.node_id = 1; inst.config = &cfg; cfg.global.bbr_max_cwnd = INFLIGHT_LIM_MAX;
|
|
inst.connections = queue_new(ua, 16, 0, 8, "windows");
|
|
inst.pkt_pool = memory_pool_init(sizeof(struct ETCP_DGRAM) + PACKET_DATA_SIZE, "window_packets");
|
|
inst.data_pool = memory_pool_init(PACKET_DATA_SIZE, "window_data");
|
|
inst.ack_pool = memory_pool_init(sizeof(struct ACK_PACKET), "window_acks");
|
|
CHECK(inst.connections && inst.pkt_pool && inst.data_pool && inst.ack_pool);
|
|
conn = etcp_connection_create(&inst, "windows"); CHECK(conn);
|
|
conn->peer_node_id = 2; conn->links_up = 1;
|
|
etcp_conn_ready(conn);
|
|
a = u_calloc(1, sizeof(*a)); b = u_calloc(1, sizeof(*b)); CHECK(a && b);
|
|
a->next = b; conn->links = a;
|
|
unsigned id = 0;
|
|
for (struct ETCP_LINK* l = a; l; l = l->next) {
|
|
l->etcp = conn; l->local_link_id = ++id; l->initialized = l->link_status = 1;
|
|
l->mtu = 1280; l->remote_addr.ss_family = AF_INET; l->inflight_lim_bytes = 65536;
|
|
l->rtt_last = 1; CHECK(etcp_tx_link_init(l) == 0);
|
|
}
|
|
etcp_conn_on_inflight_lim_changed(conn);
|
|
}
|
|
|
|
static void teardown(void) {
|
|
ack_clock_active = 0;
|
|
check_accounting();
|
|
etcp_connection_close(conn); conn = NULL;
|
|
uasync_poll(ua, 0);
|
|
queue_free(inst.connections);
|
|
memory_pool_destroy(inst.pkt_pool); memory_pool_destroy(inst.data_pool); memory_pool_destroy(inst.ack_pool);
|
|
uasync_poll(ua, 0);
|
|
CHECK(ua->timer_alloc_count == ua->timer_free_count);
|
|
uasync_destroy(ua, 0);
|
|
}
|
|
|
|
/* Вход отправителя уже после normalizer: фрагмент остаётся в реальной indexed send_q. */
|
|
static struct INFLIGHT_PACKET* enqueue(unsigned len) {
|
|
struct INFLIGHT_PACKET* p = (struct INFLIGHT_PACKET*)queue_entry_new_from_pool(conn->inflight_pool); CHECK(p);
|
|
p->seq = conn->next_tx_id++; p->state = INFLIGHT_STATE_WAIT_SEND;
|
|
p->ll.len = len; p->ll.dgram = memory_pool_alloc(inst.data_pool); p->ll.dgram_pool = inst.data_pool;
|
|
CHECK(p->ll.dgram); memset(p->ll.dgram, 0x5a, len);
|
|
conn->unacked_bytes += len;
|
|
CHECK(queue_data_put_with_index(conn->input_send_q, &p->ll) == 0);
|
|
etcp_conn_wake_sender(conn);
|
|
return p;
|
|
}
|
|
|
|
static struct ETCP_DGRAM* request(void) {
|
|
struct ETCP_DGRAM* pkt = etcp_request_pkt(conn);
|
|
if (pkt) etcp_tx_complete(pkt, 1); // ручной стенд принимает подготовленный пакет без сетевого send
|
|
check_accounting();
|
|
return pkt;
|
|
}
|
|
|
|
/* Wire/window-тесты моделируют уже ожидающий ACK, не меняя часы и не запуская sender через uasync. */
|
|
static void mature_acks(struct ETCP_LINK* link) {
|
|
for (struct ll_entry* e = link->ack_q->head; e; e = e->next)
|
|
((struct ACK_PACKET*)e)->received_at_us = get_time_us() - ETCP_ACK_DELAY_US;
|
|
}
|
|
|
|
static void fill(struct ETCP_LINK* target, unsigned count, unsigned len) {
|
|
int up_a = a->link_status, up_b = b->link_status;
|
|
a->link_status = target == a; b->link_status = target == b;
|
|
for (unsigned i = 0; i < count; i++) {
|
|
enqueue(len);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == target);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
}
|
|
a->link_status = up_a; b->link_status = up_b;
|
|
}
|
|
|
|
static void ack(uint32_t seq) {
|
|
struct INFLIGHT_PACKET* packet = (struct INFLIGHT_PACKET*)queue_find_data_by_index(conn->input_wait_ack, &seq);
|
|
if (!packet) packet = (struct INFLIGHT_PACKET*)queue_find_data_by_index(conn->input_send_q, &seq);
|
|
if (packet && packet->last_link) etcp_tx_ack(packet->last_link, seq, packet->last_attempt_id, 0, 0, 0);
|
|
else etcp_ack_recv(conn, seq);
|
|
check_accounting();
|
|
}
|
|
static struct ETCP_TX_ATTEMPT* last_attempt(struct INFLIGHT_PACKET* packet) {
|
|
return (struct ETCP_TX_ATTEMPT*)queue_find_data_by_index(packet->last_link->attempts, &packet->last_attempt_id);
|
|
}
|
|
static void ack_all(void) {
|
|
while (conn->input_wait_ack->head) ack(((struct INFLIGHT_PACKET*)conn->input_wait_ack->head)->seq);
|
|
}
|
|
|
|
/* Ждём наблюдаемое событие, а не точную границу миллисекундного таймера uasync. */
|
|
static void wait_sends(unsigned expected) {
|
|
uint64_t end = get_time_tb() + 20000;
|
|
while (sends < expected && get_time_tb() < end) test_clock_poll(ua, 1);
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_ETCP, "TX observed=%u expected=%u send_q=%d wait_ack=%d", sends, expected,
|
|
conn->input_send_q->count, conn->input_wait_ack->count);
|
|
CHECK(sends == expected); check_accounting();
|
|
}
|
|
|
|
/* В полном пути свободное окно другого линка не разрешает DATA через заполненный. */
|
|
static void selection(void) {
|
|
setup(); fill(a, 8, 1024); fill(b, 16, 1024); a->inflight_lim_bytes = 8192;
|
|
enqueue(512);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == b);
|
|
CHECK(a->inflight_bytes == 8192 && b->inflight_bytes == 16896);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
teardown();
|
|
}
|
|
|
|
static void boundaries_ack_burst(void) {
|
|
setup(); b->link_status = 0; a->inflight_lim_bytes = 8192;
|
|
fill(a, 7, 1024); enqueue(1024);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && a->inflight_bytes == 8192);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
struct INFLIGHT_PACKET* waiting = enqueue(1); CHECK(!request() && a->cwnd_limited);
|
|
struct ACK_PACKET* confirmation = (struct ACK_PACKET*)queue_entry_new_from_pool(inst.ack_pool); CHECK(confirmation);
|
|
confirmation->seq = 777;
|
|
confirmation->attempt_id = etcp_tx_attempt_id(777, 1);
|
|
CHECK(queue_data_put_with_index(a->ack_q, &confirmation->ll) == 0);
|
|
pkt = request(); CHECK(pkt && pkt->data[0] == ETCP_SECTION_ACK && pkt->data[1] == 1);
|
|
CHECK(waiting == (struct INFLIGHT_PACKET*)conn->input_send_q->head && a->inflight_bytes == 8192);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
a->burst_active = 1; a->burst_count = 2;
|
|
for (int i = 0; i < 2; i++) {
|
|
pkt = request(); CHECK(pkt && pkt->data[8] == ETCP_SECTION_MEAS_TS && (pkt->data[11] & MEAS_FLAG_IS_FILLER));
|
|
CHECK(a->inflight_bytes == 8192 && conn->input_send_q->head == &waiting->ll);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
}
|
|
CHECK(!a->burst_active);
|
|
teardown();
|
|
}
|
|
|
|
/* RTO освобождает окно; поздний ACK удаляет ожидающий retry, затем остальные повторы переходят A -> B. */
|
|
static void late_ack_and_migration(void) {
|
|
setup(); fill(a, 8, 1024); enqueue(600);
|
|
a->link_status = b->link_status = 0;
|
|
test_clock_run_for(ua, 120);
|
|
check_accounting(); CHECK(!conn->input_wait_ack->count && !a->inflight_bytes);
|
|
ack(1); ack(1); CHECK(!a->inflight_bytes);
|
|
b->link_status = 1; etcp_link_update_inflight_lim(b, 8192);
|
|
wait_sends(8);
|
|
CHECK(!conn->input_send_q->count && b->inflight_bytes == 7768 && sends == 8);
|
|
for (struct ll_entry* e = conn->input_wait_ack->head; e; e = e->next)
|
|
CHECK(((struct INFLIGHT_PACKET*)e)->last_link == b);
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
static void lost_ack_and_shrink(void) {
|
|
setup(); b->link_status = 0; a->inflight_lim_bytes = 16384; fill(a, 16, 1024);
|
|
etcp_link_update_inflight_lim(a, 8192);
|
|
enqueue(1024); CHECK(!request() && a->inflight_bytes == 16384);
|
|
wait_sends(8);
|
|
check_accounting(); CHECK(sends == 8 && a->inflight_bytes == 8192 && conn->input_send_q->count == 9);
|
|
ack_all(); wait_sends(16);
|
|
CHECK(sends == 16 && a->inflight_bytes == 8192 && conn->input_send_q->count == 1);
|
|
ack_all(); wait_sends(17); ack_all();
|
|
CHECK(!conn->unacked_bytes && !conn->input_send_q->count);
|
|
teardown();
|
|
}
|
|
|
|
static void grow_and_session(void) {
|
|
setup(); b->link_status = 0; a->inflight_lim_bytes = 8192; fill(a, 8, 1024); enqueue(100);
|
|
CHECK(!request());
|
|
uint64_t allocated = ua->timer_alloc_count;
|
|
test_clock_run_for(ua, 20); CHECK(!sends && ua->timer_alloc_count - allocated < 4);
|
|
etcp_link_update_inflight_lim(a, 16384); wait_sends(1);
|
|
CHECK(sends == 1 && a->inflight_bytes == 8292); ack_all();
|
|
conn->initialized = 0; conn->session_required = conn->reinit_pending = 1;
|
|
enqueue(100); CHECK(!request()); test_clock_run_for(ua, 10); CHECK(sends == 1);
|
|
conn->session_required = 0; etcp_conn_ready(conn); wait_sends(2);
|
|
CHECK(sends == 2 && conn->input_wait_ack->count == 1);
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
static void allocation_and_send_failure(void) {
|
|
setup(); b->link_status = 0; enqueue(100); fail_alloc = 1;
|
|
test_clock_run_for(ua, 20);
|
|
CHECK(!sends && conn->input_send_q->count == 1 && !a->inflight_bytes && conn->tx_token && conn->tx_delayed);
|
|
fail_send = 1; wait_sends(1);
|
|
CHECK(sends == 1 && conn->input_send_q->count == 1 && !a->inflight_bytes);
|
|
wait_sends(2);
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
static void removed_link_and_bbr(void) {
|
|
setup(); fill(a, 1, 1024);
|
|
etcp_link_close(a); a = NULL;
|
|
ack(1); CHECK(!conn->unacked_bytes);
|
|
b->bbr = u_calloc(1, sizeof(*b->bbr)); CHECK(b->bbr); bbr_init(b->bbr);
|
|
b->inflight_lim_bytes = 8192;
|
|
for (int i = 0; i < 7; i++) enqueue(1024);
|
|
for (int i = 0; i < 7; i++) {
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt); memory_pool_free(inst.pkt_pool, pkt);
|
|
}
|
|
enqueue(1025); CHECK(!request() && b->cwnd_limited && b->inflight_bytes < b->inflight_lim_bytes);
|
|
// Первый ACK относится к отправке при наличии ожидающих данных: она не app_limited.
|
|
struct INFLIGHT_PACKET* first = (struct INFLIGHT_PACKET*)conn->input_wait_ack->head;
|
|
CHECK(!last_attempt(first)->is_app_limited);
|
|
ack(first->seq); CHECK(bbr_calls == 1 && saw_cwnd_limited && !saw_app_limited);
|
|
teardown();
|
|
}
|
|
|
|
static void shaper_ready(void* arg) {
|
|
struct ETCP_LINK* link = arg;
|
|
link->shaper_timer = NULL;
|
|
loadbalancer_link_ready(link);
|
|
}
|
|
|
|
/* Реальный RTO: границы, округление, начальный jitter и выбор DATA/KA RTT; без подмены обработчика таймера. */
|
|
static void retransmission_timeout(void) {
|
|
const struct { uint32_t rtt_us, jitter_us; uint16_t ka_rtt_tb; uint32_t expected_us; } cases[] = {
|
|
{0, 0, 0, 1000000}, {0, UINT32_MAX, 0, 1000000}, {2000, 1000, 0, 10000},
|
|
{20000, 0, 0, 27000}, {20000, 7500, 0, 38250}, {200000, 10000, 0, 258000},
|
|
{600000, 100000, 0, 873000}, {1000000, 0, 0, 1000000}, {UINT32_MAX, UINT32_MAX, 0, 1000000},
|
|
{5833, 0, 0, 10000}, {5834, 0, 0, 10001}, {20001, 1, 0, 27003},
|
|
{20000, 10000, 0, 42000}, {200000, 100000, 0, 393000},
|
|
{0, 0, 200, 27000}, {20000, 0, 6000, 27000}, {0, 0, UINT16_MAX, 1000000},
|
|
{830832, 0, 0, 999999}, {830833, 0, 0, 1000000}, {830834, 0, 0, 1000000}
|
|
};
|
|
for (unsigned i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
|
setup(); b->link_status = 0;
|
|
conn->rtt_avg_10 = UINT16_MAX; conn->jitter = UINT16_MAX; // Общие/чужие метрики не участвуют в RTO этого пути.
|
|
a->rtt_last = cases[i].ka_rtt_tb; a->data_rtt_us = cases[i].rtt_us; a->data_jitter_us = cases[i].jitter_us;
|
|
CHECK(etcp_tx_rto_us(a) == cases[i].expected_us);
|
|
fill(a, 1, 100);
|
|
struct INFLIGHT_PACKET* packet = (struct INFLIGHT_PACKET*)conn->input_wait_ack->head;
|
|
uint64_t sent = last_attempt(packet)->sent_at_us, deadline = sent + cases[i].expected_us + 100000;
|
|
unsigned expected_tb = (cases[i].expected_us + 99) / 100;
|
|
// Блокируем только новую передачу: статус UP и настоящий retrans-таймер сохраняются.
|
|
a->shaper_timer = uasync_set_timeout(ua, expected_tb + 10000, a, shaper_ready, "test_rto_shaper");
|
|
CHECK(a->shaper_timer);
|
|
while (packet->state == INFLIGHT_STATE_WAIT_ACK && get_time_us() < deadline) test_clock_poll(ua, 10);
|
|
uint64_t elapsed = get_time_us() - sent;
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RTO: DATA_RTT_us=%u jitter_us=%u KA_RTT_tb=%u expected_us=%u observed_us=%llu state=%u",
|
|
cases[i].rtt_us, cases[i].jitter_us, cases[i].ka_rtt_tb, cases[i].expected_us, (unsigned long long)elapsed, packet->state);
|
|
CHECK(packet->state == INFLIGHT_STATE_WAIT_SEND && elapsed >= cases[i].expected_us && elapsed <= cases[i].expected_us + 100000);
|
|
CHECK(!sends && !a->inflight_bytes && conn->input_send_q->count == 1 && conn->unacked_bytes == 100);
|
|
check_accounting(); ack(packet->seq); teardown();
|
|
}
|
|
}
|
|
|
|
static void pacing_reset_close(void) {
|
|
setup(); b->link_status = 0;
|
|
a->shaper_timer = uasync_set_timeout(ua, 20, a, shaper_ready, "test_shaper"); CHECK(a->shaper_timer);
|
|
enqueue(100); CHECK(!request());
|
|
wait_sends(1); ack_all();
|
|
enqueue(100); CHECK(conn->tx_token);
|
|
etcp_conn_reinit_id(conn, "queued sender reset", conn->reset_id);
|
|
CHECK(!conn->tx_token && !conn->unacked_bytes && conn->reinit_pending);
|
|
etcp_conn_ready(conn);
|
|
enqueue(100); CHECK(conn->tx_token);
|
|
CHECK(etcp_conn_ref_take(conn) == 0);
|
|
etcp_connection_close(conn); CHECK(conn->state == 2 && !conn->tx_token);
|
|
etcp_conn_ref_free(conn);
|
|
teardown();
|
|
}
|
|
|
|
/* Смена ставки пересчитывает долг, а не оставляет таймер от прежней скорости. */
|
|
static void pacing_rate_changes(void) {
|
|
setup(); b->link_status = 0; a->data_rtt_us = 1000000;
|
|
etcp_loadbalancer_set_pacing_rate(a, 100);
|
|
enqueue(1000); wait_sends(1); enqueue(500);
|
|
uint64_t debt = a->shaper_load_time_tb - get_time_tb();
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Pacing below 1Kbit/s: rate=%u debt_tb=%llu", a->bbr_pacing_rate, (unsigned long long)debt);
|
|
CHECK(a->shaper_timer && a->bbr_pacing_rate == 100 && debt >= 99000 && debt <= 103000);
|
|
etcp_loadbalancer_set_pacing_rate(a, 1000000);
|
|
CHECK(a->shaper_load_time_tb <= get_time_tb() + 20);
|
|
wait_sends(2); ack_all(); teardown();
|
|
|
|
setup(); b->link_status = 0; a->data_rtt_us = 1000000;
|
|
etcp_loadbalancer_set_pacing_rate(a, 500000); // 1000 DATA-байтов оставляют долг даже после кредита 1мс.
|
|
enqueue(1000); wait_sends(1); enqueue(500);
|
|
etcp_loadbalancer_set_pacing_rate(a, 100);
|
|
CHECK(a->shaper_timer && a->shaper_load_time_tb > get_time_tb() + 1000);
|
|
test_clock_run_for(ua, 200); CHECK(sends == 1 && conn->input_send_q->count == 1);
|
|
etcp_loadbalancer_set_pacing_rate(a, 0);
|
|
CHECK(!a->shaper_timer && !a->shaper_sub_nanotime);
|
|
wait_sends(2); ack_all(); teardown();
|
|
}
|
|
|
|
static void pacing_timer_failure(void) {
|
|
setup(); CHECK(etcp_conn_ref_take(conn) == 0); b->link_status = 0;
|
|
etcp_loadbalancer_set_pacing_rate(a, 100); fail_shaper_timer = 1;
|
|
enqueue(100); uasync_drain_immediate(ua);
|
|
CHECK(!fail_shaper_timer && conn->state == 2 && !conn->links);
|
|
etcp_conn_ref_free(conn); teardown();
|
|
}
|
|
|
|
/* Миллисекундное округление таймеров не должно превращать 1MB/s в 0.5MB/s. */
|
|
static void pacing_continuity(void) {
|
|
setup(); b->link_status = 0; a->data_rtt_us = 1000000;
|
|
etcp_loadbalancer_set_pacing_rate(a, 1000000);
|
|
uint64_t started = get_time_tb();
|
|
for (unsigned i = 0; i < 50; i++) enqueue(1000);
|
|
wait_sends(50);
|
|
uint64_t elapsed = get_time_tb() - started;
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Pacing continuity: DATA=50000 rate=1000000 elapsed_tb=%llu", (unsigned long long)elapsed);
|
|
CHECK(elapsed >= 450 && elapsed <= 650);
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
/* Ставка BBR измеряется в DATA B/s: большой piggyback ACK не должен уменьшать допустимую скорость DATA. */
|
|
static void pacing_data_units(void) {
|
|
setup(); b->link_status = 0;
|
|
etcp_loadbalancer_set_pacing_rate(a, 100000);
|
|
uint64_t started = get_time_tb();
|
|
for (unsigned i = 0; i < 50; i++) {
|
|
while (!loadbalancer_link_can_send(a)) test_clock_poll(ua, 10);
|
|
struct ETCP_DGRAM* pkt = memory_pool_alloc(inst.pkt_pool); CHECK(pkt);
|
|
memset(pkt, 0, sizeof(*pkt)); pkt->link = a; pkt->payload_len = 100; pkt->data_len = 1000;
|
|
conn->tx_running = 1; CHECK(etcp_loadbalancer_send(pkt) >= 0); conn->tx_running = 0;
|
|
}
|
|
uint64_t elapsed = get_time_tb() - started;
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Pacing DATA units: DATA=5000 encoded=50000 rate=100000 elapsed_tb=%llu",
|
|
(unsigned long long)elapsed);
|
|
CHECK(elapsed >= 450 && elapsed <= 650);
|
|
teardown();
|
|
}
|
|
|
|
/* Изолируем pacer от cwnd: передача настоящего dgram, без логической попытки и сетевого I/O. */
|
|
static void send_pacer_data(unsigned bytes) {
|
|
struct ETCP_DGRAM* pkt = memory_pool_alloc(inst.pkt_pool); CHECK(pkt);
|
|
memset(pkt, 0, sizeof(*pkt)); pkt->link = a; pkt->payload_len = bytes; pkt->data_len = 1000;
|
|
conn->tx_running = 1; CHECK(etcp_loadbalancer_send(pkt) >= 0); conn->tx_running = 0;
|
|
}
|
|
|
|
static void pacer_ready_notify(struct ETCP_CONN* ready) {
|
|
CHECK(ready == conn && !a->shaper_timer);
|
|
pacer_ready_count++;
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Pacer ready: callbacks=%u action=%u", pacer_ready_count, pacer_ready_action);
|
|
if (pacer_ready_action == 1) etcp_conn_reinit_id(conn, "pacer readiness reset", conn->reset_id);
|
|
else if (pacer_ready_action == 2) etcp_connection_close(conn);
|
|
}
|
|
|
|
/* Используем настоящий callback, предварительно отменив его реальный таймер: нет фиктивных handles. */
|
|
static void invoke_pacer_at(uint64_t now_tb) {
|
|
CHECK(pacer_clock_active && a->shaper_timer && captured_shaper_callback && captured_shaper_arg == a);
|
|
timeout_callback_t callback = captured_shaper_callback;
|
|
void* arg = captured_shaper_arg;
|
|
uasync_cancel_timeout(ua, a->shaper_timer);
|
|
pacer_now_tb = now_tb;
|
|
callback(arg);
|
|
}
|
|
|
|
/* Таймер должен будить в первый разрешённый TB, включая дробь и ограничение длинного ожидания. */
|
|
static void pacing_first_ready(void) {
|
|
const struct { uint64_t debt; int delay; } cases[] = {
|
|
{9,0}, {10,1}, {11,2}, {30,21},
|
|
{(uint64_t)INT_MAX + 9,INT_MAX}, {(uint64_t)INT_MAX + 10,INT_MAX}
|
|
};
|
|
const uint64_t fractions[] = {0, 1, SUB_MODULO-1};
|
|
for (unsigned i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) {
|
|
for (unsigned f = 0; f < sizeof(fractions)/sizeof(fractions[0]); f++) {
|
|
setup(); b->link_status = 0; conn->link_ready_for_send_fn = pacer_ready_notify;
|
|
pacer_now_tb = __real_get_time_tb(); pacer_clock_active = capture_shaper = 1;
|
|
uint64_t start = pacer_now_tb;
|
|
etcp_loadbalancer_set_pacing_rate(a, 10000); // Один DATA-байт занимает ровно 1TB.
|
|
a->shaper_load_time_tb = start + cases[i].debt - 1;
|
|
a->shaper_sub_nanotime = fractions[f];
|
|
send_pacer_data(1);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Pacer first-ready: debt_tb=%llu fraction=%llu timer_tb=%d expected=%d",
|
|
(unsigned long long)cases[i].debt, (unsigned long long)fractions[f], captured_shaper_delay, cases[i].delay);
|
|
CHECK(a->shaper_load_time_tb == start + cases[i].debt && a->shaper_sub_nanotime == fractions[f]);
|
|
if (cases[i].delay) {
|
|
CHECK(a->shaper_timer && shaper_arm_count == 1 && captured_shaper_delay == cases[i].delay);
|
|
CHECK(!loadbalancer_link_can_send(a));
|
|
invoke_pacer_at(start + cases[i].debt - SHAPER_BURST_DELAY_TB);
|
|
CHECK(a->shaper_timer && captured_shaper_delay == 1 && !pacer_ready_count);
|
|
CHECK(!loadbalancer_link_can_send(a));
|
|
invoke_pacer_at(start + cases[i].debt - SHAPER_BURST_DELAY_TB + 1);
|
|
CHECK(!a->shaper_timer && pacer_ready_count == 1 && loadbalancer_link_can_send(a));
|
|
CHECK(a->shaper_load_time_tb == start + cases[i].debt && a->shaper_sub_nanotime == fractions[f]);
|
|
} else CHECK(!a->shaper_timer && !shaper_arm_count && loadbalancer_link_can_send(a));
|
|
pacer_clock_active = capture_shaper = 0;
|
|
teardown();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Поздний callback не превращает простой в неограниченный burst; окно не ограничивает эту проверку. */
|
|
static void pacing_late_wakeup(void) {
|
|
setup(); b->link_status = 0; conn->link_ready_for_send_fn = pacer_ready_notify;
|
|
pacer_now_tb = __real_get_time_tb(); pacer_clock_active = capture_shaper = 1;
|
|
uint64_t start = pacer_now_tb;
|
|
etcp_loadbalancer_set_pacing_rate(a, 1000000);
|
|
send_pacer_data(1000); CHECK(a->shaper_timer);
|
|
invoke_pacer_at(start + 1000); // Callback опоздал на 100мс, а разрешённый кредит остаётся 1мс.
|
|
CHECK(pacer_ready_count == 1 && loadbalancer_link_can_send(a));
|
|
unsigned before = sends;
|
|
while (loadbalancer_link_can_send(a)) {
|
|
CHECK(sends-before < 10);
|
|
send_pacer_data(1000);
|
|
}
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Pacer late wake: stall_tb=1000 immediate_DATA=%u credit_tb=%llu",
|
|
sends-before, (unsigned long long)SHAPER_BURST_DELAY_TB);
|
|
CHECK(sends-before == 2 && a->shaper_timer);
|
|
pacer_clock_active = capture_shaper = 0;
|
|
teardown();
|
|
}
|
|
|
|
/* Отказ повторной установки таймера закрывает соединение, не уведомляя о фиктивной готовности. */
|
|
static void pacing_rearm_failure(void) {
|
|
setup(); b->link_status = 0; CHECK(etcp_conn_ref_take(conn) == 0);
|
|
conn->link_ready_for_send_fn = pacer_ready_notify;
|
|
pacer_now_tb = __real_get_time_tb(); pacer_clock_active = capture_shaper = 1;
|
|
uint64_t start = pacer_now_tb;
|
|
etcp_loadbalancer_set_pacing_rate(a, 10000);
|
|
a->shaper_load_time_tb = start + 99;
|
|
send_pacer_data(1); CHECK(a->shaper_timer);
|
|
fail_shaper_timer = 1;
|
|
invoke_pacer_at(start + 90);
|
|
CHECK(!fail_shaper_timer && !pacer_ready_count && conn->close_requested);
|
|
pacer_clock_active = capture_shaper = 0;
|
|
uasync_drain_immediate(ua);
|
|
CHECK(conn->state == 2 && !conn->links);
|
|
etcp_conn_ref_free(conn); teardown();
|
|
}
|
|
|
|
/* Реальный dispatch таймера: готовность может синхронно сбросить сессию или освободить физический линк. */
|
|
static void pacing_ready_lifecycle(void) {
|
|
for (unsigned action = 1; action <= 2; action++) {
|
|
setup(); b->link_status = 0; CHECK(etcp_conn_ref_take(conn) == 0);
|
|
conn->link_ready_for_send_fn = pacer_ready_notify; pacer_ready_action = action;
|
|
etcp_loadbalancer_set_pacing_rate(a, 100000);
|
|
send_pacer_data(1000); CHECK(a->shaper_timer);
|
|
uint64_t end = get_time_us() + 1000000;
|
|
while (!pacer_ready_count && get_time_us() < end) test_clock_poll(ua, 10);
|
|
CHECK(pacer_ready_count == 1);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Pacer callback result: action=%u state=%u initialized=%u reinit=%u session=%u links=%d",
|
|
action, conn->state, conn->initialized, conn->reinit_pending, conn->session_required, conn->links != NULL);
|
|
if (action == 1) CHECK(conn->reinit_pending && !conn->initialized && conn->reinit_count == 1 && conn->links);
|
|
else CHECK(conn->state == 2 && !conn->links);
|
|
test_clock_run_for(ua, 40); CHECK(pacer_ready_count == 1);
|
|
etcp_conn_ref_free(conn); teardown();
|
|
}
|
|
}
|
|
|
|
/* Физические копии уже доставленных seq также занимают бюджет, пока не придёт собственный ACK. */
|
|
static void attempt_memory_limits(void) {
|
|
setup();
|
|
a->inflight_lim_bytes = b->inflight_lim_bytes = MAX_INFLIGHT_SIZE * 4;
|
|
for (unsigned path = 0; path < 2; path++) {
|
|
struct ETCP_LINK* link = path ? b : a;
|
|
for (unsigned i = 0; i < MAX_INFLIGHT_SIZE; i++) {
|
|
struct INFLIGHT_PACKET* packet = enqueue(1);
|
|
struct ETCP_TX_ATTEMPT* attempt = etcp_tx_prepare(link, packet); CHECK(attempt);
|
|
struct ETCP_DGRAM pkt = { .link = link, .attempt = attempt };
|
|
etcp_tx_complete(&pkt, 1); etcp_ack_recv(conn, packet->seq);
|
|
}
|
|
CHECK(link->attempts->count == MAX_INFLIGHT_SIZE);
|
|
}
|
|
CHECK(conn->active_attempts == 2 * MAX_INFLIGHT_SIZE);
|
|
struct INFLIGHT_PACKET* pending = enqueue(1);
|
|
CHECK(!etcp_loadbalancer_select_link(conn, 1, NULL));
|
|
CHECK(!etcp_tx_prepare(a, pending));
|
|
struct ETCP_TX_ATTEMPT old = *(struct ETCP_TX_ATTEMPT*)a->attempts->head;
|
|
etcp_tx_ack(a, old.seq, old.attempt_id, UINT32_MAX, 0, 0);
|
|
CHECK(etcp_loadbalancer_select_link(conn, 1, NULL) == a);
|
|
CHECK(conn->active_attempts == 2 * MAX_INFLIGHT_SIZE - 1);
|
|
for (unsigned i = 0; i < MAX_INFLIGHT_SIZE; i++) CHECK(etcp_tx_queue_ack(a, i+1, etcp_tx_attempt_id(i+1, 1)) == 0);
|
|
CHECK(etcp_tx_queue_ack(a, MAX_INFLIGHT_SIZE+1, etcp_tx_attempt_id(MAX_INFLIGHT_SIZE+1, 1)) < 0);
|
|
CHECK(a->ack_q->count == MAX_INFLIGHT_SIZE);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Attempt limits: physical=%u path=%d ACK=%d ready_seq=%u",
|
|
conn->active_attempts, a->attempts->count, a->ack_q->count, pending->seq);
|
|
teardown();
|
|
}
|
|
|
|
/* Отправитель не превращает pending transport в UP до первого INIT/подтверждения сессии. */
|
|
static void initial_readiness(void) {
|
|
setup(); conn->state = 0; conn->initialized = conn->links_up = conn->tx_state = 0;
|
|
conn->session_required = 1; enqueue(100);
|
|
test_clock_run_for(ua, 20);
|
|
CHECK(!conn->tx_state && !sends && !conn->links_up);
|
|
conn->link_ready_for_send_fn(conn); CHECK(!conn->links_up);
|
|
conn->session_required = 0; etcp_conn_ready(conn); wait_sends(1);
|
|
CHECK(conn->state == 1 && conn->links_up && conn->initialized);
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
static void config_limits(void) {
|
|
const char* values[] = {"0", "8191", "-1", "2147483648", "999999999999999999999", "abc", "8192x", "8192", "65536"};
|
|
for (unsigned i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
|
|
char text[128]; snprintf(text, sizeof(text), "[global]\nbbr_max_cwnd=%s\n", values[i]);
|
|
struct utun_config* parsed = parse_config_from_buf(text, strlen(text), "window_config");
|
|
CHECK((parsed != NULL) == (i >= 7));
|
|
free_config(parsed);
|
|
}
|
|
}
|
|
|
|
/* Отказ TX-таймера закрывает транспорт после выхода из вызывающего стека, без UAF очереди/линков. */
|
|
static void timer_failure(void) {
|
|
setup(); CHECK(!conn->tx_token && etcp_conn_ref_take(conn) == 0);
|
|
fail_tx_timer = 1; enqueue(100);
|
|
CHECK(conn->close_requested && conn->close_token && conn->state != 2 && conn->links);
|
|
uasync_drain_immediate(ua);
|
|
CHECK(conn->state == 2 && !conn->tx_token && !conn->links);
|
|
etcp_conn_ref_free(conn); teardown();
|
|
}
|
|
|
|
static void immediate_send(void) {
|
|
setup(); enqueue(100);
|
|
CHECK(conn->tx_token && !conn->tx_delayed);
|
|
uasync_drain_immediate(ua);
|
|
CHECK(sends == 1 && conn->input_wait_ack->count == 1);
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
/* Подтверждения входящего потока не должны ждать pacing собственного исходящего DATA. */
|
|
static void ack_without_data_pacing(void) {
|
|
setup(); b->link_status = 0; etcp_loadbalancer_set_pacing_rate(a, 125);
|
|
a->shaper_timer = uasync_set_timeout(ua, 1000, a, shaper_ready, "test_shaper"); CHECK(a->shaper_timer);
|
|
void* timer = a->shaper_timer;
|
|
uint64_t load = a->shaper_load_time_tb, fraction = a->shaper_sub_nanotime;
|
|
struct ACK_PACKET* confirmation = (struct ACK_PACKET*)queue_entry_new_from_pool(inst.ack_pool); CHECK(confirmation);
|
|
confirmation->seq = 777;
|
|
confirmation->attempt_id = etcp_tx_attempt_id(777, 1);
|
|
CHECK(queue_data_put_with_index(a->ack_q, &confirmation->ll) == 0);
|
|
enqueue(100); uasync_drain_immediate(ua);
|
|
CHECK(sends == 1 && conn->input_send_q->count == 1 && !a->ack_q->count);
|
|
CHECK(a->shaper_timer == timer && a->shaper_load_time_tb == load && a->shaper_sub_nanotime == fraction);
|
|
uasync_cancel_timeout(ua, timer); a->shaper_timer = NULL;
|
|
etcp_conn_wake_sender(conn); wait_sends(2); ack_all(); teardown();
|
|
}
|
|
|
|
/* ACK одной пачки приходят в один tick: скорость определяется доставкой всей группы, а не промежутком между ACK. */
|
|
static void bbr_delivery_clock(void) {
|
|
setup(); b->link_status = 0; a->data_rtt_us = 20000;
|
|
a->bbr = u_calloc(1, sizeof(*a->bbr)); CHECK(a->bbr); bbr_init(a->bbr);
|
|
fill(a, 8, 1000);
|
|
uint64_t sent_tb = get_time_tb();
|
|
test_clock_run_for(ua, 200);
|
|
for (unsigned seq = 1; seq <= 8; seq++) {
|
|
ack(seq);
|
|
uint64_t elapsed_us = (get_time_tb() - sent_tb) * 100;
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Delivery sample: seq=%u delivered=%u acked=%u interval_us=%u flight_us=%llu",
|
|
seq, last_sample.delivered, last_sample.acked_sacked, last_sample.interval_us, (unsigned long long)elapsed_us);
|
|
CHECK(last_sample.delivered == seq * 1000 && last_sample.acked_sacked == 1000);
|
|
CHECK(last_sample.interval_us >= 19000 && last_sample.interval_us <= elapsed_us + 1000);
|
|
}
|
|
CHECK(a->bbr->delivered == 8000);
|
|
teardown();
|
|
}
|
|
|
|
/* Модель и транспорт используют один DATA MSS и одно окно, в том числе ниже 8KiB. */
|
|
static void bbr_probe_window(void) {
|
|
setup(); b->link_status = 0; a->data_rtt_us = 1000000;
|
|
etcp_update_mtu(conn);
|
|
uint32_t minimum = BBR_CWND_MIN_TARGET * conn->normalizer->frag_size;
|
|
a->rtt_last = 100;
|
|
a->bbr = u_calloc(1, sizeof(*a->bbr)); CHECK(a->bbr);
|
|
a->bbr->now_tb = 10000; bbr_init(a->bbr);
|
|
a->bbr->mode = BBR_PROBE_BW; a->bbr->full_bw_reached = 1; a->bbr->cycle_idx = BBR_BW_PROBE_CRUISE;
|
|
a->bbr->min_rtt_us = a->bbr->probe_rtt_min_us = 10000; a->bbr->has_seen_rtt = 1;
|
|
a->bbr->bw_hi[0] = BW_UNIT / 10; a->bbr->now_tb += BBR_PROBE_RTT_WIN_MS * 10 + 1;
|
|
fill(a, 8, 1000); test_clock_run_for(ua, 200); ack(1);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PROBE_RTT transport: DATA_MSS=%u model_MSS=%u window=%u expected=%u",
|
|
conn->normalizer->frag_size, last_mss, a->inflight_lim_bytes, minimum);
|
|
CHECK(last_mss == conn->normalizer->frag_size && a->inflight_lim_bytes == minimum && minimum < INFLIGHT_LIM_MIN);
|
|
struct INFLIGHT_PACKET* probe = enqueue(1000); CHECK(!request());
|
|
ack(2); ack(3); ack(4); CHECK(a->bbr->probe_rtt_done_stamp_tb);
|
|
ack(5); // оставшиеся 3000 байт и новый фрагмент целиком помещаются в 4388
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && last_attempt(probe)->is_app_limited);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
test_clock_run_for(ua, 10); a->bbr->now_tb += BBR_PROBE_RTT_MS * 10;
|
|
ack(probe->seq); CHECK(a->bbr->mode == BBR_PROBE_BW);
|
|
uint32_t window = a->inflight_lim_bytes;
|
|
etcp_link_update_inflight_lim(a, 0); CHECK(a->inflight_lim_bytes == window);
|
|
etcp_link_update_inflight_lim(a, UINT32_MAX); CHECK(a->inflight_lim_bytes == conn->max_inflight);
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
/* Размер STCP-кадра 65535 не является размером DATA-фрагмента и не задаёт начальное окно. */
|
|
static void tcp_initial_window(void) {
|
|
setup();
|
|
struct ETCP_LINK* link = etcp_link_new(conn, NULL, NULL, 1); CHECK(link && link->is_tcp);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "STCP initial window: frame=%u DATA_MSS=%u window=%u", link->mtu,
|
|
conn->normalizer->frag_size, link->inflight_lim_bytes);
|
|
CHECK(link->inflight_lim_bytes == BBR_CWND_MIN_TARGET * conn->normalizer->frag_size);
|
|
CHECK(link->bbr->init_cwnd == BBR_CWND_MIN_TARGET);
|
|
teardown();
|
|
}
|
|
|
|
static void enable_models(void) {
|
|
for (struct ETCP_LINK* link = conn->links; link; link = link->next) {
|
|
link->bbr = u_calloc(1, sizeof(*link->bbr)); CHECK(link->bbr);
|
|
bbr_init(link->bbr); link->bbr->link_id = link->local_link_id;
|
|
}
|
|
}
|
|
|
|
static void receive_till(struct ETCP_LINK* link, uint32_t till, int query) {
|
|
struct ETCP_DGRAM* pkt = memory_pool_alloc(inst.pkt_pool); CHECK(pkt);
|
|
pkt->link = link; pkt->data_len = ETCP_ACK_BASE_SIZE + query;
|
|
memset(pkt->data, 0, pkt->data_len); pkt->data[0] = ETCP_SECTION_ACK;
|
|
for (unsigned i = 0; i < 4; i++) pkt->data[2 + i] = till >> (8 * i);
|
|
if (query) pkt->data[ETCP_ACK_BASE_SIZE] = ETCP_SECTION_ACK_REQUEST;
|
|
etcp_conn_input(pkt); check_accounting();
|
|
}
|
|
|
|
static void receive_fragment(struct ETCP_LINK* link, uint32_t seq, uint64_t id) {
|
|
struct ETCP_DGRAM* pkt = memory_pool_alloc(inst.pkt_pool); CHECK(pkt);
|
|
pkt->link = link; pkt->data_len = ETCP_PAYLOAD_HEADER_SIZE + 1;
|
|
memset(pkt->data, 0, pkt->data_len); pkt->data[0] = ETCP_SECTION_PAYLOAD;
|
|
for (unsigned i = 0; i < 4; i++) pkt->data[1 + i] = seq >> (8 * i);
|
|
for (unsigned i = 0; i < 2; i++) pkt->data[5 + i] = id >> (8 * i);
|
|
pkt->data[ETCP_PAYLOAD_HEADER_SIZE] = 0x5a;
|
|
etcp_conn_input(pkt);
|
|
}
|
|
|
|
/* Повтор обходит ещё UP-канал, даже если у него меньше inflight; общий RR не определяет курсор пакета. */
|
|
static void retry_rotation(void) {
|
|
setup(); fill(a, 1, 100); fill(b, 1, 1000); b->data_rtt_us = 1000000;
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
CHECK(a->link_status && b->link_status && !a->inflight_bytes && b->inflight_bytes == 1000);
|
|
conn->last_rr_link = b;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == b);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
|
|
|
setup();
|
|
struct ETCP_LINK* c = u_calloc(1, sizeof(*c)); CHECK(c);
|
|
c->etcp = conn; c->local_link_id = 3; c->initialized = c->link_status = 1;
|
|
c->mtu = 1280; c->remote_addr.ss_family = AF_INET; c->rtt_last = 1; c->inflight_lim_bytes = 65536;
|
|
CHECK(etcp_tx_link_init(c) == 0); b->next = c;
|
|
fill(a, 1, 100);
|
|
struct ETCP_LINK* expected[] = {b, c, a};
|
|
for (unsigned i = 0; i < 3; i++) {
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
conn->last_rr_link = expected[i]; // посторонний выбор/ACK меняет общий RR, но не порядок повторов
|
|
pkt = request(); CHECK(pkt && pkt->link == expected[i]);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Retry rotation: step=%u selected=%u", i, pkt->link->local_link_id);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
}
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
/* Готовый прежний путь не обходит занятую UP-альтернативу; ждём её события без новой попытки/RTO. */
|
|
static void retry_waits_for_alternative(void) {
|
|
setup(); fill(a, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
struct INFLIGHT_PACKET* packet = (struct INFLIGHT_PACKET*)conn->input_send_q->head;
|
|
CHECK(packet && packet->last_link == a && !packet->active_attempts && packet->attempt_count == 1);
|
|
b->shaper_timer = uasync_set_timeout(ua, 300, b, shaper_ready, "test_retry_alternative"); CHECK(b->shaper_timer);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Retry must wait: seq=%u previous=%u ready=1 alternative=%u pacing_blocked=1",
|
|
packet->seq, a->local_link_id, b->local_link_id);
|
|
CHECK(!request());
|
|
uint64_t allocated = ua->timer_alloc_count;
|
|
test_clock_run_for(ua, 5);
|
|
CHECK(!sends && packet->attempt_count == 1 && !packet->active_attempts && !a->retrans_timer && !b->retrans_timer);
|
|
CHECK(packet->state == INFLIGHT_STATE_WAIT_SEND && packet->last_link == a && ua->timer_alloc_count == allocated);
|
|
CHECK(etcp_tx_queue_ack(a, 99, etcp_tx_attempt_id(99, 1)) == 0);
|
|
mature_acks(a);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a && !pkt->payload_len && !pkt->attempt);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
a->burst_active = 1; a->burst_count = 1;
|
|
pkt = request(); CHECK(pkt && pkt->link == a && pkt->burst_packet && !pkt->payload_len && !pkt->attempt);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
CHECK(packet->last_link == a && packet->attempt_count == 1 && !packet->active_attempts);
|
|
wait_sends(1); CHECK(last_sent_link == b && packet->last_link == b && packet->attempt_count == 2);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Retry resumed: seq=%u selected=%u attempts=%u", packet->seq, b->local_link_id, packet->attempt_count);
|
|
ack_all(); teardown();
|
|
|
|
setup(); fill(a, 1, 100); fill(b, 1, 100); b->data_rtt_us = 1000000; b->inflight_lim_bytes = 100;
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
packet = (struct INFLIGHT_PACKET*)conn->input_send_q->head;
|
|
CHECK(packet && packet->last_link == a && !request());
|
|
CHECK(a->inflight_bytes == 0 && b->inflight_bytes == 100 && !packet->active_attempts && packet->attempt_count == 1);
|
|
ack(2); wait_sends(1); CHECK(last_sent_link == b && packet->last_link == b);
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
/* Потеря последней альтернативы будит отправителя; поздний ACK убирает ожидающий повтор. */
|
|
static void retry_alternative_lifecycle(void) {
|
|
setup(); fill(a, 1, 100); b->inflight_lim_bytes = 1;
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
CHECK(!request() && !conn->tx_token);
|
|
b->link_status = 0; etcp_on_link_down(conn, b);
|
|
CHECK(conn->tx_token); uasync_drain_immediate(ua);
|
|
CHECK(sends == 1 && last_sent_link == a); ack_all(); teardown();
|
|
|
|
setup(); fill(a, 1, 100); b->inflight_lim_bytes = 1;
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
CHECK(!request());
|
|
etcp_link_close(b); b = NULL;
|
|
uasync_drain_immediate(ua); CHECK(sends == 1 && last_sent_link == a && conn->links == a && !a->next);
|
|
ack_all(); teardown();
|
|
|
|
setup(); fill(a, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
struct INFLIGHT_PACKET* packet = (struct INFLIGHT_PACKET*)conn->input_send_q->head;
|
|
CHECK(packet); uint32_t seq = packet->seq; uint64_t old_id = packet->last_attempt_id;
|
|
b->shaper_timer = uasync_set_timeout(ua, 300, b, shaper_ready, "test_retry_late_ack"); CHECK(b->shaper_timer);
|
|
CHECK(!request()); etcp_tx_ack(a, seq, old_id, 0, 0, 0);
|
|
test_clock_run_for(ua, 40); CHECK(!sends && !conn->unacked_bytes && !conn->active_attempts && !conn->input_send_q->count);
|
|
teardown();
|
|
|
|
setup(); fill(a, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
b->initialized = 0;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
|
}
|
|
|
|
/* Заблокированный повтор не держит следующие DATA; среди готовых пакетов повтор имеет приоритет. */
|
|
static void send_queue_scan(void) {
|
|
setup(); fill(a, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
struct INFLIGHT_PACKET* blocked = (struct INFLIGHT_PACKET*)conn->input_send_q->head; CHECK(blocked);
|
|
b->inflight_lim_bytes = 1;
|
|
struct INFLIGHT_PACKET* fresh = enqueue(80);
|
|
struct ETCP_DGRAM* pkt = request();
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Send scan: blocked=%u fresh=%u selected=%u", blocked->seq, fresh->seq,
|
|
pkt && pkt->attempt ? pkt->attempt->seq : fresh->last_link ? fresh->seq : 0);
|
|
CHECK(pkt && pkt->link == a && fresh->state == INFLIGHT_STATE_WAIT_ACK && blocked->state == INFLIGHT_STATE_WAIT_SEND);
|
|
CHECK(blocked->attempt_count == 1 && !blocked->active_attempts);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
|
|
|
setup(); fill(a, 1, 100); fill(b, 1, 100);
|
|
a->data_rtt_us = 10000; b->data_rtt_us = 20000;
|
|
conn->initialized = 0; test_clock_run_for(ua, 400); conn->initialized = 1;
|
|
blocked = (struct INFLIGHT_PACKET*)queue_find_data_by_index(conn->input_send_q, &(uint32_t){1});
|
|
struct INFLIGHT_PACKET* later = (struct INFLIGHT_PACKET*)queue_find_data_by_index(conn->input_send_q, &(uint32_t){2});
|
|
CHECK(blocked && later && conn->input_send_q->head == &blocked->ll);
|
|
b->inflight_lim_bytes = 1;
|
|
pkt = request(); CHECK(pkt && pkt->link == a && later->last_link == a && blocked->state == INFLIGHT_STATE_WAIT_SEND);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
|
|
|
setup(); fill(a, 1, 100); fresh = enqueue(80);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
CHECK(conn->input_send_q->head == &fresh->ll);
|
|
pkt = request(); CHECK(pkt && pkt->link == b && fresh->state == INFLIGHT_STATE_WAIT_SEND);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
pkt = request(); CHECK(pkt && fresh->state == INFLIGHT_STATE_WAIT_ACK);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
|
|
|
setup(); fill(a, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
fresh = enqueue(80); a->burst_active = 1; a->burst_count = 1;
|
|
pkt = request(); CHECK(pkt && pkt->link == a && pkt->burst_packet && !pkt->payload_len);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(fresh->state == INFLIGHT_STATE_WAIT_SEND);
|
|
pkt = request(); CHECK(pkt && pkt->link == b && fresh->state == INFLIGHT_STATE_WAIT_SEND);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
pkt = request(); CHECK(pkt && fresh->state == INFLIGHT_STATE_WAIT_ACK);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
|
|
|
setup(); fill(a, 256, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
b->inflight_lim_bytes = 1; fresh = enqueue(80);
|
|
uint64_t started = get_time_us(); pkt = request();
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Send scan: blocked_prefix=256 elapsed_us=%llu", (unsigned long long)(get_time_us() - started));
|
|
CHECK(pkt && pkt->link == a && fresh->state == INFLIGHT_STATE_WAIT_ACK && conn->input_send_q->count == 256);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
|
}
|
|
|
|
static void queued_input_fragment(unsigned len) {
|
|
struct ETCP_FRAGMENT* fragment = (struct ETCP_FRAGMENT*)queue_entry_new_from_pool(conn->io_pool); CHECK(fragment);
|
|
fragment->ll.dgram = memory_pool_alloc(inst.data_pool); CHECK(fragment->ll.dgram);
|
|
fragment->ll.dgram_pool = inst.data_pool; fragment->ll.len = len; memset(fragment->ll.dgram, 0x5a, len);
|
|
conn->input_queue->callback_suspended = 1;
|
|
CHECK(queue_data_put(conn->input_queue, &fragment->ll) == 0);
|
|
}
|
|
|
|
/* Полностью заблокированная send_q не мешает подгрузке, но общий лимит и смена эпохи сохраняются. */
|
|
static void send_queue_input_admission(void) {
|
|
setup(); fill(a, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
b->inflight_lim_bytes = 1; queued_input_fragment(80);
|
|
CHECK(conn->input_queue->count == 1 && conn->input_send_q->count == 1);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a && pkt->payload_len == 80);
|
|
CHECK(!conn->input_queue->count && conn->input_send_q->count == 1 && conn->input_wait_ack->count == 1);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
|
|
|
setup(); fill(a, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
b->inflight_lim_bytes = 1; conn->optimal_inflight = 100; queued_input_fragment(80);
|
|
CHECK(!request() && conn->input_queue->count == 1 && conn->next_tx_id == 2 && conn->unacked_bytes == 100);
|
|
ack(1); pkt = request(); CHECK(pkt && pkt->payload_len == 80 && !conn->input_queue->count);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
|
|
|
setup(); fill(a, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
b->inflight_lim_bytes = 1; queued_input_fragment(80); fail_tx_inflight = 1;
|
|
uint32_t reset = conn->reset_count;
|
|
CHECK(!request() && !fail_tx_inflight && conn->reset_count != reset && conn->reinit_pending);
|
|
CHECK(!conn->unacked_bytes && !conn->active_attempts && !conn->input_send_q->count && !conn->input_queue->count);
|
|
teardown();
|
|
}
|
|
|
|
static struct ETCP_LINK* add_test_link(uint8_t id) {
|
|
struct ETCP_LINK* link = u_calloc(1, sizeof(*link)); CHECK(link);
|
|
link->etcp = conn; link->local_link_id = id; link->initialized = link->link_status = 1;
|
|
link->mtu = 1280; link->remote_addr.ss_family = AF_INET; link->rtt_last = 1; link->inflight_lim_bytes = 65536;
|
|
CHECK(etcp_tx_link_init(link) == 0);
|
|
struct ETCP_LINK* tail = conn->links;
|
|
while (tail->next) tail = tail->next;
|
|
tail->next = link; etcp_conn_on_inflight_lim_changed(conn);
|
|
return link;
|
|
}
|
|
|
|
/* Не возвращаемся в B после B->C, пока не опробован A; неудавшийся send не расходует новый обход. */
|
|
static void retry_full_round_history(void) {
|
|
setup(); struct ETCP_LINK* c = add_test_link(255);
|
|
fill(b, 1, 100);
|
|
struct INFLIGHT_PACKET* packet = (struct INFLIGHT_PACKET*)conn->input_wait_ack->head; CHECK(packet);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == c); memory_pool_free(inst.pkt_pool, pkt);
|
|
CHECK(etcp_packet_link_used(packet, b->local_link_id) && etcp_packet_link_used(packet, c->local_link_id));
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
a->inflight_lim_bytes = 1;
|
|
uint8_t history[ETCP_LINK_HISTORY_BYTES]; memcpy(history, packet->used_links, sizeof(history));
|
|
struct ETCP_LINK* previous_rr = conn->last_rr_link;
|
|
for (unsigned i = 0; i < 3; i++) CHECK(!request());
|
|
CHECK(packet->attempt_count == 2 && !memcmp(history, packet->used_links, sizeof(history)) && conn->last_rr_link == previous_rr);
|
|
struct INFLIGHT_PACKET* fresh = enqueue(80); uint32_t fresh_seq = fresh->seq;
|
|
pkt = request(); CHECK(pkt && fresh->state == INFLIGHT_STATE_WAIT_ACK && packet->state == INFLIGHT_STATE_WAIT_SEND);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack(fresh_seq);
|
|
etcp_link_update_inflight_lim(a, 100);
|
|
pkt = request(); CHECK(pkt && pkt->link == a); memory_pool_free(inst.pkt_pool, pkt);
|
|
CHECK(etcp_packet_link_used(packet, a->local_link_id) && etcp_packet_link_used(packet, b->local_link_id));
|
|
CHECK(etcp_packet_link_used(packet, c->local_link_id));
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
memcpy(history, packet->used_links, sizeof(history)); previous_rr = conn->last_rr_link;
|
|
pkt = etcp_request_pkt(conn); CHECK(pkt && pkt->link == b);
|
|
etcp_tx_complete(pkt, 0); memory_pool_free(inst.pkt_pool, pkt);
|
|
CHECK(!memcmp(history, packet->used_links, sizeof(history)) && conn->last_rr_link == previous_rr);
|
|
pkt = request(); CHECK(pkt && pkt->link == b); memory_pool_free(inst.pkt_pool, pkt);
|
|
CHECK(etcp_packet_link_used(packet, b->local_link_id) && !etcp_packet_link_used(packet, a->local_link_id));
|
|
CHECK(!etcp_packet_link_used(packet, c->local_link_id));
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
struct reused_slot { uint8_t id; struct ETCP_LINK* replacement; };
|
|
static void recreate_link_from_close(struct ETCP_CONN* owner, struct ETCP_LINK* link, int old_state, int old_status, void* arg) {
|
|
(void)old_state; (void)old_status;
|
|
struct reused_slot* slot = arg;
|
|
if (owner != conn || !link->closing || link->local_link_id != slot->id || slot->replacement) return;
|
|
conn->last_assigned_link_id = slot->id - 1;
|
|
int id = etcp_find_free_local_link_id(conn); CHECK(id == slot->id);
|
|
slot->replacement = add_test_link((uint8_t)id);
|
|
}
|
|
|
|
/* Callback удаления вправе немедленно создать новый линк с тем же wire ID; история старого слота уже снята. */
|
|
static void retry_history_slot_reuse(void) {
|
|
setup(); struct ETCP_LINK* c = add_test_link(255);
|
|
fill(b, 1, 100);
|
|
struct INFLIGHT_PACKET* packet = (struct INFLIGHT_PACKET*)conn->input_wait_ack->head; CHECK(packet);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == c); memory_pool_free(inst.pkt_pool, pkt);
|
|
struct reused_slot slot = {.id=b->local_link_id};
|
|
etcp_add_link_status_cbk(&inst, recreate_link_from_close, &slot);
|
|
etcp_link_close(b); b = NULL;
|
|
etcp_remove_link_status_cbk(&inst, recreate_link_from_close, &slot);
|
|
CHECK(slot.replacement && packet->last_link == c && !etcp_packet_link_used(packet, slot.id));
|
|
CHECK(etcp_packet_link_used(packet, c->local_link_id));
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
pkt = request(); CHECK(pkt && pkt->link == slot.replacement); memory_pool_free(inst.pkt_pool, pkt);
|
|
CHECK(etcp_packet_link_used(packet, slot.id) && !etcp_packet_link_used(packet, a->local_link_id));
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
/* Три потери могут быть обнаружены одним вызовом таймера; старые ACK не доказывают восстановление. */
|
|
static void fast_stall_progress(void) {
|
|
setup(); fill(a, 2, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
CHECK(a->data_rto_streak == 2 && !a->tx_stalled);
|
|
etcp_ack_recv(conn, 1); etcp_ack_recv(conn, 2);
|
|
fill(a, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
CHECK(a->data_rto_streak == ETCP_TX_STALL_THRESHOLD && a->tx_stalled && a->tx_stalled_since_us);
|
|
struct INFLIGHT_PACKET* packet = (struct INFLIGHT_PACKET*)conn->input_send_q->head; CHECK(packet);
|
|
etcp_tx_ack(a, packet->seq, packet->last_attempt_id, 0, 0, 0);
|
|
CHECK(a->tx_stalled && !conn->unacked_bytes);
|
|
packet = enqueue(100); uint32_t seq = packet->seq;
|
|
b->link_status = 0;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a); memory_pool_free(inst.pkt_pool, pkt);
|
|
uint64_t id = packet->last_attempt_id;
|
|
etcp_tx_ack(b, seq, id, 0, 0, 0); CHECK(a->tx_stalled && a->inflight_bytes == 100);
|
|
etcp_tx_ack(a, seq, id, 0, 0, 0); CHECK(!a->tx_stalled && !a->data_rto_streak && !a->inflight_bytes);
|
|
b->link_status = 1; teardown();
|
|
|
|
setup(); b->link_status = 0; a->data_rtt_us = 100000;
|
|
fill(a, 3, 100); conn->initialized = 0; test_clock_run_for(ua, 300); conn->initialized = 1;
|
|
fill(a, 1, 100);
|
|
packet = (struct INFLIGHT_PACKET*)queue_find_data_by_index(conn->input_wait_ack, &(uint32_t){4}); CHECK(packet);
|
|
id = packet->last_attempt_id;
|
|
conn->initialized = 0; test_clock_run_for(ua, 1000); conn->initialized = 1;
|
|
CHECK(a->tx_stalled && a->attempts->count == 1 && last_attempt(packet)->sent_at_us < a->tx_stalled_since_us);
|
|
etcp_tx_ack(a, 4, id, 0, 0, 0); CHECK(a->tx_stalled && a->data_rto_streak == ETCP_TX_STALL_THRESHOLD);
|
|
for (seq = 1; seq <= 3; seq++) etcp_ack_recv(conn, seq);
|
|
packet = enqueue(100); pkt = request(); CHECK(pkt); memory_pool_free(inst.pkt_pool, pkt);
|
|
ack(packet->seq); CHECK(!a->tx_stalled && !a->data_rto_streak); teardown();
|
|
}
|
|
|
|
static void stalled_retry_selection(void) {
|
|
setup(); fill(a, 3, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
CHECK(a->tx_stalled && !b->tx_stalled);
|
|
for (uint32_t seq = 1; seq <= 3; seq++) etcp_ack_recv(conn, seq);
|
|
fill(b, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
struct INFLIGHT_PACKET* packet = (struct INFLIGHT_PACKET*)conn->input_send_q->head; CHECK(packet);
|
|
b->inflight_lim_bytes = 1; CHECK(!request());
|
|
struct INFLIGHT_PACKET* fresh = enqueue(80); uint32_t fresh_seq = fresh->seq;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a && fresh->state == INFLIGHT_STATE_WAIT_ACK);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(a->tx_stalled);
|
|
etcp_link_update_inflight_lim(b, 100);
|
|
pkt = request(); CHECK(pkt && pkt->link == b && packet->last_link == b && a->tx_stalled);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
ack(fresh_seq); CHECK(!a->tx_stalled); ack_all(); teardown();
|
|
|
|
setup(); fill(a, 3, 100); fill(b, 3, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
CHECK(a->tx_stalled && b->tx_stalled);
|
|
packet = (struct INFLIGHT_PACKET*)conn->input_send_q->head; CHECK(packet);
|
|
struct ETCP_LINK* fallback = packet->last_link == a ? b : a;
|
|
struct ETCP_LINK* other = fallback == a ? b : a;
|
|
pkt = request(); CHECK(pkt && pkt->payload_len && pkt->link == fallback);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all();
|
|
CHECK(!fallback->tx_stalled); etcp_tx_link_reset(other, 0); CHECK(!other->tx_stalled && !other->data_rto_streak);
|
|
teardown();
|
|
}
|
|
|
|
static void abort_input_admission(struct ll_queue* q, void* arg) {
|
|
(void)q;
|
|
unsigned action = (unsigned)(uintptr_t)arg;
|
|
if (action == 1) etcp_conn_reinit_id(conn, "input admission callback", conn->reset_id);
|
|
else if (action == 2) etcp_connection_close(conn);
|
|
else { etcp_link_close(b); b = NULL; }
|
|
}
|
|
|
|
/* queue_data_get вызывает waiter синхронно: reset/close прекращают старый проход, удалённый burst не переиспользуется. */
|
|
static void input_admission_callbacks(void) {
|
|
for (unsigned action = 1; action <= 3; action++) {
|
|
setup(); fill(a, 1, 100);
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
b->inflight_lim_bytes = 1; queued_input_fragment(80); CHECK(etcp_conn_ref_take(conn) == 0);
|
|
if (action == 3) { b->burst_active = 1; b->burst_count = 1; }
|
|
struct queue_waiter_handle waiter = {0};
|
|
queue_set_waiter_defer(conn->input_queue, 0);
|
|
CHECK(queue_waiter_wait(conn->input_queue, &waiter, abort_input_admission, (void*)(uintptr_t)action) == 0);
|
|
struct ETCP_DGRAM* pkt = request();
|
|
CHECK(!waiter.internal && !conn->input_queue->count);
|
|
if (action == 1) CHECK(!pkt && conn->reinit_pending && !conn->unacked_bytes);
|
|
if (action == 2) CHECK(!pkt && conn->state == 2 && conn->unacked_bytes == 100 && conn->input_send_q->count == 1);
|
|
if (action == 3) {
|
|
CHECK(pkt && pkt->link == a && !a->next); memory_pool_free(inst.pkt_pool, pkt);
|
|
pkt = request(); CHECK(pkt && pkt->link == a); memory_pool_free(inst.pkt_pool, pkt); ack_all();
|
|
}
|
|
etcp_conn_ref_free(conn); teardown();
|
|
}
|
|
}
|
|
|
|
/* Поздняя копия и общий till не освобождают окно/не обучают модель другого канала. */
|
|
static void independent_attempt_acks(void) {
|
|
setup(); enable_models(); fill(a, 1, 100);
|
|
struct INFLIGHT_PACKET* packet = (struct INFLIGHT_PACKET*)conn->input_wait_ack->head;
|
|
uint64_t old_id = packet->last_attempt_id;
|
|
conn->initialized = 0; test_clock_run_for(ua, 120); conn->initialized = 1;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == b);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
packet = (struct INFLIGHT_PACKET*)conn->input_wait_ack->head; uint64_t new_id = packet->last_attempt_id;
|
|
CHECK(old_id != new_id && !a->inflight_bytes && b->inflight_bytes == 100);
|
|
etcp_tx_ack(a, packet->seq, old_id, 0, 0, 0);
|
|
CHECK(!conn->unacked_bytes && conn->ack_packets_count == 1 && b->inflight_bytes == 100);
|
|
CHECK(!a->bbr->delivered && !b->bbr->delivered);
|
|
etcp_tx_ack(b, 1, new_id, 0, 0, 0); etcp_tx_ack(b, 1, new_id, 0, 0, 0);
|
|
CHECK(!b->inflight_bytes && b->bbr->delivered == 100 && conn->ack_packets_count == 1);
|
|
check_accounting(); teardown();
|
|
|
|
setup(); enable_models(); fill(a, 1, 100);
|
|
packet = (struct INFLIGHT_PACKET*)conn->input_wait_ack->head; old_id = packet->last_attempt_id;
|
|
receive_till(b, 1, 0);
|
|
CHECK(!conn->unacked_bytes && a->inflight_bytes == 100 && !a->bbr->delivered && !b->bbr->delivered);
|
|
etcp_tx_ack(a, 1, old_id, 0, 0, 0); CHECK(!a->inflight_bytes && a->bbr->delivered == 100);
|
|
teardown();
|
|
|
|
setup(); enable_models(); fill(a, 1, 100);
|
|
packet = (struct INFLIGHT_PACKET*)queue_data_get(conn->input_wait_ack); old_id = packet->last_attempt_id;
|
|
packet->state = INFLIGHT_STATE_WAIT_SEND; CHECK(queue_data_put_with_index(conn->input_send_q, &packet->ll) == 0);
|
|
pkt = request(); CHECK(pkt && pkt->link == b && packet->active_attempts == 2);
|
|
memory_pool_free(inst.pkt_pool, pkt); new_id = packet->last_attempt_id;
|
|
etcp_tx_ack(a, 1, old_id, 0, 0, 0);
|
|
CHECK(!conn->unacked_bytes && !a->inflight_bytes && b->inflight_bytes == 100);
|
|
CHECK(a->bbr->delivered == 100 && !b->bbr->delivered);
|
|
etcp_tx_ack(b, 1, new_id, 0, 0, 0); CHECK(b->bbr->delivered == 100 && conn->ack_packets_count == 1);
|
|
check_accounting(); teardown();
|
|
}
|
|
|
|
/* Посторонний wake не снимает задержку ACK; точные границы проверяются без опроса uasync при ручных часах. */
|
|
static void ack_absolute_deadline(void) {
|
|
setup(); conn->got_initial_pkt = 1;
|
|
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
|
|
receive_fragment(a, 2, 1); CHECK(a->ack_q->count == 1 && conn->ack_resp_timer);
|
|
etcp_conn_wake_sender(conn);
|
|
struct ETCP_DGRAM* pkt = request();
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "ACK deadline: age_us=0 queued=%d sent_early=%u", a->ack_q->count, pkt != NULL);
|
|
CHECK(!pkt && a->ack_q->count == 1);
|
|
ack_now_us += 1999; CHECK(!request() && a->ack_q->count == 1);
|
|
ack_now_us++; pkt = request(); CHECK(pkt && pkt->link == a && pkt->data[1] == 1);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(!a->ack_q->count && !conn->ack_resp_timer);
|
|
teardown();
|
|
}
|
|
|
|
static void ack_piggyback_deadlines(void) {
|
|
setup(); conn->got_initial_pkt = 1;
|
|
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
|
|
uint64_t begin = ack_now_us;
|
|
receive_fragment(a, 2, 1); ack_now_us += 1000; receive_fragment(b, 3, 1);
|
|
CHECK(conn->ack_resp_deadline_us == begin + ETCP_ACK_DELAY_US);
|
|
enqueue(100);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a && pkt->payload_len && pkt->data[1] == 1);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(!a->ack_q->count && b->ack_q->count == 1);
|
|
CHECK(conn->ack_resp_timer && conn->ack_resp_deadline_us == begin + 3000);
|
|
ack_now_us = begin + 2000; CHECK(!request());
|
|
ack_now_us = begin + 2999; CHECK(!request());
|
|
ack_now_us++; pkt = request(); CHECK(pkt && pkt->link == b && !pkt->payload_len && pkt->data[1] == 1);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_resp_timer && !conn->ack_resp_deadline_us);
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
/* Частичный piggyback меняет голову: прежний deadline больше не делает молодой ACK готовым. */
|
|
static void ack_partial_piggyback(void) {
|
|
setup(); conn->got_initial_pkt = 1; b->link_status = 0;
|
|
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
|
|
uint64_t begin = ack_now_us;
|
|
receive_fragment(a, 2, 1); ack_now_us += 500; receive_fragment(a, 3, 1);
|
|
unsigned max_body = a->mtu - WIRE_OVERHEAD(AF_INET);
|
|
enqueue(max_body - ETCP_ACK_BASE_SIZE - ETCP_PAYLOAD_HEADER_SIZE - ETCP_ACK_ENTRY_SIZE - ETCP_ACK_RX_POINT_SIZE);
|
|
ack_now_us = begin + 1999;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->payload_len && pkt->data[1] == 1 && a->ack_q->count == 1);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(conn->ack_resp_deadline_us == begin + 2500);
|
|
timeout_callback_t callback = captured_ack_callback; void* arg = captured_ack_arg;
|
|
ack_now_us = begin + 2000; uasync_cancel_timeout(ua, conn->ack_resp_timer); callback(arg);
|
|
CHECK(conn->ack_resp_timer && conn->ack_resp_deadline_us == begin + 2500 && !request());
|
|
ack_now_us = begin + 2100; receive_fragment(a, 4, 1);
|
|
CHECK(conn->ack_resp_deadline_us == begin + 2500);
|
|
ack_now_us = begin + 2500; pkt = request(); CHECK(pkt && !pkt->payload_len && pkt->data[1] == 2);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(!a->ack_q->count && !conn->ack_resp_timer);
|
|
ack_all(); teardown();
|
|
}
|
|
|
|
/* Новый RX между timer и отложенным sender не запускает для старой головы очередные 2мс. */
|
|
static void ack_timer_interleaving(void) {
|
|
setup(); conn->got_initial_pkt = 1;
|
|
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
|
|
uint64_t begin = ack_now_us;
|
|
receive_fragment(a, 2, 1);
|
|
timeout_callback_t callback = captured_ack_callback; void* arg = captured_ack_arg;
|
|
ack_now_us += ETCP_ACK_DELAY_US;
|
|
uasync_cancel_timeout(ua, conn->ack_resp_timer); callback(arg);
|
|
CHECK(!conn->ack_resp_timer && conn->tx_token);
|
|
receive_fragment(a, 3, 1); CHECK(!conn->ack_resp_timer && conn->tx_token);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] == 2 && !pkt->payload_len);
|
|
const uint8_t* entry = pkt->data + ETCP_ACK_BASE_SIZE;
|
|
uint32_t delay = (uint32_t)entry[6] | ((uint32_t)entry[7] << 8) | ((uint32_t)entry[8] << 16) | ((uint32_t)entry[9] << 24);
|
|
CHECK(delay == ETCP_ACK_DELAY_US && ack_now_us == begin + ETCP_ACK_DELAY_US);
|
|
memory_pool_free(inst.pkt_pool, pkt); teardown();
|
|
}
|
|
|
|
static void ack_urgent_and_oom(void) {
|
|
for (unsigned mode = 0; mode < 2; mode++) {
|
|
setup(); conn->got_initial_pkt = 1;
|
|
ack_now_us = get_time_us(); ack_clock_active = 1;
|
|
fail_ack_timer = mode;
|
|
receive_fragment(a, 2, 1);
|
|
if (!mode) receive_till(a, 0, 1);
|
|
CHECK(a->ack_reply_pending && !fail_ack_timer);
|
|
CHECK(((struct ACK_PACKET*)a->ack_q->head)->received_at_us == ack_now_us);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] == 1 && !pkt->payload_len);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
CHECK(!a->ack_reply_pending && !conn->ack_resp_timer && !conn->ack_resp_deadline_us);
|
|
teardown();
|
|
}
|
|
}
|
|
|
|
static void ack_partial_frame_and_lifecycle(void) {
|
|
setup(); ack_now_us = get_time_us(); ack_clock_active = 1;
|
|
for (unsigned n = 0; n < 200; n++) CHECK(etcp_tx_queue_ack(a, 100+n, etcp_tx_attempt_id(100+n, 1)) == 0);
|
|
ack_now_us += ETCP_ACK_DELAY_US;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] && a->ack_q->count);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_resp_timer);
|
|
pkt = request(); CHECK(pkt && pkt->data[1] && !a->ack_q->count);
|
|
memory_pool_free(inst.pkt_pool, pkt); teardown();
|
|
|
|
setup(); ack_now_us = get_time_us(); ack_clock_active = 1;
|
|
conn->ack_till_deadline_us = ack_now_us + ETCP_ACK_DELAY_US; conn->last_delivered_id = 17;
|
|
CHECK(!request() && conn->ack_resp_timer);
|
|
ack_now_us += ETCP_ACK_DELAY_US;
|
|
pkt = request(); CHECK(pkt && !pkt->data[1] && pkt->data[2] == 17);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_till_deadline_us && !conn->ack_resp_timer);
|
|
CHECK(etcp_tx_queue_ack(a, 99, etcp_tx_attempt_id(99, 1)) == 0); CHECK(!request() && conn->ack_resp_timer);
|
|
etcp_conn_reset(conn); CHECK(!conn->ack_resp_timer && !conn->ack_resp_deadline_us && !conn->ack_till_deadline_us);
|
|
teardown();
|
|
}
|
|
|
|
static void ack_sender_failure(void) {
|
|
setup(); conn->got_initial_pkt = 1; CHECK(etcp_conn_ref_take(conn) == 0);
|
|
ack_now_us = get_time_us(); ack_clock_active = capture_ack_timer = 1;
|
|
receive_fragment(a, 2, 1);
|
|
timeout_callback_t callback = captured_ack_callback; void* arg = captured_ack_arg;
|
|
ack_now_us += ETCP_ACK_DELAY_US; fail_tx_timer = 1;
|
|
uasync_cancel_timeout(ua, conn->ack_resp_timer); callback(arg);
|
|
CHECK(!fail_tx_timer && conn->close_requested && !conn->ack_resp_timer);
|
|
ack_clock_active = 0; uasync_drain_immediate(ua);
|
|
CHECK(conn->state == 2 && !conn->links && !conn->ack_resp_deadline_us && !conn->ack_till_deadline_us);
|
|
etcp_conn_ref_free(conn); teardown();
|
|
}
|
|
|
|
/* DOWN не мешает срокам других путей; UP возвращает старые ACK сразу. Удаление головы не оставляет чужой deadline. */
|
|
static void ack_link_lifecycle(void) {
|
|
for (unsigned remove = 0; remove < 2; remove++) {
|
|
setup(); conn->got_initial_pkt = 1;
|
|
ack_now_us = get_time_us(); ack_clock_active = 1;
|
|
uint64_t begin = ack_now_us;
|
|
receive_fragment(a, 2, 1); ack_now_us += 1000; receive_fragment(b, 3, 1);
|
|
if (remove) { etcp_link_close(a); a = NULL; }
|
|
else a->link_status = 0;
|
|
ack_now_us = begin + 2000; CHECK(!request() && conn->ack_resp_deadline_us == begin + 3000);
|
|
ack_now_us = begin + 3000;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == b && pkt->data[1] == 1);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(!conn->ack_resp_timer);
|
|
if (!remove) {
|
|
a->link_status = 1; etcp_conn_wake_sender(conn);
|
|
pkt = request(); CHECK(pkt && pkt->link == a && pkt->data[1] == 1);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
}
|
|
teardown();
|
|
}
|
|
}
|
|
|
|
/* Готовый DATA другого пути не должен обходить уже наступивший срок ACK. */
|
|
static void ack_due_other_link(void) {
|
|
setup(); conn->got_initial_pkt = 1;
|
|
ack_now_us = get_time_us(); ack_clock_active = 1;
|
|
receive_fragment(b, 2, 1); b->inflight_lim_bytes = 1;
|
|
ack_now_us += ETCP_ACK_DELAY_US;
|
|
struct INFLIGHT_PACKET* fresh = enqueue(100);
|
|
struct ETCP_DGRAM* pkt = request();
|
|
CHECK(pkt && pkt->link == b && !pkt->payload_len && pkt->data[1] == 1 && fresh->state == INFLIGHT_STATE_WAIT_SEND);
|
|
memory_pool_free(inst.pkt_pool, pkt);
|
|
pkt = request(); CHECK(pkt && pkt->link == a && pkt->payload_len == 100);
|
|
memory_pool_free(inst.pkt_pool, pkt); ack_all(); teardown();
|
|
}
|
|
|
|
static void receive_admission_and_path_acks(void) {
|
|
setup(); conn->got_initial_pkt = 1;
|
|
fail_rx_data = 1; receive_fragment(a, 2, 1);
|
|
CHECK(!fail_rx_data && !conn->recv_q->count && !a->ack_q->count && !conn->last_delivered_id);
|
|
CHECK(!a->rx_data_bytes && !a->rx_data_time_us);
|
|
fail_rx_fragment = 1; receive_fragment(a, 2, 2);
|
|
CHECK(!fail_rx_fragment && !conn->recv_q->count && !a->ack_q->count);
|
|
CHECK(!a->rx_data_bytes && !a->rx_data_time_us);
|
|
queue_set_size_limit(conn->recv_q, 0); receive_fragment(a, 2, 3);
|
|
CHECK(!conn->recv_q->count && !a->ack_q->count); queue_set_size_limit(conn->recv_q, -1);
|
|
CHECK(!a->rx_data_bytes && !a->rx_data_time_us);
|
|
receive_fragment(b, 2, 4);
|
|
CHECK(conn->recv_q->count == 1 && b->ack_q->count == 1 && !a->ack_q->count);
|
|
CHECK(b->rx_data_bytes == 1 && b->rx_data_time_us && !a->rx_data_bytes);
|
|
receive_fragment(a, 2, 5);
|
|
CHECK(conn->recv_q->count == 1 && a->ack_q->count == 1 && b->ack_q->count == 1);
|
|
fail_ack_entry = 1; receive_fragment(a, 3, 6);
|
|
CHECK(!fail_ack_entry && conn->recv_q->count == 2 && a->ack_q->count == 1);
|
|
receive_fragment(b, 3, 7); CHECK(conn->recv_q->count == 2 && b->ack_q->count == 2);
|
|
CHECK(a->rx_data_bytes == 2 && b->rx_data_bytes == 2);
|
|
teardown();
|
|
}
|
|
|
|
/* Реальный ACK handler: RX clock независим, граница ограничивает BW и округляется безопасно. */
|
|
static void receiver_bandwidth_bounds(void) {
|
|
const struct { uint32_t delivered, local_us; uint64_t rx_bytes, rx_us; uint32_t expected; } cases[] = {
|
|
{170502,150732,170502,186359,186359}, {1000,10000,3000,40001,13334},
|
|
{1000,20000,5000,10000,0}, {UINT32_MAX,10000,UINT32_MAX,UINT32_MAX,UINT32_MAX},
|
|
{UINT32_MAX,10000,1,UINT32_MAX,0}, {1000,10000,1,(uint64_t)UINT32_MAX+1,0}
|
|
};
|
|
for (unsigned i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) {
|
|
setup(); b->link_status = 0; a->data_rtt_us = 1000000;
|
|
a->bbr = u_calloc(1, sizeof(*a->bbr)); CHECK(a->bbr); bbr_init(a->bbr);
|
|
fill(a,1,1); // Первая неподтверждённая попытка удерживает flight, пока фиксируется база следующей.
|
|
a->peer_rx_data_bytes = UINT64_C(1)<<60;
|
|
a->peer_rx_data_time_us = UINT64_C(1)<<61;
|
|
struct INFLIGHT_PACKET* packet = enqueue(1000);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == a); memory_pool_free(inst.pkt_pool,pkt);
|
|
struct ETCP_TX_ATTEMPT* attempt = last_attempt(packet);
|
|
attempt->delivered_at_send = 10000;
|
|
a->delivered_bytes = 10000 + (uint64_t)cases[i].delivered - 1000;
|
|
a->bbr->delivered = (uint32_t)a->delivered_bytes;
|
|
uint64_t now = get_time_us();
|
|
attempt->delivered_time_us = now-cases[i].local_us;
|
|
attempt->sent_at_us = attempt->first_tx_us = now-5000;
|
|
a->data_rtt_us = 0;
|
|
etcp_tx_ack(a,packet->seq,packet->last_attempt_id,0,
|
|
(UINT64_C(1)<<60)+cases[i].rx_bytes,(UINT64_C(1)<<61)+cases[i].rx_us);
|
|
CHECK(last_sample.delivered == cases[i].delivered && last_sample.acked_sacked == 1000);
|
|
if (i == 2) CHECK(last_sample.interval_us >= cases[i].local_us);
|
|
else CHECK(last_sample.interval_us == cases[i].expected);
|
|
if (last_sample.interval_us) CHECK((uint64_t)last_sample.delivered*cases[i].rx_us <=
|
|
(uint64_t)last_sample.interval_us*cases[i].rx_bytes);
|
|
CHECK(a->bbr->min_rtt_us == last_sample.rtt_us && a->data_rtt_us == last_sample.rtt_us);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Receiver rate bound: case=%u sample=%u/%u RX=%llu/%llu RTT_us=%u",
|
|
i,last_sample.delivered,last_sample.interval_us,(unsigned long long)cases[i].rx_bytes,
|
|
(unsigned long long)cases[i].rx_us,last_sample.rtt_us);
|
|
check_accounting(); teardown();
|
|
}
|
|
}
|
|
|
|
/* Переставленные, повторные и чужие ACK не откатывают точку пути; после простоя база начинается заново. */
|
|
static void receiver_point_lifecycle(void) {
|
|
setup(); b->link_status = 0; a->data_rtt_us = 1000000; enable_models();
|
|
fill(a,1,1);
|
|
a->peer_rx_data_bytes = 10000; a->peer_rx_data_time_us = 1000000;
|
|
struct INFLIGHT_PACKET* packets[3]; uint32_t seq[3]; uint64_t ids[3];
|
|
for (unsigned i = 0; i < 3; i++) {
|
|
packets[i] = enqueue(1000); seq[i] = packets[i]->seq;
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt); memory_pool_free(inst.pkt_pool,pkt);
|
|
ids[i] = packets[i]->last_attempt_id;
|
|
last_attempt(packets[i])->delivered_time_us = get_time_us()-10000;
|
|
}
|
|
const unsigned order[] = {2,0,1};
|
|
for (unsigned j = 0; j < 3; j++) {
|
|
unsigned i = order[j];
|
|
etcp_tx_ack(a,seq[i],ids[i],0,11000+1000*i,1020000+20000*i);
|
|
CHECK(a->peer_rx_data_bytes == 13000 && a->peer_rx_data_time_us == 1060000);
|
|
CHECK(!b->peer_rx_data_bytes && last_sample.interval_us >= (j+1)*20000);
|
|
}
|
|
unsigned calls = bbr_calls;
|
|
etcp_tx_ack(a,seq[2],ids[2],0,UINT64_MAX,UINT64_MAX);
|
|
etcp_tx_ack(a,999,UINT64_MAX,0,UINT64_MAX,UINT64_MAX);
|
|
struct INFLIGHT_PACKET* anchor = (struct INFLIGHT_PACKET*)conn->input_wait_ack->head;
|
|
CHECK(anchor && anchor->seq == 1);
|
|
etcp_tx_ack(a,999,anchor->last_attempt_id,0,UINT64_MAX,UINT64_MAX);
|
|
CHECK(bbr_calls == calls && a->peer_rx_data_bytes == 13000 && a->peer_rx_data_time_us == 1060000);
|
|
const uint64_t invalid[][2] = {{14000,0},{0,1070000},{14000,1050000},{12000,1070000},{13000,1070000}};
|
|
for (unsigned i = 0; i < sizeof(invalid)/sizeof(invalid[0]); i++) {
|
|
struct INFLIGHT_PACKET* packet = enqueue(1000);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt); memory_pool_free(inst.pkt_pool,pkt);
|
|
last_attempt(packet)->delivered_time_us = get_time_us()-10000;
|
|
etcp_tx_ack(a,packet->seq,packet->last_attempt_id,0,invalid[i][0],invalid[i][1]);
|
|
CHECK(a->peer_rx_data_bytes == 13000 && a->peer_rx_data_time_us == 1060000);
|
|
CHECK(last_sample.interval_us >= 10000);
|
|
}
|
|
ack_all(); CHECK(!a->inflight_packets);
|
|
struct INFLIGHT_PACKET* first = enqueue(1000);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt); memory_pool_free(inst.pkt_pool,pkt);
|
|
CHECK(!a->peer_rx_data_time_us && !last_attempt(first)->peer_rx_time_at_send);
|
|
struct INFLIGHT_PACKET* second = enqueue(1000);
|
|
pkt = request(); CHECK(pkt); memory_pool_free(inst.pkt_pool,pkt);
|
|
last_attempt(first)->delivered_time_us = get_time_us()-10000;
|
|
uint64_t after_idle = UINT64_C(3600)*1000000;
|
|
etcp_tx_ack(a,first->seq,first->last_attempt_id,0,14000,after_idle);
|
|
CHECK(last_sample.interval_us >= 10000 && last_sample.interval_us < 1000000);
|
|
struct INFLIGHT_PACKET* third = enqueue(1000);
|
|
pkt = request(); CHECK(pkt); memory_pool_free(inst.pkt_pool,pkt);
|
|
CHECK(last_attempt(third)->peer_rx_time_at_send == after_idle);
|
|
last_attempt(third)->delivered_time_us = get_time_us()-10000;
|
|
etcp_tx_ack(a,third->seq,third->last_attempt_id,0,15000,after_idle+20000);
|
|
CHECK(last_sample.interval_us == 20000);
|
|
CHECK(second->active_attempts == 1);
|
|
a->rx_data_bytes = 55; a->rx_data_time_us = 99;
|
|
etcp_tx_link_reset(a,1);
|
|
CHECK(!a->rx_data_bytes && !a->rx_data_time_us && !a->peer_rx_data_bytes && !a->peer_rx_data_time_us);
|
|
check_accounting(); teardown();
|
|
}
|
|
|
|
/* Точка приёма занимает место только с положительными ACK; проверяем обе стороны границы MTU. */
|
|
static void receiver_point_wire(void) {
|
|
setup(); b->link_status = 0; conn->got_initial_pkt = 1;
|
|
receive_fragment(a,2,101); receive_fragment(a,2,101);
|
|
CHECK(a->rx_data_bytes == 2 && a->ack_q->count == 1 && conn->recv_q->count == 1);
|
|
a->rx_data_bytes = UINT64_C(0xfedcba9876543210); a->rx_data_time_us = UINT64_C(0x0011223344556677);
|
|
const uint8_t expected[] = {0x10,0x32,0x54,0x76,0x98,0xba,0xdc,0xfe,0x77,0x66,0x55,0x44,0x33,0x22,0x11,0x00};
|
|
mature_acks(a);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->data[1] == 1 && !pkt->payload_len);
|
|
CHECK(pkt->data_len == etcp_ack_section_size(1) && !memcmp(pkt->data+ETCP_ACK_BASE_SIZE+ETCP_ACK_ENTRY_SIZE,expected,16));
|
|
memory_pool_free(inst.pkt_pool,pkt);
|
|
receive_till(a,0,1); pkt = request(); CHECK(pkt && !pkt->data[1] && pkt->data_len == ETCP_ACK_BASE_SIZE);
|
|
memory_pool_free(inst.pkt_pool,pkt);
|
|
a->rx_data_bytes = 2; a->rx_data_time_us = get_time_us();
|
|
unsigned max_body = a->mtu-WIRE_OVERHEAD(AF_INET);
|
|
for (unsigned space = ETCP_ACK_ENTRY_SIZE+ETCP_ACK_RX_POINT_SIZE-1; space <= ETCP_ACK_ENTRY_SIZE+ETCP_ACK_RX_POINT_SIZE; space++) {
|
|
receive_fragment(a,3+space,200+space);
|
|
struct INFLIGHT_PACKET* packet = enqueue(max_body-ETCP_ACK_BASE_SIZE-ETCP_PAYLOAD_HEADER_SIZE-space);
|
|
uint32_t seq = packet->seq;
|
|
pkt = request(); CHECK(pkt && pkt->payload_len && pkt->data_len <= max_body);
|
|
if (space == ETCP_ACK_ENTRY_SIZE+ETCP_ACK_RX_POINT_SIZE) CHECK(pkt->data[1] == 1 && pkt->data_len == max_body);
|
|
else CHECK(!pkt->data[1] && a->ack_q->count == 1);
|
|
memory_pool_free(inst.pkt_pool,pkt);
|
|
if (a->ack_q->count) {
|
|
mature_acks(a);
|
|
pkt = request(); CHECK(pkt && pkt->data[1] == 1 && !pkt->payload_len);
|
|
memory_pool_free(inst.pkt_pool,pkt);
|
|
}
|
|
ack(seq);
|
|
}
|
|
for (unsigned i = 100; i < 300; i++) receive_fragment(a,i,1000+i);
|
|
mature_acks(a);
|
|
unsigned count = (max_body-ETCP_ACK_BASE_SIZE-ETCP_ACK_RX_POINT_SIZE)/ETCP_ACK_ENTRY_SIZE;
|
|
pkt = request(); CHECK(pkt && pkt->data[1] == count && pkt->data_len <= max_body && a->ack_q->count == 200-(int)count);
|
|
memory_pool_free(inst.pkt_pool,pkt);
|
|
pkt = request(); CHECK(pkt && pkt->data[1] == 200-count && !a->ack_q->count); memory_pool_free(inst.pkt_pool,pkt);
|
|
teardown();
|
|
}
|
|
|
|
/* Усечённый feedback не подтверждает даже корректный entry, не трогает BW и не читает хвост буфера. */
|
|
static void receiver_point_truncated(void) {
|
|
setup(); b->link_status = 0; enable_models();
|
|
struct INFLIGHT_PACKET* packet = enqueue(1000);
|
|
struct ETCP_DGRAM* sent = request(); CHECK(sent); memory_pool_free(inst.pkt_pool,sent);
|
|
uint32_t seq = packet->seq; uint64_t id = packet->last_attempt_id;
|
|
unsigned calls = bbr_calls, size = etcp_ack_section_size(1);
|
|
for (unsigned len = ETCP_ACK_BASE_SIZE; len <= size; len++) {
|
|
struct ETCP_DGRAM* pkt = memory_pool_alloc(inst.pkt_pool); CHECK(pkt);
|
|
pkt->link = a; pkt->data_len = len; memset(pkt->data,0,size); pkt->data[0] = ETCP_SECTION_ACK; pkt->data[1] = 1;
|
|
for (unsigned i = 0; i < 4; i++) pkt->data[ETCP_ACK_BASE_SIZE+i] = seq >> (8*i);
|
|
for (unsigned i = 0; i < 2; i++) pkt->data[ETCP_ACK_BASE_SIZE+4+i] = id >> (8*i);
|
|
pkt->data[ETCP_ACK_BASE_SIZE+ETCP_ACK_ENTRY_SIZE] = 1;
|
|
pkt->data[ETCP_ACK_BASE_SIZE+ETCP_ACK_ENTRY_SIZE+8] = 1;
|
|
etcp_conn_input(pkt);
|
|
if (len < size) CHECK(a->inflight_bytes == 1000 && conn->unacked_bytes == 1000 && bbr_calls == calls);
|
|
else CHECK(!a->inflight_bytes && !conn->unacked_bytes && bbr_calls == calls+1);
|
|
}
|
|
teardown();
|
|
setup(); conn->got_initial_pkt = 1; a->rx_data_bytes = UINT64_MAX;
|
|
receive_fragment(a,2,1); CHECK(conn->close_requested && !conn->callbacks_running && !a->ack_q->count);
|
|
teardown();
|
|
}
|
|
|
|
static void peer_window_queries(void) {
|
|
setup(); conn->last_delivered_id = 9; conn->last_rr_link = b;
|
|
receive_till(b, 0, 1);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && pkt->link == b && !pkt->payload_len && pkt->data[2] == 9);
|
|
memory_pool_free(inst.pkt_pool, pkt); CHECK(!b->ack_reply_pending && !request()); teardown();
|
|
|
|
setup(); conn->next_tx_id = MAX_INFLIGHT_SIZE + 1;
|
|
struct ETCP_FRAGMENT* fragment = (struct ETCP_FRAGMENT*)queue_entry_new_from_pool(conn->io_pool); CHECK(fragment);
|
|
fragment->ll.len = 1; fragment->ll.dgram = memory_pool_alloc(inst.data_pool); CHECK(fragment->ll.dgram);
|
|
fragment->ll.dgram_pool = inst.data_pool;
|
|
CHECK(queue_data_put(conn->input_queue, &fragment->ll) == 0 && conn->ack_window_timer);
|
|
wait_sends(1); CHECK(last_sent_request && !last_sent_payload && last_sent_link);
|
|
receive_till(last_sent_link, MAX_INFLIGHT_SIZE, 0);
|
|
uasync_drain_immediate(ua); // queue_resume_callback выполняется через call_soon
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Peer window reply: next=%u till=%u query_timer=%d input=%d send=%d callback_pending=%d",
|
|
conn->next_tx_id, conn->rx_ack_till, !!conn->ack_window_timer, conn->input_queue->count,
|
|
conn->input_send_q->count, !!conn->input_queue->callback_suspended);
|
|
CHECK(!conn->ack_window_timer && !conn->input_queue->count && conn->unacked_bytes == 1);
|
|
CHECK(conn->input_send_q->count + conn->input_wait_ack->count == 1);
|
|
teardown();
|
|
}
|
|
|
|
static void normalizer_receive(struct ETCP_CONN* c, struct ll_entry* entry) {
|
|
CHECK(entry->len == 2 && entry->dgram[0] == 253 && received < sizeof(received_values));
|
|
received_values[received++] = entry->dgram[1];
|
|
queue_dgram_free(entry); queue_entry_free(entry);
|
|
if (receive_action == 1) etcp_conn_reinit_id(c, "callback reset", c->reset_id + 1);
|
|
if (receive_action == 2) etcp_connection_close(c);
|
|
}
|
|
|
|
static void receive_messages(uint32_t seq, unsigned count) {
|
|
CHECK(count && count <= 3);
|
|
struct ETCP_DGRAM* pkt = memory_pool_alloc(inst.pkt_pool); CHECK(pkt);
|
|
memset(pkt, 0, sizeof(*pkt)); pkt->link = a; pkt->data_len = ETCP_PAYLOAD_HEADER_SIZE + 4 * count;
|
|
pkt->data[0] = ETCP_SECTION_PAYLOAD;
|
|
for (unsigned i = 0; i < 4; i++) pkt->data[1+i] = seq >> (8*i);
|
|
pkt->data[5] = (uint8_t)seq;
|
|
for (unsigned i = 0; i < count; i++) {
|
|
unsigned offset = ETCP_PAYLOAD_HEADER_SIZE + 4*i;
|
|
pkt->data[offset] = 2; pkt->data[offset+2] = 253; pkt->data[offset+3] = 0x20+i;
|
|
}
|
|
etcp_conn_input(pkt);
|
|
}
|
|
|
|
static void confirm_test_session(void) {
|
|
etcp_session_cancel(conn); conn->session_required = 0; etcp_conn_ready(conn);
|
|
}
|
|
|
|
static void normalizer_rx_oom(void) {
|
|
for (unsigned nth = 1; nth <= 2; nth++) {
|
|
setup(); CHECK(etcp_bind(&inst, 253, normalizer_receive) == 0);
|
|
uint64_t epoch = conn->reset_id;
|
|
fail_rx_message_at = nth; receive_messages(1, 3);
|
|
CHECK(!fail_rx_message_at && received == nth-1 && conn->reset_id != epoch);
|
|
CHECK(conn->reinit_pending && conn->session_required && conn->session_timer && !conn->initialized);
|
|
CHECK(!conn->last_delivered_id && !conn->recv_q->count && !conn->output_queue->count && !a->ack_q->count);
|
|
CHECK(!conn->normalizer->recvpart && !conn->normalizer->output->count && !conn->active_attempts);
|
|
CHECK(a->link_status && b->link_status && conn->links_up);
|
|
confirm_test_session(); receive_messages(1, 1);
|
|
CHECK(received == nth && received_values[nth-1] == 0x20 && !conn->reinit_pending);
|
|
teardown();
|
|
}
|
|
}
|
|
|
|
static void normalizer_tx_oom(void) {
|
|
for (unsigned fault = 0; fault < 6; fault++) {
|
|
setup(); uint64_t epoch = conn->reset_id;
|
|
struct ll_entry* entry = ll_alloc_lldgram(64); CHECK(entry); entry->len = 64; memset(entry->dgram, 253, 64);
|
|
if (fault == 0) fail_rx_data = 1;
|
|
if (fault == 1) fail_rx_fragment = 1;
|
|
if (fault == 2) fail_tx_inflight = 1;
|
|
if (fault == 3) fail_normalizer_timer = 1;
|
|
if (fault == 4) {
|
|
conn->input_queue->callback_suspended = 1;
|
|
struct ll_entry* busy = ll_alloc_lldgram(1); CHECK(busy); busy->len = 1;
|
|
CHECK(queue_data_put(conn->input_queue, busy) == 0); fail_normalizer_waiter = 1;
|
|
}
|
|
if (fault == 5) queue_set_size_limit(conn->input_queue, 0);
|
|
CHECK(etcp_send(conn, entry) == 0); uasync_drain_immediate(ua);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "Normalizer TX fault=%u reset=%u pending=%u timer=%d DATA_sends=%u",
|
|
fault, conn->reset_count, conn->reinit_pending, !!conn->session_timer, sends);
|
|
CHECK(conn->reset_id != epoch && conn->reinit_pending && conn->session_timer);
|
|
CHECK(!conn->unacked_bytes && !conn->active_attempts && !conn->input_queue->count && !sends);
|
|
CHECK(!conn->normalizer->input->count && !conn->normalizer->data && !conn->normalizer->flush_timer);
|
|
CHECK(!fail_rx_data && !fail_rx_fragment && !fail_tx_inflight && !fail_normalizer_timer && !fail_normalizer_waiter);
|
|
queue_set_size_limit(conn->input_queue, -1); confirm_test_session();
|
|
entry = ll_alloc_lldgram(16); CHECK(entry); entry->len = 16; memset(entry->dgram, 253, 16);
|
|
CHECK(etcp_send(conn, entry) == 0); wait_sends(1); ack_all(); teardown();
|
|
}
|
|
}
|
|
|
|
static void normalizer_callback_abort(void) {
|
|
for (unsigned deferred = 0; deferred < 2; deferred++) for (unsigned action = 1; action <= 2; action++) {
|
|
setup(); CHECK(etcp_conn_ref_take(conn) == 0); CHECK(etcp_bind(&inst, 253, normalizer_receive) == 0);
|
|
queue_set_callback_defer(conn->normalizer->output, deferred); receive_action = action;
|
|
receive_messages(1, 2); uasync_drain_immediate(ua);
|
|
CHECK(received == 1 && received_values[0] == 0x20 && !conn->callbacks_running);
|
|
if (action == 1) CHECK(conn->reinit_pending && !conn->last_delivered_id && !conn->normalizer->output->count);
|
|
else CHECK(conn->state == 2 && !conn->normalizer && !conn->links);
|
|
etcp_conn_ref_free(conn); teardown();
|
|
}
|
|
}
|
|
|
|
static void ignored_queue_callback(struct ll_queue* q, void* arg) { (void)q; ++*(unsigned*)arg; }
|
|
|
|
static void reset_without_allocation(void) {
|
|
setup(); fill(a, 1, 64); unsigned notifications = 0;
|
|
struct ll_queue* queues[] = {conn->input_queue, conn->output_queue, conn->recv_q};
|
|
for (unsigned i = 0; i < 3; i++) {
|
|
queue_set_callback(queues[i], NULL, NULL);
|
|
struct ETCP_FRAGMENT* frag = (struct ETCP_FRAGMENT*)queue_entry_new_from_pool(conn->io_pool); CHECK(frag);
|
|
frag->seq = 4+i; frag->ll.len = 8; frag->ll.dgram = memory_pool_alloc(inst.data_pool); CHECK(frag->ll.dgram);
|
|
frag->ll.dgram_pool = inst.data_pool;
|
|
CHECK((i == 2 ? queue_data_put_with_index(queues[i], &frag->ll) : queue_data_put(queues[i], &frag->ll)) == 0);
|
|
}
|
|
struct PKTNORM* pn = conn->normalizer;
|
|
queue_set_callback(pn->input, NULL, NULL); queue_set_callback(pn->output, NULL, NULL);
|
|
CHECK(queue_data_put(pn->input, ll_alloc_lldgram(10)) == 0);
|
|
CHECK(queue_data_put(pn->output, ll_alloc_lldgram(12)) == 0);
|
|
CHECK(queue_waiter_wait(conn->input_queue, &pn->input_waiter_handle, ignored_queue_callback, ¬ifications) == 0);
|
|
queue_set_empty_callback(pn->input, ignored_queue_callback, ¬ifications);
|
|
queue_set_on_get(pn->input, ignored_queue_callback, ¬ifications);
|
|
pn->data = memory_pool_alloc(inst.data_pool); CHECK(pn->data); pn->data_ptr = 20;
|
|
pn->recvpart = ll_alloc_lldgram(32); CHECK(pn->recvpart); pn->recvpart->len = 3;
|
|
no_alloc = 1; etcp_conn_reset(conn); no_alloc = 0;
|
|
CHECK(!unexpected_alloc && !conn->active_attempts && !conn->unacked_bytes && !conn->input_wait_ack->count);
|
|
CHECK(!pn->data && !pn->recvpart && !pn->input->count && !pn->output->count && !pn->input_waiter_handle.internal);
|
|
uasync_drain_immediate(ua); CHECK(!notifications); teardown();
|
|
}
|
|
|
|
static void restart_allocation_failures(void) {
|
|
for (unsigned fault = 0; fault < 4; fault++) {
|
|
setup(); CHECK(etcp_conn_ref_take(conn) == 0);
|
|
if (fault < 2) fail_session_timer_at = fault + 1;
|
|
if (fault == 2) fail_rng = 1;
|
|
etcp_conn_fatal_reinit(conn, "restart fault injection");
|
|
if (fault == 1) test_clock_next_timer(ua);
|
|
if (fault == 3) {
|
|
uint64_t epoch = conn->reset_id; fail_alloc = 1;
|
|
test_clock_next_timer(ua); CHECK(!fail_alloc && conn->session_timer && !session_sends);
|
|
test_clock_next_timer(ua); CHECK(session_sends == 2 && conn->reset_id == epoch && !conn->close_requested);
|
|
} else {
|
|
CHECK(conn->close_requested && !fail_session_timer_at && !fail_rng);
|
|
uasync_drain_immediate(ua); CHECK(conn->state == 2 && !conn->session_timer);
|
|
}
|
|
etcp_conn_ref_free(conn); teardown();
|
|
}
|
|
}
|
|
|
|
/* Проходит настоящий wire decoder: номер может совпадать у разных seq, нулевой запрещён. */
|
|
static void receive_attempt_number(struct ETCP_LINK* link, uint32_t seq, uint16_t number) {
|
|
struct ETCP_DGRAM* pkt = memory_pool_alloc(inst.pkt_pool); CHECK(pkt);
|
|
pkt->link = link; pkt->data_len = etcp_ack_section_size(1);
|
|
memset(pkt->data,0,pkt->data_len); pkt->data[0] = ETCP_SECTION_ACK; pkt->data[1] = 1;
|
|
for (unsigned i = 0; i < 4; i++) pkt->data[ETCP_ACK_BASE_SIZE+i] = seq >> (8*i);
|
|
pkt->data[ETCP_ACK_BASE_SIZE+4] = number; pkt->data[ETCP_ACK_BASE_SIZE+5] = number >> 8;
|
|
etcp_conn_input(pkt); check_accounting();
|
|
}
|
|
|
|
static void compact_attempt_identity(void) {
|
|
setup(); enable_models(); fill(a,2,100); b->link_status = 0;
|
|
struct INFLIGHT_PACKET* first = (struct INFLIGHT_PACKET*)conn->input_wait_ack->head;
|
|
struct INFLIGHT_PACKET* second = (struct INFLIGHT_PACKET*)first->ll.next;
|
|
CHECK(first && second && first->seq == 1 && second->seq == 2);
|
|
CHECK(first->last_attempt_id == etcp_tx_attempt_id(1,1) && second->last_attempt_id == etcp_tx_attempt_id(2,1));
|
|
CHECK(last_attempt(first)->seq == 1 && last_attempt(second)->seq == 2 && a->attempts->count == 2);
|
|
conn->got_initial_pkt = 1;
|
|
receive_fragment(a,2,1); receive_fragment(a,3,1); receive_fragment(a,2,1);
|
|
CHECK(a->ack_q->count == 2 && a->rx_data_bytes == 3);
|
|
mature_acks(a);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt && !pkt->payload_len && pkt->data[1] == 2);
|
|
const uint8_t entry1[] = {2,0,0,0,1,0}, entry2[] = {3,0,0,0,1,0};
|
|
CHECK(!memcmp(pkt->data+ETCP_ACK_BASE_SIZE,entry1,sizeof(entry1)));
|
|
CHECK(!memcmp(pkt->data+ETCP_ACK_BASE_SIZE+ETCP_ACK_ENTRY_SIZE,entry2,sizeof(entry2)));
|
|
CHECK(pkt->data_len == etcp_ack_section_size(2)); memory_pool_free(inst.pkt_pool,pkt);
|
|
receive_attempt_number(a,1,0); CHECK(a->inflight_bytes == 200 && conn->unacked_bytes == 200 && !a->bbr->delivered);
|
|
receive_attempt_number(b,1,1); CHECK(a->inflight_bytes == 200 && conn->input_wait_ack->count == 1);
|
|
CHECK(conn->unacked_bytes == 100 && !a->bbr->delivered && !b->bbr->delivered);
|
|
receive_attempt_number(a,1,1); CHECK(a->inflight_bytes == 100 && a->bbr->delivered == 100 && !b->bbr->delivered);
|
|
receive_attempt_number(a,1,1); CHECK(a->inflight_bytes == 100 && a->bbr->delivered == 100);
|
|
receive_attempt_number(a,2,1); CHECK(!a->inflight_bytes && !conn->unacked_bytes && a->bbr->delivered == 200);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG,"Compact attempts: same number on different seq stays isolated; zero/foreign/duplicate ACK checked");
|
|
teardown();
|
|
}
|
|
|
|
/* Номер расходуется даже при отказе send; 65535 передаётся полностью, следующая попытка меняет эпоху. */
|
|
static void compact_attempt_exhaustion(void) {
|
|
setup(); b->link_status = 0;
|
|
struct INFLIGHT_PACKET* packet = enqueue(1);
|
|
struct ETCP_DGRAM* pkt = etcp_request_pkt(conn); CHECK(pkt && pkt->attempt);
|
|
CHECK(pkt->attempt->attempt_id == etcp_tx_attempt_id(1,1));
|
|
etcp_tx_complete(pkt,0); memory_pool_free(inst.pkt_pool,pkt);
|
|
CHECK(packet->attempt_count == 1 && !packet->last_attempt_id && !packet->send_count && !a->inflight_packets);
|
|
pkt = request(); CHECK(pkt && packet->attempt_count == 2 && packet->last_attempt_id == etcp_tx_attempt_id(1,2));
|
|
memory_pool_free(inst.pkt_pool,pkt); ack_all();
|
|
packet = enqueue(1); packet->attempt_count = UINT16_MAX-1;
|
|
pkt = request(); CHECK(pkt && packet->attempt_count == UINT16_MAX);
|
|
const uint8_t last_data[] = {ETCP_SECTION_PAYLOAD,2,0,0,0,255,255,0x5a};
|
|
CHECK(pkt->data_len == ETCP_ACK_BASE_SIZE+sizeof(last_data));
|
|
CHECK(!memcmp(pkt->data+ETCP_ACK_BASE_SIZE,last_data,sizeof(last_data)));
|
|
CHECK(packet->last_attempt_id == etcp_tx_attempt_id(2,UINT16_MAX)); memory_pool_free(inst.pkt_pool,pkt);
|
|
conn->initialized = 0; test_clock_run_for(ua,120); conn->initialized = 1;
|
|
CHECK(!a->inflight_packets && packet->state == INFLIGHT_STATE_WAIT_SEND);
|
|
uint64_t epoch = conn->reset_id;
|
|
CHECK(!request() && conn->reset_id != epoch && conn->reinit_pending && conn->session_required);
|
|
CHECK(!conn->active_attempts && !conn->unacked_bytes && !conn->input_send_q->count && !conn->input_wait_ack->count);
|
|
confirm_test_session(); packet = enqueue(1); pkt = request(); CHECK(pkt);
|
|
CHECK(packet->seq == 1 && packet->attempt_count == 1 && packet->last_attempt_id == etcp_tx_attempt_id(1,1));
|
|
memory_pool_free(inst.pkt_pool,pkt); ack_all(); teardown();
|
|
}
|
|
|
|
/* Полный круг seq также не должен повторять пару в прежней эпохе, даже с первым номером попытки. */
|
|
static void compact_sequence_exhaustion(void) {
|
|
setup(); b->link_status = 0;
|
|
conn->next_tx_id = UINT32_MAX; conn->rx_ack_till = UINT32_MAX-1;
|
|
const uint8_t normalized[] = {1,0,253};
|
|
CHECK(etcp_int_send(conn,normalized,sizeof(normalized)) == 0 && !conn->next_tx_id);
|
|
struct ETCP_DGRAM* pkt = request(); CHECK(pkt);
|
|
CHECK(pkt->attempt == NULL && a->attempts->count == 1);
|
|
struct ETCP_TX_ATTEMPT* attempt = (struct ETCP_TX_ATTEMPT*)a->attempts->head;
|
|
CHECK(attempt->seq == UINT32_MAX && attempt->attempt_id == etcp_tx_attempt_id(UINT32_MAX,1));
|
|
memory_pool_free(inst.pkt_pool,pkt);
|
|
uint64_t epoch = conn->reset_id;
|
|
CHECK(etcp_int_send(conn,normalized,sizeof(normalized)) == 0);
|
|
CHECK(conn->input_queue->count == 1); a->burst_active = 1;
|
|
CHECK(!request()); // input_queue callback выполняется при следующем проходе отправителя.
|
|
CHECK(conn->reset_id != epoch && conn->reinit_pending && conn->session_required && conn->next_tx_id == 1);
|
|
CHECK(!conn->active_attempts && !conn->unacked_bytes && !conn->input_queue->count);
|
|
confirm_test_session(); teardown();
|
|
}
|
|
|
|
int main(void) {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
|
|
if (getenv("UTUN_TEST_DEBUG")) debug_set_category_level(DEBUG_CATEGORY_ETCP, DEBUG_LEVEL_DEBUG);
|
|
test_clock_init(10);
|
|
size_t baseline = u_get_allocated_count();
|
|
void (*cases[])(void) = {selection, boundaries_ack_burst, late_ack_and_migration, lost_ack_and_shrink,
|
|
grow_and_session, allocation_and_send_failure, removed_link_and_bbr, pacing_reset_close, initial_readiness,
|
|
config_limits, timer_failure, immediate_send, ack_without_data_pacing, bbr_delivery_clock, bbr_probe_window, tcp_initial_window,
|
|
retransmission_timeout, pacing_rate_changes, pacing_timer_failure, retry_rotation, independent_attempt_acks,
|
|
retry_waits_for_alternative, retry_alternative_lifecycle,
|
|
send_queue_scan, send_queue_input_admission,
|
|
retry_full_round_history, retry_history_slot_reuse, fast_stall_progress, stalled_retry_selection, input_admission_callbacks,
|
|
ack_absolute_deadline, ack_piggyback_deadlines, ack_partial_piggyback, ack_timer_interleaving,
|
|
ack_urgent_and_oom, ack_partial_frame_and_lifecycle, ack_sender_failure, ack_link_lifecycle, ack_due_other_link,
|
|
receive_admission_and_path_acks, peer_window_queries, pacing_continuity, pacing_data_units, attempt_memory_limits,
|
|
normalizer_rx_oom, normalizer_tx_oom, normalizer_callback_abort, reset_without_allocation, restart_allocation_failures,
|
|
receiver_bandwidth_bounds, receiver_point_lifecycle, receiver_point_wire, receiver_point_truncated,
|
|
compact_attempt_identity, compact_attempt_exhaustion, compact_sequence_exhaustion,
|
|
pacing_first_ready, pacing_late_wakeup, pacing_rearm_failure, pacing_ready_lifecycle};
|
|
for (unsigned i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
|
cases[i](); CHECK(u_get_allocated_count() == baseline);
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "PASS ETCP window scenario %u", i);
|
|
}
|
|
test_clock_finish();
|
|
return 0;
|
|
}
|