fix(ops): #60 cap + observe queryOpSet fan-out size - #247
Open
JustMaier wants to merge 2 commits into
Open
Conversation
Pre-existing test-only rot (not introduced by any recent change). E0521 "borrowed data escapes outside of function" because axum's HeaderMap::insert requires a 'static key. All callers pass string literals, so widening the parameter to &'static str is the smallest fix. Caught while running cargo test --lib for an unrelated PR — the lib test target was failing to build, masking real test signal. This unblocks the test suite without touching production code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The WAL reader's apply_query_op_set runs `engine.execute_query` with `limit: usize::MAX` and `skip_cache: true`, then iterates over every matching slot to apply the nested ops. There is no cap on result.ids size and no concurrency gate, because the WAL reader is single-threaded — but under burst workload the per-call transient allocations (result Vec, roaring slot iteration, batched bitmap mutations) can drive RSS sharply under jemalloc's `dirty_decay_ms:0` prod config (issue #60 / #61 investigation, 2026-04-29). This change adds: - `bitdex_query_op_set_fanout_size` HistogramVec — observed BEFORE the cap so we capture the would-be-rejected upper tail. Buckets span 1 → 100M, sized for the 22.9M nsfwLevel-eq match seen during the OOM repro. - `bitdex_query_op_set_rejected_total` IntCounterVec, label `reason="fanout_too_wide"`. Alert when rate > 0 — every rejection is a missed mutation (data drift) until #62 (paginate-instead-of-skip) lands. - `bitdex_query_op_set_applied_slots_total` IntCounterVec — cumulative slot mutations, lets us distinguish narrow (postId-eq) from wide fan-outs at the work-unit level. - `BITDEX_QUERY_OP_SET_MAX_FANOUT` env var. Default `usize::MAX` (effectively no cap) so the histogram gathers prod data first. Operators flip a finite value once the distribution is in. - `metrics_bridge_handle()` accessor on ConcurrentEngine so paths outside the engine (here, ops_processor::apply_query_op_set) can read the bridge optionally — `None` in tests + dump-only contexts. Trade: when cap is exceeded the op is skipped silently (log + metric only). Cursor advances. Better than blocking the WAL reader forever or letting OOM happen. #62 paginate-instead-of-skip is the proper fix. Tests cover env parsing — default, override, malformed. The end-to-end cap path is tested via the existing apply_query_op_set route in #244's integration test once a real engine fixture is wired in (deferred to #62 since it requires a fresh-data dir + worktree dependency). Refs: scarlet's design, 2026-04-29 ratchet-vs-decay measurement (MALLOC_CONF dirty_decay_ms:0 → RSS +820MB sustained vs default decay -60MB on identical workload). Pairs with #61 (jemalloc decay tuning). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Code review passed (Scarlet, via team channel). Histogram observed before cap check captures rejected upper tail. Default |
This was referenced Apr 29, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
bitdex_query_op_set_fanout_sizehistogram +bitdex_query_op_set_rejected_totalcounter +bitdex_query_op_set_applied_slots_totalcounter so we can size a real cap from prod data instead of guessing.BITDEX_QUERY_OP_SET_MAX_FANOUTenv var. Defaultusize::MAX— ships with no enforcement so the histogram populates from real traffic. Operator sets a finite value (manifest tweak + roll) once the distribution is in.tracing::warn!+ counter increment) when fan-out > cap. Cursor advances; better than blocking the WAL reader forever or OOM'ing.src/relay/auth.rstest helper&str → &'static strto unblockcargo test --lib(separate commit8fa2cd4).Why
apply_query_op_setis invoked from the WAL reader thread to apply nested ops to every slot matching a queryOpSet's filter. The matching path:No cap on
result.idslength. Under burst load + prod's_RJEM_MALLOC_CONF=dirty_decay_ms:0,muzzy_decay_ms:0, each coldexecute_queryallocation is held in resident memory, ratcheting RSS. 2026-04-29 measurement: identical 50-fan-out burst on local rig produced +820 MB sustained, no decay with prod MALLOC_CONF vs -60 MB decay under default jemalloc settings. That's the smoking gun behind the v1.0.177-jemalloc shadow-on OOM (RSS 11.7 → 110 GB in <30s on talos-wjh-tgy).#61 (jemalloc decay tuning) closes the allocator-side half of the loop. This PR closes the per-call workload half.
What's new
bitdex_query_op_set_fanout_sizeindexbitdex_query_op_set_rejected_totalindex,reasonbitdex_query_op_set_applied_slots_totalindexBITDEX_QUERY_OP_SET_MAX_FANOUTusize::MAXHistogram buckets:
[1, 10, 100, 1K, 10K, 100K, 1M, 10M, 100M].Skip path emits:
Trade: skip semantics
When cap is exceeded the op is skipped silently (log + metric, no error to /ops POSTer). Cursor still advances. Drift consequence: the trigger that produced this queryOpSet (e.g. Post.publishedAt update fanning out to N Images) misses some rows. Acceptable because:
#62 (paginate-instead-of-skip) is the proper fix once we have a histogram-driven cap value.
Alert recommendation
Page when rejection rate > 0. Drift is invisible without this.
Test plan
cargo check --features server,pg-sync --bin bitdex-serverpasses.test_max_fanout_default_is_usize_max— unset env yieldsusize::MAX.test_max_fanout_env_override_parses—BITDEX_QUERY_OP_SET_MAX_FANOUT=100000yields 100K.test_max_fanout_invalid_env_falls_back_to_default— non-numeric env falls back to default.ConcurrentEngine— deferred to docs: generational cache invalidation design #62 since it requires the same fixture work paginate-instead-of-skip will need anyway.Refs
🤖 Generated with Claude Code