Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Time zone support #22

Merged
merged 19 commits into from
May 7, 2024
Merged

Conversation

falvarez1
Copy link
Member

@falvarez1 falvarez1 commented Apr 17, 2024

Overview

This Pull Request introduces timezone support to the CronScheduler component of our system. Previously, job scheduling was based solely on the server's local time, which could lead to discrepancies in job execution times, especially in systems deployed across multiple time zones or serving international users.

Motivation

The necessity for timezone support arises from the need to schedule and execute jobs according to various regional times, accommodating users or operations in different time zones. This enhancement ensures that NCronJob is robust, flexible, and more aligned with global use cases.

Changes Made

  1. RegistryEntry Modification:
  • Added a TimeZoneInfo property to RegistryEntry. This allows each job to specify the timezone in which its cron schedule should be evaluated.
  1. Cron Expression Evaluation:
  • Modified the ScheduleJob method to compute the next run time of a job based on the timezone specified in its RegistryEntry. This utilizes the GetNextOccurrence method from the Cronos library, which supports timezone adjustments.
  1. Job Scheduling and Execution:
  • Adjusted the job scheduling logic to use DateTimeOffset instead of DateTime, providing better support for time zones.
  • Ensured that the calculated next run time considers the job's specified timezone, converting UTC times to the appropriate local times as needed.

Impact

  • Reliability: Jobs can now be scheduled according to their designated timezones, increasing the reliability of time-sensitive job executions.
  • Usability: By supporting multiple timezones, the system can cater to a wider range of users and use cases, enhancing its overall usability.

A new `TestCancellationJob` class has been added to simulate a long-running job and handle job cancellation.
Everything was made to support async structures to better support error handling and cancellation support.
Added preliminary parallel execution support.
switched NCronTab for Cronos library for native TimeZone support
@falvarez1 falvarez1 mentioned this pull request Apr 17, 2024
@linkdotnet
Copy link
Member

I will have a look after #21 if it fine with you

@falvarez1
Copy link
Member Author

falvarez1 commented Apr 24, 2024

I will need to make some modifications to this code once #21 is merged because of the updates to CronScheduler.

…duler.

Modified `LogNextJobRun` method in CronScheduler.cs to use local time instead of UTC.

Added several tests in JobOptionBuilderTests.cs to verify correct timezone assignment, default behavior, next run time calculation, cross timezone scheduling, and daylight saving time handling.
@falvarez1
Copy link
Member Author

I resolved the merge conflicts, modified the README and added tests. I'm not sure how you want the CHANGELOG.md handled, it doesn't have a version for the last updates, just marked as unreleased.

@linkdotnet
Copy link
Member

Maybe I should have explained that earlier a bit more in detail or create a CONTRIBUTING.md.

The idea behind the CHANGELOG.md is basically only adding in the Unreleased section. When I create a release via a GitHub Action, this information will be taken and put into the release page information and (currently buggy) put into the csproj file so that you can see the changes also on NuGet.

The Action will create the version tag etc, so there is no need to enter/know upfront the version information.
Once it's released the GitHub action will make a new commit where it moves everything from Unreleased to the newly created version section in the CHANGELOG.md.

But there is a small detail: The workflow distinguishes between stable releases and previews. If it is a preview, then this process will not kick in.

var baseTime = new DateTime(2024, 1, 1, 11, 0, 0, DateTimeKind.Utc); // This is 3 AM PST, 6 AM EST
var expectedRunTime = new DateTime(2024, 1, 1, 16, 0, 0, DateTimeKind.Utc); // 8 AM PST is 11 AM EST, which is 4 PM UTC

var nextRunTime = GetNextRunTime(simpleJobEntry, baseTime, timeZoneLA);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still toying with the thought of having an integration test, which "emits" (via channel or whatnot) the execution time from a Job to the test, so that we can ensure, e2e, that everything does the right thing.

But I can toy with that on mainas well.

@linkdotnet
Copy link
Member

There seems to be a deadlock (GitHub runner timed out after 6h)

@falvarez1
Copy link
Member Author

falvarez1 commented Apr 29, 2024

There seems to be a deadlock (GitHub runner timed out after 6h)

Indeed there is, I must've missed running the retry tests. Ironically that's the deadlock I was fixing with #30
Once #30 is merged I can pull that in and try again, but trying to fix this here may be redundant.

I'll merge and test locally.

@falvarez1
Copy link
Member Author

falvarez1 commented Apr 29, 2024

This is a weird one. It works fine using Visual Studio and Jetbrains IDE testing mechanisms. Also works fine if I target the individual tests that are failing using dotnet test --filter "FullyQualifiedName=NCronJob.Tests.NCronJobRetryTests.JobShouldFailAfterAllRetries" for example. If I step through the test everything completes just fine. It only seems to deadlock if I run all of the tests using dotnet test, although the test itself is marked as Passed. Sounds like a race condition, probably with disposing objects.
This is after I merged #30 locally, meaning it doesn't change the behavior 🤔.

@linkdotnet
Copy link
Member

