Marking a resolver-generated GraphQL argument as experiment causes unbounded recursion at query time

Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.

Problem

Adding experiment: { milestone: '19.5' } to the 26 generated <name>Not arguments introduced in !256398 (merged) made every one of them fail at execution time with SystemStackError (stack level too deep), across all 8 aggregation engines. The positive filters, which carry no experiment marker, are unaffected. As a result those exclusion arguments ship unmarked in the originating MR, meaning they land as committed API in 19.5 with no experiment off-ramp.

Evidence

Reproduced deterministically outside any spec harness:

# returns normally
GitlabSchema.execute('query { group(fullPath: "g") { analytics { duoWorkflows(status: ["finished"]) { aggregated { nodes { totalCount } } } } } }')

# raises SystemStackError
GitlabSchema.execute('query { group(fullPath: "g") { analytics { duoWorkflows(statusNot: ["finished"]) { aggregated { nodes { totalCount } } } } } }')
  • Bisected to the marker itself: disabling the single line in Gitlab::Database::Aggregation::Graphql::Adapter that forwards experiment: onto the generated argument makes every exclusion argument work; restoring the line breaks them again.
  • Mechanism, as far as it has been traced: Gitlab::Graphql::Deprecations#init_gitlab_deprecation turns an experiment: marker into a deprecation_reason. Gitlab::Graphql::Tracers::InstrumentationTracer#execute_multiplex has an ensure block that calls export_query_info, then error_type, then query.result. When execution raises before the result is memoized, query.result re-enters execution, which runs the tracer again, and so on until the stack is exhausted. The recursion is in the tracer, not in the aggregation code.
  • Ruled out: adding explicit validates: { length: { maximum: ... } } to the arguments, to test whether the automatic array validation added by Types::BaseArgument was interacting with the deprecation. It made no difference.
  • Not yet established: whether this affects any experiment-marked argument, or only arguments generated dynamically on a resolver via argument(name, type, **kwargs). There are many existing experiment-marked schema items that work, so the dynamic-generation path is the suspected difference, but that has not been confirmed.
  • Impact in CI: this did not surface as a fast failure. The job rspec-ee integration pg17 clickhouse25 froze with no output for roughly 85 minutes and was killed by the 1h30m job timeout, twice, on two different pipelines and runners. The recursion logs and increments metrics at each level, so it thrashes rather than overflowing quickly, which makes it easy to misread as an infrastructure failure.

Proposed direction

Confirm the scope of the problem, then decide whether the fix belongs in the tracer (not re-entering query.result from its own ensure) or in how experiment markers are applied to resolver-generated arguments.

Edited by 🤖 GitLab Bot 🤖