blob: af744fd22622f72f216833cb7a3ee38e877ffcf7 [file] [edit]
/******************************************************************************
* 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;
}