Cover TCP timer replacement failures and pending FIN cleanup

This commit is contained in:
evgeny committed 2026-10-21 16:45:53 +02:00
1 parent d467d3aa8f
commit e5fafacf99
2 files changed
+150 -3

No files matched your search

+3
View File
@@ -536,6 +536,9 @@ test_intensive_memory_pool_LDADD = $(COMMON_LIBS)
test_tcp_io_SOURCES = test_tcp_io.c
test_tcp_io_CFLAGS = -I$(top_srcdir)/lib
test_tcp_io_LDADD = $(COMMON_LIBS)
if OS_LINUX
test_tcp_io_LDFLAGS = -Wl,--wrap=uasync_set_timeout -Wl,--wrap=shutdown
endif
test_uasync_socket_race_SOURCES = test_uasync_socket_race.c
test_uasync_socket_race_CFLAGS = -I$(top_srcdir)/lib
+147 -3
View File
@@ -4,6 +4,7 @@
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <limits.h>
#include "../lib/platform_compat.h"
#include "../lib/u_async.h"
#include "../lib/tcp_io.h"
@@ -16,6 +17,33 @@
#ifndef _WIN32
// POSIX-сценарии не компилируются на Windows, где main уже сообщает SKIP.
#ifdef __linux__
static int fail_connect_timer;
static int fail_write_shutdown;
void* __real_uasync_set_timeout(struct UASYNC*, int, void*, timeout_callback_t, const char*);
int __real_shutdown(int, int);
/* Отказ ровно одной операции: проверяем состояние владельца после ENOMEM/ошибки shutdown. */
void* __wrap_uasync_set_timeout(struct UASYNC* ua, int timeout_tb, void* arg, timeout_callback_t cb, const char* name) {
if (fail_connect_timer && name && !strcmp(name, "tcp_conn_ct")) {
fail_connect_timer = 0;
DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "test: injected connect timer allocation failure");
return NULL;
}
return __real_uasync_set_timeout(ua, timeout_tb, arg, cb, name);
}
int __wrap_shutdown(int fd, int how) {
if (fail_write_shutdown && how == SHUT_WR) {
fail_write_shutdown = 0;
errno = ENOTCONN;
DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "test: injected shutdown failure fd=%d", fd);
return -1;
}
return __real_shutdown(fd, how);
}
#endif
static int tests_run = 0;
static int tests_passed = 0;
static int tests_failed = 0;
@@ -66,6 +94,13 @@ static void reset_counters(void) {
g_last_event = 0;
}
/* Отменённые записи могут ещё находиться в heap; учитываем только действующие таймеры. */
static size_t active_timers(struct UASYNC* ua) {
size_t count = 0;
for (size_t i = 0; i < ua->timeout_heap->size; i++) if (!ua->timeout_heap->heap[i].deleted) count++;
return count;
}
static int push_write(struct tcp_conn* tc, const uint8_t* data, size_t len) {
size_t offset = 0;
while (offset < len) {
@@ -273,6 +308,7 @@ static void test_connect_detection(void) {
struct tcp_conn* tc = tcp_conn_create(ua, client_fd, 1500, 8192, 32, 8, 0, on_fin, on_error, NULL);
ASSERT_TRUE(tc != NULL, "tcp_conn_create failed");
ASSERT_EQ(tc->connected, 0, "should not be connected yet");
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 60000), 0, "arm connect timer");
int ret = connect(client_fd, (struct sockaddr*)&addr, sizeof(addr));
ASSERT_TRUE(ret < 0 && errno == EINPROGRESS, "connect should return EINPROGRESS");
@@ -282,6 +318,10 @@ static void test_connect_detection(void) {
uasync_poll(ua, 100);
ASSERT_EQ(tc->connected, 1, "connect not detected");
ASSERT_TRUE(tc->connect_timer == NULL, "successful connect left a timer");
ASSERT_EQ(active_timers(ua), 0, "successful connect left an active timer");
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 100), 0, "set timeout after connect");
ASSERT_TRUE(tc->connect_timer == NULL, "connected socket must not arm a connect timer");
close(server_fd);
tcp_conn_destroy(tc);
@@ -350,7 +390,7 @@ static void test_error_callback(void) {
}
static void test_connect_timeout(void) {
TEST_START("Connect timeout on black hole (200ms)");
TEST_START("Connect timer replacement, cancellation, errors and expiry");
#ifdef _WIN32
TEST_PASS();
return;
@@ -390,7 +430,36 @@ static void test_connect_timeout(void) {
ASSERT_TRUE(tc != NULL, "tcp_conn_create failed");
ASSERT_EQ(tc->connected, 0, "should not be connected");
tcp_conn_set_connect_timeout(tc, 200);
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 10), 0, "first connect timeout");
void* first = tc->connect_timer;
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 60000), 0, "replace connect timeout");
void* replacement = tc->connect_timer;
ASSERT_TRUE(replacement && replacement != first, "connect timer not replaced");
ASSERT_EQ(active_timers(ua), 1, "replacement left multiple active timers");
uint64_t old_deadline = get_time_tb() + 400;
while (get_time_tb() < old_deadline) uasync_poll(ua, 10);
ASSERT_EQ(g_error_count, 0, "old connect timer fired after replacement");
ASSERT_TRUE(tc->connect_timer == replacement, "old timer cleared replacement handle");
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, INT_MAX), -EINVAL, "overflowing timeout accepted");
ASSERT_TRUE(tc->connect_timer == replacement, "invalid timeout changed active timer");
ASSERT_EQ(tc->connect_timeout_ms, 60000, "invalid timeout changed setting");
#ifdef __linux__
fail_connect_timer = 1;
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 200), -ENOMEM, "allocation failure not returned");
ASSERT_EQ(fail_connect_timer, 0, "allocation failure not injected");
ASSERT_TRUE(tc->connect_timer == replacement, "allocation failure lost old timer");
ASSERT_EQ(tc->connect_timeout_ms, 60000, "allocation failure changed timeout");
ASSERT_EQ(active_timers(ua), 1, "allocation failure left wrong active timer count");
ASSERT_EQ(g_error_count, 0, "setter called on_error synchronously");
#endif
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 0), 0, "disable connect timer");
ASSERT_TRUE(tc->connect_timer == NULL, "disabled timer still has a handle");
ASSERT_EQ(active_timers(ua), 0, "zero timeout left an active timer");
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, INT_MAX / 10), 0, "maximum valid timeout rejected");
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, -1), 0, "negative timeout must disable timer");
ASSERT_TRUE(tc->connect_timer == NULL, "negative timeout left a timer");
ASSERT_EQ(tc->connect_timeout_ms, 0, "negative timeout not normalized");
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 200), 0, "final connect timeout");
uint64_t deadline = get_time_tb() + 10000;
while (g_error_count == 0 && get_time_tb() < deadline) uasync_poll(ua, 10);
@@ -398,6 +467,8 @@ static void test_connect_timeout(void) {
ASSERT_EQ(g_last_error, ETIMEDOUT, "on_error should report ETIMEDOUT");
ASSERT_EQ(tc->connected, 0, "connection should still be not connected");
ASSERT_EQ(tc->error, 1, "tc->error should be set");
ASSERT_TRUE(tc->connect_timer == NULL, "expired timer handle not cleared before on_error");
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 100), -EINVAL, "errored socket accepted a new timer");
tcp_conn_destroy(tc);
for (int i = 0; i < filled; i++) close(fillers[i]);
@@ -409,6 +480,39 @@ static void test_connect_timeout(void) {
#endif
}
/* После отложенного free прокручиваем event loop за сроки всех отменённых таймеров. */
static void test_connect_timer_destroy(void) {
TEST_START("Destroy after rearming or disabling connect timer");
for (int disable = 0; disable < 2; disable++) {
size_t allocated = u_get_allocated_count();
uasync_t* ua = uasync_create();
ASSERT_TRUE(ua != NULL, "uasync_create failed");
reset_counters();
int fd = socket(AF_INET, SOCK_STREAM, 0);
ASSERT_TRUE(fd >= 0, "socket failed");
ASSERT_EQ(socket_set_nonblocking(fd), 0, "nonblocking failed");
struct tcp_conn* tc = tcp_conn_create(ua, fd, 1500, 8192, 32, 8, 0, on_fin, on_error, NULL);
ASSERT_TRUE(tc != NULL, "tcp_conn_create failed");
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 1), 0, "first timeout");
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 2), 0, "replacement timeout");
if (disable) ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 0), 0, "disable timeout");
ASSERT_EQ(tcp_conn_push_fin(tc), 0, "queue FIN before destroy");
ASSERT_EQ(tc->fin_pending, 1, "unconnected socket must keep FIN pending");
tcp_conn_destroy(tc);
ASSERT_TRUE(tc->connect_timer == NULL, "destroy left timer handle");
ASSERT_EQ(tcp_conn_set_connect_timeout(tc, 1), -EINVAL, "destroyed socket accepted a new timer");
uasync_drain_immediate(ua);
uint64_t deadline = get_time_tb() + 100;
while (get_time_tb() < deadline) uasync_poll(ua, 10);
ASSERT_EQ(g_error_count, 0, "cancelled timer called on_error after destroy");
ASSERT_EQ(active_timers(ua), 0, "destroy left active timers");
ASSERT_EQ(ua->timer_alloc_count, ua->timer_free_count, "timer allocations leaked");
uasync_destroy(ua, 0);
ASSERT_EQ(u_get_allocated_count(), allocated, "destroy leaked memory");
}
TEST_PASS();
}
// ====================================================================
// FIN / Close через очередь — новые тесты
// ====================================================================
@@ -552,7 +656,7 @@ static void test_fin_data_ordering(void) {
}
static void test_double_push_fin(void) {
TEST_START("Double push_fin ignored");
TEST_START("Double push_fin rejected while pending and after shutdown");
uasync_t* ua = uasync_create();
ASSERT_TRUE(ua != NULL, "uasync_create failed");
@@ -566,11 +670,18 @@ static void test_double_push_fin(void) {
ASSERT_TRUE(tc != NULL, "tcp_conn_create failed");
tc->on_fin_sent = on_fin_sent_cb;
queue_set_callback_defer(tc->write_queue, 1);
ASSERT_EQ(tcp_conn_push_fin(tc), 0, "first push_fin");
ASSERT_EQ(tc->fin_pending, 1, "FIN should still be queued");
ASSERT_EQ(tc->fin_local, 0, "FIN marked sent before shutdown");
ASSERT_EQ(tcp_conn_push_fin(tc), -1, "second push_fin should return -1");
ASSERT_EQ(tc->write_queue->count, 1, "duplicate FIN entered queue");
uasync_poll(ua, 10);
ASSERT_EQ(g_fin_sent_count, 1, "on_fin_sent called more than once");
ASSERT_EQ(tc->fin_pending, 0, "FIN pending flag not cleared after shutdown");
ASSERT_EQ(tc->fin_local, 1, "successful shutdown not recorded");
ASSERT_EQ(tcp_conn_push_fin(tc), -1, "FIN accepted after shutdown");
tcp_conn_destroy(tc);
close(sv[1]);
@@ -580,6 +691,35 @@ static void test_double_push_fin(void) {
TEST_PASS();
}
#ifdef __linux__
static void test_fin_shutdown_error(void) {
TEST_START("Failed FIN shutdown does not report on_fin_sent");
uasync_t* ua = uasync_create();
ASSERT_TRUE(ua != NULL, "uasync_create failed");
reset_counters();
int sv[2];
ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0, "socketpair failed");
for (int i = 0; i < 2; i++) ASSERT_EQ(socket_set_nonblocking(sv[i]), 0, "nonblocking failed");
struct tcp_conn* tc = tcp_conn_create(ua, sv[0], 1500, 8192, 32, 8, 0, on_fin, on_error, NULL);
ASSERT_TRUE(tc != NULL, "tcp_conn_create failed");
tc->on_fin_sent = on_fin_sent_cb;
fail_write_shutdown = 1;
ASSERT_EQ(tcp_conn_push_fin(tc), 0, "FIN sentinel not accepted");
ASSERT_EQ(fail_write_shutdown, 0, "shutdown failure not injected");
ASSERT_EQ(g_fin_sent_count, 0, "failed shutdown reported FIN sent");
ASSERT_EQ(tc->fin_local, 0, "failed shutdown set fin_local");
ASSERT_EQ(tc->fin_pending, 0, "failed shutdown left FIN pending");
ASSERT_EQ(g_error_count, 1, "failed shutdown did not call on_error");
ASSERT_EQ(g_last_error, ENOTCONN, "wrong shutdown error");
tcp_conn_destroy(tc);
close(sv[1]);
uasync_drain_immediate(ua);
ASSERT_EQ(ua->timer_alloc_count, ua->timer_free_count, "timer allocations leaked");
uasync_destroy(ua, 0);
TEST_PASS();
}
#endif
static void test_double_push_close(void) {
TEST_START("Double push_close ignored");
@@ -673,10 +813,14 @@ int main(void) {
test_connect_detection();
test_error_callback();
test_connect_timeout();
test_connect_timer_destroy();
test_push_fin();
test_push_close();
test_fin_data_ordering();
test_double_push_fin();
#ifdef __linux__
test_fin_shutdown_error();
#endif
test_double_push_close();
test_fin_before_close();