Help us shape the future of Go! Take the 2026 Go Developer Survey

beat

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Sep 18, 2026 License: MIT Imports: 9 Imported by: 0

README

beat

CI Go Reference

Run one recurring job in a long-lived Go process. beat provides sequential execution, fixed-rate or fixed-delay scheduling, replica offsets, cooperative timeouts, and optional Fx integration. Requires Go 1.27 or later.

Use it for frequent polling and background work that benefits from reusable connections and in-memory state. beat has no calendar expressions, persistent schedule, replay after downtime, or coordination between replicas.

Standalone example

package main

import (
    "context"
    "log/slog"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/uchaloop/beat"
)

func main() {
    shutdown, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer cancel()

    job := func(ctx context.Context) (int, error) {
        // Replace this timer with one bounded batch of application work.
        timer := time.NewTimer(20 * time.Millisecond)
        defer timer.Stop()
        select {
        case <-timer.C:
            return 1, nil
        case <-ctx.Done():
            return 0, ctx.Err()
        }
    }
    handler := beat.HandlerFunc(func(_ context.Context, r beat.Record) {
        slog.Info("job completed", "outcome", r.Outcome,
            "processed", r.Processed, "duration", r.Duration, "missed", r.Missed)
    })
    runner, err := beat.MakeBeat(beat.Config{
        Period: 5 * time.Second, JobTimeout: 2 * time.Second, Jitter: time.Second,
    }, job, handler, beat.WithGracefulStop())
    if err != nil {
        slog.Error("configure beat", "error", err)
        return
    }
    if err := runner.Start(shutdown); err != nil {
        slog.Error("start beat", "error", err)
        return
    }
    <-shutdown.Done()
    stopCtx, stopCancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer stopCancel()
    if err := runner.Stop(stopCtx); err != nil {
        slog.Error("stop beat", "error", err)
    }
}

Start's context applies to startup only. To stop a running Beat, call Stop with a fresh context. A nil Handler is allowed.

Scheduling

Mode First run Following runs
ModeFixedRate (default) Next grid point strictly after startup Grid points k × Period + offset, anchored to the Unix epoch
ModeFixedDelay Startup time + offset At least Period after the previous Job returns

A Beat never overlaps its Job calls. Fixed-rate runs that overrun grid points move to the first available point; no backlog of runs is queued. A pending target that is already past when the loop wakes runs once immediately, so the following run may be close behind. A restart joins the grid afresh.

For example, with a 5-minute period, a 25-second offset and a 4m50s Job:

Fixed rate:   10:00:25 ── Job ── 10:05:15   → next start 10:05:25
Fixed delay:  10:00:25 ── Job ── 10:05:15   → next start 10:10:15

Run loop

flowchart TD
    A[Start: run OnStart] --> B[Choose first target]
    B --> C[Wait for target or stop request]
    C --> D{Stopping?}
    D -->|yes| Z[Exit loop and close Done]
    D -->|no| E[Run middleware and Job with JobTimeout context]
    E --> F[Build Record and call Handler inline]
    F --> G[Evaluate backoff from Record]
    G --> H[Compute next target and Missed for the next Record]
    H --> C

Duration covers the Job and its middleware, excluding Handler and backoff. The Handler and backoff callback still occupy the loop: keep both short. Fixed-rate waits periodically recheck wall time; fixed-delay waits use monotonic time. Neither mode is a real-time execution guarantee.

Configuration and options

beat accepts a Config; it does not read environment variables itself. Its env tags support loaders such as confmaker, with default instance name beat.

Field Default env name Default Constraint
Period BEAT_PERIOD Required Greater than zero
JobTimeout BEAT_JOB_TIMEOUT 1m Zero selects the default; negative values are invalid
Jitter BEAT_JITTER 0 Between zero and Period, inclusive

The timeout cancels the Job's context; it cannot interrupt the function. Jobs must respect cancellation and join their own goroutines before returning. A Job that never returns blocks subsequent runs and completion records.

Options select the mode, offset, backoff, middleware, Handler and lifecycle hooks. Repeated setter options use the last value; WithMiddleware appends wrappers, with the first wrapper outermost. WithHandler overrides the Handler passed to MakeBeat. WithGracefulStop enables draining on shutdown.

Polling with backoff

With a short fixed-delay period, continue polling when a batch is full and rest longer after an empty or partial batch:

opts := []beat.Option{
    beat.WithMode(beat.ModeFixedDelay), // for example, Config.Period = 1s
    beat.WithBackoff(func(r beat.Record) time.Duration {
        if r.Outcome != beat.OutcomeOK {
            return 30 * time.Second
        }
        if r.Processed < 1000 {
            return 5 * time.Minute
        }
        return 0
    }),
}

Backoff is a minimum pause measured from Job completion, not an addition to Period. In fixed-delay mode the next target is the latest of Job end + Period, Job end + positive backoff, and the time scheduling resumes. In fixed-rate mode it is the next eligible grid point no earlier than scheduling resumes or the backoff permits. Returning zero or a negative value adds no restriction; it does not remove Period. Deliberately bypassed backoff points are not Missed.

Replica offsets

Jitter draws one offset in [0, Jitter) per Beat; zero gives no offset. In fixed-rate mode it shifts every grid point. In fixed-delay mode it delays only the first run. Jitter == Period permits spreading across the full period. A wider range spreads planned starts but can increase initial waiting time.

To derive an offset from an application-provided identity:

offset := beat.OffsetFor(service+"/"+cluster+"/"+instance, cfg.Jitter)
runner, err := beat.MakeBeat(cfg, job, handler, beat.WithOffset(offset))

WithOffset requires 0 <= offset < Period. OffsetFor is stable only while its input is stable; replacing a Deployment Pod usually changes its name. Hashing and random draws do not guarantee gaps between replicas. Services may share the same Jitter range.

Offsets spread starts; they do not divide work or prevent duplicate effects. Queue claiming, retries and idempotency belong to the application, including when consumers run in different clusters.

Records and observability

A Handler receives one Record after each completed Job:

Fields Meaning
Iteration, Mode, Period Run number and scheduling configuration
ScheduledFor, Start, Duration Target, actual start and Job elapsed time
Processed, Err Values returned by Job
Outcome ok, error, panic, timeout or canceled
Missed Unintentional grid losses computed after the preceding run

Outcome is authoritative even if Err is nil. A recovered panic takes precedence, then cancellation of the Job context (timeout or shutdown), then the Job's error. Missed is zero in fixed-delay mode. It excludes intentional backoff and is reported with the next completed Job, so a shutdown or a stuck Job may prevent its delivery. The current Record's duration does not identify the cause of its Missed count.

Handlers run inline with a context without cancellation or a deadline. Give any I/O its own budget. MultiHandler calls sinks sequentially. For metrics, use otelbeat; the application owns its exporter.

Lifecycle and shutdown

  • Start may run once. Repeated Start returns ErrAlreadyStarted; a stopped Beat returns ErrStopped and cannot be restarted.
  • Stop prevents further scheduling. Normally it cancels the active Job at once; WithGracefulStop lets it finish within Stop's budget. JobTimeout still applies.
  • Concurrent Stop callers share the first Stop's result, but each waiting caller may return early if its own context expires.
  • Stop waits for an in-progress OnStart. If OnStart fails, OnStop is not called; the startup hook must clean up partially acquired resources on failure.
  • If OnStart succeeds but startup's context is cancelled, Start arranges OnStop with a fresh 15-second context unless a concurrent Stop already owns cleanup.
  • If startup or the loop has not finished when Stop stops waiting, Stop returns ErrStillRunning joined with its context error. OnStop is skipped permanently.
  • Otherwise OnStop runs synchronously after the loop exits. Hooks must respect their contexts; a blocked hook can outlive the supplied deadline.

Done() closes after startup/run-loop activity finishes, before OnStop. Only use it for fallback cleanup after Stop reports ErrStillRunning. Bound any additional wait according to the application's shutdown policy:

if errors.Is(stopErr, beat.ErrStillRunning) {
    select {
    case <-runner.Done():
        // Startup and the loop have ended; perform application-owned cleanup.
    case <-cleanupCtx.Done():
        // Leave final termination to the process supervisor.
    }
}

Fx and panic recovery

Supply a Config, Job, optional Handler, and at most one ordered beatfx.Options value. Static options passed to Module come first, followed by container options:

app := fx.New(
    fx.Supply(beat.Config{Period: time.Minute, JobTimeout: 45 * time.Second}),
    fx.Provide(func() beat.Job { return job }),
    fx.Provide(func(log *slog.Logger) beatfx.Options {
        return beatfx.Options{
            beat.WithMiddleware(recovery.Middleware(recovery.WithLogger(log))),
            beat.WithGracefulStop(),
        }
    }),
    fx.Supply(slog.Default()),
    beatfx.Module(),
    fx.StopTimeout(time.Minute),
)
app.Run()

Here job is the application's beat.Job. Import beat/beatfx, beat/middleware/recovery, and go.uber.org/fx for this integration. Use one beatfx Module per Fx application. Independent standalone Beat objects have independent lifecycles.

Panics propagate by default. Recovery middleware catches panics only inside the wrapped Job call and returns *beat.PanicError; it does not protect Handler, backoff, hooks, or goroutines created by the Job.

API reference · Compilable examples · MIT license

Documentation

Overview

Package beat schedules one Job in a long-lived process. Job calls are sequential within a Beat. The package accepts configuration and emits Records; it does not load configuration, export telemetry, or coordinate replicas.

Scheduling

ModeFixedRate is the default: targets lie on an absolute grid of Period since the Unix epoch, shifted by an offset. The first target is strictly after startup. After each run, unavailable points are bypassed without queuing runs. If a pending target is already past when the loop wakes, it runs once at once. ModeFixedDelay starts after the initial offset, then waits at least Period from the end of each Job. Only fixed-rate mode preserves the grid offset.

Config.Jitter draws an offset once in [0, Jitter); zero disables it. WithOffset supplies an explicit offset and OffsetFor derives one from an identity. Offsets spread starts but do not guarantee separation or ownership of work. The application owns queue claiming and idempotency.

WithBackoff sets a minimum pause from Job completion, outside JobTimeout and Record.Duration. It cannot shorten the configured schedule. Non-positive values add no pause. Backoff callbacks and Handlers execute inline.

Jobs and records

Job returns a processed count and an error. JobTimeout defaults to one minute and cancels the Job context; it cannot interrupt a function. Jobs must observe cancellation and join their own goroutines before returning.

Handler receives a Record after each completed Job. Duration includes Job middleware, but not Handler or backoff. Outcome is authoritative even when Err is nil. Missed carries previously computed unintentional grid losses, excluding backoff; it is always zero in fixed-delay mode. Delivery waits for the next completed Job, so the final losses can go unreported. A single Record cannot identify their cause. Handler gets a context without cancellation or deadline; keep it short and bound any I/O separately. MultiHandler calls sinks in order.

Lifecycle

MakeBeat builds a runner; Start calls OnStart and launches its loop. Start's context bounds startup only. A second Start fails; a stopped Beat cannot be restarted. Call Stop explicitly to stop scheduling and cancel the active Job, or use WithGracefulStop to drain within Stop's context. JobTimeout still applies.

Stop calls OnStop only after startup succeeded and the loop ended. If waiting expires first, it returns ErrStillRunning with the context error and skips OnStop permanently. Done closes when startup/loop activity has ended, not when OnStop has completed. Use Done for fallback cleanup only after ErrStillRunning. Concurrent Stop callers share the first result unless their own wait expires.

A failing OnStart is responsible for its partial cleanup; OnStop is not run. If OnStart succeeds but startup's context is cancelled, Start initiates cleanup with a fresh 15-second context unless concurrent Stop already owns cleanup. All hooks run synchronously and must respect their contexts.

Integration

The beatfx package connects the runner to Fx. The recovery middleware catches panics in the wrapped Job call as PanicError; panics otherwise propagate. It does not recover Handler, hook, backoff, or child-goroutine panics. github.com/uchaloop/otelbeat implements Handler for OpenTelemetry metrics.

beat supports frequent polling with reusable process state. It provides no calendar scheduling, persistence, replay after downtime, or delivery guarantee for business work. See the README for diagrams and usage examples.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrAlreadyStarted reports a second Start on a Beat that is already
	// running. One Beat runs one loop: a second one would break the guarantee
	// that runs never overlap.
	ErrAlreadyStarted = errors.New("beat is already started")

	// ErrStopped reports a Start on a Beat that has been stopped. A stopped
	// Beat is not restartable - build a new one with MakeBeat.
	ErrStopped = errors.New("beat is stopped")

	// ErrStillRunning reports that startup or the run loop had not finished
	// when Stop stopped waiting. In-flight Job contexts are cancelled; an
	// in-progress OnStart still follows the context supplied to Start.
	// OnStop is not run in that case, and [Beat.Done] is how
	// an application learns when the run finally ends.
	ErrStillRunning = errors.New("stop deadline elapsed before the run finished")
)

Functions

func OffsetFor added in v0.4.0

func OffsetFor(id string, max time.Duration) time.Duration

OffsetFor derives an offset in [0, max) from id, so a replica keeps the same slot for as long as that id is the same, instead of drawing a new one every time it boots:

beat.WithOffset(beat.OffsetFor(os.Getenv("POD_NAME"), 30*time.Second))

The result is stable but still arbitrary, so two replicas can land close together. When the replicas are numbered - a StatefulSet, or any deployment that can tell a pod its index - spacing them by hand is strictly better, because it is the only way to guarantee a gap:

beat.WithOffset(time.Duration(ordinal) * max / time.Duration(replicas))

An empty id or a non-positive max yields 0.

Example
package main

import (
	"context"
	"time"

	"github.com/uchaloop/beat"
)

func main() {
	cfg := beat.Config{Period: 5 * time.Minute, Jitter: 5 * time.Minute}
	offset := beat.OffsetFor("orders/cluster-a/worker-0", cfg.Jitter)
	runner, err := beat.MakeBeat(cfg,
		func(ctx context.Context) (int, error) { return 0, ctx.Err() },
		nil, beat.WithOffset(offset),
	)
	if err != nil {
		panic(err)
	}
	_ = runner
}

Types

type Beat

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

Beat is a scheduled runner for one Job. Build it with MakeBeat and drive it with Start and Stop; the beatfx subpackage does that from the Fx lifecycle.

It keeps two independent contexts: loopCtx gates scheduling (cancelled to stop launching new runs) and jobParentCtx parents each run (cancelled to abort an in-flight run). Splitting them lets stop drain - end scheduling while letting the current run finish - or cancel, depending on the graceful flag.

func MakeBeat

func MakeBeat(cfg Config, job Job, handler Handler, opts ...Option) (*Beat, error)

MakeBeat resolves cfg against opts and builds a Beat. job is required; handler may be nil (a no-op handler is used). Drive the returned Beat with Start and Stop; the beatfx subpackage does this from the Fx lifecycle.

Example
package main

import (
	"context"
	"log/slog"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/uchaloop/beat"
)

func main() {
	shutdown, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer cancel()

	job := func(ctx context.Context) (int, error) {
		// Replace this timer with one bounded batch of application work.
		timer := time.NewTimer(20 * time.Millisecond)
		defer timer.Stop()
		select {
		case <-timer.C:
			return 1, nil
		case <-ctx.Done():
			return 0, ctx.Err()
		}
	}
	handler := beat.HandlerFunc(func(_ context.Context, r beat.Record) {
		slog.Info("job completed", "outcome", r.Outcome,
			"processed", r.Processed, "duration", r.Duration, "missed", r.Missed)
	})
	runner, err := beat.MakeBeat(beat.Config{
		Period: 5 * time.Second, JobTimeout: 2 * time.Second, Jitter: time.Second,
	}, job, handler, beat.WithGracefulStop())
	if err != nil {
		slog.Error("configure beat", "error", err)
		return
	}
	if err := runner.Start(shutdown); err != nil {
		slog.Error("start beat", "error", err)
		return
	}
	<-shutdown.Done()
	stopCtx, stopCancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer stopCancel()
	if err := runner.Stop(stopCtx); err != nil {
		slog.Error("stop beat", "error", err)
	}
}

func (*Beat) Done added in v0.4.0

func (b *Beat) Done() <-chan struct{}

Done returns a channel that closes when the run loop has left and no Job is in flight - or, for a Beat that never started, as soon as it is stopped.

Stop returns on its own deadline and may return first, because a job timeout cancels a run's context but cannot interrupt it. This is how an application learns that the run has finally ended, so it can release, after the fact, a resource the Job was still using. Done does not wait for OnStop. Only use it for fallback cleanup after Stop returned ErrStillRunning; otherwise cleanup could overlap with OnStop.

func (*Beat) Start

func (b *Beat) Start(ctx context.Context) error

Start runs the OnStart hook and launches the run loop. The loop runs in its own goroutine, so Start returns once it is launched. ctx bounds the hook, and a returned error aborts startup.

