Repository navigation
Conversation
rvanmelle
marked this pull request as ready for review
October 6, 2026 21:54
This branch has not been deployed
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.
Manual profiling can remain busy after a rejected start. Automatic capture cleanup can release ownership too early and stop a newer capture.
This PR fixes both lifecycle paths. It retains the backend lock until results are collected, stops successful runs once, and clears manual busy state after a rejected start.
The defects reproduce on 0.6.0 and 0.7.0. The detailed output below uses unmodified 0.7.0.
This is a profiling reliability fix. Production frequency and performance impact are not established.
Design decisions
BaseBackend#stop_and_results, so StackProf and Vernier use the same lifecycle.stopmethod. Preserve overrides instead of calling native collectors directly from the shared helper.ensure, including collection failures.AppProfiler.stop. Cleanup must not inspect or stop a subsequent owner.Direct backend callers using separate
stopandresultscalls keep their existing behavior.This does not redesign backend switching, capture cancellation, or ownership of captures started outside app_profiler.
The lock now covers result conversion as well as capture. That extends serialization and is a deliberate tradeoff.
Validation
Failure sequences and the corrected ownership boundary
Rejected manual start leaves the server busy
The server marks itself busy before it knows whether
AppProfiler.startsucceeded.A conflict returns 409 without running the stop path that resets that flag.
The same server application then rejects later requests, even after the original capture ends.
sequenceDiagram participant A as Existing capture participant C as Manual client participant S as ProfileApplication participant P as AppProfiler A->>P: Start capture C->>S: GET /profile S->>S: profile_running = true S->>P: start P-->>S: false (conflict) S-->>C: 409 A->>P: Stop capture loop Later manual attempts C->>S: GET /profile S->>S: Busy flag is still true S-->>C: 409 endEarly unlock allows one capture to stop another
BaseBackend#runcallsstop, reads results, then callsstopagain fromensure.The first stop releases the shared lock before result collection.
A competing capture can start in that gap. StackProf startup also clears uncollected prior results.
The first run can lose its result and stop the competing capture prematurely.
sequenceDiagram participant A as Capture A run participant L as Backend lock participant P as StackProf participant B as Capture B A->>L: Acquire A->>P: Start and run workload A->>P: stop A->>L: Release B->>L: Acquire B->>P: Clear prior results and start A->>P: Read results P-->>A: No completed A profile A->>P: ensure calls stop again Note over P,B: B is stopped prematurelyCorrected completion retains ownership
sequenceDiagram participant A as Capture A participant L as Backend lock participant P as Backend stop/results participant B as Competing capture A->>L: Acquire and start A->>A: Run workload A->>P: stop_and_results P->>P: Call overridden stop once Note over A,L: Defer release on the collecting thread B->>L: Try to acquire L-->>B: Rejected while results are collected P->>P: Read and convert results opt AppProfiler.stop P->>P: Discard cached profiler end P->>L: Ensure release P-->>A: Completed profile B->>L: A later capture can acquireA process using low-rate sampling can still work successfully most of the time.
The failures require contention or a rejected manual start. The reproduction controls those conditions; it does not estimate their production frequency.
Standalone reproduction and captured before/after output
Save the following as
/tmp/reproduce_lifecycle.rb.Run
bundle exec ruby /tmp/reproduce_lifecycle.rbfrom a checkout of app_profiler.Use unmodified v0.7.0 for the baseline, then this branch for the fixed result.
No Rails app, cloud bucket, network service, or workload data is required.
The method hook inserts capture B at A's result boundary. This makes the interleaving deterministic without sleep-based races.
Captured baseline output from unmodified 0.7.0:
{ "version": "0.7.0", "manual_status_during_conflict": 409, "manual_status_after_conflict_clears": 409, "lock_free_before_results": true, "competitor_started_before_results": true, "competitor_still_running_after_first_run": false }Captured output with this patch:
{ "version": "0.7.0", "manual_status_during_conflict": 409, "manual_status_after_conflict_clears": 200, "lock_free_before_results": false, "competitor_started_before_results": false, "competitor_still_running_after_first_run": false }In the baseline, B starts successfully and A subsequently stops it.
With this patch, B cannot start during collection. Its final
falsemeans it never started in that interval.Regression commands:
Compatibility approaches considered, with generic sample fixes
An application can prepend version-specific adapters around the manual server and StackProf backend.
Those adapters depend on private server methods and duplicate collector ownership rules.
These generic excerpts illustrate the behavior considered. They are not complete installation-ready initializers.
Manual server adapter:
ProfileApplicationCompatibilitybehaviorInside the existing semaphore and after the busy guard, derive busy state from the actual result:
A manual stop adapter also needs to retain the shared lock through result collection.
Resetting only the busy flag leaves the collection race present.
Backend adapter:
StackprofBackendCompatibilitybehaviorA StackProf-only adapter can collect results before releasing its existing lock:
The complete run wrapper must also stop once, release rejected starts before running application code, preserve block exceptions, and assign duration metadata.
It must not stop another capture after releasing ownership.
A native-only implementation also bypasses backend
stopoverrides and does not cover Vernier.This PR moves those ownership rules into the shared backend lifecycle instead of requiring each application to maintain those adapters.
The existing
stopandresultsmethods remain the dispatch points.Related prior work: