I have a TopShelf service that uses async code to connect to web services and other application servers.
If it's unable to initialize its connections on startup, the service should log some errors and gracefully stop.
I've had a look at this question about stopping TopShelf when the start conditions aren't met. This answer talks about using the TopShelf HostControl to stop the service.
However, that answer relies on the ServiceConfigurator<T>.WhenStarted<T>(Func<T, HostControl, bool> start)
method.
I am currently configuring the TopShelf service in the standard way:
x.Service<MyService>(s =>
{
s.ConstructUsing(() => new MyService());
s.WhenStarted(s => s.Start());
s.WhenStopped(s => s.Stop());
});
However my service's Start()
method is actually async
, defined like this:
public async void Start()
{
await Init();
while (!_canceller.Token.IsCancellationRequested)
{
await Poll();
}
}
This seems to work fine. But I use the await keyword in several places in the function. So, I can't simply change my Start()
method to take a HostControl
and return a bool
, because I would have to return Task<bool>
from an async
method.
I'm currently allowing exceptions to bubble up from the Start() function so that TopShelf can see them and automatically stop the service when the exception bubbles up. However, the exceptions are then totally unhandled by my code, and I therefore end up with nasty unhandled exception error messages in the various logs I write to. Which I would prefer to replace with a nice error message and a clean service shut-down.
So, I have two questions:
- Is there any problem with using an
async void Start()
method for TopShelf? - Is there a way to make it so that if
Init()
throws an exception, the exception details are gracefully logged and then the service stops, given that my service runsasync
code?
Firstly, async void
is almost always incorrect, except in some truly fire-and-forget scenarios. You want to change that to async Task
.
Then sometimes you just have to use .Wait()
at the border between sync and async code. In this case you probably want to rename your current async Start()
method to StartAsync()
and add a Start()
method that calls it:
public void Start()
{
StartAsync().Wait();
}
public async Task StartAsync()
{
await Init();
while (!_canceller.Token.IsCancellationRequested)
{
await Poll();
}
}
However, you have another issue, in that TopShelf's Start()
method is not a "Run"()
method; i.e. you are supposed to return from that method as soon as your service is started, not remain there while the service runs. Given you're already using async-await, I'd probably instead not call Wait()
in Start()
, but save the Task
returned from StartAsync()
, then when Stop()
is called, signal your Task
to stop using that existing _canceller
, and only then in Stop()
call .Wait()
, leaving you with something like this:
private Task _serviceTask;
public void Start()
{
Init().Wait();
_serviceTask = ExecuteAsync();
}
public void Stop()
{
_canceller.Cancel();
_serviceTask.Wait();
}
public async Task ExecuteAsync()
{
while (!_canceller.Token.IsCancellationRequested)
{
await Poll();
}
}
I should add that the way you had it, you probably kind-of get away things to an extent, in the sense that your async Start()
method will return to TopShelf as soon as it hits the first await
, but will continue executing. If your Stop()
method calls _canceller.Cancel()
then your async Start()
method will terminate next time Poll()
is called.
However the above is cleaner, and you have to ability to wait until the last Poll()
finishes executing, which you didn't before. You will also be able to handle exceptions, as you mention.
Edit
I'd also move the Init()
call into Start()
, as above.
来源:https://stackoverflow.com/questions/39656932/how-to-handle-async-start-errors-in-topshelf