Lightweight startup readiness (ignition) coordination library for .NET with timeouts, policies, tracing, health checks, and rich diagnostics.
$ dotnet add package Veggerby.IgnitionA lightweight, extensible startup readiness ("ignition") coordination library for .NET applications. Register ignition signals representing asynchronous initialization tasks (cache warmers, external connections, background services) and await them collectively with rich diagnostics, configurable policies, timeouts, tracing, and health checks.
IIgnitionSignal abstraction (name, optional timeout, WaitAsync)IIgnitionCoordinatorignition-readiness check)IgnitionSignal.FromTask, FromTaskFactory)// Program.cs or hosting setup
builder.Services.AddIgnition(options =>
{
options.GlobalTimeout = TimeSpan.FromSeconds(10);
options.Policy = IgnitionPolicy.BestEffort; // or FailFast / ContinueOnTimeout
options.EnableTracing = true; // emits Activity if diagnostics consumed
options.ExecutionMode = IgnitionExecutionMode.Parallel; // or Sequential
options.MaxDegreeOfParallelism = 4; // limit concurrency (Parallel mode only)
options.CancelOnGlobalTimeout = true; // attempt to cancel still-running signals if global timeout hits
options.CancelIndividualOnTimeout = true; // cancel a signal if its own timeout elapses
});
// Register concrete ignition signals
builder.Services.AddIgnitionSignal(new CustomConnectionSignal());
// Wrap an existing task
builder.Services.AddIgnitionFromTask("cache-warm", cacheWarmTask, timeout: TimeSpan.FromSeconds(5));
// Wrap a cancellable task factory (invoked lazily once)
builder.Services.AddIgnitionFromTask(
name: "search-index",
readyTaskFactory: ct => indexBuilder.BuildAsync(ct),
timeout: TimeSpan.FromSeconds(30));
// Adapt a single service exposing a readiness Task
builder.Services.AddIgnitionFor<MyBackgroundWorker>(w => w.ReadyTask);
// Composite: await all instances of a service type
builder.Services.AddIgnitionForAll<ShardProcessor>(p => p.ReadyTask);
// TaskCompletionSource helpers
_startupReady.Ignited();
_startupReady.IgnitionFailed(ex);
var app = builder.Build();
// Await readiness before starting interactive loop or accepting traffic
await app.Services.GetRequiredService<IIgnitionCoordinator>().WaitAllAsync();
public sealed class CustomConnectionSignal : IIgnitionSignal
{
public string Name => "db-connection";
public TimeSpan? Timeout => TimeSpan.FromSeconds(8); // optional override
public async Task WaitAsync(CancellationToken cancellationToken = default)
{
// Perform initialization (e.g. open connection, ping server)
await DatabaseClient.InitializeAsync(cancellationToken);
}
}
Register it:
services.AddIgnitionSignal<CustomConnectionSignal>();
| Policy | Behavior |
|---|---|
| FailFast | Throws if any signal fails (aggregate exceptions). |
| BestEffort | Logs failures, continues startup (default). |
| ContinueOnTimeout | Proceeds when global timeout elapses; logs partial results. |
After ignition completes:
var coord = provider.GetRequiredService<IIgnitionCoordinator>();
var result = await coord.GetResultAsync();
if (result.TimedOut)
{
// handle degraded startup
}
foreach (var r in result.Results)
{
Console.WriteLine($"{r.Name}: {r.Status} in {r.Duration.TotalMilliseconds:F0} ms");
}
AddIgnition automatically registers a health check named ignition-readiness returning:
Set EnableTracing = true to emit an Activity named Ignition.WaitAll. Attach listeners via ActivitySource to integrate with OpenTelemetry or other observability pipelines.
The cancellable selector overloads (AddIgnitionFor<TService>(Func<TService, CancellationToken, Task>), AddIgnitionForAll<TService>(Func<TService, CancellationToken, Task>), and scoped variants) receive the cancellation token from the FIRST wait invocation. That token is linked to coordinator-driven cancellations:
CancelOnGlobalTimeout = true)CancelIndividualOnTimeout = true)Because ignition evaluation is idempotent, subsequent calls to IIgnitionCoordinator.WaitAllAsync() reuse already created tasks; the token cannot be changed after the first invocation. Selector implementations should:
await Task.Delay(timeout, token))If you require a fresh cancellation token per consumer, expose a custom IIgnitionSignal instead of using the built-in selector adapters.
Add a package reference (after publishing):
dotnet nuget add package Veggerby.Ignition
HttpEndpointSignal, ChannelDrainSignal)MIT
// Single instance
services.AddIgnitionFor<CachePrimer>(c => c.ReadyTask);
// Composite group
services.AddIgnitionForAll<Consumer>(c => c.ReadyTask, groupName: "Consumer[*]");
// Arbitrary provider-based readiness
services.AddIgnitionFromFactory(
taskFactory: sp => Task.WhenAll(
sp.GetRequiredService<PrimaryConnection>().OpenAsync(),
sp.GetRequiredService<ReplicaConnection>().WarmAsync()),
name: "datastore-connections");
// TCS helpers
_readyTcs.Ignited();
_readyTcs.IgnitionFailed(new Exception("boom"));
Ignition exposes two timeout layers:
GlobalTimeout): A soft deadline unless CancelOnGlobalTimeout = true.
TimedOut status.IIgnitionSignal.Timeout): Always enforced; if elapsed the signal is marked TimedOut and optionally cancelled when CancelIndividualOnTimeout = true.Classification summary:
| Scenario | Result.TimedOut | Signal statuses |
|---|---|---|
| Soft global timeout, all eventually succeed | False | All Succeeded |
| Soft global timeout, a signal timed out | True | TimedOut + Succeeded |
| Hard global timeout (cancel) | True | TimedOut (unfinished) + any completed |
| Per-signal timeout only | True | TimedOut + Succeeded |
This model avoids penalizing slow but successful initialization while still enabling an upper bound via opt-in cancellation.
// Soft global timeout (default behavior):
services.AddIgnition(o =>
{
o.GlobalTimeout = TimeSpan.FromSeconds(5); // deadline hint
o.CancelOnGlobalTimeout = false; // remain soft (default)
o.CancelIndividualOnTimeout = false; // per-signal timeouts won't cancel tasks
});
// Hard global timeout with cancellation:
services.AddIgnition(o =>
{
o.GlobalTimeout = TimeSpan.FromSeconds(5);
o.CancelOnGlobalTimeout = true; // cancel all outstanding signals at deadline
o.Policy = IgnitionPolicy.ContinueOnTimeout; // choose continuation policy
});
// Mixed: hard global timeout + per-signal timeout cancellation:
services.AddIgnition(o =>
{
o.GlobalTimeout = TimeSpan.FromSeconds(10);
o.CancelOnGlobalTimeout = true; // hard deadline
o.CancelIndividualOnTimeout = true; // cancel slow individual signals
o.ExecutionMode = IgnitionExecutionMode.Parallel;
o.MaxDegreeOfParallelism = 4;
});
// Defining a per-signal timeout (hard for that signal only):
services.AddIgnitionFromTask(
name: "search-index",
readyTaskFactory: ct => indexBuilder.BuildAsync(ct),
timeout: TimeSpan.FromSeconds(30) // this signal will be marked TimedOut if exceeded
);