Add externally accessible lifecycle events for silos
We recently deprecated IStartupTask and we have included a warning about its perils in the docs for quite some time now. However, because IHost can choose to start/stop IHostedService instances in any order it is difficult to be sure that the silo is in a certain state (eg, ready to receive requests) by the time IHostedService.StartAsync is called. Similarly, it's difficult to know when a silo begins shutting down and when it has completed shutting down.
To remedy this, we should offer an interface similar to IHostApplicationLifetime which can be used to hook into certain lifecycle stages externally.
One open question is whether we should expose these events using a Task or a CancellationToken. For startup, it's more convenient to wait on a Task than a CancellationToken. Additionally, callbacks on CancellationToken cannot themselves be asynchronous. Ideally, we would have something which is both awaitable and where consumers can optionally register an asynchronous method to prevent the silo from advancing (eg, continuing to shutdown) until the callback has completed - with support for Task-returning callbacks.
@ReubenBond
Ideally, we would have something which is both awaitable and where consumers can optionally register an asynchronous method to prevent the silo from advancing.
As in functionality offered by an interface i.e. exposing a Task and CancellationToken, or an actual awaitable?
I'm not completely sure yet, @ledjon-behluli. We could expose:
- A method to register a callback for participation in the lifecycle (similar to
CancellationToken, but async) - or a task for notification without participation
- or both
I'm open to suggestions.
@ReubenBond an awatiable having both would be handy and keep the interface clean, i would imagine it having at least 'Started', 'Stopped' as stages, maybe others in-between.
If you're open to, i can give it a shot?
If you're open to, i can give it a shot?
That would be great! I think we need to decide on an API design / semantics before implementing it.
For the silo lifecycle, the stages offered by IHostApplicationLifetime are sufficient for now: started, stopping, and stopped.
Hooking into the Stopping stage is useful when work needs to be done before the silo is allowed to begin shutting down, eg suspending or offloading some work. Similar for the Stopped stage - I can imagine wanting to issue some signal that a silo is definitely no longer going to be available from there.
To allow callers to hook into one of those stages, we could expose properties of some type which has a IDisposable Register(Func<object, CancellationToken, Task> callback, object state) method for registering callbacks. We could make that type awaitable (or expose a task) if we wanted to provide a signal for when all of the registered callbacks had completed.
For the startup event, however, I want to steer people away from the pitfall of registering a callback to run on startup which might fail and prevent the application from starting. That leads me to think that we should just expose a Task for the Started event, and not a Register method.
I am onboard with an awaitable that callers can await on but also register callbacks and checking via cancellation token. I think we can reuse the same across all 3 stages, and while I see your point with the Started and callbacks, I think we should either have it there and maybe mentioned in the docs to be careful, or we could throw in case that happens. It would result it a cleaner interface as opposed to going through a Task and Register method for each one of the stages...that might even be error prone i.e. people registering in the wrong stage.
I think we need to decide on an API design / semantics before implementing it.
That's fine, I can change stuff afterwards :)
I think we should either have it there and maybe mentioned in the docs to be careful
I feel this isn't enough, because developers either won't realize or won't fix it until a disaster occurs. This has been the problem with IStartupTask to begin with.