Skip to content

feat(jobs): add declarative task scheduler API (closes #159)#207

Merged
antosubash merged 2 commits into
mainfrom
task-scheduler
May 20, 2026
Merged

feat(jobs): add declarative task scheduler API (closes #159)#207
antosubash merged 2 commits into
mainfrom
task-scheduler

Conversation

@antosubash
Copy link
Copy Markdown
Owner

Summary

  • Adds a fluent IScheduler API in BackgroundJobs.Contracts so modules can declare recurring jobs at startup via services.AddScheduledJobs(scheduler => scheduler.Job<T>().DailyAt("02:00")...). Builder supports Cron / EveryMinutes / Hourly / Daily / DailyAt / Weekdays / Timezone / WithoutOverlapping / OnOneServer / WithPayload.
  • Hosted SchedulerService ticks every 30 s (configurable). Each tick reconciles in-memory definitions to a new ScheduledJobStates table, optionally acquires the OnOneServer lease, then enqueues due jobs through the existing IJobQueue. Per-definition try/catch keeps one bad cron from stopping the loop.
  • WithoutOverlapping is backed by a per-job mutex row (JobMutexes). JobProcessorService releases the mutex on completion or failure of any schedule:-sentinel entry. OnOneServer is backed by a single-lease row (JobLeases) with TTL-based takeover.
  • New sm jobs list-scheduled CLI reads ScheduledJobStates via raw ADO.NET (Sqlite or Postgres), falls back to appsettings.json, and renders Name / Job type / Cron / TZ / Next run / Last run / flags.
  • Adds docs/scheduler.md covering quick-start, DSL reference, reconciliation/mutex/lease semantics, and configuration knobs.

Closes #159.

Test plan

  • dotnet build clean across the whole solution under TreatWarningsAsErrors.
  • dotnet test — full suite green (1015 tests, 0 failures), including 31 new tests under modules/BackgroundJobs/tests/.../Scheduler/:
    • ScheduledJobBuilderTests — DSL → cron rendering + validation (bad time, bad timezone, duplicate names, default name).
    • DatabaseJobMutexTests — first-wins, contention, same-owner refresh, takeover after TTL, release.
    • DatabaseInstanceLeaderTests — same shape for the OnOneServer lease.
    • SchedulerServiceTickTests — due-job enqueue, future-job skip, bad-cron isolation, OnOneServer lets only one host enqueue, WithoutOverlapping skips when the mutex is held externally.
  • sm jobs list-scheduled --help renders correctly via the new branch.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 20, 2026

Deploying simplemodule-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 67f2010
Status: ✅  Deploy successful!
Preview URL: https://3f5e576f.simplemodule-website.pages.dev
Branch Preview URL: https://task-scheduler.simplemodule-website.pages.dev

View logs

Layers a Laravel-style fluent scheduling surface on top of the existing
BackgroundJobs infrastructure so modules can declare recurring jobs at
startup time.

- IScheduler / IScheduledJob<T> with Cron/Daily/DailyAt/EveryMinutes/
  Hourly/Weekdays/Timezone/WithoutOverlapping/OnOneServer/WithPayload
- Modules call services.AddScheduledJobs(scheduler => ...) from their
  ConfigureServices; first call creates the shared SchedulerRegistry
- Hosted SchedulerService ticks every 30 s (configurable): reconciles
  registered definitions to ScheduledJobStates, optionally takes the
  OnOneServer lease, then enqueues due jobs through IJobQueue
- WithoutOverlapping backed by JobMutexes; mutex released by
  JobProcessorService when the job finishes (success or failure)
- OnOneServer backed by JobLeases with TTL-based takeover
- Per-definition try/catch so a bad cron never stops the loop
- sm jobs list-scheduled CLI prints next/last run via raw ADO.NET
  (Sqlite + Postgres) against the configured database
- 31 xUnit tests covering DSL, mutex contention, leader election,
  tick enqueue path, failure isolation, and WithoutOverlapping skip
- docs/scheduler.md walks through registration, semantics, and config
- Reuse CronCalculator across BackgroundJobsService and JobProcessorService —
  removes three duplicate "Split(' ').Length > 5 ? IncludeSeconds : Standard"
  blocks. Adds CronCalculator.GetNextOccurrenceUtc(...) for the legacy path.
- Auto-register module job types from AddScheduledJobs so callers no longer
  need a separate AddModuleJob<T>() call — single point of registration.
- Skip the no-op UPDATE in SchedulerReconciler when nothing has changed
  (avoids per-tick write storm).
- Fresh DbContext scope per due definition in SchedulerService — prevents
  the mutex / queue SaveChangesAsync from prematurely flushing other tracked
  ScheduledJobState mutations mid-tick.
- Make IJobMutex, IInstanceLeader, DatabaseJobMutex, DatabaseInstanceLeader,
  and SchedulerService internal; only Contracts surface stays public.
- Add [NoDtoGeneration] to ModuleJobRegistration (was leaking into types.ts).
- Promote "mutex:" literal to SchedulerOptions.MutexPrefix.
- Simplify CLI ShortType() to mirror BackgroundJobsInternalConstants.GetShortTypeName.
@antosubash antosubash merged commit 5ef6222 into main May 20, 2026
6 checks passed
@antosubash antosubash deleted the task-scheduler branch May 20, 2026 13:54
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.

Add Task Scheduler API on top of BackgroundJobs (cron-like fluent scheduling)

1 participant