Giter Club home page Giter Club logo

taskschedulerengine's Introduction

TaskSchedulerEngine

A lightweight (zero dependencies, <400 lines of code) cron-like scheduler for in-memory scheduling of your code with second-level precision. Implement IScheduledTask or provide a callback, define a ScheduleRule, and Start the runtime. Schedule Rule evaluation is itself lightweight with bitwise evaluation of "now" against the rules (see ScheduleRuleEvaluationOptimized). Each invoked ScheduledTask runs on its own thread so long running tasks won't block other tasks. Targets .NET Core 3.1, .NET 6, .NET 7 (and presumably everything in between).

Quick Start

See sample/ in source tree for more detailed examples.

dotnet add package TaskSchedulerEngine

Nuget link: https://www.nuget.org/packages/TaskSchedulerEngine/

Version number scheme is (two digit year).(day of year).(minute of day).

static async Task Main(string[] args)
{
  // Instantiate TaskEvaluationRuntime.
  var runtime = new TaskEvaluationRuntime();

  // Use the fluent API to define a schedule rule, or set the corresponding properties
  // Execute() accepts an IScheduledTask or Action<ScheduleRuleMatchEventArgs, CancellationToken, bool>
  var s1 = runtime.CreateSchedule()
    .AtSeconds(0)
    .AtMinutes(0, 10, 20, 30, 40, 50)
    // .AtMonths(), .AtDays(), AtDaysOfWeek() ... etc
    // Important note that unset is always *, so if you omit AtSeconds(0) it will execute every second
    .WithName("EveryTenMinutes") // Optional ID for your reference 
    .WithTimeZone(TimeZoneInfo.Utc) // Or string such as "America/Los_Angeles"
    .Execute(async (e, token) => {
      if(!token.IsCancellationRequested)
        Console.WriteLine($"{e.TaskId}: Event intended for {e.TimeScheduledUtc:o} occurred at {e.TimeSignaledUtc:o}");
        return true; // Return success. Used by retry scenarios. 
    });

  var s2 = runtime.CreateSchedule()
    .ExecuteOnceAt(DateTimeOffset.UtcNow.AddSeconds(5))
    .Execute(async (_, _) => { Console.WriteLine("Use ExecuteOnceAt to run this task in 5 seconds. Useful for retry scenarios."); return true; });

  var s3 = runtime.CreateSchedule()
    .ExecuteOnceAt(DateTimeOffset.UtcNow.AddSeconds(1))
    .ExecuteAndRetry(
      async (e, _) => { 
          // Do something that may fail like a network call - catch & gracefully fail by returning false.
          // Exponential backoff task will retry up to MaxAttempts times. 
          return false; 
      },
      4, // MaxAttempts, inclusive of initial attempt 
      2  // BaseRetryIntervalSeconds
         // Retry delay logic: baseRetrySeconds * (2^retryCount) 
         // In this case will retry after 2, 4, 8 second waits
    );

  // You can also create rules from cron expressions; * or comma separated lists are supported 
  // (/ and - are NOT supported). 
  // Format: minute (0..59), hour (0..23), dayOfMonth (1..31), month (1..12), dayOfWeek (0=Sunday..6).
  // Seconds will always be zero.
  var s4 = runtime.CreateSchedule()
    .FromCron("0,20,40 * * * *")
    .WithName("Every20Sec") //Optional ID for your reference 
    .Execute(async (e, token) => {
      if(!token.IsCancellationRequested)
        Console.WriteLine($"Load me from config and change me without recompiling!");
        return true; 
    });

  // Finally, there are helper methods ExecuteEvery*() that execute a task at a given interval. 
  var s4 = runtime.CreateSchedule()
    // Run the task a 0 minutes and 0 seconds past the hours 0, 6, 12, and 18
    .ExecuteEveryHour(0, 6, 12, 18) 
    .Execute(async (e, token) => {
        return true; 
    });
  
  // Handle the shutdown event (CTRL+C, SIGHUP) if graceful shutdown desired
  AppDomain.CurrentDomain.ProcessExit += (s, e) => runtime.RequestStop();

  // Await the runtime.
  await runtime.RunAsync();

  // Listen for some signal to quit
  Thread.Sleep(30000);
  
  // Graceful shutdown. Request a stop and await running tasks.
  await runtime.StopAsync();
}

