elsa-workflows / elsa-workflows/elsa-core

CreateSchedulesStartupTask always registered despite being documented as LocalScheduler-only

Open
#8,156 0 comments 0 reactions 0 assignees View on GitHub
bug core elsa 3 maintenance performance prio low tech debt triaged
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.