0

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();
    }
}

John V
  • 1,286
  • 9
  • 15
CidaoPapito
  • 572
  • 1
  • 6
  • 21
  • This recent question might be relevant: [Run async hosted service every 5 minutes in ASP.NET Core](https://stackoverflow.com/questions/71636763/run-async-hosted-service-every-5-minutes-in-asp-net-core). Or this: [Run async method regularly with specified interval](https://stackoverflow.com/questions/30462079/run-async-method-regularly-with-specified-interval). – Theodor Zoulias Mar 29 '22 at 01:44

1 Answers1

2

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.

John V
  • 1,286
  • 9
  • 15
  • The Timer wont avoid the rest of the code run. My intention with the loop is block the rest of the code run. I can try thread.sleep. Any other better solution to wait either the Time out or the DB be updated? – CidaoPapito Mar 28 '22 at 08:28
  • The reason why your timeout isn't working is because you haven't started the stopwatch. If you do that, your code seems reasonable. You are right that the while loop is kind of inefficient. It would help if you posted more of your code. Do you want the code to block until the record is updated? – John V Mar 28 '22 at 09:03
  • I updated my code. What I meant is, I just want the method ```await ProcessFunctionAsync();``` be called after the record is updated. So with the While loop, I could avoid the method been called while the Timer is Enabled. What can be done instead of the while loop? – CidaoPapito Mar 28 '22 at 10:02
  • Okay, please check latest edit for a possible approach. I don't think there is anything wrong with a while loop, so I kept it, but moved it into your MyCode() function. If you're just starting out with Task, it's best to keep things simple at first. You can try fancier approaches once you are comfortable with the basics. – John V Mar 28 '22 at 19:51