Terminology

  • Schedule Rule - cron-like rule, with second-level precision. Leave a parameter unset/null to treat it as "*", otherwise set an int array for when you want to execute. See usage note above in EveryTenMinutes example.
  • Scheduled Task - the thing to execute when schedule matches. The instance is shared by all executions forever and should be thread safe (unless you're completely sure there will only ever be at most one invocation). If you need an instance per execution, make ScheduledTask.OnScheduleRuleMatch a factory pattern.
  • Schedule Rule Match - the current second ("Now") matches a Schedule Rule so the Scheduled Task should execute. A single ScheduleRule can only execute one ScheduledTask. If you need to execute multiple tasks sequentially, initiate them from your Task. Multiple Schedule Rules that fire at the same time will execute in parallel (order not guaranteed).
  • Task Evaluation Runtime - the thing that evaluates the rules each second. Evaluation runs on its own thread and spawns Tasks on their own threads.

Troubleshooting

  • My task is executing every second, but I scheduled it to run with a different interval. - You probably need to add .AtSeconds(0). Unspecified/unset/null is always treated as */every. See example above, EveryTenMinutes. While there are other ways to solve this problem, this encourages verbosity. Like Cron, consider being verbose and setting every parameter every time.

Runtime Lifecycle

  • Instantiate TaskEvaluationRuntime and use RunAsync(), optionally RequestStop(), and StopAsync() for start and graceful shutdown.
  • TaskEvaluationRuntime moves through four states:
    • Stopped: nothing happening, can Start back into a running state.
    • Running: evaluating every second
    • StopRequested: instructs the every-second evaluation loop to quit and initiates a cancellation request on the cancellation token that all running tasks have access to.
    • StoppingGracefully: waiting for executing tasks to complete
    • Back to Stopped.
  • RunAsync creates a background thread to evaluate rules. RequestStop requests the background thread to stop. Control is then handed back to RunAsync which waits for all running tasks to complete. Then control is returned from RunAsync to the awaiting caller.

Validation is basic, so it's possible to create rules that never fire, e.g., on day 31 of February.

Changes

  • June 2023:
    • Updated to include .NET 7.
    • Added cron string parsing.
    • Changed interface; use the runtime to CreateSchedule(), which will automatically add it to the runtime and update it on every configuration change. Instead of removing, call .AsActive(bool).
    • Added arbitrary timezone support (previously only local and UTC supported) ref

A note on the 2010 vs 2021 versions

Circa 2010, this project lived on Codeplex and ran on .NET Framework 4. An old version 1.0.0 still lives on Nuget. The 2021 edition of this project runs on .NET Core 3.1 and .NET 6. A lot has changed in the intervening years, namely how multithreaded programming is accomplished in .NET (async/await didn't launch until C# 5.0 in 2012). While upgrading .NET 6, I simplified the code, the end result being: this library is incompatible with the 2010 version. While the core logic and the fluent API remain very similar, the class names are incompatible, ITask has changed, and some of the multithreading behaviors are different. This should be considered a new library that happens to share a name and some roots with the old one.

taskschedulerengine's People

Contributors

pettijohn avatar tomlm avatar

Stargazers

 avatar

Watchers

 avatar James Cloos avatar

taskschedulerengine's Issues

Library could be a bit smarter about default specifications around time.

I tried several times to use the library like this:

  scheduler
       .AtHours(Enumerable.Range(0,24).ToArray()
       .Execute(...)

What would happen is that it would fire every second. I literally had to enlist in the code and walk through it to figure out that if I specify something like Hours, I have to specify minutes and seconds or it will fall through to executing once a second.

This worked:

  scheduler
       .AtHours(Enumerable.Range(0,24).ToArray()
        .AtMinutes(0)
        .AtSeconds(0)
       .Execute(...)

Expectation: the defaults for units smaller than a given range should be 0.
Example:

scheduler.AtDays(1, 3, 5)
    .Execute(...)

Should be equivelent to specifying

scheduler.AtDays(1, 3, 5)
    .AtHours(0).AtMinutes(0).AtSeconds(0)
    .Execute(...)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.