GraphQL-Houtou
view release on metacpan or search on metacpan
t/59_on_stall_native_drive.t view on Meta::CPAN
'{ user { boom } }',
context => { loader => $loader2 },
on_stall => GraphQL::Houtou::DataLoader->on_stall_for($loader2),
);
is $r2->{data}{user}{boom}, undef, 'boom_runtime executes normally after the escaped resolver-side die';
};
subtest 'a batch function dying becomes a field error, not an exception, and cleans up' => sub {
# DataLoader::dispatch's XS _dispatch_queue calls the batch sub with
# G_EVAL and settles every queued ticket with the caught error instead of
# letting the die escape - matching the framework's general "never let a
# single resolver/loader failure abort the whole request" philosophy.
my $loader = GraphQL::Houtou::DataLoader->new(batch => sub { die "batch boom\n" });
my $r = $runtime->execute_document(
$QUERY,
variables => { id => 'u3', k => 'k3' },
context => { loader => $loader },
on_stall => GraphQL::Houtou::DataLoader->on_stall_for($loader),
);
is $r->{data}{user}{team}{name}, undef, 'the team.name field is null';
like $r->{errors}[0]{message}, qr/batch boom/, 'the batch die became a field error';
assert_no_live_frames('after batch die');
my $loader2 = GraphQL::Houtou::DataLoader->new(batch => sub { return [ map { "v:$_" } @{ $_[0] } ] });
my $r2 = $runtime->execute_document(
$QUERY,
variables => { id => 'u4', k => 'k4' },
context => { loader => $loader2 },
on_stall => GraphQL::Houtou::DataLoader->on_stall_for($loader2),
);
is_deeply $r2, { data => { user => { name => 'n:u4', team => { name => 'v:k4' } } } },
'the runtime executes normally after the earlier batch die';
};
subtest 'on_stall itself dying while a plain Promise::XS is pending propagates cleanly' => sub {
# A resolver-returned Promise::XS (as opposed to a DataLoader Ticket, see
# the subtest below) is entirely internal to this request - nothing
# external keeps a reference to it - so cancelling the exec state on the
# caught die (gql_runtime_vm_call_on_stall_once) is sufficient to release
# everything.
my $pending_schema = GraphQL::Houtou::Schema->new(
query => GraphQL::Houtou::Type::Object->new(
name => 'Query',
fields => {
hang => { type => $String, resolve => sub { Promise::XS::deferred()->promise } },
},
),
);
my $pending_runtime = build_native_runtime($pending_schema, async => 1);
my $err = do {
local $@;
eval { $pending_runtime->execute_document('{ hang }', on_stall => sub { die "on_stall boom\n" }) };
$@;
};
like $err, qr/on_stall boom/, 'the on_stall die propagates';
assert_no_live_frames('after on_stall die (plain Promise::XS pending)');
};
subtest 'on_stall itself dying while a DataLoader Ticket is pending propagates cleanly' => sub {
# Unlike a plain Promise::XS, a DataLoader Ticket that never gets
# dispatched is kept alive independent of the exec state: subscribing to
# it (gql_runtime_vm_subscribe_dataloader_ticket) pushes our resolve/
# reject callback pair onto the Ticket's own subscriber list, which the
# DataLoader's own queue can keep alive well past the abandoned request.
# gql_runtime_vm_cancel_frame_tree disarms that pair's ctx (dropping its
# state_sv/frame refs) so this does not leak the frame tree.
my $loader = GraphQL::Houtou::DataLoader->new(
batch => sub { my ($ids) = @_; return [ map { "v:$_" } @$ids ] },
);
my $err = do {
local $@;
eval {
$runtime->execute_document(
$QUERY,
variables => { id => 'u5', k => 'k5' },
context => { loader => $loader },
on_stall => sub { die "on_stall boom\n" },
);
};
$@;
};
like $err, qr/on_stall boom/, 'the on_stall die propagates';
assert_no_live_frames('after on_stall die (DataLoader Ticket pending)');
};
subtest 'stall detection (on_stall makes no progress) reports the deadlock and cleans up' => sub {
# A plain Promise::XS that never settles: on_stall reporting zero
# progress every round is the deadlock path.
my $pending_schema = GraphQL::Houtou::Schema->new(
query => GraphQL::Houtou::Type::Object->new(
name => 'Query',
fields => {
hang => { type => $String, resolve => sub { Promise::XS::deferred()->promise } },
},
),
);
my $pending_runtime = build_native_runtime($pending_schema, async => 1);
my $err = do {
local $@;
eval { $pending_runtime->execute_document('{ hang }', on_stall => sub { 0 }) };
$@;
};
like $err, qr/GraphQL execution stalled.*on_stall made no progress/s,
'reports the same stall message as the Perl-driven path';
assert_no_live_frames('after stall detection');
};
subtest 'stall detection with a pending DataLoader Ticket cleans up the same way' => sub {
# Same root cause/fix as the subtest above, reached via the ordinary
# zero-progress deadlock path instead of on_stall dying.
my $loader = GraphQL::Houtou::DataLoader->new(
batch => sub { my ($ids) = @_; return [ map { "v:$_" } @$ids ] },
);
my $err = do {
local $@;
eval {
$runtime->execute_document(
$QUERY,
variables => { id => 'u8', k => 'k8' },
context => { loader => $loader },
on_stall => sub { 0 },
);
};
$@;
( run in 3.256 seconds using v1.01-cache-2.11-cpan-14f38c9f855 )