Hone logo
Hone
Problems

Implement a Go Time-Based Scheduler

This challenge asks you to build a custom scheduler in Go that can execute functions at specific intervals or after a defined delay. This is a fundamental building block for many applications, including background jobs, periodic data fetching, and event-driven systems.

Problem Description

Your task is to create a Scheduler type in Go that allows users to schedule the execution of functions (func()). The scheduler should support two main types of scheduling:

  1. Delayed Execution: Execute a function once after a specified duration.
  2. Periodic Execution: Execute a function repeatedly at a fixed interval.

The Scheduler should be able to manage multiple scheduled tasks concurrently and ensure they are executed at the correct times without blocking the main application flow.

Key Requirements:

  • NewScheduler(): A constructor function to create a new Scheduler instance.
  • ScheduleOnce(delay time.Duration, task func()): A method to schedule a task for execution after delay.
  • ScheduleInterval(interval time.Duration, task func()): A method to schedule a task for repeated execution every interval.
  • Start(): A method to begin the scheduler's operation. This method should be non-blocking and allow the scheduler to run in the background.
  • Stop(): A method to gracefully shut down the scheduler, canceling any pending or running tasks. This should allow any currently executing tasks to finish if possible (or have a mechanism to interrupt them, though finishing is preferred for this challenge).
  • Concurrency: The scheduler must handle multiple tasks concurrently.
  • Reliability: Tasks should be executed as close to their scheduled time as possible, considering system load.

Expected Behavior:

  • When Start() is called, the scheduler begins monitoring scheduled tasks.
  • Tasks scheduled with ScheduleOnce will execute after their specified delay.
  • Tasks scheduled with ScheduleInterval will execute repeatedly with the given interval.
  • When Stop() is called, the scheduler should cease scheduling new tasks and attempt to clean up existing ones.

Edge Cases to Consider:

  • Scheduling a task with a zero or negative duration/interval.
  • Calling Start() multiple times.
  • Calling Stop() before Start() or multiple times.
  • Tasks that take a very long time to execute.
  • A large number of scheduled tasks.

Examples

Example 1: Delayed Execution

package main

import (
	"fmt"
	"time"
)

func main() {
	scheduler := NewScheduler()
	go scheduler.Start() // Run scheduler in a goroutine

	task1 := func() {
		fmt.Println("Task 1 executed after 2 seconds.")
	}
	scheduler.ScheduleOnce(2*time.Second, task1)

	time.Sleep(3 * time.Second) // Allow time for task to execute
	scheduler.Stop()
	fmt.Println("Scheduler stopped.")
}

Expected Output (approximately):

Task 1 executed after 2 seconds.
Scheduler stopped.

Example 2: Periodic Execution

package main

import (
	"fmt"
	"time"
)

func main() {
	scheduler := NewScheduler()
	go scheduler.Start() // Run scheduler in a goroutine

	counter := 0
	task2 := func() {
		counter++
		fmt.Printf("Task 2 executed (count: %d) every 1 second.\n", counter)
	}
	scheduler.ScheduleInterval(1*time.Second, task2)

	time.Sleep(3500 * time.Millisecond) // Let it run for ~3.5 seconds
	fmt.Println("Stopping scheduler...")
	scheduler.Stop()
	fmt.Println("Scheduler stopped.")
}

Expected Output (approximately):

Task 2 executed (count: 1) every 1 second.
Task 2 executed (count: 2) every 1 second.
Task 2 executed (count: 3) every 1 second.
Stopping scheduler...
Scheduler stopped.

Example 3: Mixed Scheduling and Stopping

package main

import (
	"fmt"
	"time"
)

func main() {
	scheduler := NewScheduler()
	go scheduler.Start()

	taskA := func() { fmt.Println("Task A (once) executed.") }
	taskB := func() { fmt.Println("Task B (interval) executed.") }
	taskC := func() { fmt.Println("Task C (once, delayed) executed.") }

	scheduler.ScheduleOnce(500*time.Millisecond, taskA)
	scheduler.ScheduleInterval(700*time.Millisecond, taskB)
	scheduler.ScheduleOnce(2*time.Second, taskC)

	time.Sleep(1 * time.Second)
	fmt.Println("Stopping scheduler...")
	scheduler.Stop()
	fmt.Println("Scheduler stopped.")
}

Expected Output (approximately):

Task A (once) executed.
Task B (interval) executed.
Stopping scheduler...
Scheduler stopped.

(Note: Task C might not execute as the scheduler stops before its scheduled time).

Constraints

  • The scheduler must be implemented using standard Go libraries. No external third-party scheduling libraries are allowed.
  • The Scheduler type should be thread-safe. Multiple goroutines can call ScheduleOnce, ScheduleInterval, Start, and Stop.
  • Tasks scheduled with ScheduleInterval should execute at most N+1 times if Stop() is called after N executions have completed.
  • The Stop() method should signal to all active goroutines responsible for executing tasks that they should exit.

Notes

  • Consider using time.Timer and time.Ticker for managing the timing of your tasks.
  • A central loop within the Start() method will likely be necessary to manage scheduled jobs.
  • Using channels can be very effective for communication between different parts of your scheduler (e.g., signaling tasks to stop).
  • Think about how to represent scheduled tasks internally. A struct containing the task function, its schedule type, and timing information would be appropriate.
  • For ScheduleInterval, ensure that the interval between executions is consistent, even if a task takes longer than the interval to complete. A common approach is to use time.Ticker for this.
Loading editor...
go