Async-Redis
view release on metacpan or search on metacpan
t/93-socket-cleanup/close-with-watchers.t view on Meta::CPAN
my $blocking_future = $r->blpop('nonexistent:key:for:close:test', 1);
# Get the socket fileno before disconnect
my $fileno_before = fileno($r->{socket});
ok(defined $fileno_before, "socket has valid fileno before disconnect: $fileno_before");
# Now disconnect while the BLPOP is still waiting
# This is where the problem occurs - close() is called while
# Future::IO still has a read watcher on this fileno
$r->disconnect;
# The socket should be gone now
ok(!defined $r->{socket}, 'socket is undef after disconnect');
# Wait a moment for any async cleanup to propagate
run { Future::IO->sleep(0.1) };
# Check for warnings related to fileno issues
# Different backends may produce different warnings:
# - "Bad file descriptor"
# - "Invalid argument"
# - Warnings about removing non-existent watchers
my @fileno_warnings = grep {
/fileno|file descriptor|invalid|bad fd|not registered/i
} @warnings;
# This is what we're testing - ideally there should be NO warnings
# If there are warnings, it indicates the close() happened before
# the poll loop could unregister its watchers
if (@fileno_warnings) {
diag "Captured fileno-related warnings (this indicates the bug):";
diag " $_" for @fileno_warnings;
}
# For now, just note any warnings - this test documents the behavior
# After the fix, we expect zero warnings
note "Total warnings captured: " . scalar(@warnings);
for my $w (@warnings) {
note " Warning: $w";
}
# The blocking future should be cancelled/failed
ok($blocking_future->is_ready, 'blocking future is ready after disconnect');
ok($blocking_future->is_cancelled || $blocking_future->is_failed,
'blocking future is cancelled or failed');
pass('disconnect completed without crashing');
};
subtest 'rapid connect/disconnect cycles stress test' => sub {
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };
# Rapidly connect and disconnect, sometimes with pending operations
for my $i (1..10) {
my $r = Async::Redis->new(
host => $ENV{REDIS_HOST} // 'localhost',
);
run { $r->connect };
# On odd iterations, start an operation before disconnect
if ($i % 2) {
my $f = $r->ping; # Start a command
$r->disconnect; # Disconnect while it's pending
} else {
$r->disconnect; # Clean disconnect
}
}
# Give async cleanup time to complete
run { Future::IO->sleep(0.2) };
my @fileno_warnings = grep {
/fileno|file descriptor|invalid|bad fd|not registered/i
} @warnings;
if (@fileno_warnings) {
diag "Stress test captured fileno warnings:";
diag " $_" for @fileno_warnings;
}
# Document the behavior - after fix, expect zero warnings
is(scalar(@fileno_warnings), 0,
'no fileno-related warnings during rapid connect/disconnect')
or diag "Got " . scalar(@fileno_warnings) . " warnings";
pass('stress test completed without crashing');
};
subtest 'disconnect during concurrent commands' => sub {
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };
my $r = Async::Redis->new(
host => $ENV{REDIS_HOST} // 'localhost',
);
run { $r->connect };
# Fire off multiple concurrent commands
my @futures;
for my $i (1..5) {
push @futures, $r->set("close:test:$i", "value:$i");
}
# Disconnect immediately while commands are in flight
$r->disconnect;
# All futures should be ready (completed, failed, or cancelled)
run { Future::IO->sleep(0.1) };
for my $i (0..$#futures) {
ok($futures[$i]->is_ready, "future $i is ready after disconnect");
}
my @fileno_warnings = grep {
/fileno|file descriptor|invalid|bad fd|not registered/i
} @warnings;
is(scalar(@fileno_warnings), 0,
'no fileno-related warnings during concurrent disconnect')
or do {
( run in 2.870 seconds using v1.01-cache-2.11-cpan-b9db842bd85 )