-
Notifications
You must be signed in to change notification settings - Fork 328
feat(rhai): add intern_strings config option to allow elimination of RwLock contention under concurrent load (copy #9070) #9146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mergify
wants to merge
12
commits into
dev
Choose a base branch
from
mergify/copy/dev/pr-9070
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
c2687bb
feat(rhai): add max_strings_interned config option to control string …
theJC a3d7e75
docs(rhai): address AI style review feedback
theJC fcc6ce3
docs: replace specific benchmark numbers with general language
theJC 9fa0d0e
refactor(rhai): replace max_strings_interned with boolean intern_stri…
theJC c6eb033
fix(rhai): correct stale doc option name and suppress unused_must_use…
theJC bec04dc
lint
goto-bus-stop 7bb7433
exclude benchmark file that intentionally uses api-key-like string fr…
goto-bus-stop d827f94
Merge branch 'dev' into mergify/copy/dev/pr-9070
goto-bus-stop 6a0a197
try to focus the schema doc & changeset on user-relevance w/ active r…
goto-bus-stop 5c16b91
tighten up the docs a bit
goto-bus-stop 8b78af3
integrate style review feedback
goto-bus-stop e613e03
docs: apply 2 AI review suggestions across 1 file
apollo-librarian[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| ### Add `intern_strings` configuration option for the Rhai plugin | ||
|
|
||
| The Rhai plugin now exposes an `intern_strings` option that controls Rhai's internal string interning. Under high concurrency, threads encountering new strings must acquire a write lock, which can serialize Rhai execution across concurrent requests. | ||
|
|
||
| Setting `intern_strings: false` disables interning, eliminating the lock: | ||
|
|
||
| ```yaml | ||
| rhai: | ||
| scripts: ./rhai | ||
| main: main.rhai | ||
| intern_strings: false | ||
| ``` | ||
|
|
||
| String interning can alleviate memory allocation and make string equality checks a little faster. For deployments serving many concurrent requests, the cost likely outweighs the benefit, so we recommend experimenting with `intern_strings: false` and observing if it improves performance. | ||
|
|
||
| The default (`true`) preserves the existing behaviour. | ||
|
|
||
| By [@theJC](https://github.com/theJC) in https://github.com/apollographql/router/pull/9070 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
apollo-router-benchmarks/benches/fixtures/rhai/string_heavy.rhai
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // String-heavy Rhai script for benchmarking string interning performance. | ||
| // | ||
| // This script is intentionally designed to exercise the Rhai string interner | ||
| // frequently: each iteration of the loop performs string concatenation and map | ||
| // key lookups, both of which acquire the RwLock on the StringsInterner when | ||
| // the `sync` feature is enabled. | ||
| // | ||
| // The goal is to produce a measurable difference between default interning | ||
| // (256 strings, RwLock acquired per op) and disabled interning (0, no lock). | ||
|
|
||
| fn supergraph_service(service) { | ||
| let request_callback = Fn("process_request"); | ||
| service.map_request(request_callback); | ||
| } | ||
|
|
||
| fn process_request(request) { | ||
| let prefix = "x-rhai-bench-"; | ||
| let i = 0; | ||
| while i < 20 { | ||
| let key = prefix + to_string(i); | ||
| let val = "bench-value-" + to_string(i) + "-done"; | ||
| request.headers[key] = val; | ||
| i += 1; | ||
| } | ||
| request.context["rhai_bench_complete"] = "true"; | ||
| } |
239 changes: 239 additions & 0 deletions
239
apollo-router-benchmarks/benches/rhai_string_interning.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| //! Benchmark: Rhai string interner — direct engine, no router stack. | ||
| //! | ||
| //! Tests whether disabling Rhai's `RwLock<StringsInterner>` (via | ||
| //! `engine.set_max_strings_interned(0)`) improves throughput under concurrent | ||
| //! load for scripts representative of production request processing. | ||
| //! | ||
| //! The script is modelled on the real subgraph request callback work: | ||
| //! - Lookups in a context map using long string keys | ||
| //! (`"apollo::authentication::jwt_status"`, `"customer::client_name"`, ...) | ||
| //! - `in` operator containment checks (string equality under the hood) | ||
| //! - Template-string interpolation (`\`...\``) | ||
| //! - String comparisons (`starts_with`, `==`) | ||
| //! - Cookie / CSV building loops (string concatenation) | ||
| //! | ||
| //! Two configurations: | ||
| //! - `default_256_interned` — `Engine::new()` default, 256-entry interner, | ||
| //! every string op acquires `RwLock<StringsInterner>` | ||
| //! - `disabled_0_interned` — `set_max_strings_interned(0)`, interner field is | ||
| //! `None`, no lock is ever taken | ||
| //! | ||
| //! Two variants: | ||
| //! - `sequential` — single thread, measures raw Rhai execution cost | ||
| //! - `concurrent_N` — N OS threads sharing one `Arc<Engine>`, surfaces any | ||
| //! RwLock write contention on the interner | ||
| //! | ||
| //! Run with: | ||
| //! ``` | ||
| //! cargo bench -p apollo-router-benchmarks --bench rhai_string_interning | ||
| //! ``` | ||
|
|
||
| use std::sync::Arc; | ||
| use std::time::Duration; | ||
| use std::time::Instant; | ||
|
|
||
| use criterion::criterion_group; | ||
| use criterion::criterion_main; | ||
| use criterion::BenchmarkId; | ||
| use criterion::Criterion; | ||
| use criterion::Throughput; | ||
| use rhai::Engine; | ||
| use rhai::Scope; | ||
|
|
||
| // How many OS threads share the engine in the concurrent variant. | ||
| const CONCURRENCY: usize = 8; | ||
|
|
||
| // A Rhai script representative of typical subgraph request callback work. | ||
| // | ||
| // Patterns exercised: | ||
| // - Long context-key string literals (`"apollo::authentication::jwt_status"`, ...) | ||
| // - Map construction and `[]`-key lookups | ||
| // - `in` containment operator (string equality) | ||
| // - `if` / `else if` chains comparing strings | ||
| // - Template-string interpolation | ||
| // - Cookie-building loop with string concatenation | ||
| // - `starts_with` / `len` string methods | ||
| const SCRIPT: &str = r#" | ||
| // ---------- context simulation ---------------------------------------- | ||
| // Mirrors a ~40-key context map accessed across typical request callbacks. | ||
| let ctx = #{ | ||
| "apollo::authentication::jwt_status": (), | ||
| "apollo::authentication::jwt_claims": (), | ||
| "apollo::supergraph::operation_kind": "query", | ||
| "apollo::supergraph::operation_name": "TopProducts", | ||
| "customer::caller_tag": "test-client-service", | ||
| "customer::caller_cred_kind": "oauth", | ||
| "customer::caller_grants": ["role-a", "role-b"], | ||
| "customer::has_override": false, | ||
| "customer::format": "en_US", | ||
| "customer::rg": "us", | ||
| "customer::vault_gate_status": "present", | ||
| "customer::vault_gate_bearer": "Bearer eyJ...", | ||
| "customer::perms::level_resolved": (), | ||
| "customer::entity_ref_status": "present", | ||
| "customer::entity_ref": "acct-0123456789", | ||
| "customer::entity_ref_resolver": "identity-service", | ||
| "customer::tokens::validation_passed": #{ "TOKN": "abc123value", "SKEY": "xyz789value" }, | ||
| "customer::tokens::org_scope": (), | ||
| "customer::tokens::emitted": (), | ||
| "customer::net_origin": "203.0.113.42", | ||
| "customer::req_epoch_ms": 1_700_000_000_000 | ||
| }; | ||
|
|
||
| // ---------- authentication check equivalent --------------------------- | ||
| let has_auth = "customer::caller_cred_kind" in ctx; | ||
|
|
||
| // ---------- retry header equivalent ----------------------------------- | ||
| let op_kind = ctx["apollo::supergraph::operation_kind"]; | ||
| let idempotency = if op_kind == "query" { "true" } else { "" }; | ||
|
|
||
| // ---------- ext auth forwarding equivalent ---------------------------- | ||
| let ext_auth_header = ""; | ||
| if "customer::vault_gate_status" in ctx { | ||
| let status = ctx["customer::vault_gate_status"]; | ||
| if status == "failure" { | ||
| ext_auth_header = "customer-vault-gate-failure: true"; | ||
| } else if status == "invalid" { | ||
| ext_auth_header = "customer-vault-gate-invalid: true"; | ||
| } else if status == "present" && "customer::vault_gate_bearer" in ctx { | ||
| ext_auth_header = `customer-vault-gate: ${ctx["customer::vault_gate_bearer"]}`; | ||
| } | ||
| } | ||
|
|
||
| // ---------- entity ref forwarding equivalent -------------------------- | ||
| let acct_header = ""; | ||
| if "customer::entity_ref_status" in ctx { | ||
| let ref_status = ctx["customer::entity_ref_status"]; | ||
| if ref_status == "failure" { | ||
| acct_header = "customer-svc-entity-ref-failure: true"; | ||
| } else if ref_status == "invalid" { | ||
| acct_header = "customer-svc-entity-ref-invalid: true"; | ||
| } else if ref_status == "present" { | ||
| let ref_id = ctx["customer::entity_ref"]; | ||
| let ref_ns = ctx["customer::entity_ref_resolver"]; | ||
| acct_header = `customer-svc-entity-ref: ${ref_id} / ${ref_ns}`; | ||
| } | ||
| } | ||
|
|
||
| // ---------- region/format query-param forwarding equivalent ---------- | ||
| let query_string = ""; | ||
| if "customer::rg" in ctx { | ||
| query_string += `?rg=${ctx["customer::rg"]}`; | ||
| } | ||
| if "customer::format" in ctx { | ||
| if query_string == "" { | ||
| query_string += `?fmt=${ctx["customer::format"]}`; | ||
| } else { | ||
| query_string += `&fmt=${ctx["customer::format"]}`; | ||
| } | ||
| } | ||
| let path = `/data-retrieval-service/graphql${query_string}`; | ||
|
|
||
| // ---------- token forwarding equivalent (build loop) ----------------- | ||
| let valid_tokens = ctx["customer::tokens::validation_passed"]; | ||
| let cookie_string = ""; | ||
| if valid_tokens != () { | ||
| for key in valid_tokens.keys() { | ||
| let value = valid_tokens[key]; | ||
| if cookie_string != "" { cookie_string += "; "; } | ||
| cookie_string += `${key}=${value}`; | ||
| } | ||
| } | ||
|
|
||
| // ---------- token subject check equivalent --------------------------- | ||
| // (starts_with + len — mirrors sub-claim validation pattern) | ||
| let sub = "app:abcdefgh1234567890abcdefgh1234567890abcdefgh1234567890abcdefgh"; | ||
| let is_app_sub = sub.starts_with("app:") && sub.len() == 68; | ||
|
|
||
| // ---------- result (prevents dead-code elimination) ------------------ | ||
| #{ | ||
| has_auth: has_auth, | ||
| idempotency: idempotency, | ||
| ext_auth_header: ext_auth_header, | ||
| acct_header: acct_header, | ||
| path: path, | ||
| cookie_string: cookie_string, | ||
| is_app_sub: is_app_sub | ||
| } | ||
| "#; | ||
|
|
||
| fn make_engine(max_strings_interned: Option<usize>) -> Arc<Engine> { | ||
| let mut engine = Engine::new(); | ||
| if let Some(n) = max_strings_interned { | ||
| engine.set_max_strings_interned(n); | ||
| } | ||
| Arc::new(engine) | ||
| } | ||
|
|
||
| fn rhai_string_interning_benchmark(c: &mut Criterion) { | ||
| let configs: &[(&str, Option<usize>)] = &[ | ||
| ("default_256_interned", None), | ||
| ("disabled_0_interned", Some(0)), | ||
| ]; | ||
|
|
||
| for &(label, max_strings_interned) in configs { | ||
| let engine = make_engine(max_strings_interned); | ||
| let ast = Arc::new(engine.compile(SCRIPT).expect("script compiles")); | ||
|
|
||
| let mut group = c.benchmark_group("rhai_string_interning"); | ||
| group | ||
| .measurement_time(Duration::from_secs(20)) | ||
| .sample_size(200) | ||
| .throughput(Throughput::Elements(1)); | ||
|
|
||
| // --- Sequential: one scope per iteration, single thread ---------- | ||
| { | ||
| let engine = engine.clone(); | ||
| let ast = ast.clone(); | ||
| group.bench_with_input(BenchmarkId::new("sequential", label), label, |b, _| { | ||
| b.iter(|| { | ||
| let mut scope = Scope::new(); | ||
| engine | ||
| .eval_ast_with_scope::<rhai::Dynamic>(&mut scope, &ast) | ||
| .expect("eval ok") | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| // --- Concurrent: CONCURRENCY threads sharing one engine ---------- | ||
| // `iter_custom` lets us drive N threads per criterion iteration and | ||
| // report wall-clock time for the whole batch. Each thread runs | ||
| // `per_thread` iterations so the reported time covers all of them. | ||
| { | ||
| let engine = engine.clone(); | ||
| let ast = ast.clone(); | ||
| group.bench_with_input( | ||
| BenchmarkId::new(format!("concurrent_{CONCURRENCY}"), label), | ||
| label, | ||
| |b, _| { | ||
| b.iter_custom(|iters| { | ||
| let per_thread = (iters as usize).max(1).div_ceil(CONCURRENCY); | ||
| let engine = engine.clone(); | ||
| let ast = ast.clone(); | ||
| let start = Instant::now(); | ||
| std::thread::scope(|s| { | ||
| for _ in 0..CONCURRENCY { | ||
| let engine = engine.clone(); | ||
| let ast = ast.clone(); | ||
| s.spawn(move || { | ||
| for _ in 0..per_thread { | ||
| let mut scope = Scope::new(); | ||
| let _ = engine | ||
| .eval_ast_with_scope::<rhai::Dynamic>(&mut scope, &ast) | ||
| .expect("eval ok"); | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
| start.elapsed() | ||
| }); | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| group.finish(); | ||
| } | ||
| } | ||
|
|
||
| criterion_group!(benches, rhai_string_interning_benchmark); | ||
| criterion_main!(benches); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was thinking of landing it like this first, and switch the default at some point in the future. It might be overly cautious?