Developer-Dashboard
view release on metacpan or search on metacpan
t/145-collector-start-write-order.t view on Meta::CPAN
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Test::More;
use File::Spec;
use File::Temp qw(tempdir);
use lib 'lib';
use Developer::Dashboard::Collector;
use Developer::Dashboard::CollectorRunner;
use Developer::Dashboard::FileRegistry;
use Developer::Dashboard::IndicatorStore;
use Developer::Dashboard::PathRegistry;
# Hermetic runtime rooted in a throwaway HOME. The collector runner resolves its
# state roots from the deepest .developer-dashboard layer above the working
# directory, so the test chdirs into the temp home before constructing anything.
my $home = tempdir( CLEANUP => 1 );
local $ENV{HOME} = $home;
chdir $home or die "Unable to chdir to $home: $!";
my $paths = Developer::Dashboard::PathRegistry->new( home => $home );
my $runner = Developer::Dashboard::CollectorRunner->new(
collectors => Developer::Dashboard::Collector->new( paths => $paths ),
files => Developer::Dashboard::FileRegistry->new( paths => $paths ),
indicators => Developer::Dashboard::IndicatorStore->new( paths => $paths ),
paths => $paths,
);
my $name = 'order-probe';
# The window this file exists to close cannot be observed by racing two
# processes - it is microseconds wide and would make a flaky test. It can be
# observed exactly, though, by asking what is on disk AT THE MOMENT the loop
# state is written. running_loops keys on the pidfile and identifies the pid from
# the recorded state, so the invariant is simply: the pidfile must never exist
# while the state does not.
my $pidfile_existed_when_state_was_written;
my $state_written = 0;
{
no warnings 'redefine';
# A pid that is alive but is not a loop child, so nothing is actually forked.
local *Developer::Dashboard::CollectorRunner::_fork_process = sub { return $$ };
my $real_write_state = \&Developer::Dashboard::CollectorRunner::_write_loop_state;
local *Developer::Dashboard::CollectorRunner::_write_loop_state = sub {
my ( $self, $loop_name, @rest ) = @_;
$state_written++;
$pidfile_existed_when_state_was_written = -f $self->_pidfile($loop_name) ? 1 : 0;
return $self->$real_write_state( $loop_name, @rest );
};
# start_loop takes the job hashref itself, not named arguments.
$runner->start_loop(
{
name => $name,
command => 'true',
cwd => $home,
interval => 60,
schedule => 'interval',
}
);
}
is( $state_written, 1, 'start_loop records the loop state exactly once' );
is( $pidfile_existed_when_state_was_written, 0,
'the loop state is written BEFORE the pidfile, so the pidfile never exists without it' );
# The pidfile must still be written - closing the window must not cost the
# pidfile itself, which everything else keys on.
ok( -f $runner->_pidfile($name), 'start_loop still writes the pidfile' );
ok( defined scalar $runner->loop_state($name), 'start_loop still records loop state' );
# And the ordering must hold on disk, not merely at the moment of writing: a
# reader arriving after start_loop returns sees both.
my $state = $runner->loop_state($name);
is( $state->{name}, $name, 'the recorded state identifies the loop by name' );
ok( $state->{pid}, 'the recorded state carries the pid a sweep would match against' );
$runner->_cleanup_loop_files($name);
done_testing();
__END__
=head1 NAME
145-collector-start-write-order.t - pin the write order that stops a healthy
collector loop being swept away
=head1 PURPOSE
Prove that C<start_loop> records a loop's state before it writes that loop's
pidfile, so no reader can ever observe a pidfile whose loop it cannot identify.
=head1 WHY IT EXISTS
C<running_loops> lists pidfiles and, for each one, decides whether the pid belongs
to a loop it manages - recognising it either by the child's process title or by the
recorded loop state. C<start_loop> used to write the pidfile first and the state
second. Between those two writes, a child that had not yet adopted its title was
( run in 0.893 second using v1.01-cache-2.11-cpan-14f38c9f855 )