Skip to content

Reminders: durable record + scheduling decisions (Orleans wiring next) - #738

Merged
rbuergi merged 1 commit into
mainfrom
feat/platform-reminders
Aug 1, 2026
Merged

rbuergi merged 1 commit into
mainfrom
feat/platform-reminders

Conversation

@rbuergi

@rbuergi rbuergi commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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 InMemoryPublishQueue would 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 — IntervalSeconds distinguishes them.

The decisions worth pinning

Case Behaviour Why
Two replicas, same due reminder live claim blocks the second; expired claim is reclaimable a pod dying mid-run must not strand it in Firing forever
One-shot fails stops (Failed) retrying an unattended publish on a loop is how you post six times
Recurring fails keeps its cadence a transient 429 must not kill the stats refresh forever
Dormant through many intervals fires once, resumes on cadence replaying 8 missed runs bursts the API right after an outage
Run finishes late next slot still on the grid adding to now would drift the cadence later every firing

11/11 green.

Next: Orleans wiring

grep finds no UseInMemoryReminderService / UseAdoNetReminderService / AddReminders anywhere in src/ or memex/ — the silo configures no reminder service at all. So the next commit must:

  1. Register a reminder table provider on the silo — ADO.NET against the existing Postgres for the real clusters, in-memory for monolith/tests.
  2. Add an IRemindable grain that registers a reminder per node record and calls back on fire.
  3. Launch the Activity on 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 ReminderSchedule stays 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

…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
Copilot AI review requested due to automatic review settings July 31, 2026 18:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + ReminderState contract types for a durable, inspectable reminder record.
  • Added ReminderSchedule pure 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,
};
}
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

959 tests  ±0   953 ✅ ±0   4m 41s ⏱️ +8s
  5 suites ±0     6 💤 ±0 
  5 files   ±0     0 ❌ ±0 

Results for commit 360a48a. ± Comparison against base commit 41f876e.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 066 tests  ±0   963 ✅ ±0   5m 7s ⏱️ -11s
   10 suites ±0   103 💤 ±0 
   10 files   ±0     0 ❌ ±0 

Results for commit 360a48a. ± Comparison against base commit 41f876e.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 114 tests  ±0   1 926 ✅ ±0   4m 44s ⏱️ -7s
   12 suites ±0     188 💤 ±0 
   12 files   ±0       0 ❌ ±0 

Results for commit 360a48a. ± Comparison against base commit 41f876e.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   12 files  ±0     12 suites  ±0   4m 20s ⏱️ +12s
  998 tests ±0    994 ✅ ±0  4 💤 ±0  0 ❌ ±0 
1 063 runs  ±0  1 059 ✅ ±0  4 💤 ±0  0 ❌ ±0 

Results for commit 360a48a. ± Comparison against base commit 41f876e.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 189 tests  +11   1 188 ✅ +11   6m 4s ⏱️ +29s
   12 suites ± 0       1 💤 ± 0 
   12 files   ± 0       0 ❌ ± 0 

Results for commit 360a48a. ± Comparison against base commit 41f876e.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 208 tests  ±0   1 207 ✅ ±0   7m 9s ⏱️ +18s
   10 suites ±0       1 💤 ±0 
   10 files   ±0       0 ❌ ±0 

Results for commit 360a48a. ± Comparison against base commit 41f876e.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   61 files  ± 0     61 suites  ±0   32m 8s ⏱️ +50s
7 534 tests +11  7 231 ✅ +11  303 💤 ±0  0 ❌ ±0 
7 599 runs  +11  7 296 ✅ +11  303 💤 ±0  0 ❌ ±0 

Results for commit 360a48a. ± Comparison against base commit 41f876e.

@rbuergi
rbuergi marked this pull request as ready for review August 1, 2026 14:32
@rbuergi
rbuergi merged commit afb8ef2 into main Aug 1, 2026
16 checks passed
@rbuergi
rbuergi deleted the feat/platform-reminders branch August 1, 2026 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants