worker

package
v0.3.0-alpha.1 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 17 Imported by: 0

README

worker

Push-based task execution with real-time event streaming, worker pools, and supervision

worker provides a generic Handler[I, O] abstraction for executing tasks that emit events during execution — progress updates, partial results, logs — with built-in pooling, dispatch strategies, supervision, middleware, and composition patterns. Designed for use cases where callers need real-time visibility into task execution: file downloaders, CLI subprocess orchestration, parallel data processing, and long-running background jobs.

Features

  • Handler[I, O] — single generic interface for all task execution
  • Push-based events — handlers call emit() for progress, partial results, and logs during execution
  • Worker pool — fixed-size goroutine pool with per-task handles, cancellation, and graceful shutdown
  • Dispatch strategies — round-robin and least-loaded worker selection
  • Supervision — panic tracking, health monitoring, backoff, and configurable restart policies
  • Middleware — composable cross-cutting concerns (timeout, recovery) using the same Chain pattern as provider
  • Composition — FanOut, MapReduce, and Pipeline for combining handlers
  • Provider bridgesFromProvider / AsProvider for interop with provider.RequestResponse
  • Subprocess handler — wraps process.Command with line-by-line stdout/stderr streaming
  • Lock-free hot path — atomic stats, no mutex on Submit/dispatch/runWorker

Install

go get github.com/kbukum/gokit@latest

Worker is part of the core module — no separate sub-module import needed.

Quick Start

package main

import (
    "context"
    "fmt"

    "github.com/kbukum/gokit/worker"
)

func main() {
    ctx := context.Background()

    // Define a handler
    h := worker.HandlerFunc[string, string](func(
        ctx context.Context, task string, emit func(worker.Event[string]),
    ) error {
        emit(worker.ProgressEvent[string](1, 2, "processing"))
        return nil
    })

    // Create a pool
    pool := worker.NewPool(h, worker.PoolConfig{Name: "demo", Size: 4})
    defer pool.Stop(ctx)

    // Submit a task
    handle, err := pool.Submit(ctx, "hello")
    if err != nil {
        panic(err)
    }

    // Consume events
    for event := range handle.Events() {
        fmt.Printf("%s: %s\n", event.Type, event.Progress)
    }

    // Get final result
    result, err := handle.Result()
    fmt.Println(result, err)
}

Key Types & Functions

Name Description
Handler[I, O] Interface: Handle(ctx, task, emit) error — unit of work
HandlerFunc[I, O] Adapter to use ordinary functions as Handlers
Event[O] Typed event emitted during execution (progress, partial, log, result, error)
Progress Quantitative progress: current, total, percent, message
TaskHandle[O] Tracks a submitted task — Events(), Result(), Cancel(), Done()
Pool[I, O] Fixed-size worker pool with dispatch, events, and graceful shutdown
PoolConfig Pool configuration: size, queue, dispatch strategy, supervisor
PoolStats Pool utilization snapshot: active, idle, queued, total, failed
DispatchStrategy RoundRobin or LeastLoaded worker selection
SupervisorConfig Panic tracking, restart policy, backoff, health interval
RestartPolicy RestartNever, RestartOnFailure, RestartAlways
Middleware[I, O] func(Handler[I, O]) Handler[I, O] — wraps handlers
Chain Composes multiple middlewares: Chain(a, b, c)(handler) = a(b(c(handler)))
WithTimeout Middleware: enforces a deadline on each Handle call
WithRecovery Middleware: recovers panics and converts to errors
FanOut Sends same input to N handlers concurrently, collects results
NewMapReduce Split → process concurrently → combine results
NewPipeline Sequential handler chaining: output of stage N → input of stage N+1
FromProvider Bridges provider.RequestResponseHandler
AsProvider Bridges Handlerprovider.RequestResponse
NewSubprocessHandler Wraps process.Command as a Handler with line-by-line streaming

Usage Examples

Example 1: File Downloader with Progress
type DownloadInput struct {
    URL string
}
type DownloadOutput struct {
    Bytes []byte
}

downloader := worker.HandlerFunc[DownloadInput, DownloadOutput](func(
    ctx context.Context, task DownloadInput, emit func(worker.Event[DownloadOutput]),
) error {
    // Report progress during download
    for i := range 10 {
        emit(worker.ProgressEvent[DownloadOutput](
            int64(i+1), 10, fmt.Sprintf("chunk %d/10", i+1),
        ))
        // ... download chunk ...
    }
    return nil
})

