Hone logo
Hone
Problems

Go Cron Job Scheduler

This challenge asks you to build a basic cron job scheduler in Go. Cron jobs are essential for automating repetitive tasks on servers, such as running backups, sending out reports, or performing maintenance. Implementing your own scheduler will deepen your understanding of Go's concurrency primitives and time-based operations.

Problem Description

You need to create a Go program that can schedule and execute functions at specific intervals or times, mimicking the functionality of a cron system. The scheduler should allow users to:

  • Add jobs: Register functions to be executed. Each job should have a unique identifier and a schedule.
  • Define schedules: Schedules can be defined using a simplified cron-like syntax or by specifying fixed intervals.
  • Execute jobs: When a scheduled time arrives, the associated function should be executed.
  • Stop the scheduler: The scheduler should be able to be gracefully shut down, ensuring all running jobs are given a chance to complete or are properly interrupted (depending on your design choice for job cancellation).

Key Requirements:

  1. Job Registration: A mechanism to add jobs with a name (string) and a function (func()).
  2. Scheduling Options: Support for at least two types of scheduling:
    • Interval-based: Execute a job every N seconds/minutes/hours.
    • Specific time-based: Execute a job at a specific time of day (e.g., 03:00 AM) or on specific days of the week. A simplified cron-like syntax (e.g., * * * * * for every minute, 0 3 * * * for 3 AM daily) would be ideal.
  3. Concurrency: Jobs should be able to run concurrently if their scheduled times overlap or if multiple jobs are due.
  4. Scheduler Lifecycle: The scheduler needs to be started and stopped gracefully. Stopping should ideally not interrupt currently executing jobs unless explicitly designed to do so.
  5. Error Handling: Handle potential errors during job execution (e.g., panics within the job function) without crashing the entire scheduler.
  6. Job Identification: Each registered job should have a unique identifier (e.g., a string name) for potential management operations (though full management like deletion isn't strictly required for this challenge).

Expected Behavior:

The scheduler will continuously monitor time. When a registered job's schedule matches the current time, the associated function will be invoked. The scheduler should be able to run multiple jobs simultaneously without blocking each other. Upon receiving a stop signal, the scheduler should cease scheduling new jobs and allow existing jobs to finish their current execution before exiting.

Edge Cases to Consider:

  • Rapid Scheduling: What happens if jobs are scheduled for very frequent intervals (e.g., every millisecond)?
  • Job Duration: How does the scheduler handle jobs that take longer to execute than their scheduled interval?
  • Scheduler Restart: While not a primary requirement, consider how a restart might affect job scheduling.
  • System Time Changes: How would the scheduler behave if the system clock is adjusted? (For this challenge, you can assume a stable system clock).
  • Zero Jobs: The scheduler should function correctly even if no jobs are registered.

Examples

Example 1: Interval-based Job

Input:

// Assume a scheduler `s` has been initialized.
// A function `printTime` that prints the current time.
func printTime() {
    fmt.Println("Current time:", time.Now().Format(time.RFC3339))
}

// Schedule printTime to run every 5 seconds.
s.AddJob("print-every-5s", printTime, "*/5 * * * *") // Using a simplified cron syntax for demonstration

Expected Output (over several seconds):

Current time: 2023-10-27T10:00:00Z
Current time: 2023-10-27T10:00:05Z
Current time: 2023-10-27T10:00:10Z
... and so on

Explanation: The printTime function is scheduled to execute every 5 seconds. The scheduler detects the time passing and invokes printTime accordingly.

Example 2: Specific Time-based Job

Input:

// Assume a scheduler `s` has been initialized.
// A function `sendDailyReport` that simulates sending a report.
func sendDailyReport() {
    fmt.Println("Sending daily report at:", time.Now().Format(time.RFC3339))
}

// Schedule sendDailyReport to run daily at 03:00 AM.
s.AddJob("daily-report", sendDailyReport, "0 3 * * *")

Expected Output (on a specific day, around 03:00 AM):

Sending daily report at: 2023-10-28T03:00:00Z

Explanation: The sendDailyReport function is scheduled to run only once a day, specifically at 3 AM. The scheduler will wait until that exact time to execute the function.

Example 3: Concurrent Execution

Input:

// Assume a scheduler `s` has been initialized.
// Two functions: `jobA` and `jobB`.
func jobA() {
    fmt.Println("Job A started.")
    time.Sleep(3 * time.Second)
    fmt.Println("Job A finished.")
}

func jobB() {
    fmt.Println("Job B started.")
    time.Sleep(2 * time.Second)
    fmt.Println("Job B finished.")
}

// Schedule both jobs to run every second.
// This will cause them to often run concurrently.
s.AddJob("job-a", jobA, "* * * * *") // Runs every minute, demonstrating overlap if interval is shorter
s.AddJob("job-b", jobB, "* * * * *") // Runs every minute, demonstrating overlap if interval is shorter

Expected Output (interleaved and potentially overlapping):

Job A started.
Job B started.
Job B finished.
Job A finished.
Job A started.
Job B started.
Job B finished.
Job A finished.
... and so on

Explanation: Even though jobA takes longer than jobB, the scheduler does not wait for jobA to finish before starting jobB if their scheduled times are close. They run concurrently, showcasing Go's goroutine capabilities. (Note: For simplicity of demonstration, * * * * * means every minute here. A more realistic concurrent example might use a much shorter interval if the scheduler supports it).

Constraints

  • The scheduler should be implemented in pure Go, without relying on external cron libraries. Standard library packages (like time, fmt, context) are permitted.
  • The cron-like syntax should be a simplified subset. For instance, you might only need to support:
    • Minute: * or a specific minute (0-59)
    • Hour: * or a specific hour (0-23)
    • Day of Month: * or a specific day (1-31)
    • Month: * or a specific month (1-12)
    • Day of Week: * or a specific day (0-6, Sunday=0 or 7)
    • Note: For this challenge, supporting just */N for interval-based (e.g., */5 * * * * for every 5 minutes) and simple MM HH * * * for specific times (e.g., 0 3 * * * for 3 AM) is sufficient.
  • The scheduler should be reasonably efficient and not consume excessive CPU resources when idle.
  • The core scheduler logic should fit within a single Go package.

Notes

  • Consider using time.Ticker or time.Timer for managing scheduled events.
  • Goroutines will be crucial for handling concurrent job execution.
  • Think about how to manage the lifecycle of your scheduler (start, stop). context.Context is a good candidate for managing cancellation.
  • A simple approach to cron parsing is sufficient. You don't need a full-blown RFC-compliant parser.
  • For job execution, consider how to catch panics within job functions using defer and recover.
  • Success will be demonstrated by a program that reliably schedules and executes functions according to their defined schedules, handles concurrent execution, and shuts down cleanly.
Loading editor...
go