Skip to content

Replace the hand-rolled plugin task scheduler with TickerQ #233

Description

@PianoNic

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions