Skip to content

fix(rhai): run Rhai script hooks via spawn_blocking instead of the async executor#9815

Open
rohan-b99 wants to merge 5 commits into
dev-v3.xfrom
ROUTER-1876-rhai-execution-blocks-async-executor-thread
Open

fix(rhai): run Rhai script hooks via spawn_blocking instead of the async executor#9815
rohan-b99 wants to merge 5 commits into
dev-v3.xfrom
ROUTER-1876-rhai-execution-blocks-async-executor-thread

Conversation

@rohan-b99

@rohan-b99 rohan-b99 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Rhai request/response hooks were evaluated inline on the Tokio async executor thread, so a CPU-bound script (loops, regex, etc.) held that thread for the full duration of its evaluation and delayed every other in-flight request scheduled on it. execute() now runs the script on Tokio's blocking thread pool via spawn_blocking, and the subgraph gen_map_response macro was switched from a synchronous map_response() to an async and_then() so it can await it.


Checklist

Complete the checklist (and note appropriate exceptions) before the PR is marked ready-for-review.

  • PR description explains the motivation for the change and relevant context for reviewing
  • PR description links appropriate GitHub/Jira tickets (creating when necessary)
  • Changeset is included for user-facing changes
  • Changes are compatible1
  • Documentation2 completed
  • Performance impact assessed and acceptable
  • Metrics and logs are added3 and documented
  • Tests added and passing4
    • Unit tests
    • Integration tests
    • Manual tests, as necessary

Exceptions

Note any exceptions here

Notes

Footnotes

  1. It may be appropriate to bring upcoming changes to the attention of other (impacted) groups. Please endeavour to do this before seeking PR approval. The mechanism for doing this will vary considerably, so use your judgement as to how and when to do this.

  2. Configuration is an important part of many changes. Where applicable please try to document configuration examples.

  3. A lot of (if not most) features benefit from built-in observability and debug-level logs. Please read this guidance on metrics best-practices.

  4. Tick whichever testing boxes are applicable. If you are adding Manual Tests, please document the manual testing (extensively) in the Exceptions.

…ync executor

Rhai request/response hooks were evaluated inline on the Tokio async executor
thread, so a CPU-bound script (loops, regex, etc.) held that thread for the
full duration of its evaluation and delayed every other in-flight request
scheduled on it. execute() now runs the script on Tokio's blocking thread
pool via spawn_blocking, and the subgraph gen_map_response macro was switched
from a synchronous map_response() to an async and_then() so it can await it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@rohan-b99
rohan-b99 requested a review from a team as a code owner July 16, 2026 08:36
@apollo-librarian

apollo-librarian Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

✅ Docs preview ready

The preview is ready to be viewed. View the preview

File Changes

0 new, 14 changed, 0 removed
* graphos/routing/(latest)/configuration/yaml.mdx
* graphos/routing/(latest)/customization/native-plugins.mdx
* graphos/routing/(latest)/customization/coprocessor/index.mdx
* graphos/routing/(latest)/observability/router-telemetry-otel/index.mdx
* graphos/routing/(latest)/observability/router-telemetry-otel/apm-guides/datadog/router-instrumentation.mdx
* graphos/routing/(latest)/observability/router-telemetry-otel/apm-guides/datadog/connecting-to-datadog/datadog-agent/datadog-agent-traces.mdx
* graphos/routing/(latest)/observability/router-telemetry-otel/enabling-telemetry/conditions.mdx
* graphos/routing/(latest)/observability/router-telemetry-otel/enabling-telemetry/selectors.mdx
* graphos/routing/(latest)/observability/router-telemetry-otel/enabling-telemetry/spans.mdx
* graphos/routing/(latest)/performance/caching/response-caching/faq.mdx
* graphos/routing/(latest)/query-planning/caching.mdx
* graphos/routing/(latest)/security/demand-control.mdx
* graphos/routing/(latest)/upgrade/from-router-v1.mdx
* graphos/routing/(latest)/_sidebar.yaml

Build ID: 94cfaa4e3b1a87a5f64d34df
Build Logs: View logs

URL: https://www.apollographql.com/docs/deploy-preview/94cfaa4e3b1a87a5f64d34df


✅ AI Style Review — No Changes Detected

No MDX files were changed in this pull request.

Review Log: View detailed log

This review is AI-generated. Please use common sense when accepting these suggestions, as they may not always be accurate or appropriate for your specific context.

@rohan-b99

Copy link
Copy Markdown
Contributor Author

/claude-review

Comment on lines +299 to +304
let ticked = after - before;
assert!(
ticked >= 8,
"the async ticker only progressed by {ticked} ticks while the rhai script executed; \
the executor thread was blocked"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new regression test judges success by wall-clock timing rather than a condition-based wait, which matches two documented antipatterns in CLAUDE.md:

  • Fixed sleeps instead of condition-based waits — the ticker is spawned and then a fixed sleep(5ms) (line 281) is used to "let the ticker get going" before the before baseline read. This is the "spawn + sleep N ms" readiness pattern; under CI scheduling jitter 5ms may not be enough for the ticker task to be polled even once, skewing the baseline.
  • Nanosecond-timer races in testsassert!(ticked >= 8, ...) decides the outcome by racing a 1ms-interval ticker against a 50ms busy-loop with an empirically-tuned threshold (the comment itself notes "observed 1-2" vs "observed 20+"). On a loaded runner the tick count can compress even when the fix is working correctly, producing a flaky failure.

Consider synchronizing on a condition rather than a fixed warm-up (e.g. spin until ticks becomes non-zero before capturing before). Worth noting too: this is a lib unit test (binary_id apollo-router), so it is not covered by the threads-required = 4 safeguard in .config/nextest.toml, which only applies to the apollo-router::* integration binaries where timing-sensitive tests are otherwise protected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah...this smells flaky but I'm also not totally sure how else to go about it.

@goto-bus-stop goto-bus-stop left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks good really. hard to predict the performance impact. some noncommittal comments below

let response_opt = guard.take();
response_opt.unwrap()
})
.boxed_clone()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this change to BoxCloneService::new?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the other macros were using it - I've switched all 3 to use .boxed_clone for consistency

Comment on lines +299 to +304
let ticked = after - before;
assert!(
ticked >= 8,
"the async ticker only progressed by {ticked} ticks while the rhai script executed; \
the executor thread was blocked"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah...this smells flaky but I'm also not totally sure how else to go about it.

Comment thread apollo-router/src/plugins/rhai/mod.rs Outdated

let duration = start.elapsed();
let (result, duration) = match tokio::task::spawn_blocking(move || {
let start = Instant::now();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly it might be better if the waiting time for the task being scheduled is included...or if we had a histogram for rhai wait time and one for execution time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved the Instant::now call to before spawn_blocking, let me know if its worth raising a ticket to add a new metric though

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants