├── .github ├── dependabot.yml └── workflows │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── tasks.go ├── tasks_benchmarks_test.go └── tasks_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - actions 8 | - main 9 | pull_request: 10 | jobs: 11 | golangci: 12 | name: golangci 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: golangci-lint 17 | uses: golangci/golangci-lint-action@v2 18 | with: 19 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 20 | version: latest 21 | 22 | # Optional: working directory, useful for monorepos 23 | # working-directory: somedir 24 | 25 | # Optional: golangci-lint command line arguments. 26 | args: -E misspell -E revive -E errname -E gofmt 27 | 28 | # Optional: show only new issues if it's a pull request. The default value is `false`. 29 | # only-new-issues: true 30 | 31 | # Optional: if set to true then the action will use pre-installed Go. 32 | # skip-go-installation: true 33 | 34 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 35 | # skip-pkg-cache: true 36 | 37 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 38 | # skip-build-cache: true 39 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - actions 9 | - main 10 | pull_request: 11 | 12 | jobs: 13 | 14 | tests: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: 1.18 23 | 24 | - name: Install cover 25 | run: go get golang.org/x/tools/cmd/cover 26 | 27 | - name: Install goveralls 28 | run: go get -u github.com/mattn/goveralls 29 | 30 | - name: Test 31 | run: go test -race -v -covermode=atomic -coverprofile=coverage.out ./... 32 | 33 | - name: Upload coverage reports to Codecov 34 | uses: codecov/codecov-action@v4.0.1 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | slug: madflojo/tasks 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Coverage files 15 | coverage.html 16 | 17 | # vim swp files 18 | *.swp 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Tasks 2 | 3 | Thank you for being so interested in helping develop Tasks. The time, skills, and perspectives you contribute to this project are valued. 4 | 5 | ## Issues and Proposals 6 | 7 | Bugs, Proposals, & Feature Requests are all welcome. To get started, please open an issue via GitHub. Please provide as much detail as possible. 8 | 9 | ## Contributing 10 | 11 | Contributions are always appreciated, please try to maintain usage contracts. If you are unsure, please open an issue to discuss. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Benjamin Cane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tasks 2 | 3 | [![tests](https://github.com/madflojo/tasks/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/madflojo/tasks/actions/workflows/tests.yml) 4 | [![codecov](https://codecov.io/gh/madflojo/tasks/graph/badge.svg?token=882QTXA7PX)](https://codecov.io/gh/madflojo/tasks) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/madflojo/tasks)](https://goreportcard.com/report/github.com/madflojo/tasks) 6 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/madflojo/tasks)](https://pkg.go.dev/github.com/madflojo/tasks) 7 | 8 | Package tasks is an easy to use in-process scheduler for recurring tasks in Go. Tasks is focused on high frequency 9 | tasks that run quick, and often. The goal of Tasks is to support concurrent running tasks at scale without scheduler 10 | induced jitter. 11 | 12 | Tasks is focused on accuracy of task execution. To do this each task is called within it's own goroutine. This ensures 13 | that long execution of a single invocation does not throw the schedule as a whole off track. 14 | 15 | For simplicity this task scheduler uses the time.Duration type to specify intervals. This allows for a simple interface 16 | and flexible control over when tasks are executed. 17 | 18 | ## Key Features 19 | 20 | - **Concurrent Execution**: Tasks are executed in their own goroutines, ensuring accurate scheduling even when individual tasks take longer to complete. 21 | - **Optimized Goroutine Scheduling**: Tasks leverages Go's `time.AfterFunc()` function to reduce sleeping goroutines and optimize CPU scheduling. 22 | - **Flexible Task Intervals**: Tasks uses the `time.Duration` type to specify intervals, offering a simple interface and flexible control over task execution timing. 23 | - **Delayed Task Start**: Schedule tasks to start at a later time by specifying a start time, allowing for greater control over task execution. 24 | - **One-Time Tasks**: Schedule tasks to run only once by setting the `RunOnce` flag, ideal for single-use tasks or one-time actions. 25 | - **Custom Error Handling**: Define a custom error handling function to handle errors returned by tasks, enabling tailored error handling logic. 26 | 27 | ## Usage 28 | 29 | Here are some examples to help you get started with Tasks: 30 | 31 | ### Basic Usage 32 | 33 | ```go 34 | // Start the Scheduler 35 | scheduler := tasks.New() 36 | defer scheduler.Stop() 37 | 38 | // Add a task 39 | id, err := scheduler.Add(&tasks.Task{ 40 | Interval: 30 * time.Second, 41 | TaskFunc: func() error { 42 | // Put your logic here 43 | }, 44 | }) 45 | if err != nil { 46 | // Do Stuff 47 | } 48 | ``` 49 | 50 | ### Delayed Scheduling 51 | 52 | Sometimes schedules need to started at a later time. This package provides the ability to start a task only after a 53 | certain time. The below example shows this in practice. 54 | 55 | ```go 56 | // Add a recurring task for every 30 days, starting 30 days from now 57 | id, err := scheduler.Add(&tasks.Task{ 58 | Interval: 30 * (24 * time.Hour), 59 | StartAfter: time.Now().Add(30 * (24 * time.Hour)), 60 | TaskFunc: func() error { 61 | // Put your logic here 62 | }, 63 | }) 64 | if err != nil { 65 | // Do Stuff 66 | } 67 | ``` 68 | 69 | ### One-Time Tasks 70 | 71 | It is also common for applications to run a task only once. The below example shows scheduling a task to run only once 72 | after waiting for 60 seconds. 73 | 74 | ```go 75 | // Add a one time only task for 60 seconds from now 76 | id, err := scheduler.Add(&tasks.Task{ 77 | Interval: 60 * time.Second, 78 | RunOnce: true, 79 | TaskFunc: func() error { 80 | // Put your logic here 81 | }, 82 | }) 83 | if err != nil { 84 | // Do Stuff 85 | } 86 | ``` 87 | 88 | ### Custom Error Handling 89 | 90 | One powerful feature of Tasks is that it allows users to specify custom error handling. This is done by allowing users 91 | to define a function that is called when a task returns an error. The below example shows scheduling a task that logs 92 | when an error occurs. 93 | 94 | ```go 95 | // Add a task with custom error handling 96 | id, err := scheduler.Add(&tasks.Task{ 97 | Interval: 30 * time.Second, 98 | TaskFunc: func() error { 99 | // Put your logic here 100 | }, 101 | ErrFunc: func(e error) { 102 | log.Printf("An error occurred when executing task %s - %s", id, e) 103 | }, 104 | }) 105 | if err != nil { 106 | // Do Stuff 107 | } 108 | ``` 109 | 110 | For more details on usage, see the [GoDoc](https://pkg.go.dev/github.com/madflojo/tasks). 111 | 112 | ## Contributing 113 | 114 | Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for more details. 115 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/madflojo/tasks 2 | 3 | go 1.18 4 | 5 | require github.com/rs/xid v1.6.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 2 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 3 | -------------------------------------------------------------------------------- /tasks.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package tasks is an easy to use in-process scheduler for recurring tasks in Go. Tasks is focused on high frequency 3 | tasks that run quick, and often. The goal of Tasks is to support concurrent running tasks at scale without scheduler 4 | induced jitter. 5 | 6 | Tasks is focused on accuracy of task execution. To do this each task is called within it's own goroutine. 7 | This ensures that long execution of a single invocation does not throw the schedule as a whole off track. 8 | 9 | As usage of this scheduler scales, it is expected to have a larger number of sleeping goroutines. As it is 10 | designed to leverage Go's ability to optimize goroutine CPU scheduling. 11 | 12 | For simplicity this task scheduler uses the time.Duration type to specify intervals. This allows for a simple 13 | interface and flexible control over when tasks are executed. 14 | 15 | Below is an example of starting the scheduler and registering a new task that runs every 30 seconds. 16 | 17 | // Start the Scheduler 18 | scheduler := tasks.New() 19 | defer scheduler.Stop() 20 | 21 | // Add a task 22 | id, err := scheduler.Add(&tasks.Task{ 23 | Interval: time.Duration(30 * time.Second), 24 | TaskFunc: func() error { 25 | // Put your logic here 26 | }, 27 | }) 28 | if err != nil { 29 | // Do Stuff 30 | } 31 | 32 | Sometimes schedules need to started at a later time. This package provides the ability to start a task only after 33 | a certain time. The below example shows this in practice. 34 | 35 | // Add a recurring task for every 30 days, starting 30 days from now 36 | id, err := scheduler.Add(&tasks.Task{ 37 | Interval: time.Duration(30 * (24 * time.Hour)), 38 | StartAfter: time.Now().Add(30 * (24 * time.Hour)), 39 | TaskFunc: func() error { 40 | // Put your logic here 41 | }, 42 | }) 43 | if err != nil { 44 | // Do Stuff 45 | } 46 | 47 | It is also common for applications to run a task only once. The below example shows scheduling a task to run only once after 48 | waiting for 60 seconds. 49 | 50 | // Add a one time only task for 60 seconds from now 51 | id, err := scheduler.Add(&tasks.Task{ 52 | Interval: time.Duration(60 * time.Second) 53 | RunOnce: true, 54 | TaskFunc: func() error { 55 | // Put your logic here 56 | }, 57 | }) 58 | if err != nil { 59 | // Do Stuff 60 | } 61 | 62 | One powerful feature of Tasks is that it allows users to specify custom error handling. This is done by allowing users to 63 | define a function that is called when a task returns an error. The below example shows scheduling a task that logs when an 64 | error occurs. 65 | 66 | // Add a task with custom error handling 67 | id, err := scheduler.Add(&tasks.Task{ 68 | Interval: time.Duration(30 * time.Second), 69 | TaskFunc: func() error { 70 | // Put your logic here 71 | }(), 72 | ErrFunc: func(e error) { 73 | log.Printf("An error occurred when executing task %s - %s", id, e) 74 | }, 75 | }) 76 | if err != nil { 77 | // Do Stuff 78 | } 79 | */ 80 | package tasks 81 | 82 | import ( 83 | "context" 84 | "fmt" 85 | "sync" 86 | "time" 87 | 88 | "github.com/rs/xid" 89 | ) 90 | 91 | // Task contains the scheduled task details and control mechanisms. This struct is used during the creation of tasks. 92 | // It allows users to control how and when tasks are executed. 93 | type Task struct { 94 | sync.RWMutex 95 | 96 | // TaskContext allows for user-defined context that is passed to task functions. 97 | TaskContext TaskContext 98 | 99 | // Interval is the frequency that the task executes. Defining this at 30 seconds, will result in a task that 100 | // runs every 30 seconds. 101 | // 102 | // The below are common examples to get started with. 103 | // 104 | // // Every 30 seconds 105 | // time.Duration(30 * time.Second) 106 | // // Every 5 minutes 107 | // time.Duration(5 * time.Minute) 108 | // // Every 12 hours 109 | // time.Duration(12 * time.Hour) 110 | // // Every 30 days 111 | // time.Duration(30 * (24 * time.Hour)) 112 | // 113 | Interval time.Duration 114 | 115 | // RunOnce is used to set this task as a single execution task. By default, tasks will continue executing at 116 | // the interval specified until deleted. With RunOnce enabled the first execution of the task will result in 117 | // the task self deleting. 118 | RunOnce bool 119 | 120 | // RunSingleInstance is used to set a task as a single instance task. By default, tasks will continue executing at 121 | // the interval specified until deleted. With RunSingleInstance enabled a subsequent task execution will be skipped 122 | // if the previous task execution is still running. 123 | // 124 | // This is useful for tasks that may take longer than the interval to execute. This will prevent multiple instances 125 | // of the same task from running concurrently. 126 | RunSingleInstance bool 127 | 128 | // StartAfter is used to specify a start time for the scheduler. When set, tasks will wait for the specified 129 | // time to start the schedule timer. 130 | StartAfter time.Time 131 | 132 | // TaskFunc is the user defined function to execute as part of this task. 133 | // 134 | // Either TaskFunc or FuncWithTaskContext must be defined. If both are defined, FuncWithTaskContext will be used. 135 | TaskFunc func() error 136 | 137 | // ErrFunc allows users to define a function that is called when tasks return an error. If ErrFunc is nil, 138 | // errors from tasks will be ignored. 139 | // 140 | // Either ErrFunc or ErrFuncWithTaskContext must be defined. If both are defined, ErrFuncWithTaskContext will be used. 141 | ErrFunc func(error) 142 | 143 | // FuncWithTaskContext is a user defined function to execute as part of this task. This function is used in 144 | // place of TaskFunc with the difference in that it will pass the user defined context from the Task configurations. 145 | // 146 | // Either TaskFunc or FuncWithTaskContext must be defined. If both are defined, FuncWithTaskContext will be used. 147 | FuncWithTaskContext func(TaskContext) error 148 | 149 | // ErrFuncWithTaskContext allows users to define a function that is called when tasks return an error. 150 | // If ErrFunc is nil, errors from tasks will be ignored. This function is used in place of ErrFunc with 151 | // the difference in that it will pass the user defined context from the Task configurations. 152 | // 153 | // Either ErrFunc or ErrFuncWithTaskContext must be defined. If both are defined, ErrFuncWithTaskContext will be used. 154 | ErrFuncWithTaskContext func(TaskContext, error) 155 | 156 | // id is the Unique ID created for each task. This ID is generated by the Add() function. 157 | id string 158 | 159 | // running is used for RunSingleInstance tasks to track whether a previous invocation is still running. 160 | running sync.Mutex 161 | 162 | // timer is the internal task timer. This is stored here to provide control via main scheduler functions. 163 | timer *time.Timer 164 | 165 | // ctx is the internal context used to control task cancelation. 166 | ctx context.Context 167 | 168 | // cancel is used to cancel tasks gracefully. This will not interrupt a task function that has already been 169 | // triggered. 170 | cancel context.CancelFunc 171 | } 172 | 173 | type TaskContext struct { 174 | // Context is a user-defined context. 175 | Context context.Context 176 | 177 | // id is the Unique ID created for each task. This ID is generated by the Add() function. 178 | id string 179 | } 180 | 181 | // safeOps safely change task's data 182 | func (t *Task) safeOps(f func()) { 183 | t.Lock() 184 | defer t.Unlock() 185 | 186 | f() 187 | } 188 | 189 | // Scheduler stores the internal task list and provides an interface for task management. 190 | type Scheduler struct { 191 | sync.RWMutex 192 | 193 | // tasks is the internal task list used to store tasks that are currently scheduled. 194 | tasks map[string]*Task 195 | } 196 | 197 | var ( 198 | // ErrIDInUse is returned when a Task ID is specified but already used. 199 | ErrIDInUse = fmt.Errorf("ID already used") 200 | ) 201 | 202 | // New will create a new scheduler instance that allows users to create and manage tasks. 203 | func New() *Scheduler { 204 | s := &Scheduler{} 205 | s.tasks = make(map[string]*Task) 206 | return s 207 | } 208 | 209 | // Add will add a task to the task list and schedule it. Once added, tasks will wait the defined time interval and then 210 | // execute. This means a task with a 15 second interval will be triggered 15 seconds after Add is complete. Not before 211 | // or after (excluding typical machine time jitter). 212 | // 213 | // // Add a task 214 | // id, err := scheduler.Add(&tasks.Task{ 215 | // Interval: time.Duration(30 * time.Second), 216 | // TaskFunc: func() error { 217 | // // Put your logic here 218 | // }(), 219 | // ErrFunc: func(err error) { 220 | // // Put custom error handling here 221 | // }(), 222 | // }) 223 | // if err != nil { 224 | // // Do stuff 225 | // } 226 | func (schd *Scheduler) Add(t *Task) (string, error) { 227 | id := xid.New() 228 | err := schd.AddWithID(id.String(), t) 229 | if err == ErrIDInUse { 230 | return schd.Add(t) 231 | } 232 | return id.String(), err 233 | } 234 | 235 | // AddWithID will add a task with an ID to the task list and schedule it. It will return an error if the ID is in-use. 236 | // Once added, tasks will wait the defined time interval and then execute. This means a task with a 15 second interval 237 | // will be triggered 15 seconds after Add is complete. Not before or after (excluding typical machine time jitter). 238 | // 239 | // // Add a task 240 | // id := xid.New() 241 | // err := scheduler.AddWithID(id, &tasks.Task{ 242 | // Interval: time.Duration(30 * time.Second), 243 | // TaskFunc: func() error { 244 | // // Put your logic here 245 | // }(), 246 | // ErrFunc: func(err error) { 247 | // // Put custom error handling here 248 | // }(), 249 | // }) 250 | // if err != nil { 251 | // // Do stuff 252 | // } 253 | func (schd *Scheduler) AddWithID(id string, t *Task) error { 254 | // Check if TaskFunc is nil before doing anything 255 | if t.TaskFunc == nil && t.FuncWithTaskContext == nil { 256 | return fmt.Errorf("task function cannot be nil") 257 | } 258 | 259 | // Ensure Interval is never 0, this would cause Timer to panic 260 | if t.Interval <= time.Duration(0) { 261 | return fmt.Errorf("task interval must be defined") 262 | } 263 | 264 | // Create Context used to cancel downstream Goroutines 265 | t.ctx, t.cancel = context.WithCancel(context.Background()) 266 | 267 | // Add id to TaskContext 268 | t.TaskContext.id = id 269 | 270 | // Check id is not in use, then add to task list and start background task 271 | schd.Lock() 272 | defer schd.Unlock() 273 | if _, ok := schd.tasks[id]; ok { 274 | return ErrIDInUse 275 | } 276 | t.id = id 277 | 278 | // To make up for bad design decisions we need to copy the task for execution 279 | task := t.Clone() 280 | 281 | // Add task to schedule 282 | schd.tasks[t.id] = task 283 | schd.scheduleTask(task) 284 | 285 | return nil 286 | } 287 | 288 | // Del will unschedule the specified task and remove it from the task list. Deletion will prevent future invocations of 289 | // a task, but not interrupt a trigged task. 290 | func (schd *Scheduler) Del(name string) { 291 | // Grab task from task list 292 | t, err := schd.Lookup(name) 293 | if err != nil { 294 | return 295 | } 296 | 297 | // Stop the task 298 | defer t.cancel() 299 | 300 | t.Lock() 301 | defer t.Unlock() 302 | 303 | if t.timer != nil { 304 | defer t.timer.Stop() 305 | } 306 | 307 | // Remove from task list 308 | schd.Lock() 309 | defer schd.Unlock() 310 | delete(schd.tasks, name) 311 | } 312 | 313 | // Lookup will find the specified task from the internal task list using the task ID provided. 314 | // 315 | // The returned task should be treated as read-only, and not modified outside of this package. Doing so, may cause 316 | // panics. 317 | func (schd *Scheduler) Lookup(name string) (*Task, error) { 318 | schd.RLock() 319 | defer schd.RUnlock() 320 | t, ok := schd.tasks[name] 321 | if ok { 322 | return t.Clone(), nil 323 | } 324 | return t, fmt.Errorf("could not find task within the task list") 325 | } 326 | 327 | // Tasks is used to return a copy of the internal tasks map. 328 | // 329 | // The returned task should be treated as read-only, and not modified outside of this package. Doing so, may cause 330 | // panics. 331 | func (schd *Scheduler) Tasks() map[string]*Task { 332 | schd.RLock() 333 | defer schd.RUnlock() 334 | m := make(map[string]*Task) 335 | for k, v := range schd.tasks { 336 | m[k] = v.Clone() 337 | } 338 | return m 339 | } 340 | 341 | // Stop is used to unschedule and delete all tasks owned by the scheduler instance. 342 | func (schd *Scheduler) Stop() { 343 | tt := schd.Tasks() 344 | for n := range tt { 345 | schd.Del(n) 346 | } 347 | } 348 | 349 | // scheduleTask creates the underlying scheduled task. If StartAfter is set, this routine will wait until the 350 | // time specified. 351 | func (schd *Scheduler) scheduleTask(t *Task) { 352 | _ = time.AfterFunc(time.Until(t.StartAfter), func() { 353 | var err error 354 | 355 | // Verify if task has been cancelled before scheduling 356 | t.safeOps(func() { 357 | err = t.ctx.Err() 358 | }) 359 | if err != nil { 360 | // Task has been cancelled, do not schedule 361 | return 362 | } 363 | 364 | // Schedule task 365 | t.safeOps(func() { 366 | t.timer = time.AfterFunc(t.Interval, func() { schd.execTask(t) }) 367 | }) 368 | }) 369 | } 370 | 371 | // execTask is the underlying scheduler, it is used to trigger and execute tasks. 372 | func (schd *Scheduler) execTask(t *Task) { 373 | go func() { 374 | if t.RunSingleInstance { 375 | if !t.running.TryLock() { 376 | // Skip execution if task is already running 377 | return 378 | } 379 | defer t.running.Unlock() 380 | } 381 | 382 | // Execute task 383 | var err error 384 | if t.FuncWithTaskContext != nil { 385 | err = t.FuncWithTaskContext(t.TaskContext) 386 | } else { 387 | err = t.TaskFunc() 388 | } 389 | if err != nil && (t.ErrFunc != nil || t.ErrFuncWithTaskContext != nil) { 390 | if t.ErrFuncWithTaskContext != nil { 391 | go t.ErrFuncWithTaskContext(t.TaskContext, err) 392 | } else { 393 | go t.ErrFunc(err) 394 | } 395 | } 396 | 397 | // If RunOnce is set, delete the task after execution 398 | if t.RunOnce { 399 | defer schd.Del(t.id) 400 | } 401 | }() 402 | 403 | // Reschedule task for next execution 404 | if !t.RunOnce { 405 | t.safeOps(func() { 406 | t.timer.Reset(t.Interval) 407 | }) 408 | } 409 | } 410 | 411 | // ID will return the task ID. This is the same as the ID generated by the scheduler when adding a task. 412 | // If the task was added with AddWithID, this will be the same as the ID provided. 413 | func (ctx TaskContext) ID() string { 414 | return ctx.id 415 | } 416 | 417 | // Clone will create a copy of the existing task. This is useful for creating a new task with the same properties as 418 | // an existing task. It is also used internally when creating a new task. 419 | func (t *Task) Clone() *Task { 420 | task := &Task{} 421 | t.safeOps(func() { 422 | task.TaskFunc = t.TaskFunc 423 | task.FuncWithTaskContext = t.FuncWithTaskContext 424 | task.ErrFunc = t.ErrFunc 425 | task.ErrFuncWithTaskContext = t.ErrFuncWithTaskContext 426 | task.Interval = t.Interval 427 | task.StartAfter = t.StartAfter 428 | task.RunOnce = t.RunOnce 429 | task.RunSingleInstance = t.RunSingleInstance 430 | task.id = t.id 431 | task.ctx = t.ctx 432 | task.cancel = t.cancel 433 | task.timer = t.timer 434 | task.TaskContext = t.TaskContext 435 | }) 436 | return task 437 | } 438 | -------------------------------------------------------------------------------- /tasks_benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func BenchmarkTasks(b *testing.B) { 9 | // Create base scheduler to use 10 | scheduler := New() 11 | defer scheduler.Stop() 12 | 13 | // Setup a single task for re-use 14 | taskID, err := scheduler.Add(&Task{ 15 | Interval: time.Duration(1 * time.Minute), 16 | TaskFunc: func() error { return nil }, 17 | ErrFunc: func(_ error) {}, 18 | }) 19 | if err != nil { 20 | b.Fatalf("Unable to schedule example task - %s", err) 21 | } 22 | defer scheduler.Del(taskID) 23 | 24 | // Grab example for re-use 25 | exampleTask, err := scheduler.Lookup(taskID) 26 | if err != nil { 27 | b.Fatalf("Unable to lookup newly added task - %s", err) 28 | } 29 | 30 | b.Run("Adding a scheduler", func(b *testing.B) { 31 | b.ReportAllocs() 32 | b.ResetTimer() 33 | for i := 0; i < b.N; i++ { 34 | _, err := scheduler.Add(exampleTask) 35 | if err != nil { 36 | b.Fatalf("Unable to add new scheduled task - %s", err) 37 | } 38 | } 39 | }) 40 | 41 | // Clear Excess Tasks 42 | for id := range scheduler.Tasks() { 43 | if id == taskID { 44 | continue 45 | } 46 | scheduler.Del(id) 47 | } 48 | 49 | b.Run("Looking up a scheduled task", func(b *testing.B) { 50 | b.ReportAllocs() 51 | b.ResetTimer() 52 | for i := 0; i < b.N; i++ { 53 | _, err := scheduler.Lookup(taskID) 54 | if err != nil { 55 | b.Fatalf("Unable to lookup scheduled tasks - %s", err) 56 | } 57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /tasks_test.go: -------------------------------------------------------------------------------- 1 | package tasks 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/rs/xid" 11 | ) 12 | 13 | type InterfaceTestCase struct { 14 | name string 15 | task *Task 16 | id string 17 | addErr bool 18 | } 19 | 20 | type ExecutionTestCase struct { 21 | name string 22 | id string 23 | ctx context.Context 24 | cancel context.CancelFunc 25 | task *Task 26 | callsFunc bool 27 | } 28 | 29 | type Counter struct { 30 | sync.RWMutex 31 | val int 32 | } 33 | 34 | func NewCounter() *Counter { 35 | return &Counter{} 36 | } 37 | 38 | func (c *Counter) Inc() { 39 | c.Lock() 40 | defer c.Unlock() 41 | c.val++ 42 | } 43 | 44 | func (c *Counter) Dec() { 45 | c.Lock() 46 | defer c.Unlock() 47 | c.val-- 48 | } 49 | 50 | func (c *Counter) Val() int { 51 | c.RLock() 52 | defer c.RUnlock() 53 | return c.val 54 | } 55 | 56 | func TestTasksInterface(t *testing.T) { 57 | var tt []InterfaceTestCase 58 | 59 | tt = append(tt, InterfaceTestCase{ 60 | name: "Basic Valid Task", 61 | task: &Task{ 62 | Interval: time.Duration(1 * time.Second), 63 | TaskFunc: func() error { return nil }, 64 | }, 65 | }) 66 | 67 | tt = append(tt, InterfaceTestCase{ 68 | name: "Basic Valid Task with ID", 69 | task: &Task{ 70 | Interval: time.Duration(1 * time.Second), 71 | TaskFunc: func() error { return nil }, 72 | }, 73 | id: xid.New().String(), 74 | }) 75 | 76 | tt = append(tt, InterfaceTestCase{ 77 | name: "Valid Task with ErrFunc", 78 | task: &Task{ 79 | Interval: time.Duration(1 * time.Second), 80 | TaskFunc: func() error { return nil }, 81 | ErrFunc: func(_ error) {}, 82 | }, 83 | }) 84 | 85 | tt = append(tt, InterfaceTestCase{ 86 | name: "Valid Task with Context", 87 | task: &Task{ 88 | Interval: time.Duration(1 * time.Second), 89 | TaskFunc: func() error { return nil }, 90 | ErrFunc: func(_ error) {}, 91 | TaskContext: TaskContext{Context: context.Background()}, 92 | }, 93 | }) 94 | 95 | tt = append(tt, InterfaceTestCase{ 96 | name: "Valid Task with Context and WithContextFuncs", 97 | task: &Task{ 98 | Interval: time.Duration(1 * time.Second), 99 | FuncWithTaskContext: func(_ TaskContext) error { return nil }, 100 | ErrFuncWithTaskContext: func(_ TaskContext, _ error) {}, 101 | TaskContext: TaskContext{Context: context.Background()}, 102 | }, 103 | }) 104 | 105 | tt = append(tt, InterfaceTestCase{ 106 | name: "Valid Task without Context but WithContextFuncs", 107 | task: &Task{ 108 | Interval: time.Duration(1 * time.Second), 109 | FuncWithTaskContext: func(_ TaskContext) error { return nil }, 110 | ErrFuncWithTaskContext: func(_ TaskContext, _ error) {}, 111 | }, 112 | }) 113 | 114 | tt = append(tt, InterfaceTestCase{ 115 | name: "Valid Task with StartAfter", 116 | task: &Task{ 117 | Interval: time.Duration(1 * time.Second), 118 | TaskFunc: func() error { return nil }, 119 | StartAfter: time.Now().Add(time.Duration(1 * time.Second)), 120 | }, 121 | }) 122 | 123 | tt = append(tt, InterfaceTestCase{ 124 | name: "Valid Task with StartAfter but in the past", 125 | task: &Task{ 126 | Interval: time.Duration(1 * time.Second), 127 | TaskFunc: func() error { return nil }, 128 | StartAfter: time.Now().Add(time.Duration(-1 * time.Minute)), 129 | }, 130 | }) 131 | 132 | tt = append(tt, InterfaceTestCase{ 133 | name: "Valid Task with RunOnce", 134 | task: &Task{ 135 | Interval: time.Duration(1 * time.Second), 136 | TaskFunc: func() error { return nil }, 137 | RunOnce: true, 138 | }, 139 | }) 140 | 141 | tt = append(tt, InterfaceTestCase{ 142 | name: "No Interval", 143 | task: &Task{ 144 | TaskFunc: func() error { return nil }, 145 | }, 146 | addErr: true, 147 | }) 148 | 149 | tt = append(tt, InterfaceTestCase{ 150 | name: "No TaskFunc or FuncWithTaskContext", 151 | task: &Task{ 152 | Interval: time.Duration(1 * time.Second), 153 | }, 154 | addErr: true, 155 | }) 156 | 157 | // Create a base scheduler to use 158 | scheduler := New() 159 | defer scheduler.Stop() 160 | 161 | for _, tc := range tt { 162 | t.Run(tc.name, func(t *testing.T) { 163 | var err error 164 | id := tc.id 165 | 166 | // Schedule the task 167 | if tc.id != "" { 168 | err = scheduler.AddWithID(tc.id, tc.task) 169 | } else { 170 | id, err = scheduler.Add(tc.task) 171 | } 172 | if err != nil && !tc.addErr { 173 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 174 | } 175 | if err == nil && tc.addErr { 176 | t.Errorf("Expected errors when scheduling an invalid task") 177 | } 178 | defer scheduler.Del(id) 179 | 180 | if tc.id != "" { 181 | t.Run(tc.name+" - Duplicate Task", func(t *testing.T) { 182 | // Schedule the task 183 | err := scheduler.AddWithID(tc.id, tc.task) 184 | if err != ErrIDInUse { 185 | t.Errorf("Expected errors when scheduling a duplicate task") 186 | } 187 | }) 188 | } 189 | 190 | t.Run(tc.name+" - Lookup", func(t *testing.T) { 191 | // Verify if task exists 192 | _, err = scheduler.Lookup(id) 193 | if err != nil && !tc.addErr { 194 | t.Errorf("Unable to find newly scheduled task with Lookup - %s", err) 195 | } 196 | if err == nil && tc.addErr { 197 | t.Errorf("Found task that should not exist - %s", id) 198 | } 199 | }) 200 | 201 | t.Run(tc.name+" - Task List", func(t *testing.T) { 202 | // Check Task Map 203 | tasks := scheduler.Tasks() 204 | if len(tasks) != 1 && !tc.addErr { 205 | t.Errorf("Unable to find newly scheduled task with Tasks") 206 | } 207 | if len(tasks) > 0 && tc.addErr { 208 | t.Errorf("Found task that should not exist - %s", id) 209 | } 210 | }) 211 | 212 | // Reset for the next test 213 | scheduler.Del(id) 214 | }) 215 | } 216 | } 217 | 218 | func TestTaskExecution(t *testing.T) { 219 | // Create a base scheduler to use 220 | scheduler := New() 221 | defer scheduler.Stop() 222 | 223 | // Setup table tests 224 | var tt []ExecutionTestCase 225 | 226 | // Define a basic task 227 | tc := ExecutionTestCase{ 228 | name: "Valid Task", 229 | callsFunc: true, 230 | } 231 | tc.ctx, tc.cancel = context.WithCancel(context.Background()) 232 | tc.task = &Task{ 233 | Interval: time.Duration(1 * time.Second), 234 | TaskFunc: func() error { return fmt.Errorf("fake error") }, 235 | ErrFunc: func(e error) { 236 | if e != nil { 237 | tc.cancel() 238 | } 239 | }, 240 | } 241 | tt = append(tt, tc) 242 | 243 | // Define a task with TaskContext 244 | tc2 := ExecutionTestCase{ 245 | name: "Valid Task with TaskContext", 246 | callsFunc: true, 247 | } 248 | tc2.ctx, tc2.cancel = context.WithCancel(context.Background()) 249 | tc2.task = &Task{ 250 | Interval: time.Duration(1 * time.Second), 251 | TaskContext: TaskContext{Context: tc2.ctx}, 252 | FuncWithTaskContext: func(taskCtx TaskContext) error { 253 | if taskCtx.Context != tc2.ctx { 254 | t.Logf("TaskContext.Context does not match expected context") 255 | // return with no error to trigger a timeout failure 256 | return nil 257 | } 258 | return fmt.Errorf("fake error") 259 | }, 260 | ErrFuncWithTaskContext: func(taskCtx TaskContext, e error) { 261 | if taskCtx == tc2.task.TaskContext && e != nil { 262 | tc2.cancel() 263 | } 264 | if taskCtx.Context.Err() != context.Canceled { 265 | t.Errorf("TaskContext.Context should be canceled") 266 | } 267 | }, 268 | } 269 | tt = append(tt, tc2) 270 | 271 | // Define a task then cancel it 272 | tc3 := ExecutionTestCase{ 273 | name: "Cancel a Task before it's called", 274 | } 275 | tc3.ctx, tc3.cancel = context.WithCancel(context.Background()) 276 | tc3.task = &Task{ 277 | Interval: time.Duration(1 * time.Second), 278 | StartAfter: time.Now().Add(time.Duration(5 * time.Second)), 279 | TaskContext: TaskContext{Context: tc3.ctx}, 280 | TaskFunc: func() error { 281 | tc.cancel() 282 | return nil 283 | }, 284 | } 285 | tt = append(tt, tc3) 286 | 287 | // Only call ErrFunc if error 288 | tc4 := ExecutionTestCase{ 289 | name: "Only call ErrFunc if error", 290 | callsFunc: true, 291 | } 292 | tc4.ctx, tc4.cancel = context.WithCancel(context.Background()) 293 | tc4.task = &Task{ 294 | Interval: time.Duration(1 * time.Second), 295 | TaskFunc: func() error { 296 | tc4.cancel() 297 | return nil 298 | }, 299 | ErrFunc: func(error) { 300 | t.Errorf("ErrFunc should not be called") 301 | }, 302 | } 303 | tt = append(tt, tc4) 304 | 305 | // Only call ErrFuncWithTaskContext if error 306 | tc5 := ExecutionTestCase{ 307 | name: "Only call ErrFuncWithTaskContext if error", 308 | callsFunc: true, 309 | } 310 | tc5.ctx, tc5.cancel = context.WithCancel(context.Background()) 311 | tc5.task = &Task{ 312 | Interval: time.Duration(1 * time.Second), 313 | TaskContext: TaskContext{Context: tc5.ctx}, 314 | FuncWithTaskContext: func(_ TaskContext) error { 315 | tc5.cancel() 316 | return nil 317 | }, 318 | ErrFuncWithTaskContext: func(_ TaskContext, _ error) { 319 | t.Errorf("ErrFuncWithTaskContext should not be called") 320 | }, 321 | } 322 | tt = append(tt, tc5) 323 | 324 | // Validate TaskContext ID 325 | tc6 := ExecutionTestCase{ 326 | name: "Validate TaskContext ID", 327 | callsFunc: true, 328 | id: "test-id", 329 | } 330 | tc6.ctx, tc6.cancel = context.WithCancel(context.Background()) 331 | tc6.task = &Task{ 332 | Interval: time.Duration(1 * time.Second), 333 | TaskContext: TaskContext{Context: tc6.ctx}, 334 | FuncWithTaskContext: func(taskCtx TaskContext) error { 335 | if taskCtx.ID() != tc6.id { 336 | t.Errorf("TaskContext.ID does not match expected ID") 337 | } 338 | tc6.cancel() 339 | return nil 340 | }, 341 | } 342 | tt = append(tt, tc6) 343 | 344 | // Verify that StartAfter time is respected 345 | tc7 := ExecutionTestCase{ 346 | name: "Verify StartAfter time is respected", 347 | callsFunc: true, 348 | } 349 | tc7StartAfter := time.Now().Add(time.Duration(5 * time.Second)) 350 | tc7.ctx, tc7.cancel = context.WithCancel(context.Background()) 351 | tc7.task = &Task{ 352 | Interval: time.Duration(1 * time.Second), 353 | StartAfter: tc7StartAfter, 354 | TaskContext: TaskContext{Context: tc7.ctx}, 355 | FuncWithTaskContext: func(_ TaskContext) error { 356 | if time.Now().Before(tc7StartAfter) { 357 | t.Errorf("Task should not have been called before StartAfter time") 358 | return nil 359 | } 360 | tc7.cancel() 361 | return nil 362 | }, 363 | } 364 | tt = append(tt, tc7) 365 | 366 | for _, tc := range tt { 367 | t.Run(tc.name, func(t *testing.T) { 368 | var err error 369 | id := tc.id 370 | 371 | if tc.id != "" { 372 | err = scheduler.AddWithID(tc.id, tc.task) 373 | } else { 374 | id, err = scheduler.Add(tc.task) 375 | } 376 | if err != nil { 377 | t.Errorf("Unexpected errors when scheduling a task - %s", err) 378 | } 379 | 380 | // Cancel the task if it's not supposed to be called 381 | if !tc.callsFunc { 382 | scheduler.Del(id) 383 | } 384 | 385 | select { 386 | case <-tc.ctx.Done(): 387 | if tc.callsFunc { 388 | return 389 | } 390 | t.Errorf("Task was executed when it should not have been") 391 | case <-time.After(time.Duration(10 * time.Second)): 392 | if !tc.callsFunc { 393 | return 394 | } 395 | t.Errorf("Task did not execute within 10 seconds") 396 | } 397 | }) 398 | } 399 | } 400 | 401 | func TestAdd(t *testing.T) { 402 | // Create a base scheduler to use 403 | scheduler := New() 404 | defer scheduler.Stop() 405 | 406 | t.Run("Add a valid task and look it up", func(t *testing.T) { 407 | id, err := scheduler.Add(&Task{ 408 | Interval: time.Duration(1 * time.Minute), 409 | TaskFunc: func() error { return nil }, 410 | ErrFunc: func(_ error) {}, 411 | }) 412 | if err != nil { 413 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 414 | } 415 | 416 | _, err = scheduler.Lookup(id) 417 | if err != nil { 418 | t.Errorf("Unable to find newly scheduled task with Lookup - %s", err) 419 | } 420 | 421 | tt := scheduler.Tasks() 422 | if len(tt) < 1 { 423 | t.Errorf("Unable to find newly scheduled task with Tasks") 424 | } 425 | 426 | }) 427 | 428 | t.Run("Add a valid task with an id and look it up", func(t *testing.T) { 429 | id := xid.New() 430 | err := scheduler.AddWithID(id.String(), &Task{ 431 | Interval: time.Duration(1 * time.Minute), 432 | TaskFunc: func() error { return nil }, 433 | ErrFunc: func(_ error) {}, 434 | }) 435 | if err != nil { 436 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 437 | } 438 | 439 | _, err = scheduler.Lookup(id.String()) 440 | if err != nil { 441 | t.Errorf("Unable to find newly scheduled task with Lookup - %s", err) 442 | } 443 | 444 | tt := scheduler.Tasks() 445 | if len(tt) < 1 { 446 | t.Errorf("Unable to find newly scheduled task with Tasks") 447 | } 448 | 449 | }) 450 | 451 | t.Run("Add a invalid task with an duplicate id and look it up", func(t *testing.T) { 452 | // Channel for orchestrating when the task ran 453 | doneCh := make(chan struct{}) 454 | 455 | // Setup A task 456 | id, err := scheduler.Add(&Task{ 457 | Interval: time.Duration(1 * time.Second), 458 | TaskFunc: func() error { 459 | doneCh <- struct{}{} 460 | return nil 461 | }, 462 | ErrFunc: func(_ error) {}, 463 | }) 464 | if err != nil { 465 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 466 | } 467 | 468 | err = scheduler.AddWithID(id, &Task{ 469 | Interval: time.Duration(1 * time.Minute), 470 | TaskFunc: func() error { return nil }, 471 | ErrFunc: func(_ error) {}, 472 | }) 473 | if err != ErrIDInUse { 474 | t.Errorf("Expected error for task with existing id") 475 | } 476 | 477 | _, err = scheduler.Lookup(id) 478 | if err != nil { 479 | t.Errorf("Unable to find previously scheduled task with Lookup - %s", err) 480 | } 481 | }) 482 | 483 | t.Run("Check for nil callback", func(t *testing.T) { 484 | _, err := scheduler.Add(&Task{ 485 | Interval: time.Duration(1 * time.Minute), 486 | ErrFunc: func(_ error) {}, 487 | }) 488 | if err == nil { 489 | t.Errorf("Unexpected success when scheduling an invalid task - %s", err) 490 | } 491 | }) 492 | 493 | t.Run("Check for nil interval", func(t *testing.T) { 494 | _, err := scheduler.Add(&Task{ 495 | TaskFunc: func() error { return nil }, 496 | ErrFunc: func(_ error) {}, 497 | }) 498 | if err == nil { 499 | t.Errorf("Unexpected success when scheduling an invalid task - %s", err) 500 | } 501 | }) 502 | } 503 | 504 | func TestScheduler(t *testing.T) { 505 | // Create a base scheduler to use 506 | scheduler := New() 507 | defer scheduler.Stop() 508 | 509 | t.Run("Verify Tasks Run when Added", func(t *testing.T) { 510 | // Channel for orchestrating when the task ran 511 | doneCh := make(chan struct{}) 512 | 513 | // Setup A task 514 | id, err := scheduler.Add(&Task{ 515 | Interval: time.Duration(1 * time.Second), 516 | TaskFunc: func() error { 517 | doneCh <- struct{}{} 518 | return nil 519 | }, 520 | ErrFunc: func(_ error) {}, 521 | }) 522 | if err != nil { 523 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 524 | } 525 | defer scheduler.Del(id) 526 | 527 | // Make sure it runs especially when we want it too 528 | for i := 0; i < 6; i++ { 529 | select { 530 | case <-doneCh: 531 | continue 532 | case <-time.After(2 * time.Second): 533 | t.Errorf("Scheduler failed to execute the scheduled tasks %d run within 2 seconds", i) 534 | } 535 | } 536 | }) 537 | 538 | t.Run("Verify TasksWithContext Run when Added", func(t *testing.T) { 539 | // Channel for orchestrating when the task ran 540 | doneCh := make(chan struct{}) 541 | 542 | // User-defined context 543 | ctx, cancel := context.WithCancel(context.Background()) 544 | 545 | // Setup A task 546 | id, err := scheduler.Add(&Task{ 547 | Interval: time.Duration(1 * time.Second), 548 | TaskContext: TaskContext{Context: ctx}, 549 | FuncWithTaskContext: func(_ TaskContext) error { 550 | cancel() 551 | return fmt.Errorf("Fake Error") 552 | }, 553 | ErrFuncWithTaskContext: func(ctx TaskContext, _ error) { 554 | if ctx.Context != nil && ctx.Context.Err() == context.Canceled { 555 | doneCh <- struct{}{} 556 | } 557 | }, 558 | }) 559 | if err != nil { 560 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 561 | } 562 | defer scheduler.Del(id) 563 | 564 | // Make sure it runs especially when we want it too 565 | for i := 0; i < 6; i++ { 566 | select { 567 | case <-doneCh: 568 | continue 569 | case <-time.After(2 * time.Second): 570 | t.Errorf("Scheduler failed to execute the scheduled tasks %d run within 2 seconds", i) 571 | } 572 | } 573 | }) 574 | 575 | t.Run("Verify StartAfter works as expected", func(t *testing.T) { 576 | // Channel for orchestrating when the task ran 577 | doneCh := make(chan struct{}) 578 | 579 | // Create a Start time 580 | sa := time.Now().Add(10 * time.Second) 581 | 582 | // Setup A task 583 | id, err := scheduler.Add(&Task{ 584 | Interval: time.Duration(1 * time.Second), 585 | StartAfter: sa, 586 | TaskFunc: func() error { 587 | doneCh <- struct{}{} 588 | return nil 589 | }, 590 | ErrFunc: func(_ error) {}, 591 | }) 592 | if err != nil { 593 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 594 | } 595 | defer scheduler.Del(id) 596 | 597 | // Make sure it runs especially when we want it too 598 | select { 599 | case <-doneCh: 600 | if time.Now().Before(sa) { 601 | t.Errorf("Task executed before the defined start time now %s, supposed to be %s", time.Now().String(), sa.String()) 602 | } 603 | return 604 | case <-time.After(15 * time.Second): 605 | t.Errorf("Scheduler failed to execute the scheduled tasks within 15 seconds") 606 | } 607 | }) 608 | } 609 | 610 | func TestSchedulerDoesntRun(t *testing.T) { 611 | // Create a base scheduler to use 612 | scheduler := New() 613 | defer scheduler.Stop() 614 | 615 | t.Run("Verify Cancelling a StartAfter works as expected", func(t *testing.T) { 616 | // Channel for orchestrating when the task ran 617 | doneCh := make(chan struct{}) 618 | 619 | // Create a Start time 620 | sa := time.Now().Add(10 * time.Second) 621 | 622 | // Setup A task 623 | id, err := scheduler.Add(&Task{ 624 | Interval: time.Duration(1 * time.Second), 625 | StartAfter: sa, 626 | TaskFunc: func() error { 627 | doneCh <- struct{}{} 628 | return nil 629 | }, 630 | ErrFunc: func(_ error) {}, 631 | }) 632 | if err != nil { 633 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 634 | } 635 | 636 | // Remove task before it can be scheduled 637 | scheduler.Del(id) 638 | 639 | // Make sure it doesn't run 640 | select { 641 | case <-doneCh: 642 | t.Errorf("Task executed it was supposed to be cancelled") 643 | return 644 | case <-time.After(15 * time.Second): 645 | return 646 | } 647 | }) 648 | 649 | t.Run("Verify Tasks Dont run when Deleted", func(t *testing.T) { 650 | // Channel for orchestrating when the task ran 651 | doneCh := make(chan struct{}) 652 | 653 | // Setup A task 654 | id, err := scheduler.Add(&Task{ 655 | Interval: time.Duration(1 * time.Second), 656 | TaskFunc: func() error { 657 | doneCh <- struct{}{} 658 | return nil 659 | }, 660 | ErrFunc: func(_ error) {}, 661 | }) 662 | if err != nil { 663 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 664 | } 665 | defer scheduler.Del(id) 666 | 667 | // Make sure it runs especially when we want it too 668 | for i := 0; i < 6; i++ { 669 | select { 670 | case <-doneCh: 671 | if i == 2 { 672 | scheduler.Del(id) 673 | } 674 | if i > 2 { 675 | t.Errorf("Task should not have exceeded 2, count is %d", i) 676 | } 677 | continue 678 | case <-time.After(2 * time.Second): 679 | if i > 2 { 680 | return 681 | } 682 | t.Errorf("Scheduler failed to execute the scheduled tasks %d run within 2 seconds", i) 683 | } 684 | } 685 | }) 686 | } 687 | 688 | func TestSchedulerExtras(t *testing.T) { 689 | // Create a base scheduler to use 690 | scheduler := New() 691 | defer scheduler.Stop() 692 | 693 | t.Run("Verify RunOnce works as expected", func(t *testing.T) { 694 | // Channel for orchestrating when the task ran 695 | doneCh := make(chan struct{}) 696 | 697 | // Setup A task 698 | id, err := scheduler.Add(&Task{ 699 | Interval: time.Duration(1 * time.Second), 700 | RunOnce: true, 701 | TaskFunc: func() error { 702 | doneCh <- struct{}{} 703 | return nil 704 | }, 705 | ErrFunc: func(_ error) {}, 706 | }) 707 | if err != nil { 708 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 709 | } 710 | defer scheduler.Del(id) 711 | 712 | // Make sure it runs especially when we want it too 713 | for i := 0; i < 6; i++ { 714 | select { 715 | case <-doneCh: 716 | if i >= 1 { 717 | t.Errorf("Task should not have exceeded 1, count is %d", i) 718 | } 719 | continue 720 | case <-time.After(2 * time.Second): 721 | if i == 1 { 722 | return 723 | } 724 | t.Errorf("Scheduler failed to execute the scheduled tasks %d run within 2 seconds", i) 725 | } 726 | } 727 | }) 728 | 729 | t.Run("Test ErrFunc gets called on errors", func(t *testing.T) { 730 | // Create a channel to signal function exec 731 | doneCh := make(chan struct{}) 732 | 733 | // Add task 734 | _, err := scheduler.Add(&Task{ 735 | Interval: time.Duration(1 * time.Second), 736 | TaskFunc: func() error { return fmt.Errorf("Errors are bad") }, 737 | ErrFunc: func(_ error) { doneCh <- struct{}{} }, 738 | }) 739 | if err != nil { 740 | t.Errorf("Unexpected errors when scheduling a valid task - %s", err) 741 | } 742 | 743 | // Wait for success, or timeout 744 | select { 745 | case <-doneCh: 746 | return 747 | case <-time.After(2 * time.Second): 748 | t.Errorf("Error function was not called when an error occurred") 749 | } 750 | }) 751 | } 752 | 753 | func TestSingleInstance(t *testing.T) { 754 | // Create a base scheduler to use 755 | scheduler := New() 756 | defer scheduler.Stop() 757 | 758 | // Create a counter to track how many times the task is called 759 | counter := NewCounter() 760 | 761 | // Create a second counter to track number of executions 762 | counter2 := NewCounter() 763 | 764 | // Create an error channel to signal failure 765 | errCh := make(chan error) 766 | 767 | // Add a task that will increment the counter 768 | _, err := scheduler.Add(&Task{ 769 | Interval: time.Duration(500 * time.Millisecond), 770 | RunSingleInstance: true, 771 | TaskFunc: func() error { 772 | // Increment Concurrent Counter 773 | counter.Inc() 774 | if counter.Val() > 1 { 775 | return fmt.Errorf("Task ran more than once - count %d", counter.Val()) 776 | } 777 | // Increment Execution Counter 778 | counter2.Inc() 779 | 780 | // Wait for 10 seconds 781 | <-time.After(5 * time.Second) 782 | 783 | // Decrement Concurrent Counter 784 | counter.Dec() 785 | return nil 786 | }, 787 | ErrFunc: func(e error) { 788 | errCh <- e 789 | }, 790 | }) 791 | if err != nil { 792 | t.Fatalf("Unexpected errors when scheduling task - %s", err) 793 | } 794 | 795 | // Wait for tasks to run and if no error, then we are good 796 | select { 797 | case <-time.After(30 * time.Second): 798 | if counter2.Val() < 4 { 799 | t.Fatalf("Task was not called more than once successfully - count %d", counter2.Val()) 800 | } 801 | case e := <-errCh: 802 | t.Fatalf("Error function was called - %s", e) 803 | } 804 | } 805 | --------------------------------------------------------------------------------