API-Docker
view release on metacpan or search on metacpan
t/connect_timeout.t view on Meta::CPAN
extends 'API::Docker';
has seen => (is => 'ro', required => 1);
has pending => (is => 'ro', default => sub { [] });
sub _build__socket {
my ($self) = @_;
my $pending = $self->_pending_connect;
push @{ $self->seen }, $pending ? $pending->{timeout} : 'no pending';
push @{ $self->pending }, $pending;
die "no socket here\n";
}
package main;
my $client = API::Docker->new(
host => 'unix:///nonexistent.sock',
api_version => '1.41',
);
# ---------------------------------------------------------------------------
subtest 'the attribute is off by default and off when set to 0' => sub {
is $client->connect_timeout, undef,
'no default -- every existing caller connects exactly as it always did';
is $client->_connect_timeout_value(undef), undef, 'undef is off';
is $client->_connect_timeout_value(0), undef,
'0 is off too, which is how a client-wide default is turned off for one '
. 'request';
is $client->_connect_timeout_value(2.5), 2.5, 'a fraction is kept as one';
my $set = API::Docker->new(
host => 'unix:///nonexistent.sock',
api_version => '1.41',
connect_timeout => 5,
);
is $set->connect_timeout, 5, 'and it is a constructor argument';
};
subtest 'a value that is not a number is refused, not rounded' => sub {
for my $bad ('soon', '', [], {}, -1) {
eval { $client->_connect_timeout_value($bad) };
like $@, qr/connect_timeout must be a non-negative number/,
'refused: ' . (ref $bad || "'$bad'");
}
# The message names connect_timeout rather than read_timeout: the two share
# one check, and a caller who mistyped one must not be told about the other.
eval { $client->_read_timeout_value('soon') };
like $@, qr/read_timeout must be a non-negative number/,
'and the read timeout still names itself';
};
# ---------------------------------------------------------------------------
# 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: $!";
( run in 1.996 second using v1.01-cache-2.11-cpan-e623d60df62 )