pool := worker.NewPool(downloader, worker.PoolConfig{
    Name: "downloader",
    Size: 8,
})
defer pool.Stop(ctx)

handle, _ := pool.Submit(ctx, DownloadInput{URL: "https://example.com/file"})
for event := range handle.Events() {
    if event.Progress != nil {
        fmt.Printf("%.0f%%\n", event.Progress.Percent*100)
    }
}
Example 2: Middleware Composition
// Wrap handler with timeout and panic recovery
safe := worker.Chain(
    worker.WithTimeout[string, string](30 * time.Second),
    worker.WithRecovery[string, string](),
)(myHandler)

pool := worker.NewPool(safe, worker.PoolConfig{Name: "safe", Size: 4})
Example 3: Supervised Pool
pool := worker.NewPool(handler, worker.PoolConfig{
    Name: "supervised",
    Size: 8,
    Supervisor: &worker.SupervisorConfig{
        RestartPolicy:  worker.RestartOnFailure,
        MaxRestarts:    5,
        BackoffBase:    time.Second,
        HealthInterval: 30 * time.Second,
    },
})

When a task panics, the worker goroutine survives (panics are caught per-task). The supervisor tracks per-worker panic counts and marks workers unhealthy after exceeding MaxRestarts. Unhealthy workers are skipped by the dispatcher.

Example 4: MapReduce — Parallel Processing with Combine
mr := worker.NewMapReduce(worker.MapReduceConfig[string, string, int]{
    Name: "word-count",
    Split: func(doc string) []string {
        return strings.Split(doc, "\n") // split by line
    },
    Handler: lineCounter, // Handler[string, int]
    Combine: func(counts []int) (int, error) {
        total := 0
        for _, c := range counts {
            total += c
        }
        return total, nil
    },
    PoolSize: 4,
})

// Use as a regular handler
err := mr.Handle(ctx, document, emit)
Example 5: FanOut — Same Input to Multiple Handlers
// Send same audio to multiple transcription engines
multi := worker.FanOut("multi-transcribe", engineA, engineB, engineC)

// Result type is []TranscriptionResult
var results []TranscriptionResult
err := multi.Handle(ctx, audioInput, func(e worker.Event[[]TranscriptionResult]) {
    if e.Type == worker.EventResult {
        results = e.Data
    }
})
Example 6: Pipeline — Sequential Stages
pipeline := worker.NewPipeline[RawAudio, Text]("transcribe-pipeline",
    worker.PipelineStage{Name: "decode", Handler: audioDecoder},
    worker.PipelineStage{Name: "transcribe", Handler: transcriber},
    worker.PipelineStage{Name: "format", Handler: formatter},
)

err := pipeline.Handle(ctx, rawAudio, emit)
Example 7: Subprocess with Line Streaming
h := worker.NewSubprocessHandler(worker.SubprocessConfig{
    Command: process.Command{
        Binary:      "ffmpeg",
        Dir:         "/tmp",
        GracePeriod: 10 * time.Second,
    },
})

handle, _ := pool.Submit(ctx, worker.SubprocessInput{
    Args: []string{"-i", "input.mp4", "-f", "wav", "output.wav"},
})

// Each stdout/stderr line arrives as an EventPartial
for event := range handle.Events() {
    if event.Type == worker.EventPartial {
        fmt.Printf("[%s] %s\n", event.Data.Stream, event.Data.Line)
    }
}
Example 8: Provider Bridge
// Wrap a provider.RequestResponse as a worker Handler
handler := worker.FromProvider(myProvider)

// Use in a pool
pool := worker.NewPool(handler, worker.PoolConfig{Name: "bridged", Size: 4})

// Or expose a Handler as a provider.RequestResponse
prov := worker.AsProvider(myHandler, worker.AsProviderConfig{
    ProviderName: "my-worker",
})
result, err := prov.Execute(ctx, input)
Example 9: Batch Submission
handles, err := pool.SubmitBatch(ctx, []string{"task1", "task2", "task3"})
if err != nil {
    // All previously submitted tasks are canceled on error
    log.Fatal(err)
}

for _, h := range handles {
    result, err := h.Result()
    fmt.Println(result, err)
}
Example 10: Pool-Level Event Monitoring
pool := worker.NewPool(handler, worker.PoolConfig{
    Name:        "monitored",
    Size:        4,
    EventBuffer: 128,
})

