Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle async Start() errors in TopShelf

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:

  1. Is there any problem with using an async void Start() method for TopShelf?
  2. 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 runs async code?
like image 882
Hydrargyrum Avatar asked Sep 23 '16 09:09

Hydrargyrum


1 Answers

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.

like image 127
sellotape Avatar answered Nov 06 '22 12:11

sellotape