It may be called once. A second call reports ErrAlreadyStarted, and a call on a stopped Beat reports ErrStopped; neither launches a second loop, which is what keeps runs from overlapping. If the hook fails the Beat is left stopped, and the OnStop hook is not run - the hooks are a pair, and the first one did not complete. If OnStart succeeds but ctx has been cancelled, Start invokes Stop with a fresh 15-second cleanup context and returns both errors. A concurrent Stop owns cleanup instead. Hooks must respect their contexts.

func (*Beat) Stop

func (b *Beat) Stop(ctx context.Context) error

Stop ends scheduling, waits for the run loop to leave (bounded by ctx), then runs the OnStop hook. By default the in-flight run's context is cancelled at once; with graceful stop the run is left to finish and is only cancelled if it outlives ctx.

If ctx elapses while a run is still going, Stop cancels the run, returns ErrStillRunning joined with ctx's error, and does not run the OnStop hook: the hook is documented to run after the loop has stopped, and running it against a Job that is still working would race it for the very resources the hook exists to release. Beat.Done reports when the run finally ends.

Stop is safe to call more than once and from several goroutines: the first call does the work, and the others report its result without running the hook again. Stopping a Beat that was never started does nothing and reports no error. Waiting callers may return their own context error before the first Stop finishes. The deadline bounds waiting; OnStop runs synchronously and must respect ctx. Stop during OnStart waits for that hook before cleanup.

type Config

type Config struct {
	// Period is how often the Job runs. Under ModeFixedRate it is the spacing
	// of the grid the runs sit on; under ModeFixedDelay it is the pause between
	// the end of one run and the start of the next. The deployment has to
	// supply it: there is no period that makes sense for every job.
	Period time.Duration `env:"PERIOD,notEmpty"`

	// JobTimeout cancels a run's context after this duration. Zero selects one
	// minute. Cancellation is cooperative: Job must return and join its own
	// goroutines. A longer timeout can allow a run to cover multiple grid points.
	JobTimeout time.Duration `env:"JOB_TIMEOUT"`

	// Jitter is the upper bound of the random offset that staggers replicas so
	// they do not all fire at once. The draw is half-open, [0, Jitter), and
	// happens once when the Beat is built, so the offset holds for the life of
	// the process. Zero disables it; WithOffset replaces the draw with a value
	// the application chooses.
	//
	// It may be as large as Period - spreading replicas over the whole period is
	// what an evenly polling deployment wants. Work tied to a boundary is the
	// exception: a wide jitter delays it by that much.
	//
	// It spreads load. It is not a guard against two replicas doing the same
	// work: runs longer than the offset overlap regardless.
	Jitter time.Duration `env:"JITTER"`
}

Config is the configuration beat declares. The env tags are inert strings: an application loads them with github.com/uchaloop/confmaker and provides the filled Config into the container. beat itself never reads the environment.

func (Config) ConfigName added in v0.3.2

func (Config) ConfigName() string

ConfigName is the default instance name, "beat": a loader such as confmaker reads BEAT_PERIOD and the rest of BEAT_* unless the application names the instance itself.

func (*Config) SetDefaults added in v0.2.0

func (c *Config) SetDefaults()

SetDefaults sets JobTimeout to one minute. Configuration loaders call it before applying input. MakeBeat also defaults a zero JobTimeout.

func (Config) Validate

func (c Config) Validate() error

Validate reports whether the Config is usable. confmaker calls it after filling the struct, and it reports every problem at once rather than the first: a deployment is fixed in a config map and rolled out, so one report is one round trip.

type Handler

type Handler interface {
	Handle(ctx context.Context, r Record)
}

Handler receives a Record after every run. It is the single observability seam of beat: metrics, logging and tracing are all built on top of it, so the core depends on none of them. It mirrors slog.Handler.

Handle runs inline on the run loop, so its time comes out of the schedule: a slow Handler delays the next run and can cost a scheduled point. It is handed a context that is deliberately not cancellable, so that the last record of a shutdown still arrives - which means a Handler that blocks holds the loop, and Stop will give up on it with ErrStillRunning. Return promptly, and give any network call a budget of its own.

func MultiHandler

func MultiHandler(handlers ...Handler) Handler

MultiHandler returns a Handler that passes each Record to every handler in turn, skipping nil ones. Use it to run several sinks - metrics and logging, say - behind the single Handler slot. Handlers run in order and are not isolated: the core does not recover a panicking handler, so one panic propagates and crashes the process. Keep handlers panic-free.

type HandlerFunc