This is a weird one. It works fine using Visual Studio and Jetbrains IDE testing mechanisms. Also works fine if I target the individual tests that are failing using dotnet test --filter "FullyQualifiedName=NCronJob.Tests.NCronJobRetryTests.JobShouldFailAfterAllRetries" for example. If I step through the test everything completes just fine. It only seems to deadlock if I run all of the tests using dotnet test, although the test itself is marked as Passed. Sounds like a race condition, probably with disposing objects. This is after I merged #30 locally, meaning it doesn't change the behavior 🤔.

It indeed sounds like Dispose. For me EachJobRunHasItsOwnScope is deadlocking inside the while-loop.

@linkdotnet
Copy link
Member

On another side note: GitHub runners are only single threaded (v)CPU's IIRC.
So this could also influence local VS GH Runner

@linkdotnet
Copy link
Member

linkdotnet commented Apr 29, 2024

It indeed sounds like Dispose. For me EachJobRunHasItsOwnScope is deadlocking inside the while-loop.

The test in question has the following definition:

WithCronExpression("* * * * *").And.WithCronExpression("* * * * *")

On main that leads to two entries in the jobQueue - on the future branch it doesn't (only 1 entry).

Edit 1:
registry.GetAllCronJobs() yields only one element instead of two.

@linkdotnet
Copy link
Member

linkdotnet commented Apr 29, 2024

Okay I found the culprit -.- and I am not sure why it did work on main:

cronJobs = jobs.Where(c => c.CronExpression is not null).ToFrozenSet();

As we are using a record, two equal definitions of a CRON job will of course only result in one element in a set.
So probably we want a ImmutableArray<RegistryEntry> here.

Edit: public sealed class CrontabSchedule from NCrontab and public sealed class CronExpression: IEquatable<CronExpression> from CRONOS.

So yes - before two CRON Expressions were always different where as with CRONOS they can be the same.

@falvarez1
Copy link
Member Author

Oh wow I just found the same thing and came here... I was chasing a red herring thinking the issue was with Retry tests.

@falvarez1
Copy link
Member Author

I'd argue that we don't want two separate but identical Schedule entries for the same Job. If the point is to run that same Job twice the concurrency control will help here. But I don't think we should allow the same schedule to be created more than once per Job per service.

@linkdotnet
Copy link
Member

Well that depends - if you want to run multiple jobs at the same time with different parameters I do think that is totally fine and valid.

For the sake of simplicity I generalized that - so if you explicitly add the same cron expression twice you can run it twice.

For sure we could also turn it around and have the schedule as the principal and the job as dependent so that there is a 1 to n relation ship between schedule and job.
This would only allow one run in this case.

@falvarez1
Copy link
Member Author

falvarez1 commented Apr 29, 2024

Well that depends - if you want to run multiple jobs at the same time with different parameters I do think that is totally fine and valid.

Yes that is valid, but wouldn't storing as a record and pass different parameters determine that they are two separate records and then add them to the FrozenSet just fine?

Anyhow I converted to ImmutableArray and the test passes

    private readonly JobExecutor jobExecutor;
    private readonly ILogger<CronRegistry> logger;
    private readonly ImmutableArray<RegistryEntry> cronJobs;

    public CronRegistry(IEnumerable<RegistryEntry> jobs,
        JobExecutor jobExecutor,
        ILogger<CronRegistry> logger)
    {
        this.jobExecutor = jobExecutor;
        this.logger = logger;
        cronJobs = [..jobs.Where(c => c.CronExpression is not null)];
    }

If you prefer we can decide if this is the correct approach in a separate Issue.

@falvarez1
Copy link
Member Author

FYI the test also passes if you keep the FrozenSet and use different parameters like so:

ServiceCollection.AddNCronJob(n => n.AddJob<ScopedServiceJob>(
    p => p.WithCronExpression("* * * * *").WithParameter("First")
        .And
        .WithCronExpression("* * * * *").WithParameter("Second")));

@linkdotnet
Copy link
Member

Yeah - let's discuss the semantics around this in a separate issue.

It triggered the tests again - they weren't stuck, but some seemed to fail. I will have to check this the upcoming days and change something in the Actions, because .net9.0 interferes with the output so that we can't see the failing test.

@@ -61,6 +61,7 @@ jobs:
- [x] Integrated in ASP.NET - Access your DI container like you would in any other service
- [x] Get notified when a job is done (either successfully or with an error).
- [x] Retries - If a job fails, it will be retried.
- [x] The job scheduler supports TimeZones. Defaults to UTC time.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the time being, can you add a new entry in the README.md in the sections below?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, this plus the rebase/merge. Is that the last thing pending?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and the usual CHANGELOG.md stuff.

I am in the midst of creating the documentation - I will move a big part of the README towards the docs.
But that has nothing to do with this PR. All in all great job!

@linkdotnet
Copy link
Member

LGTM so far - I guess only a rebase / merge is one open thing.

@linkdotnet
Copy link
Member

linkdotnet commented May 7, 2024

@falvarez1 I took the liberty to update the branch for you.

That said I will squash and merge that thing and will create some documentation.

@linkdotnet linkdotnet merged commit e2acd0a into NCronJob-Dev:main May 7, 2024
1 of 3 checks passed
@falvarez1 falvarez1 deleted the time-zone-support branch May 19, 2024 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Time zone support
2 participants