// Monitor all events across all workers
go func() {
    for event := range pool.Events() {
        log.Printf("[%s] worker=%s task=%s type=%s",
            pool.Stats().Active, event.WorkerID, event.TaskID, event.Type)
    }
}()

Architecture

Push vs Pull
Aspect worker (push) pipeline (pull)
Direction Handler pushes events to caller via emit() Downstream pulls from upstream via Next()
Backpressure Bounded event channels; drops if full Natural — producer waits for consumer
Use case Long tasks with progress, subprocess streaming Data transformation, batch processing
Lifecycle Task-scoped with explicit pool management Lazy evaluation, runs on terminal operator

Use worker when you need real-time visibility into task execution. Use pipeline for composable data transformations with backpressure.

Handler ↔ Provider ↔ Process
provider.RequestResponse[I, O]
    ↕  FromProvider / AsProvider
worker.Handler[I, O]
    ↑  NewSubprocessHandler
process.Command → line-by-line streaming
  • provider = one-in-one-out, synchronous completion
  • worker = one-in-many-events, push-based streaming during execution
  • process = subprocess execution with full output capture

SubprocessConfig wraps process.Command directly — shared type, no field duplication.

Testing

Use HandlerFunc for deterministic test handlers:

func TestMyWorker(t *testing.T) {
    h := worker.HandlerFunc[int, int](func(
        ctx context.Context, n int, emit func(worker.Event[int]),
    ) error {
        emit(worker.ProgressEvent[int](1, 1, "done"))
        return nil
    })

    pool := worker.NewPool(h, worker.PoolConfig{Name: "test", Size: 2})
    defer pool.Stop(context.Background())

    handle, err := pool.Submit(context.Background(), 42)
    if err != nil {
        t.Fatal(err)
    }
    if _, err := handle.Result(); err != nil {
        t.Fatal(err)
    }
}

Performance Considerations

  • Lock-free hot pathstopped is atomic.Bool, worker stats use atomic.Int32. No mutex on Submit, dispatch, or runWorker
  • Non-blocking event forwarding — pool-level events use select/default to avoid blocking workers; per-task events are buffered
  • Timer management — backoff uses time.NewTimer + Stop() (no time.After leaks)
  • Context.AfterFunc — ties task context to pool context for zero-overhead cancellation propagation (Go 1.21+)
  • Atomic statsPoolStats reads use no locks, only atomic loads
  • provider — Generic provider framework with RequestResponse, Stream, Sink patterns
  • pipeline — Pull-based data pipeline with composable operators
  • process — Subprocess execution with context cancellation and signal handling
  • dag — DAG execution engine for dependency-ordered orchestration

⬅ Back to main README

Documentation

Overview

Package worker provides push-based task execution with real-time event streaming, worker pools, and supervision.

The core abstraction is a Handler — a function that receives typed input, does work, and calls emit() to push events (progress, partial results, logs) back to the caller during execution. Context carries cancellation.

Handler

The Handler interface is the unit of work:

h := worker.HandlerFunc[string, string](func(
    ctx context.Context, task string, emit func(worker.Event[string]),
) error {
    emit(worker.ProgressEvent[string](50, 100, "halfway"))
    return nil
})

Pool

Pool manages N goroutines running the same handler with dispatch strategies, event aggregation, and graceful shutdown:

pool := worker.NewPool(h, worker.PoolConfig{Name: "example", Size: 4})
handle, _ := pool.Submit(ctx, "hello")
for event := range handle.Events() {
    fmt.Println(event.Type, event.Data)
}

Middleware

Middleware[I, O] wraps a Handler with cross-cutting behavior. Chain composes multiple middlewares (same pattern as provider.Middleware):

wrapped := worker.Chain(
    worker.WithTimeout[In, Out](30 * time.Second),
    worker.WithRecovery[In, Out](),
)(myHandler)

Composition

Handlers compose via FanOut (same input to N handlers), NewMapReduce (split → process → combine), and NewPipeline (sequential chaining).

Provider Integration

FromProvider bridges a provider.RequestResponse into a Handler. AsProvider bridges a Handler back into a provider.RequestResponse. NewSubprocessHandler bridges process.Run() into a Handler with line streaming.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrQueueFull is returned when a task cannot be enqueued immediately.
	ErrQueueFull = gkerrors.New(gkerrors.ErrCodeRateLimited, "worker queue is full", http.StatusTooManyRequests)
	// ErrTaskDropped is reported to a task that was evicted by DropOldest.
	ErrTaskDropped = gkerrors.Canceled("worker task dropped due to overflow")
)

