Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fixed starts and reservations

A task with a fixed start should begin at a given time. cronoq treats this as a capacity commitment: it books capacity when the task is submitted and holds a worker shortly before the start.

Task::new(job)
    .start_at(when)                          // or .start_in(Duration)
    .start_tolerance(Duration::from_secs(5)) // default 1 s
    .on_miss(MissPolicy::Skip)               // default RunLate

1. Admission: reservations

The task occupies a reservation interval in its pool:

[start − tolerance,  start + estimated_duration + tolerance]

At any instant, at most floor(max_workers × reservable_fraction) reservations may overlap (reservable_fraction defaults to 0.5, and at least one reservation is always allowed). If the new interval would exceed that, submission fails with ReservationConflict. When max_workers is 2 or more, a fraction below 1 keeps part of the pool free for ad-hoc work. A pool with max_workers = 1 can be fully reserved, because one reservation is always allowed.

2. Holding a worker

Shortly before the start, the task asks for a hold: the next free slot of its pool is kept idle for it instead of taking regular work. How early depends on the pool's own behaviour:

hold_lead = min(max(tolerance, p90 run time of regular tasks in the pool), max_hold)

The idea is to hold a slot just early enough for the regular work that would otherwise take it to finish. The lead is computed from the run times of the pool's tasks without a fixed start, and it is re-evaluated as the start approaches (the first check happens at start − max_hold, max_hold = 5 minutes by default), so a pool whose tasks get slower starts holding earlier without any configuration. If no slot is free when the hold is requested, the first one to free up is held, before any queued task can take it.

Reservations starting within the autoscaler's horizon also add to the pool's desired capacity, so an autoscaled pool grows before the hold is needed.

3. Start

At the start time (and once its dependencies are satisfied) the task runs on its held slot. The recorded start time is the actual dispatch time; the pool's fixed_start_hit_rate counts the task as a hit if |actual − start| ≤ tolerance.

A tolerance of zero is allowed: the task must start exactly at its time (in practice, on the first scheduling step at or after it), and anything later is a miss.

4. Missing the start

If the task has not started by start + tolerance, its miss policy applies:

PolicyEffect
RunLate (default)Run as soon as a slot is free, ahead of all regular work.
SkipEnd with status Missed.
FailEnd with status Failed.

Typical reasons for a miss: all held capacity is in use by other fixed tasks, the pool has no workers (server mode with no connected workers), or a dependency finished late.

Interaction with other features

  • Dependencies: a fixed-start task with unmet dependencies waits for both its time and its dependencies.
  • Resources: a fixed-start task also needs its resources; if they are not available at the start time it waits for them while keeping its slot.
  • Retries: a retried attempt has no held slot and no miss policy. It waits in the due queue and takes the next free slot ahead of regular work.
  • Overload: fixed-start tasks are never shed by the shed_lowest / shed_oldest overflow policies; their capacity was promised at admission.
  • Recurring schedules create fixed-start tasks; see Recurring schedules.

Configuration

SettingDefaultWhere
Tolerance1 sper task
Miss policyRunLateper task
reservable_fraction0.5per pool
max_hold5 minper pool (PoolConfig)
default_duration1 sper pool; used when no estimate exists