tasks

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 11, 2026 License: MIT Imports: 6 Imported by: 44

README

Tasks

tests codecov Go Report Card PkgGoDev

A small in-process scheduler for Go tasks that need to run on time.

Tasks is built for recurring, quick-running jobs where scheduler-induced jitter needs to stay low and the scheduler should stay out of the way. Each invocation runs in its own goroutine, so one slow task does not make the whole schedule trip over its shoelaces.

Use Tasks when you want recurring work without cron wiring, a worker fleet, or a pile of scheduling boilerplate. It is an in-process scheduler, not a durable queue or distributed job runner; if your process exits, your schedule exits with it. That tradeoff keeps the package small, fast, and easy to reason about.

Install

go get github.com/madflojo/tasks

Why Tasks

  • Accurate recurring execution: Each invocation runs independently, so a long-running task does not block unrelated schedules.
  • Small API: Intervals use Go's time.Duration; no custom cron language required.
  • Delayed and one-time runs: Use StartAfter and RunOnce for jobs that should begin later or run just once.
  • Overlap control: Use RunSingleInstance to skip a run when the previous invocation is still working. No dogpiling.
  • Task context support: Pass user-defined context and task metadata into callbacks with FuncWithTaskContext, ErrFuncWithTaskContext, and TaskContext.ID().
  • Stable IDs and branchable errors: Use AddWithID for deterministic identifiers and errors.Is for scheduler errors.

Lifecycle Guarantees

Calling Del or Stop prevents delayed or future invocations, including tasks waiting on StartAfter. These methods do not interrupt task functions that have already started; once your callback is running, it gets to finish its lap.

Error handlers run as part of a task's execution lifecycle. For RunOnce tasks, self-deletion happens after the task function and any configured error handler finish.

Error Handling

Scheduler validation and lookup errors are exposed as sentinel errors so callers can branch with errors.Is:

  • ErrNilTask
  • ErrIDInUse
  • ErrInvalidID
  • ErrMissingTaskFunc
  • ErrInvalidInterval
  • ErrTaskNotFound
  • ErrTaskPanic
if errors.Is(err, tasks.ErrIDInUse) {
  // Pick another ID or update the existing task.
}

If a task callback panics with a non-nil recovered value, Tasks recovers it and reports ErrTaskPanic through the task error callback. If the error callback itself panics, Tasks recovers and drops that panic.

Error callbacks run in the same goroutine as the task execution. Slow error handlers extend the task lifecycle, including RunOnce deletion and RunSingleInstance overlap prevention.

Usage

Here are some examples to help you get Tasks doing useful work without much ceremony.

Basic Usage
// Start the Scheduler
scheduler := tasks.New()
defer scheduler.Stop()

// Add a task
id, err := scheduler.Add(&tasks.Task{
  Interval: 30 * time.Second,
  TaskFunc: func() error {
    // Put your logic here
    return nil
  },
})
if err != nil {
  // Handle error
}
Delayed Scheduling

Sometimes schedules need to start later, not right now with a tiny starter pistol. Set StartAfter to delay the start of a task's interval schedule. Deleting the task or stopping the scheduler before StartAfter prevents the delayed run from being scheduled.

// Add a recurring task for every 30 days, starting 30 days from now
id, err := scheduler.Add(&tasks.Task{
  Interval: 30 * (24 * time.Hour),
  StartAfter: time.Now().Add(30 * (24 * time.Hour)),
  TaskFunc: func() error {
    // Put your logic here
    return nil
  },
})
if err != nil {
  // Handle error
}
One-Time Tasks

Some jobs only need one lap. The example below schedules a task to run once after waiting for 60 seconds.

// Add a one-time task for 60 seconds from now
id, err := scheduler.Add(&tasks.Task{
  Interval: 60 * time.Second,
  RunOnce:  true,
  TaskFunc: func() error {
    // Put your logic here
    return nil
  },
})
if err != nil {
  // Handle error
}
Custom Error Handling

Tasks lets callers define custom error handling with a callback that runs when a task returns an error. The example below schedules a task that logs when things go sideways.

If both ErrFunc and ErrFuncWithTaskContext are set, ErrFuncWithTaskContext is used.

// Add a task with custom error handling
id, err := scheduler.Add(&tasks.Task{
  Interval: 30 * time.Second,
  TaskFunc: func() error {
    // Put your logic here
    return nil
  },
  ErrFunc: func(e error) {
    log.Printf("An error occurred when executing task %s - %s", id, e)
  },
})
if err != nil {
  // Handle error
}
Single-Instance Tasks