type HandlerFunc func(ctx context.Context, r Record)

HandlerFunc adapts an ordinary function to a Handler, like http.HandlerFunc.

func (HandlerFunc) Handle

func (f HandlerFunc) Handle(ctx context.Context, r Record)

Handle calls f. A nil HandlerFunc is a no-op.

type Job

type Job func(ctx context.Context) (int, error)

Job is the unit of work beat runs on each tick. It reports how many items it processed - return 0 when the job is not item-oriented - and an error if the run failed. A Job must respect ctx: beat bounds it with the job timeout and, unless graceful stop is enabled, cancels it on shutdown.

type Middleware

type Middleware func(next Job) Job

Middleware wraps a Job to add behaviour around it - recovery, context enrichment, extra logging. It is classic Go composition: a Middleware receives the next Job and returns a Job that calls it.

A Middleware does not decide when the next run happens: pausing inside the Job would spend the job timeout and land in Record.Duration. Use WithBackoff for that.

type Mode

type Mode string

Mode selects how the start of one run relates to the next.

const (
	// ModeFixedRate starts runs on an absolute grid: every Period since the
	// Unix epoch, shifted by the offset. The cadence does not depend on how
	// long a run takes, so two processes that share a Period agree on the
	// points without sharing anything else - which is what lets a per-replica
	// offset hold them apart for as long as they live.
	//
	// Runs still never overlap. A run that outlives its period covers the
	// points it ran through, and they are reported as Record.Missed rather
	// than queued for later.
	ModeFixedRate Mode = "fixed_rate"

	// ModeFixedDelay starts the next run one Period after the previous one
	// ended, so a slow run pushes the whole schedule back. Use it for a job
	// that should rest between runs rather than keep a cadence.
	//
	// There is no grid, so nothing is ever missed and an offset only delays
	// the first run: the phase follows the job's duration and is not held.
	ModeFixedDelay Mode = "fixed_delay"
)

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option customizes a Beat built by MakeBeat.

func WithBackoff added in v0.4.0

func WithBackoff(fn func(Record) time.Duration) Option

WithBackoff computes a minimum pause from Job completion. The callback runs inline after Handler and must return promptly. A non-positive result adds no restriction; it does not remove Period. The next target must satisfy both the schedule and the pause. Intentionally bypassed points are not Missed. Backoff is outside JobTimeout and Record.Duration.

Example
package main

import (
	"context"
	"time"

	"github.com/uchaloop/beat"
)

func main() {
	job := func(ctx context.Context) (int, error) { return 0, ctx.Err() }
	runner, err := beat.MakeBeat(beat.Config{
		Period: time.Second, JobTimeout: 30 * time.Second,
	}, job, nil,
		beat.WithMode(beat.ModeFixedDelay),
		beat.WithBackoff(func(r beat.Record) time.Duration {
			if r.Outcome != beat.OutcomeOK {
				return 30 * time.Second
			}
			if r.Processed < 1000 {
				return 5 * time.Minute
			}
			return 0
		}),
	)
	if err != nil {
		panic(err)
	}
	// Call runner.Start and runner.Stop from the application's lifecycle.
	_ = runner
}

func WithGracefulStop

func WithGracefulStop() Option

WithGracefulStop lets the active Job finish while Stop waits. JobTimeout still applies. If Stop's context expires before startup/the loop finishes, Stop cancels the Job context, returns ErrStillRunning and skips OnStop.

func WithHandler

func WithHandler(h Handler) Option

WithHandler sets the observability Handler. It overrides a Handler provided through the Fx container.

func WithMiddleware

func WithMiddleware(mw ...Middleware) Option

WithMiddleware wraps the Job. The first middleware is the outermost layer and runs first.

func WithMode added in v0.4.0

func WithMode(m Mode) Option

WithMode selects the scheduling policy. ModeFixedRate is the default; ModeFixedDelay waits at least Period from the end of the previous Job.

func WithOffset added in v0.4.0

func WithOffset(d time.Duration) Option

WithOffset sets the offset within the period explicitly, instead of drawing one below Config.Jitter. Use it to give each replica a slot that survives a restart - see OffsetFor - or to spread replicas evenly by their index. The offset must be in [0, Period).

func WithOnStart

func WithOnStart(h func(context.Context) error) Option

