How to handle async Start() errors in TopShelf
Asked Answered
F

1

11

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?
Filemon answered 23/9, 2016 at 9:8 Comment(0)
L
17

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.

Looseleaf answered 23/9, 2016 at 19:17 Comment(4)
How do I make sure the _serviceTask returned by ExecuteAsync() actually starts, without awaiting it in the Start() method? I can't await it there, because that would require Start() to be async. In test code with a mock Poll() method based on Task.Delay(), when I call _serviceTask.Start(), an InvalidOperationException is thrown with the message Start may not be called on a promise-style task. If I don't await or start the Task it seems to remain in the WaitingForActivation state?Filemon
Never mind, my test code sucked. ExecuteAsync() returns a running Task, so I don't need to do anything special to start it. I'm not sure why the Task's status was WaitingForActivation rather than running, but the Task does actually run and eventually completes.Filemon
One problem with this is, if the ExecuteAsync throws a exception this will never be noticed?Mccourt
@Mccourt as you probably know the exceptions thrown in ExecuteAsync will be waiting untill some one calls _serviceTask.Result or checks the _serviceTask.ExcetpionsTraver

© 2022 - 2024 — McMap. All rights reserved.