Use RunSingleInstance when a task might take longer than its interval and overlapping executions should be skipped. No dogpiling, no duplicate workers stepping on each other.

id, err := scheduler.Add(&tasks.Task{
  Interval:          30 * time.Second,
  RunSingleInstance: true,
  TaskFunc: func() error {
    // Put your logic here
    return nil
  },
})
if err != nil {
  // Handle error
}
Task Context

Use the context-aware callbacks when you want to pass a user-defined context into task execution and error handling.

ctx := context.Background()

id, err := scheduler.Add(&tasks.Task{
  Interval:    30 * time.Second,
  TaskContext: tasks.TaskContext{Context: ctx},
  FuncWithTaskContext: func(taskCtx tasks.TaskContext) error {
    log.Printf("running task %s", taskCtx.ID())
    return nil
  },
  ErrFuncWithTaskContext: func(taskCtx tasks.TaskContext, err error) {
    log.Printf("task %s failed: %v", taskCtx.ID(), err)
  },
})
if err != nil {
  // Handle error
}
Custom Task IDs

Use AddWithID when you want to provide your own stable identifier for a task. Handy when "whatever ID the scheduler picked" is not quite descriptive enough for future you.

err := scheduler.AddWithID("nightly-report", &tasks.Task{
  Interval: time.Hour,
  TaskFunc: func() error {
    // Put your logic here
    return nil
  },
})
if err != nil {
  // Handle error
}

For more details on usage, see the GoDoc.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for more details.

Development

Common local workflows are available through the repository Makefile:

  • make build
  • make tests
  • make benchmarks
  • make coverage
  • make lint
  • make format

Documentation

Overview

Package tasks is an easy to use in-process scheduler for recurring tasks in Go. Tasks is focused on high frequency tasks that run quick, and often. The goal of Tasks is to support concurrent running tasks at scale without scheduler induced jitter.

Tasks is focused on accuracy of task execution. To do this each task is called within it's own goroutine. This ensures that long execution of a single invocation does not throw the schedule as a whole off track.

As usage of this scheduler scales, it is expected to have a larger number of sleeping goroutines. As it is designed to leverage Go's ability to optimize goroutine CPU scheduling.

For simplicity this task scheduler uses the time.Duration type to specify intervals. This allows for a simple interface and flexible control over when tasks are executed.

Below is an example of starting the scheduler and registering a new task that runs every 30 seconds.

// Start the Scheduler
scheduler := tasks.New()
defer scheduler.Stop()

// Add a task
id, err := scheduler.Add(&tasks.Task{
	Interval: time.Duration(30 * time.Second),
	TaskFunc: func() error {
		// Put your logic here
		return nil
	},
})
if err != nil {
	// Do Stuff
}

Sometimes schedules need to started at a later time. This package provides the ability to start a task only after a certain time. The below example shows this in practice.

// Add a recurring task for every 30 days, starting 30 days from now
id, err := scheduler.Add(&tasks.Task{
	Interval: time.Duration(30 * (24 * time.Hour)),
	StartAfter: time.Now().Add(30 * (24 * time.Hour)),
	TaskFunc: func() error {
		// Put your logic here
		return nil
	},
})
if err != nil {
	// Do Stuff
}

It is also common for applications to run a task only once. The below example shows scheduling a task to run only once after waiting for 60 seconds.

// Add a one time only task for 60 seconds from now
id, err := scheduler.Add(&tasks.Task{
	Interval: time.Duration(60 * time.Second),
	RunOnce:  true,
	TaskFunc: func() error {
		// Put your logic here
		return nil
	},
})
if err != nil {
	// Do Stuff
}

One powerful feature of Tasks is that it allows users to specify custom error handling. This is done by allowing users to define a function that is called when a task returns an error. The below example shows scheduling a task that logs when an error occurs.

// Add a task with custom error handling
id, err := scheduler.Add(&tasks.Task{
	Interval: time.Duration(30 * time.Second),
	TaskFunc: func() error {
		// Put your logic here
		return nil
	},
	ErrFunc: func(e error) {
		log.Printf("An error occurred when executing task %s - %s", id, e)
	},
})
if err != nil {
	// Do Stuff
}

Tasks also supports single-instance execution for tasks that should never overlap.

