Hone logo
Hone
Problems

Go Task Scheduler

Design and implement a task scheduler in Go that allows users to schedule functions to run at specific times or after a certain delay. This is a common building block for many applications, enabling background processing, periodic updates, and time-based event handling.

Problem Description

Your goal is to create a TaskScheduler struct in Go that can manage and execute scheduled tasks. The scheduler should support two primary scheduling mechanisms:

  1. Delay-based scheduling: Schedule a task to run after a specified duration.
  2. Periodic scheduling: Schedule a task to run repeatedly at a fixed interval.

The scheduler should be able to:

  • Add new tasks with their respective scheduling configurations.
  • Start and stop the scheduler.
  • Handle multiple tasks concurrently.
  • Gracefully stop and cancel running tasks upon scheduler shutdown.

Key Requirements:

  • NewTaskScheduler(): A constructor function to create a new instance of the TaskScheduler.
  • AddTask(taskName string, taskFunc func(), schedule ScheduleConfig): A method to add a task.
    • taskName: A unique identifier for the task.
    • taskFunc: The function to be executed.
    • schedule: A configuration struct defining how and when the task should run.
  • ScheduleConfig struct: This struct should encapsulate scheduling parameters. It needs to be flexible enough to support both delay and periodic tasks. Consider fields like Delay time.Duration and Interval time.Duration. You'll need to define how these fields are interpreted (e.g., if Delay is set, Interval might be ignored for a one-off execution).
  • Start(): A method to begin executing scheduled tasks. This method should ideally not block indefinitely, but rather start a goroutine to manage task execution.
  • Stop(): A method to gracefully shut down the scheduler. This should signal all running tasks to stop and wait for them to complete or be cancelled.
  • Concurrency: The scheduler should be able to run multiple tasks concurrently without interfering with each other.

Expected Behavior:

  • Tasks scheduled with a Delay should execute exactly once after that delay.
  • Tasks scheduled with an Interval should execute repeatedly every Interval duration. If Delay is also specified for a periodic task, the first execution should happen after Delay, and subsequent executions should follow the Interval.
  • Calling Stop() should halt any new task executions and allow currently running tasks to finish (or be cancelled if they respect cancellation signals).

Edge Cases:

  • What happens if Stop() is called multiple times?
  • What happens if a task taskFunc panics? The scheduler should ideally not crash.
  • What if a task takes longer to execute than its Interval? The next execution should still be scheduled after the Interval has passed since the start of the previous execution (or the previous schedule time, depending on your design choice – be explicit). For this challenge, let's assume the next execution should be scheduled Interval after the start of the previous one.
  • Scheduling a task with zero Delay and zero Interval.

Examples

Example 1: Delay-based Task

package main

import (
	"fmt"
	"time"
)

func main() {
	scheduler := NewTaskScheduler()

	task1Func := func() {
		fmt.Println("Task 1 executed after 2-second delay.")
	}
	task1Schedule := ScheduleConfig{Delay: 2 * time.Second}
	scheduler.AddTask("task1", task1Func, task1Schedule)

	fmt.Println("Starting scheduler...")
	scheduler.Start()

	// Keep the main goroutine alive to allow tasks to run
	time.Sleep(3 * time.Second)

	fmt.Println("Stopping scheduler...")
	scheduler.Stop()
	fmt.Println("Scheduler stopped.")
}

Output:

Starting scheduler...
Task 1 executed after 2-second delay.
Stopping scheduler...
Scheduler stopped.

Explanation: task1 is scheduled to run after a 2-second delay. The scheduler is started, and after approximately 2 seconds, task1Func is executed. The program then waits for 3 seconds to ensure the task has time to run, and then stops the scheduler.

Example 2: Periodic Task

package main

import (
	"fmt"
	"time"
)

func main() {
	scheduler := NewTaskScheduler()

	task2Func := func() {
		fmt.Printf("Task 2 executed at: %s\n", time.Now().Format("15:04:05"))
	}
	task2Schedule := ScheduleConfig{Interval: 1 * time.Second}
	scheduler.AddTask("task2", task2Func, task2Schedule)

	fmt.Println("Starting scheduler...")
	scheduler.Start()

	// Let tasks run for a few seconds
	time.Sleep(3 * time.Second)

	fmt.Println("Stopping scheduler...")
	scheduler.Stop()
	fmt.Println("Scheduler stopped.")
}

Output (approximate):

Starting scheduler...
Task 2 executed at: 10:30:01
Task 2 executed at: 10:30:02
Task 2 executed at: 10:30:03
Stopping scheduler...
Scheduler stopped.

Explanation: task2 is scheduled to run every 1 second. The scheduler starts, and task2Func is executed roughly every second for 3 seconds. Then, the scheduler is stopped.

Example 3: Combined Delay and Interval

package main

import (
	"fmt"
	"time"
)

func main() {
	scheduler := NewTaskScheduler()

	task3Func := func() {
		fmt.Printf("Task 3 executed at: %s\n", time.Now().Format("15:04:05"))
	}
	// Runs after 1 second, then every 500ms
	task3Schedule := ScheduleConfig{Delay: 1 * time.Second, Interval: 500 * time.Millisecond}
	scheduler.AddTask("task3", task3Func, task3Schedule)

	fmt.Println("Starting scheduler...")
	scheduler.Start()

	// Let tasks run for a few seconds
	time.Sleep(3 * time.Second)

	fmt.Println("Stopping scheduler...")
	scheduler.Stop()
	fmt.Println("Scheduler stopped.")
}

Output (approximate):

Starting scheduler...
Task 3 executed at: 10:30:01
Task 3 executed at: 10:30:01 // (approx 500ms later)
Task 3 executed at: 10:30:02 // (approx 500ms later)
Task 3 executed at: 10:30:02 // (approx 500ms later)
Task 3 executed at: 10:30:03 // (approx 500ms later)
Stopping scheduler...
Scheduler stopped.

Explanation: task3 has an initial delay of 1 second, followed by periodic executions every 500 milliseconds. The first execution occurs after 1 second, and then it continues to run every 500ms for the duration the scheduler is active.

Constraints

  • The TaskScheduler should handle at least 100 concurrent tasks.
  • Task names (taskName) are unique strings.
  • time.Duration values for Delay and Interval will be non-negative.
  • The Stop() method should aim to return within 5 seconds of being called, allowing for ongoing tasks to finish gracefully.
  • The solution should use standard Go libraries.

Notes

  • Consider using channels for communication between the scheduler and task execution goroutines.
  • A sync.WaitGroup can be helpful for managing the completion of tasks during Stop().
  • Think about how to safely manage shared state within the TaskScheduler struct.
  • Consider how to implement the ScheduleConfig to elegantly handle one-off (delay only) and recurring (interval) tasks. You might want to define a clear precedence or interpretation rule for Delay and Interval when both are present.
  • For panic recovery within taskFunc, you might consider using defer and recover within the goroutine that executes the task.
Loading editor...
go