microsoft / microsoft/duroxide

Add an absolute-deadline timer variant (e.g. `schedule_timer_until(deadline)`)

Open
#34 0 comments 0 reactions 0 assignees View on GitHub

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 - now to a non-negative Duration (duration_since returns Err when the deadline is already in the past). A schedule_timer_until can define this once (past deadline ⇒ fire immediately).
  • One fewer recorded syscall. The deadline - now form needs a utc_now() reading purely to compute the delay. With an absolute timer the caller often already has the deadline and doesn't need an extra recorded utc_now event 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.