Functions

func AsProvider

func AsProvider[I, O any](h Handler[I, O], cfg AsProviderConfig) provider.RequestResponse[I, O]

AsProvider wraps a Handler as a provider.RequestResponse. Runs the handler, waits for completion, returns the final EventResult data. Progress and partial events are discarded.

Types

type AsProviderConfig

type AsProviderConfig struct {
	// ProviderName identifies this provider (implements provider.Provider.Name).
	ProviderName string `yaml:"provider_name" mapstructure:"provider_name"`
}

AsProviderConfig configures how a Handler maps to a provider.

type Broadcaster

type Broadcaster interface {
	BroadcastToPattern(pattern string, data []byte)
}

Broadcaster is the minimal SSE-style fan-out abstraction the worker package depends on. Defined locally so the worker package stays transport-agnostic — anything matching this method set (notably *sse.Hub) satisfies it without an import edge.

type DispatchStrategy

type DispatchStrategy string

DispatchStrategy controls how tasks are assigned to workers.

const (
	RoundRobin  DispatchStrategy = "round_robin"  // rotate through workers sequentially
	LeastLoaded DispatchStrategy = "least_loaded" // pick the worker with fewest active tasks
)

type Event

type Event[O any] struct {
	Type      EventType      `json:"type"`
	TaskID    string         `json:"task_id"`
	WorkerID  string         `json:"worker_id"`
	Progress  *Progress      `json:"progress,omitempty"`
	Data      O              `json:"data,omitempty"`
	Error     error          `json:"error,omitempty"`
	Timestamp time.Time      `json:"timestamp"`
	Metadata  map[string]any `json:"metadata,omitempty"`
}

Event is a typed message emitted by a handler during execution.

func LogEvent

func LogEvent[O any](msg string, meta map[string]any) Event[O]

LogEvent creates a log event with optional metadata.

func PartialEvent

func PartialEvent[O any](data O) Event[O]

PartialEvent creates a partial-result event.

func ProgressEvent

func ProgressEvent[O any](current, total int64, msg string) Event[O]

ProgressEvent creates a progress event with the given current/total counts.

type EventType

type EventType int

EventType identifies the kind of event emitted by a handler.

const (
	EventProgress EventType = iota // Progress update (bytes, percent, message)
	EventPartial                   // Usable partial result before completion
	EventLog                       // Structured log from the handler
	EventResult                    // Final result (auto-emitted on success)
	EventError                     // Error (auto-emitted on failure)
)

func (EventType) String

func (t EventType) String() string

String returns a human-readable event type name.

type Handler

type Handler[I, O any] interface {
	Handle(ctx context.Context, task I, emit func(Event[O])) error
}

Handler processes a task and emits events during execution. The handler MUST check ctx.Done() for cooperative cancellation.

func FanOut

func FanOut[I, O any](name string, handlers ...Handler[I, O]) Handler[I, []O]

FanOut sends the same input to N handlers concurrently. Returns when all complete. Events from all handlers are merged into the composite emit. Results are collected in the same order as handlers.

func FromProvider

func FromProvider[I, O any](p provider.RequestResponse[I, O]) Handler[I, O]

FromProvider wraps a provider.RequestResponse as a Handler. The handler checks IsAvailable before executing; returns an error if unavailable. Emits a single EventResult on success. No progress events.

func NewMapReduce

func NewMapReduce[I, O, R any](cfg MapReduceConfig[I, O, R]) Handler[I, R]

NewMapReduce creates a handler that splits input, processes sub-tasks concurrently via Handler, and combines results. If cfg.Pool is set, it is reused across invocations (caller manages its lifecycle). Otherwise a temporary pool is created and stopped per call.

func NewPipeline

func NewPipeline[I, O any](name string, stages ...PipelineStage) Handler[I, O]

NewPipeline chains handlers: output of stage N is input to stage N+1. Events from all stages are merged into the composite emit.

Due to Go's generics limitations, pipeline stages use any internally with runtime type assertions. For compile-time safety, compose handlers manually or use dag with typed ports.

func NewSubprocessHandler

func NewSubprocessHandler(cfg SubprocessConfig) Handler[SubprocessInput, SubprocessOutput]

