API-Docker
view release on metacpan or search on metacpan
t/connect_timeout.t view on Meta::CPAN
# ---------------------------------------------------------------------------
# What counts as the bound firing, and what does not. This is the whole of the
# discrimination: a refused connection and a missing socket path are diagnoses
# the caller can act on, and reporting them as a timeout would replace one with
# a cause that is not true.
subtest '_connect_expired: only the bound firing counts as a timeout' => sub {
{
# IO::Socket's own marker for its select() running out, measured against a
# host that drops SYNs.
local $@ = 'IO::Socket::INET: connect: timeout';
local $! = Errno::ETIMEDOUT();
ok $client->_connect_expired(2), q{'connect: timeout' is the bound};
ok !$client->_connect_expired(undef),
'but not with no connect_timeout in force -- the kernel produces '
. 'ETIMEDOUT on its own after two minutes';
}
{
# The unix:// shape: IO::Socket does the timed connect non-blocking and an
# AF_UNIX connect has no in-progress state, so a full backlog comes back
# at once with EAGAIN instead of blocking.
local $@ = 'connect: Resource temporarily unavailable';
local $! = Errno::EAGAIN();
ok $client->_connect_expired(1), 'EAGAIN with a bound in force is it too';
ok !$client->_connect_expired(0), 'and 0 is no bound';
}
{
local $@ = 'connect: Connection refused';
local $! = Errno::ECONNREFUSED();
ok !$client->_connect_expired(2),
'a refused connection is not a timeout, bound or no bound';
}
{
local $@ = 'connect: No such file or directory';
local $! = Errno::ENOENT();
ok !$client->_connect_expired(2), 'nor is a socket path that is not there';
}
};
subtest 'a missing socket still croaks the string it always did' => sub {
my $missing = API::Docker->new(
host => 'unix:///nonexistent-api-docker-61.sock',
api_version => '1.41',
connect_timeout => 5,
);
eval { $missing->get('/probe') };
my $err = $@;
ok !(ref $err && $err->isa('API::Docker::Error::Timeout')),
'not turned into a timeout by having a connect_timeout set';
like "$err", qr/Cannot connect to Unix socket/,
'the diagnosis is the one the caller can act on';
};
# ---------------------------------------------------------------------------
# The one end-to-end assertion. An AF_UNIX connect blocks in exactly one
# situation -- the listener's backlog is full and nobody is accepting --
# measured with Listen => 1 and no accept: still blocked after 8 seconds. So
# the backlog is filled here, and then the bound has something to bound.
subtest 'the real socket: a connect that would block raises the timeout'
=> sub {
my $dir = tempdir(CLEANUP => 1);
my $path = $dir . '/backlog.sock';
my $srv = IO::Socket::UNIX->new(Local => $path, Listen => 1)
or plan skip_all => "cannot listen on a Unix socket here: $!";
# Filled with the same bounded connect the code under test uses, so this
# loop cannot be the thing that hangs. Nothing ever accepts on $srv.
my @held;
my $full = 0;
for (1 .. 200) {
my $c = IO::Socket::UNIX->new(
Peer => $path, Type => SOCK_STREAM, Timeout => 1);
unless ($c) {
$full = ($! == Errno::EAGAIN() || $! == Errno::EWOULDBLOCK()) ? 1 : 0;
last;
}
push @held, $c;
}
plan skip_all => 'the listen backlog does not fill the way this platform '
. 'was measured to' unless $full;
my $blocked = API::Docker->new(
host => 'unix://' . $path,
api_version => '1.41',
connect_timeout => 1,
);
# The alarm is the harness's own bound, not the one under test: without the
# Timeout on the constructor this connect blocks forever, and a test that
# hangs reports nothing. With it, the alarm never fires.
my $t0 = time;
eval {
local $SIG{ALRM} = sub { die "the connect was never bounded at all\n" };
alarm 10;
$blocked->get('/probe');
alarm 0;
1;
};
my $err = $@;
alarm 0;
my $elapsed = time - $t0;
isa_ok $err, 'API::Docker::Error::Timeout';
is ref $err && $err->phase, 'connect',
'the phase says the daemon was never reached';
is ref $err && $err->timeout, 1, 'and names the bound that expired';
is ref $err && $err->partial, '',
'with no partial: there is no response to have part of';
is ref $err && $err->summary, undef, 'and no summary';
like "$err", qr{\QGET /v1.41/probe\E},
'the request is named in the string';
like "$err", qr/connect timeout/, 'and it says which bound this was';
cmp_ok $elapsed, '<', 5,
'it gave up rather than hanging (' . sprintf('%.2fs', $elapsed) . ')';
# Without the bound this same connect blocks indefinitely, so it is not
# attempted here -- the assertion above is that the bound is what ended it,
# and the measurement that it blocks without one is in the POD.
close $_ for @held;
close $srv;
};
# ---------------------------------------------------------------------------
subtest 'the per-request option overrides the attribute either way' => sub {
# What reached _build__socket, read where the constructors read it. Nothing
# about this can be seen from the outside -- the connect either blocks or it
# does not -- so the assertion is made at the seam the value has to cross.
my @seen;
my $probe = Test::ConnectTimeout::Probe->new(
host => 'unix:///nonexistent-api-docker-61.sock',
api_version => '1.41',
connect_timeout => 5,
seen => \@seen,
);
eval { $probe->get('/probe') };
eval { $probe->get('/probe', connect_timeout => 2) };
# Resolved with exists rather than truth, so 0 is "wait as long as it takes"
# rather than "no opinion", which is how a client-wide default is turned off
# for one request.
eval { $probe->get('/probe', connect_timeout => 0) };
is_deeply \@seen, [5, 2, undef],
'the attribute, then the option overriding it, then an explicit 0 '
. 'turning it off';
is_deeply [ map { $_->{endpoint} } @{ $probe->pending } ],
[ ('GET /v1.41/probe') x 3 ],
'and the endpoint travels with it, so the exception can name the request';
};
subtest 'the pending bound does not outlive the request that set it' => sub {
my $missing = API::Docker->new(
host => 'unix:///nonexistent-api-docker-61.sock',
api_version => '1.41',
( run in 1.388 second using v1.01-cache-2.11-cpan-800906f7e73 )