PAGI-Server

 view release on metacpan or  search on metacpan

t/http2/18-transport-leak.t  view on Meta::CPAN

            on_frame_recv      => $overrides{on_frame_recv}      // sub { 0 },
            on_data_chunk_recv => $overrides{on_data_chunk_recv} // sub { 0 },
            on_stream_close    => $overrides{on_stream_close}    // sub { 0 },
        },
    );
}

sub pump {
    my ($client, $client_sock, $cond) = @_;
    for (1 .. 100) {
        $loop->loop_once(0.02);
        my $buf = '';
        $client_sock->sysread($buf, 65536);
        $client->mem_recv($buf) if length($buf);
        my $out = $client->mem_send;
        $client_sock->syswrite($out) if length($out);
        last if $cond && $cond->();
    }
}

subtest 'h2 transport handle (and its stream-state cycle) is collected after the request' => sub {
    # The app weak-probes its OWN transport handle. This is race-free: the app
    # is guaranteed to run, so the probe is always captured while the handle is
    # live (unlike polling $conn->{h2_streams} from outside, which misses a fast
    # request that is created and torn down within a single loop tick).
    my ($saw_handle, $probe);

    my $app = async sub {
        my ($scope, $receive, $send) = @_;
        await $receive->();

        my $t = $scope->{'pagi.transport'};
        $saw_handle = $t ? 1 : 0;
        weaken($probe = $t);   # weak: survives the request only if leaked

        await $send->({
            type    => 'http.response.start',
            status  => 200,
            headers => [['content-type', 'text/plain']],
        });
        # Stream a few chunks so the handle is genuinely exercised (send_queue
        # populated, the cycle fully formed).
        for my $i (1 .. 3) {
            await $send->({ type => 'http.response.body', body => "chunk$i", more => 1 });
        }
        await $send->({ type => 'http.response.body', body => 'final', more => 0 });
    };

    my ($conn, $stream_io, $client_sock, $server) = create_h2c_connection(app => $app);

    my $stream_closed = 0;
    my $client = create_client(on_stream_close => sub { $stream_closed = 1; return 0 });

    # h2c handshake.
    $client->send_connection_preface;
    $client_sock->syswrite($client->mem_send);
    pump($client, $client_sock);

    $client->submit_request(
        method    => 'GET',
        path      => '/streaming',
        scheme    => 'http',
        authority => 'localhost',
    );
    $client_sock->syswrite($client->mem_send);

    # Pump until the stream completes and closes (request fully handled).
    pump($client, $client_sock, sub { $stream_closed });

    ok($stream_closed, 'stream completed and closed');
    ok($saw_handle,    'transport handle was attached to the h2 scope');

    # Drive deferred teardown (loop->later) AND adopted-future cleanup so the
    # scope/coroutine that transiently hold the handle are released. Break early
    # once the probe is collected.
    for (1 .. 200) {
        last unless defined $probe;
        $loop->loop_once(0.01);
    }

    is($probe, undef,
        'transport handle (and its $ss cycle) collected after teardown; no leak');

    $stream_io->close_now;
    $loop->remove($server);
};

done_testing;



( run in 0.616 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )