How to periodically check for status using an asynchronous method and support timeout?

1.1k Views Asked by At

I am trying to monitor if a DB record is updated by an outside process. So I would like to check if the record is updated every 3 seconds, and timeout after 30 seconds if it has not updated.

My current approach uses a Timer to check the status periodically and a Stopwatch to check for Timeout, but I'm not sure if this is the best approach since my DB check function and update record function are asynchronous.

private System.Timers.Timer aTimer;

public async Task MyCode()
{
    aTimer = new System.Timers.Timer { Interval = 3000 };
    var stopWatch = new Stopwatch(); // to control the timeout

    // Hook up the Elapsed event for the timer. 
    aTimer.Elapsed += async (sender, e) => await 
    VerifyRecordIsUpdatedAsync(sender, e, recordId);

    // Have the timer fire repeated events (true is the default)
    aTimer.AutoReset = true;

    // Start the timer
    aTimer.Enabled = true;

    while (aTimer.Enabled)
    {
        var ts = stopWatch.Elapsed;
        if (ts.TotalSeconds > 30)
        {
           aTimer.Stop();
        }
    }

    await ProcessFunctionAsync()
}

private async Task VerifyRecordIsUpdatedAsync(object sender, ElapsedEventArgs e, Guid recordId)
{
    var recordDb = await _context.Records.FirstOrDefaultAsync(x => x.Id == recordId);
    if (recordDb.Status == Status.Updated)
    {
        aTimer.Stop();
    }
}

1

There are 1 best solutions below

4
John V On

There are several approaches to solve this problem, but this is one of the simplest ones if you're just starting out with async/await Tasks. One of the advantages of async/await is that you can write code similar to how you would write it synchronously, and you don't have to use timers, callbacks, or Thread.Sleep() or any of that.

Some things to note:

  • I made your asynchronous functions return Task<bool>. This means that the function will return true or false to the callers.
  • I stored the timeout and delay as TimeSpan instead of number types. This is a matter of preference, but Stopwatch.Elapsed is a TimeSpan, and Task functions accept TimeSpan, so I think it is more readable and avoids problems converting units.
//since Stopwatch.Elapsed is a TimeSpan, store the timeout as one too
private readonly static TimeSpan Timeout = TimeSpan.FromSeconds(30);

private readonly static TimeSpan TimeBetweenChecks = TimeSpan.FromSecond(3);

// returns true if the record was processed, false
// if the record was not updated within the timeout.
public async Task<bool> MyCode(Guid recordId)
{
    // start the stopwatch
    var stopWatch = Stopwatch.StartNew();
        
    while (true) //loop until the record is updated or we timeout
    {
        // check if the record is updated
        if (await VerifyRecordIsUpdated(recordId))
        {
            break; // the record is updated, so we can exit the loop!
        }

        // check if we have exceeded our timeout
        if (stopWatch.Elapsed > Timeout)
        {
            return false; // return false if we timed out
        }

        // the record isn't updated & we haven't timed out, so the 
        // loop will repeat. we're worried about querying the DB 
        // too often, so we add a delay. this will work a lot like 
        // a timer, but it is async and avoids reentrancy issues.
        await Task.Delay(TimeBetweenChecks);
    }

    // at this point, we know recordStatus == Status.Updated.
    await ProcessFunctionAsync(); 

    // the record was updated and we processed it.
    return true;
}
    
//returns true if the record's status is Status.Updated
private async Task<bool> VerifyRecordIsUpdatedAsync(Guid recordId)
{
    var recordDb = await _context.Records.FirstOrDefaultAsync(
                                        x => x.Id == recordId);
    return recordDb.Status == Status.Updated
}

There is one problem with this code; it does not provide any way to cancel the operation before the 30 seconds is up, or to interrupt the 3-second delay. This would involve CancellationTokens and would be better asked as a separate question if you're interested in that.

If you want a more general purpose solution for the question of "how do I wait for a task with a timeout" that uses more advanced Task library features (like CancellationToken and Task.WhenAny()), then I'd suggest reading this: Asynchronously wait for a Task to complete with a timeout.