EV-Kafka
view release on metacpan or search on metacpan
t/22_lifecycle.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use IO::Socket::INET;
use Errno qw(EAGAIN EWOULDBLOCK);
use Scalar::Util qw(weaken);
use B;
use EV;
use EV::Kafka;
# Regression tests for connection object lifecycle and memory ownership:
# M1 response mortals are reclaimed per event, not when EV::run returns
# M2 DESTROY fires every pending callback (not just the first)
# M5 a custom EV::Loop is refcounted by the conn
# M6 explicit DESTROY inerts the blessed ref; stale use croaks
# M7 conn_emit_error warns (not croaks) when no on_error is installed
# M8 disconnect() from on_disconnect/on_error neither recurses nor
# fires on_disconnect twice
# M9 pending-request error callbacks fire before on_disconnect
#
# Uses the in-process mock-broker pattern from t/20_mock_conn.t â no live
# broker needed.
plan tests => 21;
sub i16 { pack 'n', $_[0] }
sub i32 { pack 'N', $_[0] }
my $apis_body =
i16(0) # no error
. i32(2) # 2 entries
. i16(0) . i16(0) . i16(7) # API_PRODUCE, v0..v7
. i16(18) . i16(0) . i16(0); # API_API_VERSIONS, v0..v0
# Start a mock broker on a free port. $on_request->($api_key, $corr_id,
# $send, $ctx) runs for each full request frame; $send->($corr, $body)
# frames and writes a response. $ctx keeps watchers/sockets/timers alive
# and offers $ctx->{close_client}. Returns ($port, $ctx).
sub mock_broker {
my (%opt) = @_;
my $server = IO::Socket::INET->new(
LocalAddr => '127.0.0.1',
LocalPort => 0,
Listen => 1,
Proto => 'tcp',
ReuseAddr => 1,
) or BAIL_OUT "cannot bind localhost listener: $!";
$server->blocking(0);
my $ctx = { server => $server, timers => [] };
my $on_request = $opt{on_request} || sub {
my ($api, $corr, $send) = @_;
$send->($corr, $apis_body) if $api == 18;
};
$ctx->{close_client} = sub {
undef $ctx->{read_w};
close $ctx->{client_fh} if $ctx->{client_fh};
undef $ctx->{client_fh};
};
$ctx->{accept_w} = EV::io fileno($server), EV::READ, sub {
my $fh = $server->accept or return;
$fh->blocking(0);
$ctx->{client_fh} = $fh;
my $send = sub {
my ($corr, $body) = @_;
return unless $ctx->{client_fh};
my $payload = i32($corr) . $body;
syswrite $ctx->{client_fh}, i32(length $payload) . $payload;
};
my $incoming = '';
$ctx->{read_w} = EV::io fileno($fh), EV::READ, sub {
my $buf;
my $n = sysread $fh, $buf, 4096;
if (!defined $n) {
return if $!{EAGAIN} || $!{EWOULDBLOCK};
undef $ctx->{read_w};
return;
}
if ($n == 0) { undef $ctx->{read_w}; return; }
$incoming .= $buf;
while (length($incoming) >= 4) {
my $size = unpack 'N', substr($incoming, 0, 4);
last if length($incoming) < 4 + $size;
my $api = unpack 'n', substr($incoming, 4, 2);
my $corr = unpack 'N', substr($incoming, 8, 4);
substr($incoming, 0, 4 + $size) = '';
$on_request->($api, $corr, $send, $ctx);
}
};
};
return ($server->sockport, $ctx);
}
sub timeout_w { EV::timer 5, 0, sub { diag "test timed out"; EV::break } }
# --- M1: a parsed response must not stay alive past its event -------------
# Broker answers the first Metadata request immediately and the second
# 0.2s later, so the two callbacks run in SEPARATE watcher events. The
# first callback keeps only a weak ref to its result; if response mortals
# are pinned until EV::run returns (the bug), $saved is still defined when
# the second callback runs.
{
my $meta_body = i32(0) . i32(1) . i32(0); # v1: 0 brokers, controller 1, 0 topics
my $meta_seen = 0;
my ($port, $broker) = mock_broker(on_request => sub {
my ($api, $corr, $send, $ctx) = @_;
if ($api == 18) { $send->($corr, $apis_body); return; }
if ($api == 3) {
$meta_seen++;
if ($meta_seen == 1) {
$send->($corr, $meta_body);
} else {
push @{ $ctx->{timers} },
EV::timer(0.2, 0, sub { $send->($corr, $meta_body) });
}
}
});
my $conn = EV::Kafka::Conn::_new('EV::Kafka::Conn', undef);
my $saved;
$conn->on_error(sub { diag "M1 error: $_[0]"; EV::break });
$conn->on_connect(sub {
$conn->metadata(undef, sub { $saved = $_[0]; weaken($saved); });
$conn->metadata(undef, sub {
ok !defined($saved),
'M1: response mortal is reclaimed when its event ends';
EV::break;
});
});
$conn->connect('127.0.0.1', $port, 5.0);
my $t = timeout_w();
EV::run;
}
# --- M2: DESTROY must fire every pending callback -------------------------
{
my ($port, $broker) = mock_broker(); # handshake only, swallows the rest
my $conn = EV::Kafka::Conn::_new('EV::Kafka::Conn', undef);
my $ready = 0;
$conn->on_error(sub { diag "M2 error: $_[0]"; EV::break });
$conn->on_connect(sub { $ready = 1; EV::break });
$conn->connect('127.0.0.1', $port, 5.0);
my $t = timeout_w();
EV::run;
ok $ready, 'M2: handshake complete';
my (@fired, @refs);
for (1..3) {
my $cb = sub { push @fired, $_[1] };
push @refs, $cb;
weaken($refs[-1]);
$conn->metadata(undef, $cb);
}
is $conn->pending, 3, 'M2: three requests in flight';
$conn->DESTROY;
( run in 0.538 second using v1.01-cache-2.11-cpan-14f38c9f855 )