Repository navigation
Reminders: durable record + scheduling decisions (Orleans wiring next) - #738
Merged
Merged
Conversation
…ted)
First half of the platform reminder primitive: a Reminder NODE plus the
pure functions that decide when it runs. The Orleans wiring lands next —
the silo currently configures no reminder service at all.
A reminder is a node, not an in-process timer, because a timer dies with
the pod and an in-memory queue is pod-local: with two replicas only one
knows the work exists and a restart loses it silently. That is exactly
how core's never-registered InMemoryPublishQueue would have lost every
scheduled post.
One-shot (publish this post at its slot) and recurring (refresh stats
4×/day) are the same record; IntervalSeconds distinguishes them.
The decisions worth pinning, all covered by tests:
- a live claim blocks a second replica; an EXPIRED claim is reclaimable,
so a pod that dies mid-run cannot strand a reminder in Firing forever
- a failed one-shot STOPS (retrying an unattended publish on a loop is
how you post six times) while a failed recurring one keeps its cadence
(a transient 429 must not kill the stats refresh forever)
- a reminder dormant through many intervals fires ONCE and resumes on
cadence rather than replaying every missed slot — replaying would
burst the API right after an outage
- slots step the interval instead of adding to `now`, so a slow run does
not drift the cadence later on every firing
11/11 green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9UHhiZ3AamCogfjJBztoC
Contributor
There was a problem hiding this comment.
Pull request overview
Introduces the first half of a durable reminder primitive by adding a contract-level Reminder record plus a pure “decision” layer (ReminderSchedule) that determines due-ness, claiming/lease expiry, and rescheduling behavior. This sets up the scheduling semantics to be deterministic and unit-tested before Orleans wiring is added in a follow-up commit.
Changes:
- Added
Reminder+ReminderStatecontract types for a durable, inspectable reminder record. - Added
ReminderSchedulepure functions for due/claim/lease-expiry and next-occurrence calculations. - Added unit tests pinning the intended behaviors (one-shot vs recurring, claim reclaim, drift-free cadence, dormant catch-up behavior).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| test/MeshWeaver.Data.Test/ReminderScheduleTest.cs | Adds unit tests that pin reminder scheduling/claiming decisions as pure-function behavior. |
| src/MeshWeaver.Mesh.Contract/ReminderSchedule.cs | Adds the pure scheduling/claiming/rescheduling decision helpers for reminders. |
| src/MeshWeaver.Mesh.Contract/Reminder.cs | Adds the durable reminder record and lifecycle state enum in the mesh contract. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+27
to
+29
| /// A durable, mesh-native TIME TRIGGER: at <see cref="DueAt"/> the reminder service launches an | ||
| /// Activity against <see cref="TargetPath"/>. One-shot when <see cref="IntervalSeconds"/> is null | ||
| /// (publish this post at its slot), recurring otherwise (refresh stats four times a day). |
Comment on lines
+148
to
+159
| public static Reminder Recurring( | ||
| string targetPath, string kind, DateTime firstDueAt, int intervalSeconds, string? argument = null) => | ||
| new() | ||
| { | ||
| TargetPath = targetPath, | ||
| Kind = kind, | ||
| Argument = argument, | ||
| DueAt = firstDueAt, | ||
| IntervalSeconds = intervalSeconds, | ||
| State = ReminderState.Pending, | ||
| }; | ||
| } |
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First half of the reminder primitive: the durable record and the pure decisions. Orleans wiring is the next commit — see below.
Why a node, not a timer
A timer dies with the pod; an in-memory queue is pod-local, so with two replicas only one knows the work exists and a restart loses it silently. That is precisely how core's never-registered
InMemoryPublishQueuewould have lost every scheduled post. A reminder is a node: it survives restarts, is inspectable, and is cancellable like any other content.One-shot (publish this post at its slot) and recurring (refresh stats 4×/day) are the same record —
IntervalSecondsdistinguishes them.The decisions worth pinning
FiringforeverFailed)nowwould drift the cadence later every firing11/11 green.
Next: Orleans wiring
grepfinds noUseInMemoryReminderService/UseAdoNetReminderService/AddRemindersanywhere insrc/ormemex/— the silo configures no reminder service at all. So the next commit must:IRemindablegrain that registers a reminder per node record and calls back on fire.Activityon the target node's hub from that callback.Orleans guarantees a single activation per grain, so its reminders give exactly-once firing on their own. The claim/lease in
ReminderSchedulestays as defence for the non-Orleans hosting paths (monolith) and for reclaiming a crashed run — it is not redundant with Orleans, it covers the case where the activity itself dies.🤖 Generated with Claude Code