DBIO-Async
view release on metacpan or search on metacpan
t/04-bind-release.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use Scalar::Util 'weaken';
use Future;
# OFFLINE regression test for the "async bind-value leak" concern.
# Ported from dbio-postgresql-async t/05-bind-release.t.
#
# WHAT THIS TEST PROVES: bind values issued through the async query
# path are retained only by the issuing scope's lexical, not by any
# storage/pool cache. Once that scope ends, the bind arrayref is freed.
# This is a pure Perl reference-graph question -- no DB needed.
#
# The test uses a mock storage whose _query_async / _query_async_pinned
# faithfully emulate the contract: the completion callback is captured,
# but the $bind arrayref itself is NOT.
# --- FakeConn: minimal connection stand-in ---
{
package FakeConn4;
sub new { bless { queue => [] }, shift }
sub query_params {
my ($self, $sql, $bind, $cb) = @_;
my @serialized = map { defined $_ ? "$_" : undef } @$bind;
push @{ $self->{queue} }, { cb => $cb, wire => \@serialized };
}
sub pending_count { scalar @{ $_[0]->{queue} } }
sub complete_one {
my $self = shift;
my $entry = shift @{ $self->{queue} } or return;
$entry->{cb}->([[1]], undef);
}
}
# --- TestStorage4: mock async storage that uses FakeConn4 ---
{
package TestStorage4;
use base 'DBIO::Async::Storage';
sub sql_maker_class { 'DBIO::SQLMaker' }
sub _transform_sql { $_[1] }
sub _post_insert_sql { '' }
sub _normalize_conninfo { $_[1] }
sub _txn_context_class { 'DBIO::Async::TransactionContext' }
sub _txn_conn_accessor { 'txn_conn' }
sub _pipeline_enter { }
sub _pipeline_sync { Future->done }
sub _pipeline_exit { }
sub _conn_ready { 1 }
sub _create_pool_connection { FakeConn4->new }
sub _shutdown_pool_connection { }
# Override _query_async / _query_async_pinned to use FakeConn4
# directly, bypassing the Future::IO seam (which needs a real fd).
sub _query_async {
my ($self, $sql, $bind) = @_;
$bind //= [];
return $self->pool->acquire->then(sub {
my $conn = shift;
my $f = Future->new;
$conn->query_params($sql, $bind, sub {
my ($rows, $err) = @_;
if ($err) { $f->fail($err) } else { $f->done(ref $rows eq 'ARRAY' ? @$rows : $rows) }
$self->pool->release($conn);
});
return $f;
});
}
sub _query_async_pinned {
my ($self, $conn, $sql, $bind) = @_;
$bind //= [];
my $f = Future->new;
$conn->query_params($sql, $bind, sub {
my ($rows, $err) = @_;
if ($err) { $f->fail($err) } else { $f->done(ref $rows eq 'ARRAY' ? @$rows : $rows) }
});
return $f;
}
}
# Build a storage whose pool uses FakeConn4.
sub new_storage {
my $storage = TestStorage4->new(undef);
$storage->connect_info([{ host => 'localhost' }]);
return $storage;
}
# --- 1. single pooled query: bind released once the future completes ---
{
my $storage = new_storage;
my $pool = $storage->pool;
my $weak;
my $f;
{
my $bind = [ 'payload' x 64 ];
weaken($weak = $bind);
$f = $storage->_query_async('SELECT $1', $bind);
}
# The issuing-scope strong lexical dies here.
# Complete the query on the FakeConn4.
my $conn = $pool->{_connections}[0];
$conn->complete_one;
$f->get;
ok !defined $weak,
'pooled query: bind arrayref is freed after the future completes '
. '(completion closure does not retain it)';
}
# --- 2. pinned (transaction) query: same release contract ---
{
my $storage = new_storage;
my $pool = $storage->pool;
# Acquire a connection manually to simulate a pinned transaction conn.
my $conn = $pool->acquire->get;
my $weak;
my $f;
{
my $bind = [ 'txn-payload' x 64 ];
weaken($weak = $bind);
$f = $storage->_query_async_pinned($conn, 'SELECT $1', $bind);
}
$conn->complete_one;
$f->get;
ok !defined $weak,
'pinned (txn) query: bind arrayref is freed after completion too';
}
# --- 3. many in-flight queries: each bind released, no accumulation ---
{
my $storage = new_storage;
my $pool = $storage->pool;
# Pre-spawn enough connections for N concurrent queries.
# Pool size is 5 by default; use 3 queries to stay within.
my $N = 3;
my @weak;
my @f;
for my $i (1 .. $N) {
my $w;
{
my $bind = [ "row$i" x 32 ];
weaken($w = $bind);
push @f, $storage->_query_async("SELECT \$1 -- $i", $bind);
}
push @weak, $w;
}
my $held_inflight = grep { defined } @weak;
is $held_inflight, 0,
"no bind arrayref is retained while $N queries are in-flight "
. '(no per-pool/per-future bind cache)';
# Drain them.
for my $i (0 .. $N - 1) {
$pool->{_connections}[$i]->complete_one;
$f[$i]->get;
}
my $held_after = grep { defined } @weak;
is $held_after, 0,
"after completing all $N queries, still zero binds held "
. '(memory bounded by outstanding queries, not total issued)';
}
done_testing;
( run in 1.122 second using v1.01-cache-2.11-cpan-995e09ba956 )