Documentation
¶
Overview ¶
Package periodic schedules a task function to run repeatedly at a fixed interval, with optional random jitter and a per-invocation context timeout.
Problem ¶
Background workers that poll an external resource, flush a cache, or emit heartbeats are common in production services. Implementing them correctly requires handling context cancellation, per-call timeouts, graceful shutdown, and the thundering-herd problem when many instances restart simultaneously. Writing this loop from scratch every time is repetitive and error-prone.
Solution ¶
New constructs a Periodic scheduler from an interval, a jitter ceiling, a per-call timeout, and a TaskFn. Periodic.Start runs the task in a dedicated goroutine and Periodic.Stop shuts it down cleanly:
p, err := periodic.New(
30*time.Second, // run every 30 s
5*time.Second, // add up to 5 s of random jitter
10*time.Second, // each call gets a 10 s deadline
myTask,
)
if err != nil {
log.Fatal(err)
}
p.Start(ctx)
defer p.Stop()
Features ¶
- Fixed interval with random jitter: the actual pause between calls is interval + rand(0, jitter), spreading load across a fleet and avoiding the thundering-herd problem (https://en.wikipedia.org/wiki/Thundering_herd_problem).
- Per-call timeout: each TaskFn invocation receives a context.Context derived from the parent with an independent deadline, preventing a single slow call from blocking the scheduler.
- Context-aware shutdown: Periodic.Start accepts a parent context; canceling it (or calling Periodic.Stop) stops the loop after the current task invocation returns — no goroutine leaks.
- Eager first execution: the first call fires after ~1 ns so the task runs immediately on start rather than waiting for the first full interval.
- Simple API: three methods (New, Periodic.Start, Periodic.Stop) and one function type (TaskFn) cover the entire surface area.
Constraints ¶
- interval must be > 0.
- jitter must be >= 0 (pass 0 to disable jitter entirely).
- timeout must be > 0.
- task must not be nil.
- task must not panic — it runs in the background goroutine with no recovery, so a panic crashes the process (recover inside the task if needed).
Benefits ¶
This package eliminates the recurring boilerplate of ticker-based background loops in Go services, providing built-in jitter, cancellation, and timeout handling in a minimal, dependency-free implementation.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Periodic ¶
type Periodic struct {
// contains filtered or unexported fields
}
Periodic schedules a TaskFn to run repeatedly with configurable interval and jitter. Create one with New, start it with Periodic.Start, and stop it with Periodic.Stop. The zero value is not usable; always use New.
func New ¶
func New(interval time.Duration, jitter time.Duration, timeout time.Duration, task TaskFn) (*Periodic, error)
New constructs a Periodic scheduler with constraints on interval, jitter, timeout, and task validation. Returns error if any parameter violates its constraint; call Start() to begin execution.
Example ¶
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/tecnickcom/nurago/pkg/periodic"
)
func main() {
count := make(chan int, 1)
count <- 0
// example task to execute periodically
task := func(_ context.Context) {
v := <-count
count <- (v + 1)
}
interval := 20 * time.Millisecond
jitter := 2 * time.Millisecond
timeout := 2 * time.Millisecond
// create a new periodic job
p, err := periodic.New(interval, jitter, timeout, task)
if err != nil {
close(count)
log.Fatal(err)
}
// start the periodic job
p.Start(context.TODO())
// wait for 3 times the interval
wait := 3 * interval
time.Sleep(wait)
// stop the periodic job
p.Stop()
fmt.Println(<-count)
close(count)
}
Output: 3
func (*Periodic) Start ¶
Start begins periodic task execution in a background goroutine. First invocation fires almost immediately; subsequent calls are at interval+rand(0,jitter) after each completion. The loop exits when ctx is canceled or Stop() is called. A second Start on an already-started or already-stopped instance is a no-op, so it never leaks a goroutine — the instance is single-use.
If ctx is canceled just before the first tick, the task may still run once (with the already-canceled context); a well-behaved task returns promptly.
func (*Periodic) Stop ¶
func (p *Periodic) Stop()
Stop cancels the execution loop and waits for the current task invocation to complete. Safe to call multiple times, before Start(), or concurrently with Start(): whatever the ordering, once Stop returns the scheduler is stopped and will not (re)start — a Stop that wins the race before Start prevents the loop from ever launching. The instance is single-use and cannot be restarted after Stop.
type TaskFn ¶
TaskFn is the function signature executed on each scheduler tick. The context passed in carries the deadline configured via the timeout parameter of New. Implementations should respect context cancellation and return promptly when ctx.Done() is closed.
The task runs in the scheduler's background goroutine with no panic recovery: a panic in the task crashes the process. Recover inside the task if it may panic and the scheduler must survive.