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 )