dotnet-labs / ServiceWorkerCronJob

Schedule Cron Jobs using HostedService in ASP.NET Core
MIT License
265 stars 71 forks source link

Next occurrence interval bigger than int.MaxValue #14

Closed MatheusXavier closed 1 year ago

MatheusXavier commented 1 year ago

I have a cron job with the following cron schedule: 0 0 1 * * This cron schedule indicates that the next occurrence will be At 12:00 AM, on day 1 of the month, today it is 10/03/2022 so the next occurence is 11/01/2022, the difference beetween today and the next execution in milliseconds is bigger than int.MaxValue causing the following exception:

System.ArgumentException: Invalid value '2478456815.9281' for parameter 'interval'.
   at System.Timers.Timer..ctor(Double interval)

The error occurs in this point of the code: image

MatheusXavier commented 1 year ago

I could solve the issue in this way:

private async Task ScheduleJob(CancellationToken cancellationToken)
{
    var next = _expression.GetNextOccurrence(DateTimeOffset.UtcNow, _timeZoneInfo);

    if (!next.HasValue)
    {
        return;
    }

    var delay = next.Value - DateTimeOffset.UtcNow;

    if (delay.TotalMilliseconds <= 0)
    {
        await ScheduleJob(cancellationToken);
        return;
    }

    var isNextExecutionTooFar = delay.TotalMilliseconds > int.MaxValue;

    if (isNextExecutionTooFar)
    {
        ScheduleToTryAgain(TimeSpan.FromDays(1).TotalMilliseconds, cancellationToken);
    }
    else
    {
        ScheduleJobExecution(delay.TotalMilliseconds, cancellationToken);
    }
}

private void ScheduleJobExecution(double internval, CancellationToken cancellationToken)
{
    _timer = new Timer(internval);
    _timer.Elapsed += async (sender, args) =>
    {
        _timer.Dispose();  // reset and dispose timer
        _timer = null;

        if (!cancellationToken.IsCancellationRequested)
        {
            await DoWork(cancellationToken);
        }

        if (!cancellationToken.IsCancellationRequested)
        {
            await ScheduleJob(cancellationToken);
        }
    };

    _timer.Start();
}

private void ScheduleToTryAgain(double internval, CancellationToken cancellationToken)
{
    _timer = new Timer(internval);
    _timer.Elapsed += async (sender, args) =>
    {
        _timer.Dispose();
        _timer = null;

        if (!cancellationToken.IsCancellationRequested)
        {
            await ScheduleJob(cancellationToken);
        }
    };

    _timer.Start();
}

Basically if the next execution is too far away we try to schedule again tomorrow.

changhuixu commented 1 year ago

Yes, it would work.

What I usually do is to simply schedule the job to run every day and checks the day of month in the logic of the actual job. If it's day 1 then continue, otherwise return.