feat(rhai): add intern_strings config option to allow elimination of RwLock contention under concurrent load (copy #9070)#9146
feat(rhai): add intern_strings config option to allow elimination of RwLock contention under concurrent load (copy #9070)#9146mergify[bot] wants to merge 12 commits intodevfrom
Conversation
…interning When the router's Rhai engine is compiled with the `sync` feature (required for multi-threaded use), string interning is protected by a `RwLock<StringsInterner>`. Under high concurrency every string operation that encounters a new string must acquire a write lock, serializing those operations across all concurrent requests. Adds a `max_strings_interned: Option<usize>` field to the Rhai plugin configuration. Setting it to `0` calls `engine.set_max_strings_interned(0)`, which sets the interner to `None` and eliminates the lock path entirely. The default (`null`) preserves Rhai's existing behaviour of 256 interned strings for full backward compatibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> (cherry picked from commit 12ca04d)
- Remove articles before standalone product names (the Rhai/Router → Rhai/Router) - Remove quotes from section title reference in YAML comment - 'may' → 'might' for potential occurrences (2 instances) - 'a large number of' → 'many' - 'amortizing allocation costs' → 'spreading out allocation costs' - 'Cons' → 'Considerations' (neutral framing) - Remove ending punctuation from list item fragments - 'before changing' → 'before you change' (active, reader-framed voice) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> (cherry picked from commit f4cd9d8)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> (cherry picked from commit c8ae40e)
…ngs config Users would only ever set max_strings_interned to 0 (disable) or leave it at the default (256), making the numeric option unnecessarily complex. A boolean intern_strings (default: true) is clearer and prevents footguns from arbitrary values. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> (cherry picked from commit d19f8f4)
… warning in benchmark - Replace `max_strings_interned: 0` comment in rhai/index.mdx with correct `intern_strings: false` - Bind benchmark eval result with `let _ =` to suppress unused_must_use compiler warning Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> (cherry picked from commit c24b800)
|
@mergify[bot]: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Apollo Contributor License Agreement here: https://contribute.apollographql.com/ |
✅ Docs preview readyThe preview is ready to be viewed. View the preview File Changes 0 new, 1 changed, 0 removedBuild ID: bb1e92ca3619d59c877cc24c URL: https://www.apollographql.com/docs/deploy-preview/bb1e92ca3619d59c877cc24c
|
…om gitleaks check
|
Please make sure @jhrldev is in the loop on progressing merging this change in (ref: TSH-22414) |
|
Can we make #9072 a prerequisite for this, so that customers are able to have metrics that could help them make an informed decision on the impact of using this change or not? |
|
|
||
| This is particularly risky when using closures within callbacks while referencing external data. Take particular care to avoid this kind of situation by making copies of data when required. The [examples/surrogate-cache-key directory](https://github.com/apollographql/router/tree/main/examples/surrogate-cache-key) contains an example of this, where "closing over" `response.headers` would cause a deadlock. To avoid this, a local copy of the required data is obtained and used in the closure. | ||
|
|
||
| ### String interning and concurrent performance |
There was a problem hiding this comment.
@apollographql/docs Could you give this section a look? It's quite an in-depth feature and I'm not totally confident on the level of technical detail to give here.
Applied suggestions from AI review 20260410-911566dd-9146-961e53dd87883704: - docs/source/routing/customization/rhai/index.mdx:555: Use an authoritative and encouraging tone to guide the user toward best practice... - docs/source/routing/customization/rhai/index.mdx:537: Remove the hyphen from 'frequently-used' as it is not a compound modifier requir... Review: #9146 Triggered by: renee.kooi@apollographql.com
carodewig
left a comment
There was a problem hiding this comment.
Overall looks great! Mostly just wondering about whether this should be an opt-out change rather than an opt-in, given the performance results
| } | ||
|
|
||
| fn default_intern_strings() -> bool { | ||
| true |
There was a problem hiding this comment.
Given the performance tests above, is there enough data to suggest that the default should actually be false?
There was a problem hiding this comment.
Was thinking of landing it like this first, and switch the default at some point in the future. It might be overly cautious?
Closes #9070
The router's Rhai engine is compiled with the
syncfeature (required for multi-threaded operation), which causes string interning to be protected by aRwLock<StringsInterner>. Under high concurrency, threads that encounter new strings must acquire a write lock, which serializes those operations across all concurrent requests and can become a throughput bottleneck in script-heavy workloads.This PR adds an
intern_strings: boolfield to the Rhai plugin configuration. Setting it tofalsecallsengine.set_max_strings_interned(0), which sets the interner field toNoneand eliminates theRwLockpath entirely. The default (true) preserves Rhai's existing behaviour of 256 interned strings for full backward compatibility — no existing configuration is affected.Configuration
Benchmark results
Measured directly against the Rhai engine (no router stack overhead) using a script representative of typical subgraph request callback work: context map lookups with long string keys,
in-operator containment checks, template-string interpolation, string comparisons, and a cookie-building loop.The sequential result shows the
RwLockis nearly free when uncontested. The concurrent result shows it becomes a genuine serialization point under load — disabling interning removes that bottleneck entirely.The benchmark is included in
apollo-router-benchmarks/benches/rhai_string_interning.rsand can be run with:Trade-offs
Disabling interning increases per-string allocation pressure (repeated identical strings are no longer shared) and removes the pointer-equality fast path for string comparisons. For low-concurrency deployments these costs may outweigh the benefit. The documentation covers this in detail.
Checklist
Complete the checklist (and note appropriate exceptions) before the PR is marked ready-for-review.
Exceptions
No new metrics or logs — this is a configuration option that controls an existing engine behaviour with no observable side-effects beyond performance characteristics.
No integration tests — the feature is covered by unit tests exercising the full config deserialization → plugin initialization path (
it_creates_plugin_with_intern_strings_false) and adeny_unknown_fieldsrejection test, in addition to the engine-level behavioural tests.Notes
This is an automatic copy of pull request #9070 done by [Mergify](https://mergify.com).
Footnotes
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. ↩
Configuration is an important part of many changes. Where applicable please try to document configuration examples. ↩
A lot of (if not most) features benefit from built-in observability and
debug-level logs. Please read this guidance on metrics best-practices. ↩Tick whichever testing boxes are applicable. If you are adding Manual Tests, please document the manual testing (extensively) in the Exceptions. ↩