// Add a single-instance task
id, err := scheduler.Add(&tasks.Task{
	Interval:          time.Duration(30 * time.Second),
	RunSingleInstance: true,
	TaskFunc: func() error {
		// Put your logic here
		return nil
	},
})
if err != nil {
	// Do Stuff
}

When you need access to a user-defined context or the task ID during execution, use the context-aware callbacks.

// Add a task with context-aware callbacks
id, err := scheduler.Add(&tasks.Task{
	Interval: time.Duration(30 * time.Second),
	TaskContext: tasks.TaskContext{
		Context: context.Background(),
	},
	FuncWithTaskContext: func(taskCtx tasks.TaskContext) error {
		log.Printf("running task %s", taskCtx.ID())
		return nil
	},
	ErrFuncWithTaskContext: func(taskCtx tasks.TaskContext, err error) {
		log.Printf("task %s failed: %s", taskCtx.ID(), err)
	},
})
if err != nil {
	// Do Stuff
}

If you need a deterministic identifier, tasks can also be added with a custom ID.

err = scheduler.AddWithID("nightly-report", &tasks.Task{
	Interval: time.Duration(1 * time.Hour),
	TaskFunc: func() error {
		// Put your logic here
		return nil
	},
})
if err != nil {
	// Do Stuff
}

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNilTask is returned when a nil task is provided to Add or AddWithID.
	ErrNilTask = errors.New("task cannot be nil")

	// ErrIDInUse is returned when a Task ID is specified but already used.
	ErrIDInUse = errors.New("ID already used")

	// ErrInvalidID is returned when AddWithID is called with an empty ID.
	ErrInvalidID = errors.New("task ID cannot be empty")

	// ErrMissingTaskFunc is returned when a task has no executable callback.
	ErrMissingTaskFunc = errors.New("either TaskFunc or FuncWithTaskContext must be provided")

	// ErrInvalidInterval is returned when a task interval is not greater than zero.
	ErrInvalidInterval = errors.New("task interval must be greater than zero")

	// ErrTaskNotFound is returned when a task ID does not exist in the scheduler.
	ErrTaskNotFound = errors.New("task not found")

	// ErrTaskPanic is returned when a user-supplied task callback panics.
	ErrTaskPanic = errors.New("task panicked")
)

Functions

This section is empty.

Types

type Scheduler

type Scheduler struct {
	// contains filtered or unexported fields
}

Scheduler stores the internal task list and provides an interface for task management.

func New

func New() *Scheduler

New will create a new scheduler instance that allows users to create and manage tasks.

func (*Scheduler) Add

func (schd *Scheduler) Add(t *Task) (string, error)

Add will add a task to the task list and schedule it. Once added, tasks will wait the defined time interval and then execute. This means a task with a 15 second interval will be triggered 15 seconds after Add is complete. Not before or after (excluding typical machine time jitter).

// Add a task
id, err := scheduler.Add(&tasks.Task{
	Interval: time.Duration(30 * time.Second),
	TaskFunc: func() error {
		// Put your logic here
		return nil
	},
	ErrFunc: func(err error) {
		// Put custom error handling here
	},
})
if err != nil {
	// Do stuff
}

func (*Scheduler) AddWithID added in v1.0.1

func (schd *Scheduler) AddWithID(id string, t *Task) error

AddWithID will add a task with an ID to the task list and schedule it. It will return ErrInvalidID if the ID is empty or ErrIDInUse if the ID is in-use. Once added, tasks will wait the defined time interval and then execute. This means a task with a 15 second interval will be triggered 15 seconds after Add is complete. Not before or after (excluding typical machine time jitter).

// Add a task
id := xid.New()
err := scheduler.AddWithID(id, &tasks.Task{
	Interval: time.Duration(30 * time.Second),
	TaskFunc: func() error {
		// Put your logic here
		return nil
	},
	ErrFunc: func(err error) {
		// Put custom error handling here
	},
})
if err != nil {
	// Do stuff
}

func (*Scheduler) Del

func (schd *Scheduler) Del(name string)

Del will unschedule the specified task and remove it from the task list. Deletion stops delayed or future invocations of a task, but does not interrupt a task function that has already started.

func (*Scheduler) Lookup

func (schd *Scheduler) Lookup(name string) (*Task, error)

Lookup will find the specified task from the internal task list using the task ID provided. It returns ErrTaskNotFound when no task exists for the ID.

The returned task is a copy of the scheduled task configuration. Scheduler-owned runtime state is intentionally excluded from the returned task.

