Summary
df.wait_for_schedule() in src/dsl.rs pre-computes wait_seconds at graph construction time (when df.start() is called), not at execution time. If there is any delay between df.start() and when the background worker runs the WAIT_SCHEDULE node, the wait will be too short and the function may wake before the intended cron tick.
// Compute wait duration NOW (at DSL time) for deterministic orchestration replay
let now = Utc::now();
let next = schedule.upcoming(Utc).next()...
let duration_secs = (next - now).num_seconds().max(0) as u64;
The stored JSON is {"wait_seconds": N} — a fixed offset, not a target timestamp.
Severity
Low — the code comment acknowledges the trade-off (deterministic replay). Real-world impact is bounded by BGW queue latency (typically seconds), but under load the window can grow.
Root Cause / Trade-off
Pre-computing makes duroxide replay safe because the orchestration must be deterministic. Calling Utc::now() inside the orchestration is forbidden. The fix must compute remaining wait inside an activity (which is allowed I/O).
Fix
Store the target timestamp (next cron tick) instead of wait_seconds. Add an activity (compute_cron_wait) that receives the target timestamp, computes max(0, target - now()), and returns the seconds to wait. The orchestration schedules that activity first, then passes the result to schedule_timer. This keeps the orchestration deterministic while correctly measuring remaining time at actual execution.
Summary
df.wait_for_schedule()insrc/dsl.rspre-computeswait_secondsat graph construction time (whendf.start()is called), not at execution time. If there is any delay betweendf.start()and when the background worker runs theWAIT_SCHEDULEnode, the wait will be too short and the function may wake before the intended cron tick.The stored JSON is
{"wait_seconds": N}— a fixed offset, not a target timestamp.Severity
Low — the code comment acknowledges the trade-off (deterministic replay). Real-world impact is bounded by BGW queue latency (typically seconds), but under load the window can grow.
Root Cause / Trade-off
Pre-computing makes duroxide replay safe because the orchestration must be deterministic. Calling
Utc::now()inside the orchestration is forbidden. The fix must compute remaining wait inside an activity (which is allowed I/O).Fix
Store the target timestamp (next cron tick) instead of
wait_seconds. Add an activity (compute_cron_wait) that receives the target timestamp, computesmax(0, target - now()), and returns the seconds to wait. The orchestration schedules that activity first, then passes the result toschedule_timer. This keeps the orchestration deterministic while correctly measuring remaining time at actual execution.