You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Core primitives to make recurring/cron schedules operable: attribute occurrences back to their
schedule, record last-run outcome, and allow a manual "run now". These are the framework-side pieces
that a monitoring console (e.g. CritterWatch) then surfaces -- see Related issues below.
Environment
WolverineFx 6.34.0, Marten 9.33.0 on PostgreSQL, RabbitMQ, .NET 9. Observed while running a sample of
40 recurring schedules (full cron grammar + time zones) with three deliberately-failing schedules.
So: current + next occurrence and pause/resume exist. Nothing about the actual runs.
Gaps (core)
GAP-1 -- No per-run execution record (last run time / status / error)
No LastRunAt, LastStatus, LastError, LastDurationMs, or success/failure on the descriptor or
tracking row. Verified behaviorally: FailingHealthProbe fires every 20s and throws every time, yet
nothing on the schedule reflects that its last occurrence failed.
GAP-3 -- No manual re-trigger ("run now")
IRecurringScheduleControl exposes only Pause/Resume/query. There's no "trigger now / run once" verb.
Today you'd hand-publish the message type out of band, which also bypasses the occurrence/dedup
machinery.
GAP-4 -- Occurrences aren't attributed to their schedule
A published occurrence carries its message type but not the schedule name or the scheduled
occurrence time. So a failure/DLQ entry -- or an OTel span -- can't be tied back to which schedule /
which scheduled run produced it, especially when two schedules share a message type or a type is
also sent ad hoc. This is the prerequisite for GAP-1 and for OTel correlation.
Run history (GAP-2) and full spans are intentionally left to OpenTelemetry -- see the design
discussion. Core only needs last-run state + attribution + trigger.
Design discussion (from the thread)
The ask (paraphrased): record the state/error of each run at the cron-job level (rather than
looking it up in the DLQ), keep a history of runs, show if the last run failed, and allow manual
re-trigger -- typical schedule-job use cases.
Jeremy's responses:
"Wonder if it would help to move them to being sagas. Hmm."
"There is already some of that. I purposely didn't try to persist the state of the run because we
can get that off of open-telemetry data."
"Yup. Wanna keep a list of all of this on an issue?"
Trade-offs:
OTel as the source of run state is great for dashboards/tracing but limited as the authoritative
"did the last run fail?": it's often sampled, retention-limited, and lives in an external
backend a console can't cheaply query per schedule. A small per-schedule "last run:
{time, status, error}" record is cheap and directly drives a status badge + the re-trigger decision
-- without persisting full history. Reasonable split: last-run = tiny durable record; history/spans = OTel.
Sagas: a saga per schedule could hold last-run/next-run/pause + a bounded recent-runs ring and
expose trigger/pause/resume as saga messages -- a natural fit for per-schedule state + control. A
saga per occurrence is likely too heavy (churn) unless a run genuinely spans steps/time. Worth
prototyping against the descriptor/tracking-row approach.
Suggested minimal, additive direction
Attribution: stamp each occurrence envelope with schedule-name + occurrence-time headers
(small, no new storage; enables GAP-4 and OTel correlation).
Last-run state: add LastRunAt / LastStatus / LastError to the recurring tracking row and RecurringScheduleDescriptor (last-run only, not history).
Trigger: add TriggerAsync(name) to IRecurringScheduleControl that publishes one occurrence
through the normal path (respecting occurrence/dedup).
History/spans remain in OTel.
Reproduction
Self-contained sample available: CronDemoService (40 recurring schedules across the full cron grammar
four time zones + three deliberately-failing schedules).
Core primitives to make recurring/cron schedules operable: attribute occurrences back to their
schedule, record last-run outcome, and allow a manual "run now". These are the framework-side pieces
that a monitoring console (e.g. CritterWatch) then surfaces -- see Related issues below.
Environment
WolverineFx 6.34.0, Marten 9.33.0 on PostgreSQL, RabbitMQ, .NET 9. Observed while running a sample of
40 recurring schedules (full cron grammar + time zones) with three deliberately-failing schedules.
What exists today (verified against source)
Per-schedule surface --
RecurringScheduleDescriptor(
Wolverine/Configuration/Capabilities/RecurringScheduleDescriptor.cs):Runtime control --
IRecurringScheduleControl(
Wolverine/Runtime/Recurring/RecurringScheduleControl.cs):Tracking row --
RecurringMessageRecord(Wolverine/Persistence/Durability/IRecurringMessageStore.cs):So: current + next occurrence and pause/resume exist. Nothing about the actual runs.
Gaps (core)
GAP-1 -- No per-run execution record (last run time / status / error)
No
LastRunAt,LastStatus,LastError,LastDurationMs, or success/failure on the descriptor ortracking row. Verified behaviorally:
FailingHealthProbefires every 20s and throws every time, yetnothing on the schedule reflects that its last occurrence failed.
GAP-3 -- No manual re-trigger ("run now")
IRecurringScheduleControlexposes only Pause/Resume/query. There's no "trigger now / run once" verb.Today you'd hand-publish the message type out of band, which also bypasses the occurrence/dedup
machinery.
GAP-4 -- Occurrences aren't attributed to their schedule
A published occurrence carries its message type but not the schedule name or the scheduled
occurrence time. So a failure/DLQ entry -- or an OTel span -- can't be tied back to which schedule /
which scheduled run produced it, especially when two schedules share a message type or a type is
also sent ad hoc. This is the prerequisite for GAP-1 and for OTel correlation.
Design discussion (from the thread)
The ask (paraphrased): record the state/error of each run at the cron-job level (rather than
looking it up in the DLQ), keep a history of runs, show if the last run failed, and allow manual
re-trigger -- typical schedule-job use cases.
Jeremy's responses:
Trade-offs:
"did the last run fail?": it's often sampled, retention-limited, and lives in an external
backend a console can't cheaply query per schedule. A small per-schedule "last run:
{time, status, error}" record is cheap and directly drives a status badge + the re-trigger decision
-- without persisting full history. Reasonable split: last-run = tiny durable record;
history/spans = OTel.
expose trigger/pause/resume as saga messages -- a natural fit for per-schedule state + control. A
saga per occurrence is likely too heavy (churn) unless a run genuinely spans steps/time. Worth
prototyping against the descriptor/tracking-row approach.
Suggested minimal, additive direction
schedule-name+occurrence-timeheaders(small, no new storage; enables GAP-4 and OTel correlation).
LastRunAt / LastStatus / LastErrorto the recurring tracking row andRecurringScheduleDescriptor(last-run only, not history).TriggerAsync(name)toIRecurringScheduleControlthat publishes one occurrencethrough the normal path (respecting occurrence/dedup).
Reproduction
Self-contained sample available:
CronDemoService(40 recurring schedules across the full cron grammarRelated issues
timestamptzwrite bug) -- the durable path that would carry the new
LastRun*fields is currently broken fornon-UTC schedules.
the console view that surfaces last-run, history, trigger, and failure attribution depends on the
core surface proposed here.