Plugin background work is currently a loop the host writes itself, in PluginHost.RunTaskLoop:
plugin.TaskCts = new CancellationTokenSource();
foreach (var task in tasks)
_ = RunTaskLoop(task, plugin.Provider, plugin.TaskCts.Token);
That has a handful of problems that are only going to get worse as more plugins ship sync tasks:
- Fire and forget.
_ = RunTaskLoop(...) leaves the loop unobserved; nothing supervises it or restarts it if it dies.
- Interval only, and it drifts. The delay starts after the run finishes, so the real period is interval plus duration. There is no way to say "06:00 daily".
- Nothing survives a restart.
PluginSchedulerRegistry is a ConcurrentDictionary, so run history and next-run times reset. Every task then fires at once on startup, and any run that was due during downtime is silently skipped.
- No retries. A failed run waits a full interval before trying again.
- No overlap protection. Two backend replicas would each run every task, syncing everything twice.
TickerQ covers all of it: EF Core persistence for the schedule and run history, cron and one-shot tickers, per-job Retries with RetryIntervals, and a dashboard for live status. It is source-generated rather than reflection-based.
builder.Services.AddTickerQ(options =>
{
options.AddEntityFrameworkCorePersistence()
.AddDbContext<SchulyDbContext>();
});
The hard part, worth settling before any code. TickerQ discovers ticker functions with a source generator at compile time, while plugins are loaded at runtime into collectible AssemblyLoadContexts (see PluginLoadContext). A generator running inside a plugin assembly emits its registrations into that assembly, so the host has to pick them up across load contexts, and unloading a plugin has to withdraw its schedules. That interaction is the actual risk in this migration, not the scheduling itself.
GET /api/plugins should keep working, backed by TickerQ instead of the in-memory registry, so PluginSchedulerRegistry can go.
Depends on schulydev/SchulyPluginAbstractions#138.
Plugin background work is currently a loop the host writes itself, in
PluginHost.RunTaskLoop:That has a handful of problems that are only going to get worse as more plugins ship sync tasks:
_ = RunTaskLoop(...)leaves the loop unobserved; nothing supervises it or restarts it if it dies.PluginSchedulerRegistryis aConcurrentDictionary, so run history and next-run times reset. Every task then fires at once on startup, and any run that was due during downtime is silently skipped.TickerQ covers all of it: EF Core persistence for the schedule and run history, cron and one-shot tickers, per-job
RetrieswithRetryIntervals, and a dashboard for live status. It is source-generated rather than reflection-based.The hard part, worth settling before any code. TickerQ discovers ticker functions with a source generator at compile time, while plugins are loaded at runtime into collectible
AssemblyLoadContexts (seePluginLoadContext). A generator running inside a plugin assembly emits its registrations into that assembly, so the host has to pick them up across load contexts, and unloading a plugin has to withdraw its schedules. That interaction is the actual risk in this migration, not the scheduling itself.GET /api/pluginsshould keep working, backed by TickerQ instead of the in-memory registry, soPluginSchedulerRegistrycan go.Depends on schulydev/SchulyPluginAbstractions#138.