| /****************************************************************************** |
| * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| * |
| * Test conmgr pollctl interrupt lost-wakeup (Ticket 25227). |
| *****************************************************************************/ |
| |
| #include <stdlib.h> |
| #include <unistd.h> |
| |
| #include "slurm/slurm_errno.h" |
| |
| #include "src/common/log.h" |
| #include "src/common/read_config.h" |
| #include "src/conmgr/polling.h" |
| |
| #include <check.h> |
| |
| static void setup(void) |
| { |
| log_options_t log_opts = LOG_OPTS_INITIALIZER; |
| const char *debug_env = getenv("SLURM_DEBUG"); |
| const char *debug_flags_env = getenv("SLURM_DEBUG_FLAGS"); |
| |
| if (debug_env) |
| log_opts.stderr_level = log_string2num(debug_env); |
| if (debug_flags_env) |
| debug_str2flags(debug_flags_env, &slurm_conf.debug_flags); |
| log_init("polling-test", log_opts, 0, NULL); |
| } |
| |
| static void teardown(void) |
| { |
| log_fini(); |
| } |
| |
| static int _noop_event(int fd, pollctl_events_t events, void *arg) |
| { |
| return SLURM_SUCCESS; |
| } |
| |
| static int _fail_event(int fd, pollctl_events_t events, void *arg) |
| { |
| ck_abort_msg("stale poll() event re-delivered for fd:%d events:0x%x", |
| fd, events); |
| return SLURM_ERROR; |
| } |
| |
| /* |
| * Core of the lost-wakeup regression (Ticket 25227): request an interrupt |
| * while not actively polling, then poll. The caller selects the polling |
| * backend (default or POLL_MODE_POLL) before calling this. |
| * |
| * Before the fix, pollctl_interrupt() silently dropped the request when |
| * pctl.polling was false. There is a window where a poll is queued but the |
| * worker has not yet entered epoll_wait()/poll() and set pctl.polling; an |
| * interrupt requested in that window (notably on shutdown) was lost and the |
| * following pollctl_poll() then blocked forever in epoll_wait(timeout=-1) with |
| * no further wakeup. With the fix the poll returns immediately; without it the |
| * poll blocks and the test is killed at the tcase timeout (reported as a |
| * failure). |
| */ |
| static void _run_interrupt_before_poll(void) |
| { |
| int p[2] = { -1, -1 }; |
| |
| ck_assert_int_eq(pipe(p), 0); |
| |
| pollctl_init(8); |
| |
| /* |
| * Link a real fd so the poll backend does not short-circuit on having |
| * only the internal interrupt pipe registered (fd_count <= 1). |
| */ |
| ck_assert_int_eq(pollctl_link_fd(p[0], PCTL_TYPE_READ_ONLY, "test", |
| __func__), |
| SLURM_SUCCESS); |
| |
| /* Request an interrupt while not polling - the lost-wakeup window. */ |
| pollctl_interrupt(__func__); |
| |
| /* Must return promptly instead of blocking in epoll_wait()/poll(). */ |
| ck_assert_int_eq(pollctl_poll(__func__), SLURM_SUCCESS); |
| |
| /* poll() leaves polling set until for_each_event() processes results. */ |
| ck_assert_int_eq(pollctl_for_each_event(_noop_event, NULL, "noop", |
| __func__), |
| SLURM_SUCCESS); |
| |
| ck_assert_int_eq(pollctl_unlink_fd(p[0], "test", __func__), |
| SLURM_SUCCESS); |
| pollctl_fini(); |
| |
| close(p[0]); |
| close(p[1]); |
| } |
| |
| /* Exercise the fix on the default backend (epoll where available). */ |
| START_TEST(test_interrupt_before_poll) |
| { |
| _run_interrupt_before_poll(); |
| } |
| |
| END_TEST |
| |
| /* |
| * Exercise the same fix on the poll(2) backend. A standalone libcheck binary |
| * never reads slurm.conf, so SlurmctldParameters=conmgr_use_poll does not |
| * reach it; select the poll backend explicitly. pollctl_set_mode() may only be |
| * called once per process, which is fine here because libcheck runs each test |
| * case in its own forked child (CK_FORK, the default). |
| */ |
| START_TEST(test_interrupt_before_poll_poll) |
| { |
| pollctl_set_mode(POLL_MODE_POLL); |
| _run_interrupt_before_poll(); |
| } |
| |
| END_TEST |
| |
| /* |
| * Regression for the stale-revents half of Ticket 25227 on the poll(2) |
| * backend: a revents value left in pctl.events[] by one poll() cycle must not |
| * be re-delivered by a later cycle that skips poll(). |
| * |
| * A real poll() cycle marks a slot readable (POLLIN), and for_each_event() |
| * leaves that revents in the persistent array. The fd is then drained so it is |
| * no longer readable, and an interrupt makes the next _poll() skip the blocking |
| * poll(). With the fix that skip clears every slot's revents, so nothing is |
| * delivered; without it for_each_event() re-scans and delivers the stale event |
| * for a descriptor that is no longer ready (here _fail_event() aborts). |
| */ |
| START_TEST(test_stale_revents_poll) |
| { |
| int p[2] = { -1, -1 }; |
| char buf[8]; |
| |
| ck_assert_int_eq(pipe(p), 0); |
| |
| pollctl_set_mode(POLL_MODE_POLL); |
| pollctl_init(8); |
| |
| ck_assert_int_eq(pollctl_link_fd(p[0], PCTL_TYPE_READ_ONLY, "test", |
| __func__), |
| SLURM_SUCCESS); |
| |
| /* |
| * Linking an fd requests an interrupt on the poll backend, so drain it |
| * with one poll cycle before running a real poll(). After this the |
| * pending interrupt is cleared and pctl.polling is false again. |
| */ |
| ck_assert_int_eq(pollctl_poll(__func__), SLURM_SUCCESS); |
| ck_assert_int_eq(pollctl_for_each_event(_noop_event, NULL, "noop", |
| __func__), |
| SLURM_SUCCESS); |
| |
| /* |
| * Cycle 1: make p[0] readable and run a real poll() so its events[] slot |
| * gets a non-zero revents that for_each_event() leaves in place. |
| */ |
| ck_assert_int_eq(write(p[1], "x", 1), 1); |
| ck_assert_int_eq(pollctl_poll(__func__), SLURM_SUCCESS); |
| ck_assert_int_eq(pollctl_for_each_event(_noop_event, NULL, "noop", |
| __func__), |
| SLURM_SUCCESS); |
| |
| /* |
| * Drain p[0]: the fd is no longer readable, but the stale POLLIN revents |
| * is still sitting in its pctl.events[] slot. |
| */ |
| ck_assert_int_eq(read(p[0], buf, sizeof(buf)), 1); |
| |
| /* |
| * Cycle 2: request an interrupt so _poll() skips the blocking poll(), |
| * then assert the stale revents from cycle 1 is not re-delivered. |
| */ |
| pollctl_interrupt(__func__); |
| ck_assert_int_eq(pollctl_poll(__func__), SLURM_SUCCESS); |
| ck_assert_int_eq(pollctl_for_each_event(_fail_event, NULL, "fail", |
| __func__), |
| SLURM_SUCCESS); |
| |
| ck_assert_int_eq(pollctl_unlink_fd(p[0], "test", __func__), |
| SLURM_SUCCESS); |
| pollctl_fini(); |
| |
| close(p[0]); |
| close(p[1]); |
| } |
| |
| END_TEST |
| |
| int main(void) |
| { |
| int number_failed; |
| Suite *s; |
| TCase *tc; |
| SRunner *sr; |
| |
| s = suite_create("polling"); |
| tc = tcase_create("pollctl_interrupt"); |
| tcase_add_unchecked_fixture(tc, setup, teardown); |
| |
| /* |
| * Bound the runtime so the pre-fix hang (a blocking poll) is reported |
| * as a failure instead of stalling the suite indefinitely. |
| */ |
| tcase_set_timeout(tc, 10); |
| tcase_add_test(tc, test_interrupt_before_poll); |
| tcase_add_test(tc, test_interrupt_before_poll_poll); |
| tcase_add_test(tc, test_stale_revents_poll); |
| suite_add_tcase(s, tc); |
| |
| sr = srunner_create(s); |
| srunner_run_all(sr, CK_ENV); |
| number_failed = srunner_ntests_failed(sr); |
| srunner_free(sr); |
| |
| return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; |
| } |