You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Native: one schedule's persistent database error stalls every schedule, the repair pass and readiness #955
Found while answering General's read-only question for Workflow #875 (native schedule bounds). Native 3.0.x, source 923d45b3.
Mechanism
The maintenance loop (execution.rsTimerScheduler::start, every 250 ms when DW_RUST_MAINTENANCE_LOOP=1, which the image sets) runs fire_due_timers()?, then fire_due_schedules()?, then the repair pass.
fire_due_schedules (schedule_execution.rs) fires up to 100 due schedules ordered by next_fire_at, id.
A non-database error goes to record_failure, which advances next_fire_at past now. Good: that's bounded.
A RuntimeError::Database error returns from the whole tick (Err(error @ RuntimeError::Database(_)) => return Err(error)). The schedule keeps its stored occurrence, by design, so a transient error retries from durable state. The unit test failed_due_admission_rolls_back_start_and_retains_original_occurrence_and_quota pins that.
The loop sets healthy=false on any failed tick, and schema_ready() (/api/ready) requires healthy.
Consequence if the error is persistent
If one due schedule's start fails with a database-class error every time, that schedule stays at the head of the due ordering. Every tick on every node then:
fails at that schedule, so later due schedules in the batch never fire;
skips the repair pass (run deadlines, task repair, activity and service-call timeouts), which runs after schedules;
leaves /api/ready answering 503, so the whole server drops out of rotation.
Timers still fire, because they run first.
Not yet established
I haven't found a natural input that makes a schedule start fail persistently with a database-class error (rather than a validation refusal). The existing test reaches the path only with an injected SQLite trigger. Candidates worth checking: column length or truncation limits for generated workflow ids or memo/search-attribute values on MySQL/PostgreSQL, and constraint conflicts with existing instance ids.
Expected
A persistent failure in one schedule must not stop other schedules, the repair pass or readiness. For example: retry a database-class failure a bounded number of times, then record it via record_failure and continue the batch, keeping the overall tick error only for a real storage outage. Behavior for transient errors (retry from durable state, occurrence kept) should stay as it is.
Taking this (Server, session a411b7c3). The fix follows published PHP Core 2.15.12 ScheduleManager::tick, which catches every per-schedule error, calls recordStartFailure and goes on with the batch:
Transient (transient_reason is lock pressure or backend unavailable): unchanged. The occurrence is kept and the tick fails and retries from durable state. This is a superset of PHP's SQLite 5/6 carve-out.
Any other per-schedule error, including non-transient database errors: record_failure (advances next_fire_at, failures_count +1), then continue with the next schedule. Applies to both the buffered-drain and due phases.
The recorded reason for a database error is storage_unavailable, as the HTTP surface reports it, so driver text that may hold payloads stays out of recent_actions. The bounded diagnostic is logged.
A failure to record is itself a storage failure and still fails the tick.
Tests: the head-of-line scenario (a schedule that always fails, ahead of a healthy one), the rewritten atomic-rollback test (quota kept, failure recorded, next occurrence), and real SQLITE_BUSY keeping the occurrence. A draft PR follows.
Activity
rmcdaniel commentedon Nov 3, 2026
Taking this (Server, session a411b7c3). The fix follows published PHP Core 2.15.12
ScheduleManager::tick, which catches every per-schedule error, callsrecordStartFailureand goes on with the batch:transient_reasonis lock pressure or backend unavailable): unchanged. The occurrence is kept and the tick fails and retries from durable state. This is a superset of PHP's SQLite 5/6 carve-out.record_failure(advancesnext_fire_at,failures_count+1), then continue with the next schedule. Applies to both the buffered-drain and due phases.storage_unavailable, as the HTTP surface reports it, so driver text that may hold payloads stays out ofrecent_actions. The bounded diagnostic is logged.Tests: the head-of-line scenario (a schedule that always fails, ahead of a healthy one), the rewritten atomic-rollback test (quota kept, failure recorded, next occurrence), and real SQLITE_BUSY keeping the occurrence. A draft PR follows.
Keep one failing schedule from stalling the maintenance tick (durable…
Published Compose: default to the 3.0.5 release
Merge pull request durable-workflow#995 from durable-workflow/rust-95…
Chart 1.0.3: default to native Server 3.0.5