Doug Dawson
Doug Dawson

Reputation: 1273

How should a Task signal a Windows service shutdown correctly?

I'm working on a bug for a Windows service. The service uses a field to track a reference to a single Task. OnStart, the task executes a single method. The method has a loop inside it and calls a database on a configurable interval to monitor the work another system is doing.

protected override void OnStart(string[] args)
{
    _processorTask = Task.Run(() => StartProcessor());
}

We've occasionally had an issue where the Task dies and logs the exception just fine, but the existing plumbing wasn't telling the service to stop, so our service monitors didn't know anything was wrong.

At first, I tried to add just add a call to Stop().

private void StartProcessor()
{
    var processor = new PEMonitoringProcessor(_tokenSource.Token);
    
    try
    {
        // The process loop is in the function. If this method exits, good or bad, the service should stop.
        processor.ProcessRun();
    }
    catch (Exception ex)
    {
        // An exception caught here is most likely fatal. Log the ex and start the service shutdown.
        if (log.IsFatalEnabled) { log.Fatal("A fatal error has occurred.", ex); };
    }
    finally
    {
        Stop();
    }
}

However, one of my fellow devs noticed in the OnStop method that a token is used to signal the Task to stop and then it waits. If the Task calls Stop and waits for Stop to return and OnStop is waiting for the Task to end, that does not bode well for this code.

protected override void OnStop()
{
    _tokenSource.Cancel();

    try
    {
        _processorTask.Wait();
    }
    // logging & clean-up... 
}

I considered a separate Task that doesn't get waited on by OnStop that checks the status of the first and calls Stop if the first Task is Completed, Faulted, etc., but that seemed a touch weird. I also considered raising an Event and trying something like BeginInvoke.

Intentionally stopping the service works fine because the OnStop signals via Token that a shutdown is happening. I'm trying to cover the possibility that the Task method returns or throws unexpectedly and I want the service to stop instead of becoming a zombie.

Upvotes: 1

Views: 468

Answers (1)

Evk
Evk

Reputation: 101453

The most straightforward way I see is something like this:

protected override void OnStart(string[] args) {
    _processorTask = Task.Run(() => StartProcessor());
    _processorTask.ContinueWith(x => {
        // x.Exception contains exception if any, maybe log it here
        Stop();
    }, TaskContinuationOptions.NotOnCanceled);
}

protected override void OnStop() {
    //or !_processorTask.IsCompleted && !_processorTask.IsCanceled && !_processorTask.IsFaulted
    if (_processorTask.Status == TaskStatus.Running) {
        // only cancel and wait if still running. Won't be the case if service is stopping from ContinueWith above
        _tokenSource.Cancel();
        _processorTask.Wait(); 
    }
}

Alternative way to do the same:

protected override async void OnStart(string[] args) {
    _processorTask = Task.Run(() => StartProcessor());
    bool cancelled = false;
    try {
        await _processorTask;
    }
    catch (OperationCanceledException) {
        // cancelled
        cancelled = true;
    }
    catch (Exception ex) {
        // log it?
    }

    if (!cancelled)
        Stop();
}

// OnStop stays the same

Upvotes: 1

Related Questions