func (*Scheduler) Stop

func (schd *Scheduler) Stop()

Stop is used to unschedule and delete all tasks owned by the scheduler instance. Stop prevents delayed or future invocations, but does not interrupt task functions that have already started.

func (*Scheduler) Tasks

func (schd *Scheduler) Tasks() map[string]*Task

Tasks is used to return a copy of the internal tasks map.

Each task in the returned map is a copy of the scheduled task configuration. Scheduler-owned runtime state is intentionally excluded from the returned tasks.

type Task

type Task struct {
	// TaskContext allows for user-defined context that is passed to task functions.
	TaskContext TaskContext

	// Interval is the frequency that the task executes. Defining this at 30 seconds, will result in a task that
	// runs every 30 seconds.
	//
	// The below are common examples to get started with.
	//
	//  // Every 30 seconds
	//  time.Duration(30 * time.Second)
	//  // Every 5 minutes
	//  time.Duration(5 * time.Minute)
	//  // Every 12 hours
	//  time.Duration(12 * time.Hour)
	//  // Every 30 days
	//  time.Duration(30 * (24 * time.Hour))
	//
	Interval time.Duration

	// RunOnce is used to set this task as a single execution task. By default, tasks will continue executing at
	// the interval specified until deleted. With RunOnce enabled the task self deletes after the first execution
	// lifecycle completes. When an error handler is configured, that handler is part of the execution lifecycle.
	RunOnce bool

	// RunSingleInstance is used to set a task as a single instance task. By default, tasks will continue executing at
	// the interval specified until deleted. With RunSingleInstance enabled a subsequent task execution will be skipped
	// if the previous task execution is still running.
	//
	// This is useful for tasks that may take longer than the interval to execute. This will prevent multiple instances
	// of the same task from running concurrently.
	RunSingleInstance bool

	// StartAfter is used to specify when the task's interval schedule starts. When set to a future time, the task waits
	// until StartAfter before starting its recurring interval timer.
	StartAfter time.Time

	// TaskFunc is the user defined function to execute as part of this task.
	// Panics with non-nil recovered values are surfaced as ErrTaskPanic.
	//
	// Either TaskFunc or FuncWithTaskContext must be defined. If both are defined, FuncWithTaskContext will be used.
	TaskFunc func() error

	// ErrFunc allows users to define a function that is called when tasks return an error. If ErrFunc and
	// ErrFuncWithTaskContext are nil, errors from tasks will be ignored. Panics are recovered and ignored.
	//
	// Either ErrFunc or ErrFuncWithTaskContext must be defined. If both are defined, ErrFuncWithTaskContext will be used.
	ErrFunc func(error)

	// FuncWithTaskContext is a user defined function to execute as part of this task. This function is used in
	// place of TaskFunc with the difference in that it will pass the user defined context from the Task configurations.
	// Panics with non-nil recovered values are surfaced as ErrTaskPanic.
	//
	// Either TaskFunc or FuncWithTaskContext must be defined. If both are defined, FuncWithTaskContext will be used.
	FuncWithTaskContext func(TaskContext) error

	// ErrFuncWithTaskContext allows users to define a function that is called when tasks return an error.
	// If ErrFunc and ErrFuncWithTaskContext are nil, errors from tasks will be ignored. This function is used in place
	// of ErrFunc with the difference in that it will pass the user defined context from the Task configurations.
	// Panics are recovered and ignored.
	//
	// Either ErrFunc or ErrFuncWithTaskContext must be defined. If both are defined, ErrFuncWithTaskContext will be used.
	ErrFuncWithTaskContext func(TaskContext, error)
	// contains filtered or unexported fields
}

Task contains the scheduled task details and control mechanisms. This struct is used during the creation of tasks. It allows users to control how and when tasks are executed.

func (*Task) Clone added in v1.1.0

func (t *Task) Clone() *Task

Clone will create a copy of the existing task definition. Scheduler-owned runtime state is intentionally excluded so the returned task can be treated as a reusable configuration template.

type TaskContext added in v1.1.0

type TaskContext struct {
	// Context is a user-defined context.
	Context context.Context
	// contains filtered or unexported fields
}

TaskContext stores the user-defined context passed to task callbacks.

func (TaskContext) ID added in v1.1.0

func (ctx TaskContext) ID() string

ID will return the task ID. This is the same as the ID generated by the scheduler when adding a task. If the task was added with AddWithID, this will be the same as the ID provided.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL