/* * test_netio_arm_failure.c — libxce-failover: a poll-arm failure must fail LOUD. * * Root cause (bug-646): endpoint_update_epoll() swallowed iomgr_update_poll() * (EPOLL_CTL_MOD) failures — it logged "rc1=-1 < 0" but returned 0 and left the * poll interest unarmed. On the synchronous client send path (endpoint_enq), * a failed arm meant the request bytes were never flushed, no reply ever * arrived, and the caller blocked forever in cbuf_get() while a perfectly * healthy io_worker slept in epoll_wait(). (EPOLL_CTL_MOD fails with ENOENT * when the fd is no longer registered — e.g. after a failed reconnect.) * * Contract under test: when the poll interest cannot be armed, endpoint_enq() * must (1) return a negative, RETRIABLE error so the caller propagates it * (request.c senders return it; the client failover loop rotates — bug-650), * and (2) must NOT synchronously invoke the endpoint's err_handler. endpoint_enq * runs on the caller's thread, which may already hold a lock the err_handler * needs (the controller's primary_heartbeat_fn holds ha->lock and * vote_ep_err_handler re-acquires it → self-deadlock, bug-652). The real * teardown stays on the io_worker thread, off the next epoll error event. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include "netio.h" /* ── err_handler invocation tracker ──────────────────────────────── */ static int g_err_count = 0; static int err_handler_stub(void *ep) { (void)ep; g_err_count++; return 0; } /* * Arming EPOLL_CTL_MOD on an fd that was never EPOLL_CTL_ADD'd fails with * ENOENT — the exact post-failed-reconnect condition. endpoint_enq() must * surface it (negative rc) WITHOUT calling the err_handler (no re-entrancy). */ static void test_enq_arm_failure_is_loud_no_errhandler(void) { iomgr_t iomgr; memset(&iomgr, 0, sizeof(iomgr)); TEST_ASSERT_EQUAL_INT(0, iomgr_init(&iomgr)); endpoint_t *ep = endpoint_get(); TEST_ASSERT_NOT_NULL(ep); /* A real socket fd that is NOT registered in iomgr's epoll: EPOLL_CTL_MOD * on it fails with ENOENT, mirroring a send after a failed reconnect. */ ep->fd = socket(AF_INET, SOCK_STREAM, 0); TEST_ASSERT_TRUE(ep->fd >= 0); ep->iomgr = &iomgr; ep->ssl_handshake_done = 1; /* past handshake: calc_flags arms EPOLLIN */ ep->flags = 0; /* force flags != calc_flags → iomgr_update_poll */ endpoint_set_err_handler(ep, (err_handler_t)err_handler_stub); g_err_count = 0; /* One packet on the outq so there is real outbound work to strand. The * endpoint owns it after enq; endpoint_fini drains it via outq_item_killer. */ io_packet_t *pkt = (io_packet_t *)calloc(1, sizeof(*pkt)); TEST_ASSERT_NOT_NULL(pkt); int rc = endpoint_enq(ep, &pkt->link); /* Loud failure (negative, retriable) so the caller propagates and rotates — * but the err_handler must NOT be invoked from this synchronous send path * (re-entrancy would self-deadlock the controller, bug-652). */ TEST_ASSERT_TRUE(rc < 0); TEST_ASSERT_EQUAL_INT(0, g_err_count); endpoint_fini(ep); free(ep); close(iomgr.epoll_fd); printf("test_enq_arm_failure_is_loud_no_errhandler: PASS\n"); } /* ── suite entry point ───────────────────────────────────────────── */ void test_netio_arm_failure(void) { printf("\n>> netio poll-arm failure tests\n\n"); RUN_TEST(test_enq_arm_failure_is_loud_no_errhandler); }