2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-09-25 09:14:05 +00:00

test(xcat-core): nothing covers the order of a node's requests when one is lost

The install monitor answers a node's requests in the order they arrived.
Two ways to lose that order had no test.

The first is a queued client that gives up. The parent holds the later
connections for a busy node unread, so a client that closes its socket
must not let the request behind it overtake the request that is running.
The new case runs one request, queues two more, drops the middle client,
and asserts the last request starts after the running one ends. It fails
when the per-node queue is removed.

The second is a fork that fails. The monitor then answers the node
itself, in line. The new case makes one fork fail and asserts the request
is answered and that the node's next request starts only after it. It
fails when the fallback drops the connection instead.

The file also asserts the lifted service holds no literal /var/run path.
Every access to the pid file goes through $installm_pidfile, so pointing
that variable at the scratch tree redirects all of them, and a path
written out again inside the routine would reach the host file.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
Daniel Hilst
2026-09-24 00:44:53 -03:00
parent ee41e98751
commit 5f5ebcde7e
@@ -71,6 +71,11 @@ $PIDFILE or die "xcatd no longer declares \$installm_pidfile";
is($PIDFILE, '/var/run/xcat/installservice.pid',
'the monitor still claims the pid file xcatd and its restart handshake use');
# One variable, so pointing it at the scratch tree below redirects the whole routine. A path
# written out again inside the routine would reach the host file whatever this test sets.
unlike($service, qr{/var/run/},
'the monitor reaches its pid file only through $installm_pidfile');
# The monitor writes a pid file. Point it at the scratch tree: the live monitor's file is how a
# restarting xcatd tells the running one to let go of the port, and a test that runs as root
# would otherwise leave this process's pid in it.
@@ -80,6 +85,10 @@ my @HOST_PIDFILE = stat($PIDFILE);
# Every test client connects from 127.0.0.1, so the monitor's own reverse lookup cannot tell
# them apart. Name them in accept order instead.
our @PEER_QUEUE;
# Each entry answers one xfork: true forks, false fails. Empty means fork normally. A case sets
# it before the monitor starts, so the monitor's fork-failure fallback can be exercised.
our @FORK_PLAN;
BEGIN { *CORE::GLOBAL::gethostbyaddr = sub { return (shift(@main::PEER_QUEUE) || 'unknown', '') } }
# Whole microseconds. A %.3f stamp rounds, so an event can be recorded at a time LATER than the
@@ -181,8 +190,10 @@ sub wait_for_event {
*xCAT::NetworkUtils::clearcache = sub { };
*xCAT::NetworkUtils::getNodeDomains = sub { return {} };
*xCAT::TableUtils::getTftpDir = sub { return '/tmp' };
*xCAT::Utils::xfork = sub { return fork() };
*xCAT::Utils::xfork = sub {
if (@main::FORK_PLAN) { return undef unless shift @main::FORK_PLAN }
return fork();
};
*t::rescan::new = sub { return bless {}, shift };
*t::rescan::can_read = sub { return () };
}
@@ -316,6 +327,68 @@ sub stop_monitor {
stop_monitor($mon);
}
# --- a queued connection whose client goes away keeps the order ---------------
# One request runs for a node and two more are queued for it. The client of the middle one gives
# up. The parent holds queued connections unread, so dropping one must not let the request
# behind it overtake the request that is still running.
{
$EVENTS = "$SCRATCH/events-abandoned";
my $port = free_port();
my $mon = open_monitor($port, qw(portprobe gonenode gonenode gonenode));
my $one = talk_to($port, 'installstatus slow');
wait_for_event('start gonenode slow', 10)
or diag('the first request never reached its plugin');
my $two = talk_to($port, 'installstatus middle');
my $three = talk_to($port, 'installstatus last');
close $two;
$two = undef;
ok(wait_for_event('end gonenode last', 30),
'the request behind the abandoned one was served in the end');
ok(wait_for_event('end gonenode slow', 30), 'the running request finished');
my $running_end = event_index('end gonenode slow');
my $last_start = event_index('start gonenode last');
cmp_ok($running_end, '>=', 0, 'the running request is recorded as finished');
cmp_ok($last_start, '>=', 0, 'the request behind the abandoned one is recorded as started');
cmp_ok($last_start, '>', $running_end,
'an abandoned queued connection does not release the request behind it')
or diag('a client that gave up let a later request for the node overtake the running one');
close $_ for grep { $_ } $one, $three;
stop_monitor($mon);
}
# --- the fork-failure fallback answers in line, in order ----------------------
# A monitor that cannot fork answers the node itself. The request must be answered rather than
# dropped, and the node's next request must wait for that answer.
{
$EVENTS = "$SCRATCH/events-nofork";
my $port = free_port();
@FORK_PLAN = (1, 0); # the port probe gets a handler; the first real request does not
my $mon = open_monitor($port, qw(portprobe forknode forknode));
@FORK_PLAN = ();
my $inline = talk_to($port, 'installstatus slow');
ok(wait_for_event('start forknode slow', 20),
'the monitor answered the request in line when it could not fork')
or diag('a monitor that cannot fork drops the request instead of answering it itself');
my $after = talk_to($port, 'installstatus after');
ok(wait_for_event('end forknode after', 30), 'the next request for the node was served');
my $inline_end = event_index('end forknode slow');
my $after_start = event_index('start forknode after');
cmp_ok($inline_end, '>=', 0, 'the in-line request is recorded as finished');
cmp_ok($after_start, '>', $inline_end,
'the request after an in-line one starts only once the in-line one has finished')
or diag('the fork-failure fallback released the next request before its own had finished');
close $_ for grep { $_ } $inline, $after;
stop_monitor($mon);
}
# --- a handler that dies must not take the monitor with it --------------------
{