microsoft / microsoft/duroxide
Add an absolute-deadline timer variant (e.g. `schedule_timer_until(deadline)`)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 217
- Forks
- 61
- Avg merge
- 2d 22h
- Merged PRs (30d)
- 3
Description
Summary
Add a variant of schedule_timer that accepts an absolute wall-clock deadline instead of a relative Duration, e.g.:
/// Schedule a timer that fires at an absolute point in time.
pub fn schedule_timer_until(&self, deadline: std::time::SystemTime) -> DurableFuture<()>;
This is a small, unopinionated primitive that complements the existing relative-delay timer.
Motivation
Today the only timer primitive is relative:
pub fn schedule_timer(&self, delay: std::time::Duration) -> DurableFuture<()>
Internally, however, the timer is already modeled as an absolute fire time. schedule_timer reads the current time and converts the delay into an absolute timestamp that is recorded durably in history and replayed verbatim:
// src/lib.rs (schedule_timer)
let now = inner.now_ms();
let fire_at_ms = now.saturating_add(delay_ms);
inner.emit_action(Action::CreateTimer { scheduling_event_id: 0, fire_at_ms });
// ... recorded as EventKind::TimerCreated { fire_at_ms }, replayed verbatim
So the durable record (Action::CreateTimer { fire_at_ms } / TimerCreated { fire_at_ms }) is already an absolute ms-since-epoch deadline. A schedule_timer_until(deadline) variant would simply set fire_at_ms = deadline_ms directly instead of now + delay. No changes to the action/event schema, the provider's visible_at handling, or replay are required — it's a thin constructor over machinery that already exists.
Why a deadline timer is better than schedule_timer(deadline - now)
A deadline can be expressed today by reading the deterministic clock and subtracting:
let now = ctx.utc_now().await?; // recorded syscall
let delay = deadline.duration_since(now).unwrap_or(Duration::ZERO);
ctx.schedule_timer(delay).await;
This works, but it has rough edges that a first-class primitive removes:
- Intent. "Fire at T" is clearer than "fire after (T − now)" and avoids a category of off-by-reference bugs where the delta is computed against one clock reading but applied against another.
- Negative / overflow handling. Callers must manually clamp
deadline - nowto a non-negativeDuration(duration_sincereturnsErrwhen the deadline is already in the past). Aschedule_timer_untilcan define this once (past deadline ⇒ fire immediately). - One fewer recorded syscall. The
deadline - nowform needs autc_now()reading purely to compute the delay. With an absolute timer the caller often already has the deadline and doesn't need an extra recordedutc_nowevent in history.
How this helps pg_durable
pg_durable (microsoft/pg_durable) builds durable workflows on top of duroxide and exposes a df.wait_for_schedule('<cron>') step that suspends an orchestration until the next cron tick. The natural, replay-safe implementation is:
let now = ctx.utc_now().await?; // deterministic
let next = cron.next_tick_after(now); // pure computation
ctx.schedule_timer_until(next).await; // fire exactly at the cron tick
The cron target is intrinsically an absolute timestamp, so being able to hand that timestamp directly to the timer matches the domain exactly. It removes the manual next - now clamp (cron ticks can land in the past by the time the node runs, especially across continue_as_new loop iterations used for recurring schedules) and expresses "wake at the tick" precisely. More generally, any "wait until a deadline / SLA / scheduled-at time" pattern (delayed jobs, timeouts expressed as absolute times, scheduled pipelines) maps directly onto this primitive.
Proposed API
impl OrchestrationContext {
/// Schedule a timer that fires at an absolute wall-clock deadline.
///
/// Equivalent to `schedule_timer` but anchored to an absolute point in time
/// rather than a delay from "now". A deadline already in the past fires
/// immediately (next turn). The deadline is recorded durably and replayed
/// verbatim, exactly like the existing relative timer.
pub fn schedule_timer_until(&self, deadline: std::time::SystemTime) -> DurableFuture<()>;
}
Implementation sketch: convert deadline to ms-since-epoch and emit Action::CreateTimer { fire_at_ms } directly, reusing the existing timer future/replay path. schedule_timer(delay) could optionally be reimplemented as schedule_timer_until(now + delay) to keep a single code path.
Backwards compatibility
Purely additive — new method, no change to existing signatures, the on-history action/event format, or replay semantics.
Happy to send a PR if this direction looks good.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/lib.rs at schedule_timer, then trace Action::CreateTimer and EventKind::TimerCreated to understand the existing absolute fire-time and replay path. Add the public schedule_timer_until entry point using the existing timer machinery; done means past deadlines fire immediately and deadlines replay verbatim without changing the action or event schema.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100