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 )