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:
- Delay-based scheduling: Schedule a task to run after a specified duration.
- 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 theTaskScheduler.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.
ScheduleConfigstruct: This struct should encapsulate scheduling parameters. It needs to be flexible enough to support both delay and periodic tasks. Consider fields likeDelay time.DurationandInterval time.Duration. You'll need to define how these fields are interpreted (e.g., ifDelayis set,Intervalmight 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
Delayshould execute exactly once after that delay. - Tasks scheduled with an
Intervalshould execute repeatedly everyIntervalduration. IfDelayis also specified for a periodic task, the first execution should happen afterDelay, and subsequent executions should follow theInterval. - 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
taskFuncpanics? 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 theIntervalhas 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 scheduledIntervalafter the start of the previous one. - Scheduling a task with zero
Delayand zeroInterval.
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
TaskSchedulershould handle at least 100 concurrent tasks. - Task names (
taskName) are unique strings. time.Durationvalues forDelayandIntervalwill 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.WaitGroupcan be helpful for managing the completion of tasks duringStop(). - Think about how to safely manage shared state within the
TaskSchedulerstruct. - Consider how to implement the
ScheduleConfigto elegantly handle one-off (delay only) and recurring (interval) tasks. You might want to define a clear precedence or interpretation rule forDelayandIntervalwhen both are present. - For panic recovery within
taskFunc, you might consider usingdeferandrecoverwithin the goroutine that executes the task.