GraphQL-Houtou

 view release on metacpan or  search on metacpan

t/59_on_stall_native_drive.t  view on Meta::CPAN

  my $err = do {
    local $@;
    eval {
      $runtime->execute_document(
        $QUERY,
        variables => { id => 'u8', k => 'k8' },
        context => { loader => $loader },
        on_stall => sub { 0 },
      );
    };
    $@;
  };
  like $err, qr/GraphQL execution stalled.*on_stall made no progress/s, 'reports the stall';
  assert_no_live_frames('after stall detection (DataLoader Ticket pending)');
};

subtest 'multiple loaders via on_stall_for still resolve across rounds' => sub {
  my $Post = GraphQL::Houtou::Type::Object->new(
    name => 'MultiLoaderPost',
    fields => {
      title => {
        type => $String,
        args => { key => { type => $String } },
        resolve => sub { my (undef, $a, $ctx) = @_; return $ctx->{titles}->load($a->{key}) },
      },
      author => {
        type => GraphQL::Houtou::Type::Object->new(
          name => 'MultiLoaderAuthor',
          fields => {
            name => {
              type => $String,
              args => { key => { type => $String } },
              resolve => sub { my (undef, $a, $ctx) = @_; return $ctx->{authors}->load($a->{key}) },
            },
          },
        ),
        resolve => sub { {} },
      },
    },
  );
  my $ml_schema = GraphQL::Houtou::Schema->new(
    query => GraphQL::Houtou::Type::Object->new(
      name => 'Query',
      fields => { post => { type => $Post, resolve => sub { {} } } },
    ),
  );
  my $ml_runtime = build_native_runtime($ml_schema, async => 1);
  my $titles = GraphQL::Houtou::DataLoader->new(batch => sub { return [ map { "title:$_" } @{ $_[0] } ] });
  my $authors = GraphQL::Houtou::DataLoader->new(batch => sub { return [ map { "author:$_" } @{ $_[0] } ] });
  my $r = $ml_runtime->execute_document(
    'query q($tk: String, $ak: String) { post { title(key: $tk) author { name(key: $ak) } } }',
    variables => { tk => 't1', ak => 'a1' },
    context => { titles => $titles, authors => $authors },
    on_stall => GraphQL::Houtou::DataLoader->on_stall_for($titles, $authors),
  );
  is_deeply $r, { data => { post => { title => 'title:t1', author => { name => 'author:a1' } } } },
    'both loaders settle across on_stall_for rounds';
  assert_no_live_frames('after multi-loader request');
};

subtest 'repeated nested-suspend requests stay clean over many iterations' => sub {
  for my $i (1 .. 200) {
    my $loader = GraphQL::Houtou::DataLoader->new(
      batch => sub { my ($ids) = @_; return [ map { "v:$_" } @$ids ] },
    );
    my $r = $runtime->execute_document(
      $QUERY,
      variables => { id => "u$i", k => "k$i" },
      context => { loader => $loader },
      on_stall => GraphQL::Houtou::DataLoader->on_stall_for($loader),
    );
    is_deeply $r, {
      data => { user => { name => "n:u$i", team => { name => "v:k$i" } } },
    }, "iteration $i resolved" or last;
  }
  assert_no_live_frames('200 native-drive iterations');
};

# Item 1 (Phase 9): shapes the fast lane does not cover (a runtime directive
# present, or the nesting-depth guard exceeded) fall back to the generic
# executor, which still builds its own Promise::XS internally - but that
# promise is now driven from C too (gql_runtime_vm_drive_promise_with_on_stall_sv),
# not handed back to Perl's _settle_result. These pin the same settle/
# reject/stall/cleanup contract for that fallback path that the subtests
# above already pin for the fast lane.
subtest 'a runtime-directive sibling forces the generic-executor fallback, still driven natively' => sub {
  my $loader = GraphQL::Houtou::DataLoader->new(
    batch => sub { my ($ids) = @_; return [ map { "v:$_" } @$ids ] },
  );
  my $directive_schema = GraphQL::Houtou::Schema->new(
    query => GraphQL::Houtou::Type::Object->new(
      name => 'Query',
      fields => {
        plain => { type => $String, resolve => sub { 'plain-val' } },
        loaded => {
          type => $String,
          args => { key => { type => $String } },
          resolve => sub { my (undef, $a, $ctx) = @_; return $ctx->{loader}->load($a->{key}) },
        },
      },
    ),
  );
  my $directive_runtime = build_native_runtime($directive_schema, async => 1);
  my $r = $directive_runtime->execute_document(
    'query Q($skipIt: Boolean!, $k: String) { plain @skip(if: $skipIt) loaded(key: $k) }',
    variables => { skipIt => 1, k => 'd1' },
    context => { loader => $loader },
    on_stall => GraphQL::Houtou::DataLoader->on_stall_for($loader),
  );
  ok !exists($r->{data}{plain}), 'skipped field is absent';
  is $r->{data}{loaded}, 'v:d1', 'the pending sibling still resolves via the natively-driven fallback';
  assert_no_live_frames('after runtime-directive fallback settles');
};

subtest 'nesting deeper than the recursion guard falls back safely, still driven natively' => sub {
  # GQL_VM_FAST_LANE_MAX_NESTING_DEPTH is 16 - one level deeper than that
  # forces the generic-executor fallback (see
  # t/39_fast_lane_promise_fallback.t's equivalent depth-guard subtest),
  # this time with a genuinely pending DataLoader ticket at the bottom so
  # the fallback's own Promise::XS response is driven through on_stall.
  my $depth = 17;
  my $innermost = GraphQL::Houtou::Type::Object->new(
    name => 'DeepDriveLevelBottom',
    fields => {
      v => {
        type => $String,
        args => { key => { type => $String } },
        resolve => sub { my (undef, $a, $ctx) = @_; return $ctx->{loader}->load($a->{key}) },
      },
    },
  );
  my @levels = ($innermost);
  for (my $lvl = $depth - 1; $lvl >= 0; $lvl--) {
    my $child = $levels[0];
    unshift @levels, GraphQL::Houtou::Type::Object->new(
      name => "DeepDriveLevel$lvl",



( run in 0.593 second using v1.01-cache-2.11-cpan-4ab04211f4c )