Cover TCP timer replacement failures and pending FIN cleanup
This commit is contained in:
1 parent
d467d3aa8f
commit
e5fafacf99
2 files changed
+150
-3
No files matched your search
@@ -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
@@ -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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user