WithOnStart registers a hook run once before the loop starts. A returned error aborts startup and leaves the Beat stopped, without running the OnStop hook - the two are a pair, and this one did not complete. If the hook succeeds after the startup context is cancelled, Start arranges cleanup; see Beat.Start.

func WithOnStop

func WithOnStop(h func(context.Context) error) Option

WithOnStop registers a hook run once after the loop has stopped.

"After the loop has stopped" is exact: if Stop's context elapses while a run is still going, the hook does not run at all, because it would otherwise race the Job for whatever it is meant to release. Watch Beat.Done for the moment the run finally ends, after Stop returned ErrStillRunning. Done does not wait for OnStop. The hook runs synchronously and must respect its context.

type Outcome added in v0.4.0

type Outcome string

Outcome classifies how a run ended. It is the one label an observability adapter needs; Record.Err carries the detail behind it.

const (
	// OutcomeOK is a run that finished and returned no error.
	OutcomeOK Outcome = "ok"

	// OutcomeError is a run that returned an error of its own.
	OutcomeError Outcome = "error"

	// OutcomePanic is a run whose panic the recovery middleware turned into a
	// *PanicError. Without that middleware a panic never reaches a Record - it
	// crashes the process.
	OutcomePanic Outcome = "panic"

	// OutcomeTimeout means the Job's context deadline expired before the
	// outcome was classified. It is reported only after Job returns, even if
	// Job returns nil. It does not imply that execution was forcibly stopped.
	OutcomeTimeout Outcome = "timeout"

	// OutcomeCanceled means the Job context was cancelled by shutdown before
	// the outcome was classified. It is separate from a Job-returned error.
	OutcomeCanceled Outcome = "canceled"
)

type PanicError

type PanicError struct {
	Value any
	Stack []byte
}

PanicError wraps a value recovered from a panic in the Job. The recovery middleware returns it as the run error, so a Handler can tell a panic from an ordinary error - Record.Outcome reads OutcomePanic - and inspect the recovered value and its stack. The beat core does not recover panics; without the recovery middleware a panic crashes the process (with a stack on stderr).

func (*PanicError) Error

func (e *PanicError) Error() string

type Record

type Record struct {
	// Iteration is the 1-based run counter for this Beat, useful as a
	// correlation id. It counts runs, not scheduled points - see Missed.
	Iteration int64

	// ScheduledFor is the point this run was aiming at. Start minus this is how
	// late the run actually began.
	ScheduledFor time.Time

	// Start is the wall-clock time the run began.
	Start time.Time

	// Duration is how long the Job took. It excludes the Handler and any
	// backoff, so it stays comparable with Period.
	Duration time.Duration

	// Period is the configured period, so a Handler can report Duration over
	// Period - how close the job is to outgrowing its schedule - without being
	// handed the configuration separately.
	Period time.Duration

	// Mode is the timing policy that produced this run.
	Mode Mode

	// Processed is the number of items the Job reported for this run.
	Processed int

	// Err is the error the Job returned, or nil on success. A panic recovered
	// by the recovery middleware is reported as a *PanicError.
	Err error

	// Outcome classifies the run and is authoritative: a run can time out with
	// a nil Err, and a *PanicError reads as OutcomePanic.
	Outcome Outcome

	// Missed is how many scheduled points passed without a run between the
	// previous run and this one - because that run and its Handler outlasted
	// them, or because the clock moved.
	//
	// Points the loop chose not to serve are not counted: a backoff asking it to
	// wait is the schedule the application requested, not a loss, and counting
	// those would make this number loudest on a healthy idle poller. It is
	// always 0 under ModeFixedDelay, which has no grid to miss.
	//
	// This Record's timestamps and Duration describe the current run, not the
	// preceding work that caused Missed. A single Record cannot identify the
	// cause; comparing successive records may help diagnose it.
	Missed int
}

Record describes a single completed run of the Job. It is handed to a Handler after each run, mirroring the shape of slog.Record: the library produces the data, the caller decides what to do with it (metrics, logging, tracing).

Directories

Path Synopsis
Package beatfx connects one beat.Beat to an Fx lifecycle.
Package beatfx connects one beat.Beat to an Fx lifecycle.
middleware
recovery
Package recovery supplies middleware that converts panics in the wrapped Job call into beat.PanicError, including the recovered value and stack.
Package recovery supplies middleware that converts panics in the wrapped Job call into beat.PanicError, including the recovered value and stack.

Jump to

Keyboard shortcuts

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