Async-Redis
view release on metacpan or search on metacpan
t/92-concurrency/reader-fatal-paths.t view on Meta::CPAN
use strict;
use warnings;
use Test2::V0;
use Scalar::Util qw(blessed);
use Future::AsyncAwait;
use Future::IO;
use Async::Redis;
subtest 'detached inflight futures fail with typed error, not generic cancellation' => sub {
my $c = Async::Redis->new(host => 'x', port => 1);
$c->{_socket_live} = 1;
$c->{connected} = 1;
my $f1 = Future->new;
my $f2 = Future->new;
$c->{inflight} = [
{ future => $f1, cmd => 'GET', args => ['x'], deadline => 0 },
{ future => $f2, cmd => 'SET', args => ['y', 'z'], deadline => 0 },
];
my $err = Async::Redis::Error::Timeout->new(
message => 'test timeout',
timeout => 5,
);
$c->_reader_fatal($err);
ok $f1->is_failed, 'f1 failed';
my ($e1) = $f1->failure;
ok blessed($e1) && $e1->isa('Async::Redis::Error::Timeout'),
'f1 carries the typed error, not cancellation';
ok $f2->is_failed, 'f2 failed';
my ($e2) = $f2->failure;
ok blessed($e2) && $e2->isa('Async::Redis::Error::Timeout'),
'f2 also carries the typed error';
is scalar @{$c->{inflight}}, 0, 'inflight drained';
is $c->{_socket_live}, 0, '_socket_live cleared';
is $c->{connected}, 0, 'connected cleared';
};
subtest 'reentrance during same call is a no-op (idempotence guard)' => sub {
my $c = Async::Redis->new(host => 'x', port => 1);
$c->{_socket_live} = 1;
$c->{connected} = 1;
my $on_disc_calls = 0;
# Set up a callback that tries to call _reader_fatal again during
# _reader_fatal. The guard should prevent a second on_disconnect fire.
$c->{on_disconnect} = sub {
$on_disc_calls++;
$c->_reader_fatal(Async::Redis::Error::Connection->new(
message => 'reentrant', host => 'x', port => 1,
));
};
$c->_reader_fatal(Async::Redis::Error::Connection->new(
message => 'first', host => 'x', port => 1,
));
is $on_disc_calls, 1, 'on_disconnect fired exactly once';
is $c->{_fatal_in_progress}, 0, 'guard cleared after transition';
};
subtest 'second fatal after first completes is a fresh transition' => sub {
my $c = Async::Redis->new(host => 'x', port => 1);
$c->{_socket_live} = 1;
$c->{connected} = 1;
my $on_disc_calls = 0;
$c->{on_disconnect} = sub { $on_disc_calls++ };
$c->_reader_fatal(Async::Redis::Error::Connection->new(
message => 'first', host => 'x', port => 1,
));
# Re-arm _socket_live and connected to simulate a reconnect, then
# fatal again. This should count as a NEW transition.
$c->{_socket_live} = 1;
$c->{connected} = 1;
$c->_reader_fatal(Async::Redis::Error::Connection->new(
message => 'second', host => 'x', port => 1,
));
is $on_disc_calls, 2, 'on_disconnect fired twice (once per transition)';
};
subtest 'on_disconnect skipped when not previously connected' => sub {
my $c = Async::Redis->new(host => 'x', port => 1);
$c->{_socket_live} = 1;
$c->{connected} = 0; # e.g., during handshake
my $on_disc_calls = 0;
$c->{on_disconnect} = sub { $on_disc_calls++ };
$c->_reader_fatal(Async::Redis::Error::Connection->new(
( run in 1.478 second using v1.01-cache-2.11-cpan-85d3896f969 )