Skip to content

Bake pause/resume into SpansRecorder with a depth counter#74

Merged
rubenvanassche merged 1 commit into
mainfrom
pausable-spans-recorder
May 20, 2026
Merged

Bake pause/resume into SpansRecorder with a depth counter#74
rubenvanassche merged 1 commit into
mainfrom
pausable-spans-recorder

Conversation

@rubenvanassche

Copy link
Copy Markdown
Member

Summary

  • The PausableRecorder trait only worked for a single ignored event with nothing nested under it. A normal recordStart/recordEnd pair under a pause, or a second ignored event on the same recorder, silently desynced the resume.
  • Pause/resume is now a feature of SpansRecorder itself. Static $pauseOwner and $pauseDepth track ownership and call depth across the recorder hierarchy. startSpan and endSpan carry the counter math: increments while owner-paused do not produce real spans, and resume fires the moment depth returns to zero.
  • Lifecycle::flush() calls SpansRecorder::resetPauseState() on every existing flush trigger (subtask end, terminate, trash, shutdown), so long-running runtimes (Octane, queue workers) cannot leak pause state across executions.
  • The CommandRecorder, JobRecorder, and QueueRecorder consumers drop the trait, the if (pausedTrace()) preambles in recordEnd/recordFailed, and the $ourTracePause ownership flag. Their public surface is unchanged; the ignored branches still call pauseTrace().

Why a counter

The old single-boolean approach treated any recordEnd after a pause as the matching one. The counter replaces "next end resumes" with "end that lands on zero resumes," which is the only rule that holds under nesting.

The motivating shape (ignored A → normal B → ignored C, ends in reverse) is locked in by a unit test, along with: single ignored event, repeated pauseTrace() by the owner, non-owner pause attempts as no-ops, and explicit resetPauseState() behavior. The global Pest.php beforeEach resets the static state so it cannot leak between tests.

The PausableRecorder trait could only handle a single ignored event with
nothing nested under it. Anything trickier (a normal recordStart/recordEnd
under a pause, or a second ignored event) silently desynced the resume.

Pause/resume is now a feature of SpansRecorder itself. Static $pauseOwner and
$pauseDepth track ownership and call depth across the recorder hierarchy.
startSpan and endSpan carry the counter math: increments while owner-paused
do not produce real spans, and resume fires the moment depth returns to zero.
Lifecycle::flush() clears the static state at every subtask/request boundary
so long-running runtimes cannot leak pause state across executions.
@rubenvanassche rubenvanassche merged commit 9319f00 into main May 20, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant