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 )