Add Layer-2 concurrency stress specs#2821
Open
ericproulx wants to merge 1 commit into
Open
Conversation
Three targeted tests firing barrier-released concurrent requests at one
shared compiled API, each asserting an exact thread-specific response so
cross-request contamination fails deterministically rather than being
observed by luck:
- Array index tracking isolation: each thread plants the invalid element
at its own index and must get exactly its own index back in the error
message — pins ParamScopeTracker's fiber-local per-request state
(teeth-verified: a corrupted store_index fails the spec).
- Mutable default isolation: each request must see exactly its own
mutation of a default: { tags: [] } param. Pins the end-to-end
property enforced jointly by DefaultValidator's per-request dup and
the params container's copy-on-assignment.
- Cold-boot contention: fresh uncompiled apps hit by racing first
requests, exercising Instance#compile! (double-checked LOCK) and the
first traversal of the shared coercer graph.
On MRI these catch logic bleed; the edge workflow runs the full suite on
jruby-head/truffleruby-head, so they also run under true parallelism
there (Layer 3) with no extra wiring.
Test-only; no production changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Danger ReportWarnings
MarkdownsHere's an example of a CHANGELOG.md entry: * [#2821](https://github.com/ruby-grape/grape/pull/2821): Add layer-2 concurrency stress specs - [@ericproulx](https://github.com/ericproulx). |
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
Test-only PR completing the thread-safety test plan started with the Layer-1 invariant specs (#2820). Layer 2 exercises the live request path under real threads: barrier-released concurrent requests against one shared compiled API, where every thread asserts an exact, thread-specific response. Any cross-request state bleed produces a deterministic failure message — the tests never rely on catching a torn write by luck, which is what keeps them non-flaky on MRI.
Independent of the #2817 → #2819 → #2820 stack, so it targets
masterdirectly.The three tests
1. Array index tracking isolation.
ParamScopeTrackerholds per-request bracket indices in fiber-local storage (Fiber[]). Eight threads hit an Array-scope endpoint, each planting the invalid element at its own index; each must get exactlylist[<own index>][name] is missingback. If tracker state ever leaked across threads/requests, another thread's index would surface in the error. Teeth-verified: a temporary patch corruptingstore_index(what a cross-thread clobber produces) fails the spec.2. Mutable default isolation. Each request mutates a
default: { tags: [] }param and must see exactly its own mutation. Teeth-checking this one produced the most interesting finding of the PR: a patch makingDefaultValidatorhand out a shared default object does not break the test — because assigning into the params container (HashWithIndifferentAccess) deep-converts the value, an independent second layer of protection. The spec comment documents this honestly: it pins the end-to-end property, which fails only if both layers regress (e.g. a future params builder storing defaults by reference).3. Cold-boot contention. Four fresh, deliberately uncompiled apps are each hit by eight racing first requests — exercising
Instance#compile!'s double-checkedLOCKpath and the first traversal of the shared coercer graph, the only windows where boot-phase state is still being published.Mechanics
Queue-based ready/gate barrier holds all threads until every one is spawned, then releases them together, maximizing overlap.Rack::MockRequestper request — never a shared Rack::Test session, which is not thread-safe and would produce false alarms.Thread#valuere-raises anything a worker raised, so an exception in any thread fails the example with its own backtrace.Layer 3 comes free
The
edgeworkflow runs the full suite (bundle exec rspec) onjruby-headandtruffleruby-head, so once merged these specs also run under true parallelism there — no tagging or CI wiring needed. On MRI they catch logic bleed; on JRuby/TruffleRuby they additionally cover memory-visibility races the GVL masks.Full suite: 2370 examples, 0 failures. RuboCop clean. No CHANGELOG entry (test-only, per #2818/#2820 precedent).
🤖 Generated with Claude Code