elsa-workflows / elsa-workflows/elsa-core
CreateSchedulesStartupTask always registered despite being documented as LocalScheduler-only
- Dominant language
- C#
- Stars
- 7.9k
- Forks
- 1.5k
- Avg merge
- 15h 22m
- Merged PRs (30d)
- 114
Description
## Summary
`CreateSchedulesStartupTask` is documented and intended for the **in-process LocalScheduler** (no durable job store), but `SchedulingFeature` / shell `SchedulingFeature` **always** register it. Quartz and Hangfire replace `IWorkflowScheduler` via `elsa-extensions` yet still inherit this startup rebuild of every Cron/Timer/StartAt/Delay trigger + bookmark.
That is the core wiring root of long-standing Hangfire duplicate-on-restart reports and unnecessary O(N) schedule rebuild work when a persistent scheduler already owns the jobs.
## Evidence (main)
- Docstring on the task itself:
- `src/modules/Elsa.Scheduling/StartupTasks/CreateSchedulesStartupTask.cs` — *\"Enqueues schedule creation when using the default scheduler, which doesn't have its own persistence layer like Quartz or Hangfire.\"*
- Always registered:
- `src/modules/Elsa.Scheduling/Features/SchedulingFeature.cs` → `.AddStartupTask()`
- `src/modules/Elsa.Scheduling/ShellFeatures/SchedulingFeature.cs` → same
- Persistent schedulers only swap `WorkflowScheduler` / cron parser; they do **not** remove the startup task:
- `elsa-extensions` `QuartzSchedulerFeature` / `HangfireSchedulerFeature` (`DependsOn(typeof(SchedulingFeature))`)
- Asymmetry after rebuild:
- **Quartz** `QuartzWorkflowScheduler.ScheduleJobAsync` — trigger key = taskName (bookmark id); duplicate → `ObjectAlreadyExists` skipped (mostly idempotent, still pays load + attempt cost).
- **Hangfire** `HangfireWorkflowScheduler.ScheduleAtAsync` — `backgroundJobClient.Schedule(...)` creates a **new** job every time (no deterministic id / no unscheduling first) → duplicates accumulate.
## Why it matters
1. Hangfire: each app restart re-schedules all suspended Delay/Timer/StartAt bookmarks → duplicate `ResumeWorkflowJob` rows (#3110, elsa-extensions #121).
2. Quartz: usually safe due to trigger identity, but startup still pages and attempts to schedule the full bookmark set needlessly.
3. Local: rebuild is **required** (timers are in-memory) — gating must preserve that path.
4. Compounds #7735/#7470/#8155: every orphaned or legitimate bookmark is fed into the rebuild regardless of scheduler durability.
## Subtractive direction (not a design)
Prefer gating over new infrastructure:
1. Register `CreateSchedulesStartupTask` only when the effective `IWorkflowScheduler` is the local/default implementation (or an explicit opt-in on `SchedulingOptions`, defaulting to \"local only\").
2. Alternatively: let Quartz/Hangfire features **remove** or replace the startup task when they install (extensions change), keeping core default for Local.
3. Do not fire-and-forget duplicate Hangfire schedules as a workaround — fix identity/unschedule-first belongs with #3110 / extensions #121 / #197; this issue is the **\"should we rebuild at all?\"** gate.
## Related (do not duplicate)
- #3110 / elsa-extensions #121 — Hangfire duplicate jobs on restart (symptom).
- elsa-extensions #197 — Hangfire `UnscheduleAsync` recurring ResumeWorkflowJob gap.
- #7735 / #7470 / #8155 — backlog size / orphan StoredBookmark purge (orthogonal levers).
- #7746 / #7747 — paging + past-due stagger for the local rebuild path (still valuable when LocalScheduler is in use).
## Out of scope
- Changing Quartz trigger/cancellation semantics.
- Closing #3110 / extensions #121 (those remain the Hangfire-side trackers; this is the core registration bug).
Contributor guide
Research direction
Start with src/modules/Elsa.Scheduling/StartupTasks/CreateSchedulesStartupTask.cs and the registrations in SchedulingFeature.cs and ShellFeatures/SchedulingFeature.cs. Then read QuartzSchedulerFeature, HangfireSchedulerFeature, and their scheduler implementations to understand how they replace IWorkflowScheduler. Done means the startup task remains available for LocalScheduler but is not registered for persistent schedulers, without changing their scheduling semantics.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100