NewSubprocessHandler creates a Handler that runs a subprocess and emits each stdout/stderr line as an EventPartial. Uses the process package for argv-only execution, process group isolation, and SIGTERM→SIGKILL graceful shutdown.

type HandlerFunc

type HandlerFunc[I, O any] func(ctx context.Context, task I, emit func(Event[O])) error

HandlerFunc is an adapter to use ordinary functions as Handlers.

func (HandlerFunc[I, O]) Handle

func (f HandlerFunc[I, O]) Handle(ctx context.Context, task I, emit func(Event[O])) error

type Job

type Job struct {
	// Name identifies the job in health reports and logs.
	Name string
	// Interval between consecutive runs.
	Interval time.Duration
	// RunOnStart causes the job to execute once immediately when the scheduler starts,
	// before entering its periodic loop.
	RunOnStart bool
	// Fn is the work to perform on each tick.
	Fn TickerFunc
}

Job defines a single periodic task managed by a Scheduler.

type KeyedPool

type KeyedPool[K comparable, I, O any] struct {
	// contains filtered or unexported fields
}

KeyedPool wraps a Pool with singleflight-style coalescing on a caller-defined key K: at most one in-flight task per key. Concurrent submissions for the same key attach to the running task and observe the same outcome.

Typical use cases: cache warmups, image pulls, per-resource background jobs where duplicate work is wasteful or incorrect.

State model

An entry exists in the inflight map for `key` if and only if work is in flight under that key. "In flight" spans three phases:

  1. Reserved — a caller has won the race to submit but pool.Submit has not yet returned a TaskHandle.
  2. Running — the TaskHandle is published; the underlying pool is executing the task.
  3. Done watcher — the task has finished but the eviction goroutine has not yet run. Brief.

All public methods agree on this invariant. Get blocks through phase 1 so it never lies about state; Cancel works in any phase.

KeyedPool is safe for concurrent use.

func NewKeyedPool

func NewKeyedPool[K comparable, I, O any](pool *Pool[I, O]) *KeyedPool[K, I, O]

NewKeyedPool wraps an existing Pool with a keyed coalescer.

The caller retains ownership of the underlying Pool — KeyedPool does not stop it. Multiple KeyedPools (or direct Submit calls) may share a single Pool when desirable.

func (*KeyedPool[K, I, O]) Active

func (k *KeyedPool[K, I, O]) Active() int

Active returns the number of in-flight entries (all phases).

func (*KeyedPool[K, I, O]) Cancel

func (k *KeyedPool[K, I, O]) Cancel(key K) bool

Cancel cancels in-flight work under key. Returns true when an entry was found. Cancel is non-blocking and safe in any phase:

  • Phase 1 (reservation): cancels the Submit ctx, causing Submit to return ctx.Err(); SubmitOrAttach publishes the error and removes the entry.
  • Phase 2 (running): cancels the published TaskHandle. The eviction goroutine removes the entry once the task observes cancellation.
  • Phase 3 (done-watcher window): both the cancelSubmit and handle.Cancel calls are idempotent no-ops.

Concurrent Cancel calls under the same key are safe and idempotent.

func (*KeyedPool[K, I, O]) Get

func (k *KeyedPool[K, I, O]) Get(ctx context.Context, key K) (*TaskHandle[O], bool, error)

Get returns the in-flight handle for key. The boolean is false when no work is in flight under key. The error is non-nil when an in-flight submission failed before publishing a handle (phase 1 failure).

Get blocks through the reservation window: if an entry exists but the handle is not yet published, Get waits on the entry until the handle materializes (or the submission fails), honoring ctx.

func (*KeyedPool[K, I, O]) Keys

func (k *KeyedPool[K, I, O]) Keys() []K

Keys returns a snapshot of in-flight keys (all phases). Order is unspecified.

func (*KeyedPool[K, I, O]) SubmitOrAttach

func (k *KeyedPool[K, I, O]) SubmitOrAttach(ctx context.Context, key K, task I) (*TaskHandle[O], bool, error)

SubmitOrAttach submits task under key, or attaches to an existing in-flight submission for the same key. Returns the shared TaskHandle and attached=true when a prior submission was found.

