clokwerk

repository·master·Indexed 17 days ago

https://github.com/mdsherry/clokwerk

A simple, DSL-based recurring task scheduler for Rust, inspired by Python's schedule and Ruby's clockwork. It allows developers to schedule tasks using human-readable intervals and times instead of cron strings. The library provides both a synchronous Scheduler with manual or background thread execution and an AsyncScheduler for tasks returning Futures, supporting custom timezones and flexible scheduling logic via a fluent API.

Tokens
7.9K
Snippets
31
Records
34
Agent score
63%

What's inside clokwerk

  1. Overview of Clokwerk scheduler

    master

    Clokwerk is a simple scheduler for Rust inspired by Python's Schedule and Ruby's clockwork. Instead of parsing cron strings, it uses a Domain Specific Language (DSL) for scheduling tasks.

    By default, all times and dates are relative to the local timezone. However, you can initialize a scheduler with a specific timezone using Scheduler::with_tz.

  2. Initialize a Clokwerk Scheduler

    master

    You can create a scheduler in two ways:

    1. Default (Local Timezone): Use Scheduler::new() to use the system's local timezone.
    2. Custom Timezone: Use Scheduler::with_tz(timezone) to specify a different timezone (e.g., using chrono::Utc).
    use clokwerk::Scheduler;
    use chrono::Utc;
    
    // Local timezone
    let mut scheduler = Scheduler::new();
    
    // Specific timezone
    let mut scheduler = Scheduler::with_tz(Utc);
  3. Run the scheduler loop

    master

    Clokwerk provides two ways to execute the scheduled tasks:

    1. Manual Event Loop

    You can manually control the execution by calling run_pending() inside your own loop. This is useful if you need to integrate the scheduler into an existing loop or control the sleep duration between checks.

    2. Background Thread

    You can offload the scheduler to a background thread using watch_thread(duration). This returns a handle that manages the scheduler's lifecycle. The scheduler will stop running when the handle is dropped or when .stop() is explicitly called on the handle.

    use clokwerk::Scheduler;
    use std::thread;
    use std::time::Duration;
    
    let mut scheduler = Scheduler::new();
    
    // --- Option 1: Manual Loop ---
    for _ in 1..10 {
        scheduler.run_pending();
        thread::sleep(Duration::from_millis(10));
    }
    
    // --- Option 2: Background Thread ---
    let thread_handle = scheduler.watch_thread(Duration::from_millis(100));
    
    // To stop the background scheduler:
    thread_handle.stop();
  4. Use the synchronous Scheduler

    master

    For synchronous scheduling, use the Scheduler struct. It provides a DSL for scheduling jobs at specific intervals or times. You can manage the lifecycle of scheduled tasks using ScheduleHandle.

    // See `Scheduler` in the documentation for specific API methods.
  5. Use AsyncScheduler for asynchronous task scheduling

    master

    AsyncScheduler is used to schedule tasks that return Futures. Unlike the synchronous Scheduler, it does not provide a watch_thread method to avoid being tied to a specific runtime. Instead, you are responsible for driving the scheduler by calling run_pending().await within a loop in your chosen async runtime (e.g., tokio or async_std).

    To run the scheduler, you typically spawn a task that loops, calls run_pending(), and then sleeps for a short duration to prevent busy-waiting.

    // Example using tokio
    let mut scheduler = AsyncScheduler::new();
    tokio::spawn(async move {
      loop {
        scheduler.run_pending().await;
        tokio::time::sleep(Duration::from_millis(100)).await;
      }
    });
  6. Understand scheduling caveats

    master
    Clokwerk uses a DSL rather than cron strings. Be aware that certain combinations of intervals and specific times can lead to unexpected behavior. For example, combining every(10.seconds()).at("16:00") will result in the job running at the next 4 PM that occurs after the next 10-second interval boundary.
  7. Implement a custom TimeProvider for testing

    master

    The TimeProvider trait allows you to specify the source of DateTime values used by the scheduler. While the default ChronoTimeProvider uses the system clock, you can implement TimeProvider to provide controlled, deterministic time, which is useful for writing tests that depend on specific time intervals or schedules.

    use clokwerk::TimeProvider;
    use chrono::{DateTime, TimeZone};
    
    struct MockTimeProvider;
    
    impl TimeProvider for MockTimeProvider {
        fn now<Tz>(tz: &Tz) -> DateTime<Tz>
        where
            Tz: TimeZone + Sync + Send,
        {
            // Return a fixed time for testing
            tz.with_ymd_and_hms(2023, 1, 1, 0, 0, 0).unwrap()
        }
    }
  8. Use the asynchronous AsyncScheduler

    master

    If the async feature is enabled, you can use AsyncScheduler for non-blocking job execution. This is suitable for integration into async runtimes like Tokio. It uses AsyncJob to define tasks.

    // Requires the `async` feature to be enabled.
    // See `AsyncScheduler` in the documentation for specific API methods.
  9. Configure complex schedules with `RunConfig`

    master

    A RunConfig defines a complete schedule by combining a base Interval with optional adjustments. This allows you to express complex patterns like "Every three days at 3 AM" or "Every Tuesday, plus 6 hours, plus 5 minutes".

    Workflow:

    1. Create a base config using RunConfig::from_interval(base_interval).
    2. Add a specific time of day using .with_time(naive_time).
    3. Add additional offsets using .with_subinterval(sub_interval).
    # use clokwerk::{RunConfig, TimeUnits, Interval};
    # use chrono::NaiveTime;
    
    // Example: Every Tuesday at 15:00:00
    let rc = RunConfig::from_interval(Interval::Tuesday)
        .with_time(NaiveTime::from_hms(15, 0, 0));
    
    // Example: Every 3 days, plus 6 hours, plus 5 minutes
    let rc_complex = RunConfig::from_interval(3.days())
        .with_subinterval(6.hours())
        .with_subinterval(5.minutes());
  10. Schedule jobs with Clokwerk

    master

    Jobs are added to the scheduler using the .every(interval) method, which returns a SyncJob builder. You can chain various modifiers to define complex schedules before calling .run(callback) to register the task.

    Common Scheduling Patterns:

    • Intervals: Use .every(interval) with durations (e.g., 10.minutes(), 1.day()) or specific days (e.g., Wednesday, Weekday).
    • Time Offsets: Use .plus(duration) to add to an interval or .and_every(interval) to require multiple intervals to coincide.
    • Specific Times: Use .at("HH:MM") for string-based time or .at_time(chrono::NaiveTime) for precise time objects.
    • Execution Limits:
      • .once(): Run the job only once.
      • .count(n): Run the job exactly n times.
      • .repeating_every(interval).times(n): Run a job at a specific time, then repeat it every interval for n times.

    Note: The .run() method must be called at the end of the builder chain to actually register the job.

    use clokwerk::{Scheduler, Interval::*};
    use std::time::Duration;
    
    let mut scheduler = Scheduler::new();
    
    // Every 10 minutes plus 30 seconds
    scheduler.every(10.minutes()).plus(30.seconds()).run(|| println!("Periodic task"));
    
    // Daily at a specific time
    scheduler.every(1.day()).at("3:20 pm").run(|| println!("Daily task"));
    
    // Weekly on Wednesday
    scheduler.every(Wednesday).at("14:20:17").run(|| println!("Weekly task"));
    
    // Biweekly (Tuesday at 14:20 AND Thursday at 15:00)
    scheduler.every(Tuesday).at("14:20:17").and_every(Thursday).at("15:00").run(|| println!("Biweekly task"));
    
    // Run 10 times on weekdays at noon
    scheduler.every(Weekday).at("12:00").count(10).run(|| println!("Countdown"));
    
    // Run once
    scheduler.every(1.day()).at("3:20 pm").once().run(|| println!("I only run once"));
  11. Schedule tasks using the Clokwerk DSL

    master

    Clokwerk uses a fluent API to define task intervals and specific execution times. You can combine intervals, specific times of day, and specific days of the week.

    Key components:

    • every(interval): Defines the frequency (e.g., .seconds(), .minutes(), .day()).
    • .plus(offset): Adds an offset to the interval.
    • .at(time_string): Specifies a specific time (e.g., "3:20 pm" or "14:20:17").
    • .and_every(weekday): Allows scheduling on multiple specific days of the week.
    • .run(closure): Attaches the task to be executed.
    use clokwerk::{Scheduler, TimeUnits};
    use clokwerk::Interval::*;
    use clokwerk::WeekDay::*;
    
    let mut scheduler = Scheduler::new();
    
    // Every 10 minutes and 30 seconds
    scheduler.every(10.minutes()).plus(30.seconds()).run(|| println!("Periodic task"));
    
    // Daily at a specific time
    scheduler.every(1.day()).at("3:20 pm").run(|| println!("Daily task"));
    
    // Biweekly on Tuesday and Thursday at specific times
    scheduler.every(Tuesday).at("14:20:17").and_every(Thursday).at("15:00").run(|| println!("Biweekly task"));
  12. Calculate the next scheduled time using `RunConfig`

    master

    Once you have a RunConfig, you can determine when the next occurrence will be relative to a given DateTime using the .next() method. This method implements the NextTime trait.

    # use clokwerk::{RunConfig, TimeUnits};
    # use chrono::{DateTime, Utc, NaiveTime};
    
    let rc = RunConfig::from_interval(1.day()).with_time(NaiveTime::from_hms(15, 0, 0));
    let now = Utc::now();
    let next_run = rc.next(&now);