Skip to content

Recurring schedules: add a manual trigger ("run now") to IRecurringScheduleControl #4446

Closed

Description

@jeremydmiller

Split out of #4437 (GAP-3). Scoped to the manual "run now" verb; attribution and durable last-run state are separate issues.

The gap

IRecurringScheduleControl (src/Wolverine/Runtime/Recurring/RecurringScheduleControl.cs:39-60) exposes PauseAsync, ResumeAsync and QueryAsync — no way to fire an occurrence on demand. Today an operator hand-publishes the message type out of band, which bypasses the occurrence and deduplication machinery entirely.

The constraint that shapes the design

The caller is very often not on the node running the agent. That is already why PauseAsync goes through the store rather than marking the local agent, and the reasoning is written down at RecurringScheduleControl.cs:83-88: a local mark that a resume on another node cannot clear would pause the schedule forever. TriggerAsync has the identical constraint.

Suggested shape — follow the dead-letter replay precedent

The house answer to "ask the owning agent to do something from another node" already exists, and it is a durable flag the agent picks up:

  • MessageDatabase.DeadLetterAdminService.cs:193-205 sets replayable = true.
  • MoveReplayableErrorMessagesToIncomingOperation.cs:25 is the durability agent acting on it.
  • Surfaced as IDeadLetters.ReplayAsync (IDeadLetters.cs:73), over HTTP (DeadLettersEndpointExtensions.cs:44) and via CLI (StorageCommand.cs:81).

Applied here: TriggerAsync(name) sets a flag/requested-at on the schedule's tracking row, and the agent's existing tick honours it and publishes one occurrence through publishOccurrenceAsync (currently private, RecurringMessageAgent.cs:382). Add it to IRecurringScheduleControl next to the existing verbs, reusing assertKnown (:117-123) so an unregistered name throws UnknownRecurringScheduleException as pause/resume already do.

A cheaper variant worth considering when a pending occurrence already exists: IScheduledMessages.RescheduleAsync (MessageDatabase.ScheduledMessages.cs:98-121) moves an existing scheduled envelope's execution_time, so setting it to now is effectively "run it now" with no new publish at all.

Open question — deduplication

The occurrence dedup id is derived purely from the occurrence instant (RecurringMessage.cs:55-58), and registering a schedule turns deduplication on (RecurringMessageCollection.cs:112). Two manual triggers inside the same instant would therefore collapse to a single handling. Whether a manual trigger should carry a distinct id (so repeated "run now" really runs each time) or deliberately inherit the collapse is the main semantic decision here.

Also worth deciding as part of this: whether triggering a paused schedule is allowed.

🤖 Filed with Claude Code from the #4437 analysis

Activity

  1. added a commit that references this issue on Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions