fix: remove no-op SaveJobs calls, fix PendingRuns lifecycle and cap

Implements items 4-5 of the whole-project review's suggested order
(docs/PROJECT_REVIEW_PLAN.md):

- Drop the three SaveJobs calls in the run lifecycle (startRunLocked,
  executeRun, SetGlobalPause): none of them change a durable Job field,
  everything they touch lives on JobRuntime, which is never persisted.
  Retire TestStartRunLockedRollbackOnSaveFailure with the rollback it
  guarded, since a run can no longer fail to start this way.
- Clear PendingRuns (the "queue" overlap policy's backlog) when a job is
  disabled or the scheduler is globally paused, so resuming or
  re-enabling a job no longer replays a deferred run left over from
  before the pause/disable. Cap it at maxPendingRuns (10) so a job whose
  runs take longer than its own interval stops accumulating an unbounded
  backlog. Surface the queued count in the details pane via DisplayStats.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 17:06:55 +03:00
parent 1242b22e4f
commit 263717874c
9 changed files with 147 additions and 82 deletions
+7
View File
@@ -32,6 +32,13 @@ the app icon (experimental).**
loss mid-write can no longer leave a truncated or empty file. `Service.Stop()`
is now called when the app quits, which also makes the run context
cancellation reach in-flight runs on shutdown.
- Fixed the "queue" overlap policy's backlog (`PendingRuns`): it no longer
survives a global pause or a job being disabled, so resuming or re-enabling a
job can no longer replay a deferred run left over from before the pause/
disable. It is also capped at 10 queued occurrences, so a job whose runs take
longer than its own interval no longer accumulates an unbounded backlog that
then runs back-to-back indefinitely. The job details pane now shows the
queued-run count (", N queued") whenever it is non-zero.
**Jobs:**
+9
View File
@@ -79,6 +79,15 @@ change to their shape has to stay compatible on its own.
mid-session (see [ROADMAP.md](ROADMAP.md)).
- **`--start-in-tray` defers to config.** A stale autostart shortcut that still
passes the flag does not hide the window when `KeepRunningInTray` is off.
- **`JobRuntime.PendingRuns` (the "queue" overlap policy's backlog) is capped at
`maxPendingRuns` (10) and cleared on pause or disable.** A job whose runs take
longer than its interval stops accumulating backlog once the cap is hit —
further overlaps are dropped like the "skip" policy until the backlog drains
below the cap. `SetGlobalPause(true)` and `SetEnabled(id, false)` both zero
the counter, so resuming or re-enabling a job never replays a deferred run for
an occurrence that fired before the pause/disable. The details pane appends
", N queued" to the statistics line via `DisplayStats` whenever the count is
non-zero.
## Out of scope
+3 -2
View File
@@ -136,6 +136,7 @@ Tests all mutating operations on the Service, scheduler integration, and setting
| `TestDeleteJobNotFound` | Verifies that `DeleteJob` returns an error for an unknown job ID. |
| `TestSetEnabledNotFound` | Verifies that `SetEnabled` returns an error for an unknown job ID. |
| `TestSetEnabledToggles` | Verifies that `SetEnabled` flips the enabled flag and persists the change. |
| `TestSetEnabledClearsPendingRuns` | Verifies that disabling a job zeroes a `PendingRuns` backlog it was carrying, so re-enabling it later does not replay a stale deferred run. |
#### Global pause / run-now / run-due
@@ -187,11 +188,11 @@ and scheduler edge cases using injected `runJob` and `primeDue`.
| `TestRunDueSkipDropsOverlap` | Global skip: no second concurrent run, `PendingRuns` stays 0. |
| `TestRunDueQueueRerunsAfterFinish` | Queue: one deferred run after an in-flight finish; also covers an empty per-job policy inheriting the global default. |
| `TestRunDueQueueDrainsMultipleOverlaps` | Queue: multiple missed ticks drain as separate runs. |
| `TestRunDueQueueCapsPendingRuns` | Regression: `PendingRuns` stops growing at `maxPendingRuns` instead of accumulating without bound for a job that never keeps up with its schedule. |
| `TestRunDuePerJobQueueOverridesGlobalSkip` | Per-job `queue` beats global `skip`. |
| `TestRunDuePerJobSkipOverridesGlobalQueue` | Per-job `skip` beats global `queue`. |
| `TestRunNowSequentialGuard` | Manual run refused while another job runs in sequential mode. |
| `TestStartRunLockedRollbackOnSaveFailure` | Regression: run does not start when `SaveJobs` fails. |
| `TestRunDueQueueDrainSkippedWhenPaused` | Queued overlaps are not drained while the scheduler is paused. |
| `TestRunDueQueueDrainSkippedWhenPaused` | Queued overlaps are not drained while the scheduler is paused, and pausing clears the backlog rather than leaving it to fire a stale deferred run on resume. |
| `TestEffectiveTimeout` | Verifies the three-state resolution: `nil` inherits the global default, a positive value overrides it, and an explicit `0` means no timeout without inheriting. |
---