4 Commits

Author SHA1 Message Date
yetuge
052ad76f04
fix(scheduler): re-read the run row under the writer before guarding update_status (#5860)
update_status guarded expected_lease_owner (and the protect_terminal
backfill) on a plain session.get. On SQLite a concurrent requeue plus
re-claim can commit between that read and this session's write, so the
stale guard passed and the late write clobbered the fresh claim —
clearing the new owner's lease fencing and overwriting its status. The
guard now re-reads under the parent's writer lock (the same task -> run
lock order as fail_launching_run / expire_queued_runs), with
populate_existing so the identity map does not serve the pre-lock row
back untouched. This is the run-row counterpart of the staleness #5777
fixed for the task-level lease release.

Red/green: tests/test_scheduled_task_dispatch_race.py gains a parking
session proxy that commits a requeue + re-claim by another scheduler
between update_status's read and its write — on main the late write
returned True and clobbered the claim; with the change it is fenced
(False) and the re-claim survives.

Signed-off-by: yetuge <2219677952@qq.com>
2026-09-27 21:00:09 +08:00
Hyeonsang Cho
509151d006
fix(scheduler): read the lease owner under SQLite's writer lock (#5777)
* fix(scheduler): read the lease owner under SQLite's writer lock

release_dispatch_lease guards on the lease owner and then writes the
status its caller asked for — "enabled" for a scheduled dispatch. It read
the row with session.get(..., with_for_update=True), and FOR UPDATE is
ignored on SQLite, so that guard ran against whatever snapshot the plain
SELECT saw.

A user pausing a task while its dispatch was in flight therefore lost the
pause: pause clears lease_owner, which is exactly what the guard checks,
but the release had already read the pre-pause row, passed the check, and
overwrote status with "enabled". The API had answered "paused", the task
row went back to enabled with next_run_at untouched, and the scheduler
kept firing it.

The read now goes through _lock_task, the helper every other mutating
path here uses, which takes the writer up front on SQLite (FOR UPDATE
keeps doing the work on Postgres, where a pause that commits first is
seen and the release declines). The reconciliation paths in this file
keep their documented "no SQLite writer emulation" trade-off.

* docs(changelog): note the scheduler lease-release lock fix (#5777)

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-24 18:06:51 +08:00
Aari
645ca08f16
fix(scheduler): enqueue busy scheduled task runs (#4918)
* fix(frontend): clarify reuse-thread scheduling behavior

* fix(scheduler): enqueue overlapping scheduled runs

* fix(scheduler): preserve queue lease fencing

* fix(scheduler): close queue concurrency races

* fix(scheduler): harden queue timeout bookkeeping

* fix(scheduler): preserve manual failure schedule

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-08-24 15:38:32 +08:00
Daoyuan Li
ca3e510b7d
fix(scheduler): close duplicate dispatch race (#4105)
Enforce one queued or running scheduled-task run per task with a partial unique index. The migration resolves legacy duplicates before creating the index, and losing inserts use the existing conflict or skip outcomes.
2026-07-24 21:41:09 +08:00