jonnifyredis

R4.02.3 · dive 3

Start-to-start cadence

The registry holds only the next run. When a job is produced the next timestamp is computed and the single score rewritten — start-to-start, anchored to the clock, not start-to-finish.

A scheduler keeps one entry per schedule, scored by the next run. Start-to-start measures the next run from this run's scheduled time plus the interval, so the cadence stays on the clock. Start-to-finish measures from when this run finished, so a slow run pushes every later run later. Set the run duration on the right and watch start-to-finish drift while start-to-start holds.

Three fires of an every-15-min schedule · start-to-start holds, start-to-finish drifts by the run duration

4
clock from 09:00 — every 15 min, three fires start-to-start (next = this run + 15) start-to-finish (next = finish + 15) run takes 4 min · start-to-finish drifts 4 min per fire

One entry, rewritten each cycle

The registry is a sorted set scored by next-run time. A schedule occupies one member. When the scheduler produces the next job, it computes the next run from the cadence and ZADDs the new score against the same member — rewriting the single entry, not appending a new one.

The registry's size is the number of distinct schedules, never the number of runs. A schedule that has fired ten thousand times still holds exactly one row. The question this dive answers is what the next run is computed from. Advance the fire count below: the same row's score is rewritten with the next run, and the registry stays at one entry.

One row, rewritten each fire · the score advances, the registry stays at one entry

0
emq:{queue}:repeat (ZSET) — one row, rewritten each fire sched:reports → score = 09:00 (next run) fire 0 · ZADD rewrote the one row to 09:00 registry size: 1 entry (never appended)

The registry holds one entry per schedule, scored by the next run and rewritten each cycle; its size tracks schedules, not runs.

Start-to-start vs start-to-finish

Consider an every-15-minute schedule where a run takes 4 minutes. Start-to-start: the run scheduled for 09:00 produces the next at 09:15, the one after at 09:30 — the cadence is anchored to the clock and the 4-minute run does not move it. Start-to-finish: the 09:00 run finishes at 09:04, so the next is scheduled for 09:19, which finishes at 09:23, so the next is 09:38 — every run drifts later by the run duration, and after enough cycles the schedule has slid far off the clock.

Start-to-start keeps fifteen-minute marks at :00, :15, :30, :45; start-to-finish lets them creep. The interval scheduler computes the next millisecond from the previous scheduled millisecond plus the gap, never from a finish time.

start-to-start
next = thisRun + interval — anchored to the scheduled time; the run duration does not move it.
start-to-finish
next = finish + interval — anchored to the finish; a slow run drifts every later run.
drift
Per-fire slip of start-to-finish equals the run duration; it compounds across cycles.

Start-to-start anchors the next run to the scheduled time, so the cadence holds; start-to-finish anchors it to the finish, so a slow run drifts every later run.

The marker rings at the next run

Once the next run is known, the scheduler writes the wake marker so a blocked worker wakes at the right moment rather than on a poll tick. The marker carries the next fire-time as its score — ZADD markerKey nextTimestamp "1" — the same marker mechanism the blocking-vs-polling module uses, here scored by the scheduler's next run. The worker parked on the marker wakes exactly when the next scheduled run is due.

After computing the next run, the scheduler scores the wake marker by it, so the parked worker wakes at the next run rather than on a poll tick.

In EchoMQ — next-millis from the schedule

The interval path in addJobScheduler-11.lua computes the next run start-to-start and rewrites the single registry entry.

-- addJobScheduler-11.lua (real)
nextMillis = prevMillis + every                -- start-to-start: from the prior SCHEDULED time
if nextMillis < now then
  nextMillis = math.floor(now / every) * every + every   -- snap forward to the next slot
end
-- storeJobScheduler rewrites the one registry entry
ZADD repeatKey nextMillis schedulerId
ZADD markerKey nextTimestamp "1"          -- wake the worker at the next run

prevMillis + every is the start-to-start step; the snap-forward handles a missed slot without abandoning the clock anchor. The next run is derived from the schedule, never from how long a run took. The full scheduler-vs-delayed reconciliation across a worker pool — how a missed run, an overlapping run, and a paused queue interact — is the scheduler subsystem the EchoMQ course covers.

the pattern

A textbook delayed queue scores one run and is done; a scheduler rewrites its single score each cycle, and how it computes the next score governs whether the cadence holds. Start-to-start anchors to the clock; the registry stays one entry; the marker rings at the next run.

in EchoMQ

addJobScheduler-11.lua computes nextMillis = prevMillis + every (start-to-start), snaps forward on a missed slot, rewrites the :repeat entry with ZADD repeatKey nextMillis schedulerId, and scores the wake marker with ZADD markerKey nextTimestamp "1".

The pattern says compute the next run from the schedule so the cadence holds; in EchoMQ the step is prevMillis + every and the rewrite is one ZADD on the :repeat entry, with a second on the marker.

Door · not depth The full scheduler-vs-delayed reconciliation — a missed run while the queue was paused, an overlapping run, and the offset arithmetic that keeps a long-running schedule on its slots — is the scheduler subsystem the dedicated EchoMQ course covers at E6 · Lifecycle controls. This dive teaches the start-to-start rewrite and the marker that rings at the next run; that course teaches the reconciliation around it.

References

The command that rewrites the single registry score, the probe that reads the prior scheduled time, the sorted set the schedule lives on, and the queue protocol whose job-scheduler cadence EchoMQ ports.

Sources

Related in this course