EV-Pg

 view release on metacpan or  search on metacpan

t/12_bug_regressions.t  view on Meta::CPAN

    my $out;
    {
        local $/;
        $out = <$rd>;
    }
    close $rd;
    $out = '' unless defined $out;
    chomp $out;
    if (my $sig = $status & 127) { return ("signal:$sig", $out) }
    my $exit = $status >> 8;
    return ($exit == 0 ? 'ok' : "exit:$exit", $out);
}

# 1. Regression: skip off-by-one misdelivery.  In pipeline mode with a large
# single-row result A, calling skip_pending from A's first-row callback and
# then queueing D must deliver D's OWN result ('DDD'), never 'CCC'
# (pre-fix: D got 'CCC').
{
    my $pg;
    my $skipped = 0;
    my ($d_calls, $d_value) = (0, undef);
    $pg = EV::Pg->new(
        conninfo => $conninfo,
        on_connect => sub {
            $pg->enter_pipeline;
            $pg->query_params(
                "select repeat('x',200) from generate_series(1,50000)", [], sub {
                my ($r, $e) = @_;
                return if $skipped;
                $skipped = 1;
                $pg->skip_pending;
                $pg->query_params("select 'DDD'::text", [], sub {
                    my ($r2, $e2) = @_;
                    $d_calls++;
                    $d_value = $r2->[0][0] if ref $r2 && @$r2;
                });
                $pg->pipeline_sync(sub { EV::break });
            });
            $pg->set_single_row_mode;
            $pg->query_params("select 'BBB'::text", [], sub { });
            $pg->query_params("select 'CCC'::text", [], sub { });
            $pg->send_flush_request;
        },
        on_error => sub { diag "Error: $_[0]"; EV::break },
    );
    my $t = EV::timer(10, 0, sub { EV::break });
    EV::run;
    $pg->finish if $pg->is_connected;
    is($d_calls, 1, 'skip off-by-one: D callback fired exactly once');
    is($d_value, 'DDD', "skip off-by-one: D received its own 'DDD' (pre-fix: 'CCC')");
}

# 2. Regression: COPY-OUT skip livelock.  skip_pending from a COPY_OUT tag
# callback with ~20MB still in flight must not spin at 100% CPU, and the
# connection must recover.  Forked: a livelock regression hangs the process
# where no EV timer can fire, so only a parent-side wall-clock kills it.
{
    my ($st, $out) = run_isolated(sub {
        my $wr = shift;
        my $pg;
        my ($skipped, $alive) = (0, '');
        my $retry;
        $pg = EV::Pg->new(
            conninfo => $conninfo,
            on_connect => sub {
                $pg->enter_pipeline;
                $pg->query_params(
                    "copy (select repeat('x',100) from generate_series(1,200000)) to stdout",
                    [], sub {
                    my ($r, $e) = @_;
                    if (!$skipped && defined $r && !ref $r && $r eq 'COPY_OUT') {
                        $skipped = 1;
                        $pg->skip_pending;
                        # libpq is still in COPY state until the skip drain
                        # finishes in the io watcher; retry queueing 'alive'
                        # until the connection accepts new commands again.
                        $retry = EV::timer(0.05, 0.05, sub {
                            my $ok = eval {
                                $pg->query_params("select 'alive'::text", [], sub {
                                    my ($r2, $e2) = @_;
                                    $alive = $r2->[0][0] if ref $r2 && @$r2;
                                    EV::break;
                                });
                                1;
                            };
                            if ($ok) {
                                undef $retry;
                                $pg->send_flush_request;
                            }
                        });
                    }
                });
                $pg->query_params("select 'after'::text", [], sub { });
                $pg->pipeline_sync(sub { });
            },
            on_error => sub { warn "Error: $_[0]"; EV::break },
        );
        my $t = EV::timer(8, 0, sub { EV::break });
        EV::run;
        print $wr "$alive\n";
    }, 10);
    is($st, 'ok', 'COPY-OUT skip: no livelock (pre-fix: 100% CPU hang)');
    is($out, 'alive', "COPY-OUT skip: connection recovered, 'alive' returned");
}

# 3. Regression: use-after-free on a custom (non-default) loop.  Freeing the
# EV::Loop and then the EV::Pg must not segfault.  Forked: a regression kills
# the process with SIGSEGV, which the parent detects via the wait status.
{
    my ($st, $out) = run_isolated(sub {
        my $wr = shift;
        my $loop = EV::Loop->new;
        my $pg = EV::Pg->new(loop => $loop);
        $pg->on_error(sub { warn "Error: $_[0]"; $loop->break });
        $pg->on_connect(sub { $loop->break });
        $pg->connect($conninfo);
        my $guard = $loop->timer(5, 0, sub { $loop->break });
        $loop->run;
        undef $guard;
        $pg->on_connect(undef);   # drop closure capturing $loop
        $pg->on_error(undef);
        undef $loop;              # ev_loop_destroy; $pg's loop pointer dangles
        undef $pg;                # DESTROY must not touch the freed loop
        print $wr "survived\n";
    }, 10);
    is($st, 'ok', "custom loop UAF: clean exit after undef loop + undef pg (pre-fix: SIGSEGV)");
}

# 4. Regression: re-entrant skip double-count.  A's "skipped" callback
# re-enters skip_pending; queries queued afterwards must each get their OWN
# result.  Forked: pre-fix E was misdelivered and then the connection hung.
{
    my ($st, $out) = run_isolated(sub {
        my $wr = shift;
        my $pg;
        my $reentered = 0;
        my ($e_val, $f_val) = ('', '');
        $pg = EV::Pg->new(
            conninfo => $conninfo,
            on_connect => sub {
                $pg->enter_pipeline;
                $pg->query_params("select 'A'::text", [], sub {
                    my ($r, $e) = @_;
                    if (!$reentered) {
                        $reentered = 1;
                        $pg->skip_pending;    # inner re-entrant skip
                    }
                });
                $pg->query_params("select 'B'::text", [], sub { });
                $pg->query_params("select 'C'::text", [], sub { });
                $pg->skip_pending;            # outer skip over A,B,C
                $pg->query_params("select 'EEE'::text", [], sub {
                    my ($r, $e) = @_;
                    $e_val = $r->[0][0] if ref $r && @$r;
                });
                $pg->query_params("select 'FFF'::text", [], sub {
                    my ($r, $e) = @_;
                    $f_val = $r->[0][0] if ref $r && @$r;
                });
                $pg->pipeline_sync(sub { EV::break });
                $pg->send_flush_request;
            },
            on_error => sub { warn "Error: $_[0]"; EV::break },



( run in 0.695 second using v1.01-cache-2.11-cpan-a5162978ef8 )