0

I have a task which is supposed to grab some information from an API on each hour. By each hour, I mean literally on each hour, like: 10:00, 11:00, etc. The following code executes DoWork method on each hour but not on every rounded hour (like 10:00) but 1 hour after the service starts instead.

public class SelfHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

    public SelfHostedService(ILogger<SelfHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is starting.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero,
            TimeSpan.FromHours(1));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation("Timed Background Service is working.");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Edit:

public class BotHostedService : IHostedService
{
    private readonly ILogger _logger;
    private CancellationTokenSource _tokenSource;
    private Task _timer;

    public BotHostedService(ILogger<BotHostedService> logger)
    {
        _logger = logger;
    }

    private DateTime RoundCurrentToNextOneHour()
    {
        DateTime now = DateTime.Now, result = new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0);
        return result.AddMinutes(((now.Minute / 60) + 1) * 60);
    }

    private async Task RunPeriodically(Action action, DateTime startTime, TimeSpan interval, CancellationToken token)
    {
        DateTime nextRunTime = startTime;

        try
        {
            while (true)
            {
                TimeSpan delay = nextRunTime - DateTime.Now;

                if (delay > TimeSpan.Zero)
                {
                    await Task.Delay(delay, token);
                }

                action();

                nextRunTime += interval;
            }
        }
        catch (OperationCanceledException)
        {
            return;
        }
    }

    private void DoWork()
    {
        _logger.LogInformation($"Timed Background Service is working. Current time: {DateTime.Now.ToLocalTime()}");
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is starting.");

        if (_timer == null)
        {
            _tokenSource = new CancellationTokenSource();
            DateTime startTime = RoundCurrentToNextOneHour();
            _timer = RunPeriodically(DoWork, startTime, TimeSpan.FromHours(1), _tokenSource.Token);
        }

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is stopping.");

        if (_timer != null)
        {
            _timer = null;

            _tokenSource.Cancel();
            _tokenSource.Dispose();
            _tokenSource = null;
        }

        return Task.CompletedTask;
    }
}
nop
  • 4,711
  • 6
  • 32
  • 93
  • So, figure out how long it is until the next hour, and set your timer for that time. For example, look at `DateTime.Now.Minute`. It's worth re-doing that calculation every time it ticks rather than setting it for exactly 1 hour -- a timer with a period of 1 hour will slowly drift over time (albeit by a very small relative amount) – canton7 Aug 14 '19 at 16:37
  • Initialize the timer with the time remaing for the full hour and upon the first DoWork change it to be precisly 1 hour. – Rand Random Aug 14 '19 at 16:37
  • See my answer on marked duplicate. In particular, the second example which uses `RoundCurrentToNextFiveMinutes()`. Just modify that method to round to the next hour instead of the next five minutes (and of course, rename it so that the name still correctly indicates what it does). – Peter Duniho Aug 14 '19 at 18:30
  • Thank you! I edited my answer. Is that accurate when converted to `TimeSpan`? – nop Aug 14 '19 at 20:37
  • It looks okay as far as it goes. But keep in mind my answer on that other question also ensures that the timing is synchronized to the clock, so that "every five minutes" or "every hour" will in fact continue to occur on exactly (or as close as possible) to the actual five-minute/hourly marks. Built-in timers are inherently inaccurate and will drift over time, so you may find that after some time (perhaps hours, perhaps days), your timer is no longer firing right at the top of the hour. – Peter Duniho Aug 14 '19 at 21:53
  • (By the way, if you want a specific user to be notified when you post a comment, e.g. if you intend the comment to be directed to them, you need to include their name in the comment, with an `@` character immediately before. E.g. @nop. In certain cases, users are notified even without that -- if the comment is below their post, or a post they edited, or they are the only person to have commented on the post so far -- but generally it's a good idea to be explicit if you want to make sure they get are notified.) – Peter Duniho Aug 14 '19 at 21:55
  • @PeterDuniho, thank you for the amazing explanation and help! – nop Aug 15 '19 at 00:20
  • @PeterDuniho, I almost forgot, how do I stop the timer? `_tokenSource?.Cancel();`? And is it a good practice to dispose the CancellationTokenSource? I edited the question once again with complete code. – nop Aug 15 '19 at 00:46
  • Yes, cancelling the token source is the intended design there. Note that you don't save the `Task` object, which means you'll never observe the exception that terminates the timer loop, so you might as well put `try { ... } catch (OperationCancelledException) { return; }` around the whole loop. I would also add explicit checks for null on the start/stop methods, rather than using `?.`, and don't allow starting if already started, nor stopping if not running, and set the `_tokenSource` to `null` when not running (i.e. after stopped, dispose and set to `null`). – Peter Duniho Aug 15 '19 at 00:55
  • @PeterDuniho, thanks! I edited my question with the final code. Is that what you meant? – nop Aug 15 '19 at 09:24
  • @PeterDuniho something is wrong with this method or I did something wrong. The result is: `Timed Background Service is working. Current time: 8/15/2019 12:59:59 PM`. Somehow the timer ticks at x:59:59 instead of x:00:00. – nop Aug 15 '19 at 10:49
  • `info: Project.Services.BotHostedService[0] Timed Background Service is working. Current time: 8/15/2019 12:59:59 PM info: Project.Services.BotHostedService[0] Timed Background Service is working. Current time: 8/15/2019 2:00:00 PM` – nop Aug 15 '19 at 11:39
  • It looks mostly fine to me. Keep in mind that there are limits to Windows' own timing and thread scheduling mechanisms. Over an hour, imprecision in the system clock itself could lead to the _appearance_ of an early triggering. Conversely, imprecision in the thread scheduling can result in an actual early triggering. Either way, I suspect that if you were to print the milliseconds as well, you'd find that the actual clock time when triggered is going to be less than half a second off either way. – Peter Duniho Aug 16 '19 at 03:22
  • I will point out a couple of notes on your version of `RoundCurrentToNextOneHour()`: because the `Minute` property is always less than 60, the calculation will always just add 60 minutes to whatever the initial `result` value is. You might as well just implement the method like `DateTime now = DateTime.UtcNow; return new DateTime(now.Year, now.Month, now.Day, now.Hour + 1, 0, 0);`. Note also that it is better to use `UtcNow` instead of `Now` for all internal time calculations, so that your timing isn't screwed up by time zone issues, like Daylight Saving. ... – Peter Duniho Aug 16 '19 at 03:27
  • ... You should only use non-UTC time when actually _displaying_ the time to the user (assuming local time is desired in that context, which it usually is). You would convert from UTC to local for that purpose, and that purpose only. – Peter Duniho Aug 16 '19 at 03:27

1 Answers1

-1

Make another timer with 1 minute interval that checks for minute value then when minutes comes to 0 it launch the main timer then turn the assist timer off

Bakaji
  • 88
  • 9