Skip to content

fix: stop PeriodicScheduler from re-arming itself after stop() - #1324

Closed
davidberenstein1957 wants to merge 3 commits into
masterfrom
fix/scheduler-self-reschedule-race
Closed

fix: stop PeriodicScheduler from re-arming itself after stop()#1324
davidberenstein1957 wants to merge 3 commits into
masterfrom
fix/scheduler-self-reschedule-race

Conversation

@davidberenstein1957

Copy link
Copy Markdown
Collaborator

What changed

PeriodicScheduler.start() now checks _stopped on the reschedule path too (inside the lock), _run() re-checks the flag before invoking the payload, and stop() reads the flag under the lock.

Why

_run() called start(from_run=True), which deliberately bypassed the _stopped check, so a fired timer always armed a successor. A stop() landing between the timer firing and _run() reaching start() cancelled an already-fired (uncancellable) timer and was then silently undone — the scheduler kept firing forever. The payload also ran to completion after stop() returned, so _measure_power_and_energy could mutate tracker state and call live_out after EmissionsTracker.stop() had written the final row and exited the handlers.

The elif not self._stopped: return branch preserves today's "a second start() on a running scheduler is a no-op" semantics. The payload deliberately stays outside the lock, so stop() never blocks for a whole measurement (and must not be called from the payload itself).

Verification

New tests/test_scheduler.py. test_stop_during_timer_firing_does_not_rearm fails on master (scheduler keeps firing, _stopped flips back to False) and passes here; the other three cover stop-mid-payload, double start() arming one timer, and stop() before start() / twice. tests/test_emissions_tracker.py passes unchanged.

Closes #1308

🤖 Generated with Claude Code

_run() rescheduled unconditionally via start(from_run=True), so a stop()
landing between the timer firing and the reschedule was silently undone
and the scheduler became unstoppable. The payload also ran to completion
after stop() returned, letting a measurement mutate tracker state whose
output handlers were already exited.

Serialize both paths over _stopped under the lock, re-check the flag in
_run() before invoking the payload, and read it under the lock in stop().

Closes #1308

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.44%. Comparing base (eb2cfef) to head (f562682).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1324      +/-   ##
==========================================
+ Coverage   91.43%   91.44%   +0.01%     
==========================================
  Files          49       49              
  Lines        5057     5063       +6     
==========================================
+ Hits         4624     4630       +6     
  Misses        433      433              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@davidberenstein1957

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #1339, which fixes this same re-arm bug plus the re-entrancy problem in the same file. Two open PRs rewriting scheduler.py is review burden for no extra benefit, so it's better to have one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PeriodicScheduler re-arms itself after stop(), so measurements can outlive tracker.stop()

2 participants