Cancellation: canceling the returned handle (or any caller's submission ctx via Cancel) terminates the single shared attempt for ALL attached observers — the documented semantic for coalesced work.

Concurrency: KeyedPool.mu is released across pool.Submit, so submissions for different keys never serialize on each other (F-076 #64). Same-key racers wait on the entry's `ready` channel, ctx-aware.

type MapReduceConfig

type MapReduceConfig[I, O, R any] struct {
	Name     string
	Split    func(I) []O          // split input into sub-tasks
	Handler  Handler[O, R]        // process each sub-task
	Combine  func([]R) (R, error) // reduce partial results
	PoolSize int                  // concurrency for map phase (default: len(splits))
	Pool     *Pool[O, R]          // optional reusable pool; if nil, a temporary pool is created per call
}

MapReduceConfig configures a map-reduce handler.

type Middleware

type Middleware[I, O any] func(Handler[I, O]) Handler[I, O]

Middleware wraps a Handler to add cross-cutting behavior.

func Chain

func Chain[I, O any](middlewares ...Middleware[I, O]) Middleware[I, O]

Chain composes multiple middlewares into one. Middlewares are applied in order: the first middleware is outermost (executes first on the way in, last on the way out).

Chain(a, b, c)(handler) is equivalent to a(b(c(handler))).

func WithRecovery

func WithRecovery[I, O any]() Middleware[I, O]

WithRecovery returns a Middleware that recovers from panics and converts them to errors.

func WithTimeout

func WithTimeout[I, O any](d time.Duration) Middleware[I, O]

WithTimeout returns a Middleware that enforces a deadline on each Handle call.

type OverflowPolicy

type OverflowPolicy string

OverflowPolicy controls what happens when the pool queue is full.

const (
	// OverflowBlock waits until queue capacity is available.
	OverflowBlock OverflowPolicy = "block"
	// OverflowReject fails the submission immediately.
	OverflowReject OverflowPolicy = "reject"
	// OverflowDropOldest evicts the oldest queued task to make room.
	OverflowDropOldest OverflowPolicy = "drop_oldest"
)

func (OverflowPolicy) MarshalText

func (o OverflowPolicy) MarshalText() ([]byte, error)

MarshalText serializes an overflow policy for config encoders.

func (*OverflowPolicy) UnmarshalText

func (o *OverflowPolicy) UnmarshalText(text []byte) error

UnmarshalText parses an overflow policy from config text.

type PanicError

type PanicError struct {
	Value any
}

PanicError wraps a recovered panic value as an error.

func (*PanicError) Error

func (e *PanicError) Error() string

type PipelineStage

type PipelineStage struct {
	Name    string
	Handler Handler[any, any]
}

PipelineStage defines one step in a handler pipeline.

type Pool

type Pool[I, O any] struct {
	// contains filtered or unexported fields
}

Pool manages a fixed set of worker goroutines executing a Handler.

func NewPool

func NewPool[I, O any](handler Handler[I, O], cfg PoolConfig) *Pool[I, O]

NewPool creates a new worker pool with the given handler and configuration.

func (*Pool[I, O]) Events

func (p *Pool[I, O]) Events() <-chan Event[O]

Events returns an aggregated event channel from all workers.

func (*Pool[I, O]) Stats

func (p *Pool[I, O]) Stats() PoolStats

Stats returns current pool statistics.

func (*Pool[I, O]) Stop

func (p *Pool[I, O]) Stop(ctx context.Context) error

Stop performs graceful shutdown: stops accepting tasks, waits for in-flight work to finish within GracePeriod, then force-cancels remaining.

func (*Pool[I, O]) Submit

func (p *Pool[I, O]) Submit(ctx context.Context, task I) (*TaskHandle[O], error)

Submit sends a task to the pool. Returns a handle to track the task.

func (*Pool[I, O]) SubmitBatch

func (p *Pool[I, O]) SubmitBatch(ctx context.Context, tasks []I) ([]*TaskHandle[O], error)

SubmitBatch sends multiple tasks. Returns handles in the same order.

type PoolConfig

type PoolConfig struct {
	Name        string            `yaml:"name"         mapstructure:"name"`
	Size        int               `yaml:"size"         mapstructure:"size"`         // fixed pool size (default: runtime.NumCPU)
	QueueSize   int               `yaml:"queue_size"   mapstructure:"queue_size"`   // bounded task queue (0 = unbuffered)
	Overflow    OverflowPolicy    `yaml:"overflow"     mapstructure:"overflow"`     // block | reject | drop_oldest (default: block)
	EventBuffer int               `yaml:"event_buffer" mapstructure:"event_buffer"` // event channel buffer per task (default: 64)
	GracePeriod time.Duration     `yaml:"grace_period" mapstructure:"grace_period"` // shutdown grace (default: 5s)
	Dispatch    DispatchStrategy  `yaml:"dispatch"     mapstructure:"dispatch"`     // round_robin | least_loaded (default: round_robin)
	Supervisor  *SupervisorConfig `yaml:"supervisor,omitempty" mapstructure:"supervisor"`
}

PoolConfig configures a worker pool.

type PoolStats

type PoolStats struct {
	Active int `json:"active"` // workers currently executing tasks
	Idle   int `json:"idle"`   // workers waiting for tasks
	Queued int `json:"queued"` // tasks waiting in the queue
	Total  int `json:"total"`  // total tasks submitted
	Failed int `json:"failed"` // tasks that returned an error
}

PoolStats reports pool utilization.

type Progress

type Progress struct {
	Current int64   `json:"current"`           // e.g., bytes downloaded
	Total   int64   `json:"total"`             // total expected (-1 if unknown)
	Percent float64 `json:"percent,omitempty"` // 0.0–1.0 (auto-computed if Total > 0)
	Message string  `json:"message,omitempty"` // human-readable status
}

Progress reports quantitative progress.

type RestartPolicy

type RestartPolicy string

RestartPolicy controls when a crashed worker should be restarted.

const (
	RestartNever     RestartPolicy = "never"
	RestartOnFailure RestartPolicy = "on_failure"
	RestartAlways    RestartPolicy = "always"
)

type SSEBridge

type SSEBridge[I, O any] struct {
	// contains filtered or unexported fields
}

SSEBridge connects a worker pool's events to an SSE broadcaster for real-time progress streaming.

func NewSSEBridge

func NewSSEBridge[I, O any](pool *Pool[I, O], broadcaster Broadcaster, opts ...SSEBridgeOption) *SSEBridge[I, O]

NewSSEBridge creates a bridge that forwards pool events to an SSE broadcaster.

func (*SSEBridge[I, O]) Start

func (b *SSEBridge[I, O]) Start(ctx context.Context) (stop func())

Start begins forwarding pool events to SSE clients. Returns a stop function that terminates the bridge goroutine.

type SSEBridgeOption

type SSEBridgeOption func(*sseBridgeConfig)

SSEBridgeOption configures an SSEBridge.

func WithEnvelope

func WithEnvelope(fn func(event Event[any]) any) SSEBridgeOption

WithEnvelope replaces the default JSON event payload with one returned by fn. Use this to project worker events into a domain-specific schema (e.g. adding workspace_id, attempt_id, ts) without modifying the bridge.

The function receives the event projected to Event[any] so it can be shared across input/output type parameters. The returned value is JSON marshaled directly — return any serializable type.

func WithTopicFunc

func WithTopicFunc(fn func(event Event[any]) string) SSEBridgeOption

WithTopicFunc sets a custom function to derive the SSE broadcast pattern from a worker event. Defaults to "task:{taskID}".

type Scheduler

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

Scheduler is a Component that manages multiple periodic jobs. Each job runs in its own goroutine via an internal TickerWorker, giving independent intervals, health, and non-overlap guarantees.

Scheduler implements component.Component and component.Describable.

Example:

s := worker.NewScheduler("background-jobs",
    worker.Job{Name: "catalog-refresh", Interval: 6 * time.Hour, RunOnStart: true, Fn: refreshFn},
    worker.Job{Name: "cleanup", Interval: 24 * time.Hour, Fn: cleanupFn},
)
registry.Register(s)

func NewScheduler

func NewScheduler(name string, jobs ...Job) *Scheduler

NewScheduler creates a Scheduler with the given name and jobs.

func (*Scheduler) Describe

func (s *Scheduler) Describe() component.Description

Describe returns summary information for the bootstrap startup display.

func (*Scheduler) Health

func (s *Scheduler) Health(ctx context.Context) component.Health

Health aggregates health from all jobs. The scheduler is healthy only if every job is healthy. If any job is degraded or unhealthy, the scheduler reports the worst status.

func (*Scheduler) Name

func (s *Scheduler) Name() string

Name returns the scheduler's component name.

func (*Scheduler) Start

func (s *Scheduler) Start(ctx context.Context) error

Start launches all jobs. If any job fails to start, previously started jobs are stopped and the first error is returned.

func (*Scheduler) Stop

func (s *Scheduler) Stop(ctx context.Context) error

Stop signals all jobs to exit and waits for each to finish.

func (*Scheduler) Workers

func (s *Scheduler) Workers() []*TickerWorker

Workers returns the internal TickerWorkers for inspection (e.g. in tests).

type SubprocessConfig

type SubprocessConfig struct {
	// Command defines the binary, working directory, environment, and grace period. Args
	// and Stdin in Command are ignored — use SubprocessInput for per-task values.
	Command process.Command `yaml:"command" mapstructure:"command"`
}

SubprocessConfig configures a subprocess-based handler. Uses process.Command for the static command definition; per-task arguments and stdin are supplied via SubprocessInput.

type SubprocessInput

type SubprocessInput struct {
	Args  []string
	Stdin io.Reader
}

SubprocessInput is the task input for SubprocessHandler.

type SubprocessOutput

type SubprocessOutput struct {
	Stream string // "stdout" or "stderr"
	Line   string
}

SubprocessOutput represents one line of subprocess output.

type SupervisorConfig

type SupervisorConfig struct {
	RestartPolicy  RestartPolicy `yaml:"restart_policy"  mapstructure:"restart_policy"`  // never | on_failure | always
	MaxRestarts    int           `yaml:"max_restarts"    mapstructure:"max_restarts"`    // 0 = unlimited
	BackoffBase    time.Duration `yaml:"backoff_base"    mapstructure:"backoff_base"`    // exponential backoff base (default: 1s)
	HealthInterval time.Duration `yaml:"health_interval" mapstructure:"health_interval"` // health check frequency (default: 30s)
}

SupervisorConfig configures worker supervision.

type TaskHandle

type TaskHandle[O any] struct {
	// contains filtered or unexported fields
}

TaskHandle tracks a submitted task's lifecycle.

func (*TaskHandle[O]) Cancel

func (h *TaskHandle[O]) Cancel()

Cancel requests cancellation of this specific task.

func (*TaskHandle[O]) Done

func (h *TaskHandle[O]) Done() <-chan struct{}

Done returns a channel that is closed when the task completes.

func (*TaskHandle[O]) Events

func (h *TaskHandle[O]) Events() <-chan Event[O]

Events returns a channel of events for this task. Closed when task completes.

func (*TaskHandle[O]) ID

func (h *TaskHandle[O]) ID() string

ID returns the unique task identifier.

func (*TaskHandle[O]) Result

func (h *TaskHandle[O]) Result() (O, error)

Result blocks until the task completes and returns the final result.

type TickerFunc

type TickerFunc func(ctx context.Context) error

TickerFunc is the callback invoked on every tick.

type TickerOption

type TickerOption func(*TickerWorker)

TickerOption configures optional TickerWorker behavior.

func WithOnError

func WithOnError(fn func(error)) TickerOption

WithOnError registers a callback that is invoked after every tick that returns a non-nil error. Useful for logging or alerting.

func WithRunOnStart

func WithRunOnStart() TickerOption

WithRunOnStart causes the worker to execute fn once immediately when the background goroutine starts, before entering the periodic loop. The initial run does NOT block app startup — it runs inside the goroutine launched by Start.

type TickerWorker

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

TickerWorker is a Component that runs a function on a fixed interval.

Start launches a background goroutine; Stop signals it and waits for a clean exit. Health reports the last-run time and any recent errors.

Example:

tw := worker.NewTickerWorker("cache-cleanup", 30*time.Second, func(ctx context.Context) error {
    return cache.Cleanup(ctx)
}, worker.WithRunOnStart())
registry.Register(tw)

func NewTickerWorker

func NewTickerWorker(name string, interval time.Duration, fn TickerFunc, opts ...TickerOption) *TickerWorker

NewTickerWorker creates a TickerWorker with the given name, interval, and handler.

func (*TickerWorker) FailCount

func (w *TickerWorker) FailCount() uint64

FailCount returns the total number of failed ticks.

func (*TickerWorker) Health

Health returns the current health status.

func (*TickerWorker) Name

func (w *TickerWorker) Name() string

Name returns the component name.

func (*TickerWorker) RunCount

func (w *TickerWorker) RunCount() uint64

RunCount returns the total number of completed ticks.

func (*TickerWorker) Start

func (w *TickerWorker) Start(_ context.Context) error

Start launches the ticker loop in a background goroutine.

func (*TickerWorker) Stop

func (w *TickerWorker) Stop(_ context.Context) error

Stop signals the ticker loop to exit and waits for it to finish.

Jump to

Keyboard shortcuts

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