Fix scheduling startup backlog catch-up#7747
Conversation
Rebuild local schedules in bounded pages and stagger past-due specific-instant catch-up so orphaned scheduling bookmarks do not flood dispatch during startup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Greptile SummaryThis PR forward-ports the scheduling startup backlog catch-up fix from release/3.8.0 to
Confidence Score: 4/5Safe to merge for the paging, staggering, and cache-key fixes; the catch-up path in DefaultTriggerScheduler still re-schedules the same past-due StartAt triggers on every subsequent restart because the trigger records are never removed or marked after dispatch. The paged startup loading, PastDueScheduleStaggerer, and CachingTriggerStore cache-key corrections are all well-implemented and well-tested. The one open concern — raised in the prior review — is that DefaultTriggerScheduler now schedules past-due StartAt triggers but does not remove or flag them afterwards, so each application restart will re-trigger the same one-shot StartAt workflows for as long as their trigger records remain in the store. src/modules/Elsa.Scheduling/Services/DefaultTriggerScheduler.cs — the catch-up scheduling path does not clean up or mark past-due one-shot StartAt triggers after dispatch.
|
| Filename | Overview |
|---|---|
| src/modules/Elsa.Scheduling/Services/DefaultTriggerScheduler.cs | Past-due StartAt triggers are now scheduled instead of skipped; trigger records are not removed/marked after catch-up, so each restart will re-schedule the same past-due triggers (flagged in prior review). |
| src/modules/Elsa.Scheduling/StartupTasks/CreateSchedulesStartupTask.cs | Replaces single unbounded query with a paged while-loop for both triggers and bookmarks; termination logic is correct via Items.Count==0 and nextOffset>=TotalCount guards. |
| src/modules/Elsa.Scheduling/Services/PastDueScheduleStaggerer.cs | New service that distributes past-due schedule activations over a bounded window using an atomic sequence counter; logic is correct and thread-safe. |
| src/modules/Elsa.Scheduling/ScheduledTasks/ScheduledSpecificInstantTask.cs | Adds a 6-arg ActivatorUtilities constructor that accepts PastDueScheduleStaggerer; the 5-arg constructor delegates to a static default instance, keeping backward compatibility for non-DI callers. |
| src/modules/Elsa.Scheduling/Options/SchedulingOptions.cs | New options class exposing StartupSchedulePageSize, MinimumPastDueScheduleDelay, PastDueScheduleStaggerInterval, and PastDueScheduleStaggerWindow with sensible defaults. |
| src/modules/Elsa.Persistence.EFCore/Modules/Runtime/BookmarkStore.cs | Adds paged FindManyAsync overload using CountAsync then QueryAsync with OrderBy+Paginate; standard two-phase pattern consistent with other EFCore stores in this codebase. |
| src/modules/Elsa.Workflows.Runtime/Stores/CachingTriggerStore.cs | Fixes cache key collision by including pageArgs (and order) in the hash for both paginated FindManyAsync overloads. |
| src/modules/Elsa.Workflows.Runtime/Contracts/IBookmarkStore.cs | Adds new paged FindManyAsync method to the interface with a doc comment reminding implementors to page at the persistence layer. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant ST as CreateSchedulesStartupTask
participant TStore as ITriggerStore
participant BStore as IBookmarkStore
participant TSched as ITriggerScheduler
participant BSched as IBookmarkScheduler
participant DTS as DefaultTriggerScheduler
participant Staggerer as PastDueScheduleStaggerer
participant Task as ScheduledSpecificInstantTask
ST->>TStore: "FindManyAsync(filter, page=0, size=N)"
TStore-->>ST: "Page<StoredTrigger>(items, totalCount)"
ST->>TSched: ScheduleAsync(items)
TSched->>DTS: (delegate)
DTS->>DTS: "executeAt < now? log catch-up, no skip"
DTS->>Task: create ScheduledSpecificInstantTask(startAt)
Task->>Staggerer: GetDelay(negativeDelay)
Staggerer->>Staggerer: Interlocked.Increment(_sequence)
Staggerer-->>Task: "minimumDelay + slot*interval"
Task->>Task: Start timer with staggered delay
ST->>TStore: "FindManyAsync(filter, page=N, size=N)"
TStore-->>ST: Page (empty or next batch)
Note over ST: Loop until totalCount exhausted
ST->>BStore: "FindManyAsync(bookmarkFilter, page=0, size=N)"
BStore-->>ST: "Page<StoredBookmark>"
ST->>BSched: ScheduleAsync(items)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant ST as CreateSchedulesStartupTask
participant TStore as ITriggerStore
participant BStore as IBookmarkStore
participant TSched as ITriggerScheduler
participant BSched as IBookmarkScheduler
participant DTS as DefaultTriggerScheduler
participant Staggerer as PastDueScheduleStaggerer
participant Task as ScheduledSpecificInstantTask
ST->>TStore: "FindManyAsync(filter, page=0, size=N)"
TStore-->>ST: "Page<StoredTrigger>(items, totalCount)"
ST->>TSched: ScheduleAsync(items)
TSched->>DTS: (delegate)
DTS->>DTS: "executeAt < now? log catch-up, no skip"
DTS->>Task: create ScheduledSpecificInstantTask(startAt)
Task->>Staggerer: GetDelay(negativeDelay)
Staggerer->>Staggerer: Interlocked.Increment(_sequence)
Staggerer-->>Task: "minimumDelay + slot*interval"
Task->>Task: Start timer with staggered delay
ST->>TStore: "FindManyAsync(filter, page=N, size=N)"
TStore-->>ST: Page (empty or next batch)
Note over ST: Loop until totalCount exhausted
ST->>BStore: "FindManyAsync(bookmarkFilter, page=0, size=N)"
BStore-->>ST: "Page<StoredBookmark>"
ST->>BSched: ScheduleAsync(items)
Reviews (2): Last reviewed commit: "Use stable CShells packages from NuGet" | Re-trigger Greptile
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Updated the package source mapping so the already-pinned CShells 0.0.28 stable packages restore from NuGet.org instead of the stale cshells Feedz source.\n\nValidation:\n- |
Forward-port/main PR for the scheduling startup backlog fix from #7746.
This keeps the same main-targeted change available after #7746 is retargeted to release/3.8.0.
Validation from original PR/session: