Repository navigation
Background Scheduler
Recurring / delayed / queued work runs in-process via TickerQ. One library, one operational store, one migrations history — see ADR-0019.
| File | Role |
|---|---|
src/ERP/Infrastructure/Persistence/PlanDbContext.cs |
Hosts TickerQ's three entity configurations (TimeTickers, CronTickers, CronTickerOccurrences) under the ticker schema |
src/ERP/Infrastructure/Persistence/PersistenceServiceCollectionExtensions.cs |
AddTickerQ(...).AddOperationalStore(...UseApplicationDbContext<PlanDbContext>(IgnoreModelCustomizer)) |
src/ApiService/Program.cs |
app.UseTickerQ() after migrations apply |
src/ApiService/AutoIngestStartup.cs |
Reconciles the auto-ingest cron against AutoIngest:Enabled at startup |
src/ERP/Infrastructure/AutoIngestJob.cs |
[TickerFunction("auto-ingest-sav-watcher")] job body |
src/ERP/Infrastructure/AutoIngestOptions.cs |
Options bound from FactoryState:Satisfactory:AutoIngest
|
Why apply TickerQ configurations directly in
OnModelCreating?ConfigurationType.UseModelCustomizer(the default) relies on host DI being active during model build, which the design-timeIDesignTimeDbContextFactory<T>intentionally bypasses fordotnet ef migrations add. DirectApplyConfigurationworks at both runtime and design time so the model snapshot stays consistent.
FactoryState:Satisfactory:AutoIngest:Enabled = true registers a cron entry
ticking once per minute (0 * * * * *). The job polls the configured
SaveGames directory, compares LastWriteTimeUtc against the currently-loaded
source, and dispatches IngestSaveCommand via Wolverine when a newer save is
present.
Setting Enabled = false (the default) deletes the cron entry — the
acceptance criterion is "no background activity" when disabled, so attribute-
level cron registration would always tick and isn't a fit.
The job opens a service scope per tick (Wolverine bus is scoped). Cost is negligible at 1 tick/min.
public sealed class MyJob(IServiceScopeFactory scopeFactory, ILogger<MyJob> log)
{
[TickerFunction("my-job-name")]
public async Task RunAsync(TickerFunctionContext ctx, CancellationToken ct)
{
await using var scope = scopeFactory.CreateAsyncScope();
var bus = scope.ServiceProvider.GetRequiredService<IMessageBus>();
// …
}
}Register in AddErpInfrastructure and either:
-
Attribute-level cron — add
[TickerFunction("name", cronExpression: "...")]for jobs that always run. -
Imperative reconcile at startup — for jobs whose schedule depends on
config (mirror
AutoIngestStartup.EnsureCronRegistrationAsync).
Want to see what's queued? The TickerQ tables live alongside plans in the same SQLite/Postgres database:
-- SQLite
SELECT * FROM "ticker.TimeTickers";
SELECT * FROM "ticker.CronTickers";
SELECT * FROM "ticker.CronTickerOccurrences" ORDER BY "ExecutionTime" DESC LIMIT 20;(Postgres uses the schema-qualified ticker.TimeTickers etc.)
Start here
How it works
Reference
Off-wiki