Skip to content

jaredjakacky/workerkit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Workerkit

Release CI Go Support License

Overview

Workerkit is a transport-agnostic Go runtime for domain workers: long-running loops, pollers, subscribers, command-driven workers, and other worker-oriented service components.

It gives ordinary Go workers a real production shell: lifecycle control, readiness, graceful shutdown, retries, jitter, concurrency limits, failure policy, panic handling, status inspection, and structured observability — without turning your service into a framework.

If your service owns domain workers that actually matter, Workerkit gives them the same kind of operational story HTTP servers already expect.

Why Workerkit exists

Worker-oriented services deserve the same kind of coherent runtime story that HTTP services already expect.

The hard part is not starting a goroutine. The hard part is everything that appears after the goroutine matters: lifecycle, readiness, deploy drain, shutdown deadlines, retry policy, failure visibility, status inspection, command routing, concurrency limits, panic recovery, and useful telemetry.

Those concerns are not domain logic, but they show up in every production service that owns workers. Without a runtime, they tend to spread across channels, health flags, admin handlers, retry loops, shutdown hooks, and disconnected logs.

Workerkit pulls that operational layer into one reusable runtime. Applications register workers, define the policies that matter, and keep the worker code focused on domain behavior.

A Workerkit worker is still normal Go code. It can run a long-lived loop, watch external systems, consume from a broker, poll an API, maintain in-memory state, or expose domain-specific commands. Workerkit does not decide what the worker does. It gives the worker a predictable operational envelope.

Workerkit also stands next to Opskit and Servekit instead of reinventing an operations registry or HTTP service layer. In the composed Kit Series path, a Workerkit runtime is one Opskit component. Servekit consumes the Opskit registry for /readyz and generic read-only admin component routes. Workerkit stays transport-neutral; Servekit keeps owning the HTTP baseline.

What you get

With one runtime, Workerkit gives you:

  • explicit worker registration
  • startup, drain, and graceful shutdown
  • aggregate runtime status and per-worker snapshots
  • readiness reporting and readiness aggregation
  • worker-owned command registration and direct dispatch
  • bounded retry with backoff and jitter
  • runtime-wide and worker-local concurrency limits
  • panic and failure policy
  • structured observer hooks
  • optional slog and OpenTelemetry adapters
  • Opskit component, readiness, and inspection support
  • periodic execution of Opskit check and check-group contracts
  • generic execution of Opskit command handlers
  • optional Servekit-backed Workerkit command and lifecycle routes

This is the operational layer teams rebuild around serious worker components. Workerkit makes it the baseline instead of the afterthought.

What Workerkit is not

Workerkit is not a workflow engine, job queue, scheduler, or durable orchestration system.

It does not provide durable workflow state, queue persistence, distributed leasing, task assignment, or cross-service coordination. It does not replace brokers, databases, schedulers, or orchestrators. It does not own your domain model.

Workerkit runtime state is process-local. In Kubernetes, that means pod-local. /readyz, Opskit admin component views, Workerkit inspection, command dispatch, and lifecycle controls describe or affect only the runtime in the process that handles the request. Workerkit does not provide deployment-wide worker orchestration, distributed locking, leader election, replica coordination, or fleet-wide command broadcast.

It is also not an application framework. You still write normal Go workers, your own business loops, your own side effects, and your own command contracts. Workerkit is the runtime and control surface around those workers, not the application itself.

And Workerkit is not fundamentally tied to HTTP. The core runtime is transport-agnostic. Commands, readiness, lifecycle, failure handling, status, and Opskit inspection all exist as ordinary Go concepts first. If you want the Kit Series HTTP operations path, register the runtime in an Opskit registry and pass that registry to Servekit. If you want Workerkit-specific HTTP command or lifecycle routes, the optional opshttp package can mount those into Servekit.

Good fit / not a fit

Workerkit is a good fit when:

  • your service runs domain workers, long-lived loops, pollers, subscribers, schedulers, or command-driven workers that need explicit operational control
  • you want one runtime to own lifecycle, readiness, shutdown, status, retries, failure handling, concurrency limits, and observability
  • your workers own business logic, but you want a consistent production shell around them
  • some workers expose operational or domain commands like index/rebuild, cache/refresh, snapshot/prune, or queue/drain
  • you want a transport-agnostic worker runtime with the option to add an HTTP operations surface when HTTP is useful
  • you want production-oriented defaults without adopting a full framework

Workerkit is probably not a fit when:

  • you need a durable workflow engine, queue system, distributed lock manager, or fleet-wide orchestrator
  • you want built-in persistence for workflow state, retries across restarts, or task coordination across services
  • you want the runtime to understand and enforce your business domain instead of leaving that logic inside your workers
  • your service already has a mature worker runtime and Workerkit would mostly duplicate it
  • you only need a tiny helper around one short-lived goroutine, not a managed runtime

Installation

go get github.com/jaredjakacky/workerkit
import workerkit "github.com/jaredjakacky/workerkit"

Quick Start

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	workerkit "github.com/jaredjakacky/workerkit"
)

type printerWorker struct{}

func (printerWorker) Start(ctx context.Context) error {
	fmt.Println("worker started")
	return nil
}

func (printerWorker) Stop(ctx context.Context) error {
	fmt.Println("worker stopped")
	return nil
}

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

	runtime, err := workerkit.New(workerkit.Identity{Name: "quickstart"})
	if err != nil {
		log.Fatal(err)
	}

	err = runtime.Register(workerkit.WorkerSpec{
		Name:        "printer",
		Description: "prints worker-owned output",
		Worker:      printerWorker{},
	})
	if err != nil {
		log.Fatal(err)
	}

	if err := runtime.StartAll(ctx); err != nil {
		log.Fatal(err)
	}

	status := runtime.RuntimeStatus()
	fmt.Printf("runtime=%s state=%s ready=%t workers=%d\n",
		status.Name, status.State, status.Ready, status.Workers)

	for _, worker := range runtime.Workers() {
		fmt.Printf("worker=%s state=%s ready=%t\n",
			worker.QualifiedName, worker.Status.State, worker.Status.Ready)
	}

	shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	if err := runtime.Shutdown(shutdownCtx); err != nil {
		log.Fatal(err)
	}
}

That one runtime already gives you the operational pieces that usually get rebuilt around production workers:

  • explicit worker registration
  • managed startup and graceful shutdown
  • aggregate runtime status and per-worker snapshots
  • readiness aggregation
  • drain-before-stop behavior
  • production-oriented panic and failure handling
  • extension points for commands, retry, concurrency limits, and observers

In practice, you get a real worker runtime without hand-building lifecycle bookkeeping, readiness flags, shutdown ordering, status inspection, command admission, and failure reporting yourself.

The Core Model

Workerkit is deliberately built around one runtime boundary and ordinary Go workers.

Runtime

Runtime represents one service boundary. It owns worker registration, lifecycle control, readiness aggregation, status snapshots, worker-owned command dispatch, retry execution, concurrency limits, failure policy, and observer callbacks.

Most services should start with one runtime.

Worker

Worker is the lifecycle contract managed by the runtime:

type Worker interface {
    Start(context.Context) error
    Stop(context.Context) error
}

The worker owns domain behavior: loops, input sources, side effects, persistence, broker clients, caches, indexes, and business rules. Workerkit owns how the worker starts, stops, reports readiness, accepts commands, records failure, and exposes status.

Use NewLoopWorker when the worker is primarily a long-running loop that should stop when its context is canceled.

Worker Runtime

WorkerRuntimeFromContext gives managed worker code a worker-scoped handle for runtime signals:

  • SetReady
  • SetAcceptingWork
  • ReportFailure
  • Status

That lets a worker report warmup, pause command admission, or record asynchronous background failure without receiving full runtime authority.

Commands

Commands are worker-owned domain operations. They are not lifecycle controls, and they are not inherently HTTP.

Register commands with WithCommand or WithCommandSpec, discover them with Runtime.Commands, and execute them directly with Runtime.Dispatch. Workerkit routes and observes commands, but it does not interpret payloads. The worker owns the command contract.

Why This Works

Workerkit rests on three choices:

  1. It keeps worker code ordinary Go.
  2. It gives worker-oriented service components a coherent operational envelope.
  3. It keeps HTTP optional and outside the core runtime.

That is why the package can stay small while still feeling production-ready from the first constructor call.

Advanced capabilities

Workerkit has a short normal path, but it is not limited to startup and shutdown. Advanced hooks include:

  • readiness contribution policy per worker
  • runtime-wide and worker-local command concurrency limits
  • bounded retry with backoff, jitter, and retry predicates
  • lifecycle and command attempt timeouts
  • panic recovery or crash policy
  • isolated, unready, or failed aggregate runtime failure policy
  • worker-owned command discovery and dispatch
  • Opskit checker and check-group execution through managed workers
  • Opskit command-handler execution through CommandFromOpskit
  • structured slog observer support
  • OpenTelemetry observer support
  • Opskit-backed readiness and generic admin inspection through Servekit
  • opt-in HTTP command dispatch and privileged lifecycle controls
  • managed Servekit service composition with servekitservice.NewManaged

The advanced path is documented in docs/advanced.md, with policy details in docs/policy.md and Servekit composition in docs/composition.md.

Kit Series Operations Path

Workerkit, Opskit, and Servekit can be used independently. The preferred composed path is:

  • Opskit defines passive component metadata/read models plus explicit check and command execution hooks. Opskit does not schedule or invoke those hooks.
  • Workerkit runtime implements Opskit component, readiness, and inspection contracts.
  • Workerkit executes active Opskit work through NewCheckLoop, NewCheckGroupLoop, and CommandFromOpskit.
  • The application registers the runtime in an Opskit registry.
  • Servekit consumes that registry with servekit.WithOps(...).
  • Servekit presents /readyz and generic Opskit admin routes.

Servekit owns the HTTP service baseline: server construction, middleware, authentication, readiness endpoints, request policy, endpoint timeouts, response handling, and service lifecycle. Workerkit owns worker runtime semantics: lifecycle, readiness, status, command dispatch, admission, failure policy, and telemetry.

ops := opskit.NewRegistry()

runtime, err := workerkit.New(workerkit.Identity{Name: "workers"})
if err != nil {
    return err
}

ops.MustRegister(runtime, opskit.Required())

server := servekit.New(
    servekit.WithOps(ops, servekit.WithOpsAdmin()),
)

This gives Servekit a pod-local view of the Workerkit runtime. It reports only the runtime state inside this process; it is not a fleet-wide worker registry or distributed control plane.

Active Opskit contracts enter the same Workerkit execution model as native work:

if err := runtime.Register(workerkit.WorkerSpec{
    Name:   "dependency_checks",
    Worker: workerkit.NewCheckLoop(dependency),
}); err != nil {
    return err
}

if err := runtime.Register(commandWorker,
    workerkit.WithCommandSpec(
        workerkit.CommandFromOpskit(commandDescriptor, component),
    ),
); err != nil {
    return err
}

NewCheckLoop and NewCheckGroupLoop return normal Workerkit workers. Workerkit owns their interval, jitter, cooperative timeout, cancellation, panic recovery, and readiness/failure integration. A checker cannot be forcibly interrupted, but a result returned after its deadline is not applied to readiness. CommandFromOpskit returns a normal command spec, so dispatch keeps Workerkit admission, timeout, retry, concurrency, panic, observation, and lifecycle behavior.

opshttp is an optional Workerkit-specific HTTP control surface. It is useful when operators specifically need Workerkit command dispatch or privileged lifecycle controls; it is not the primary read-only/readiness composition path. Mutating routes are disabled unless explicitly enabled.

opshttp.Mount adds Workerkit-shaped read-only inspection by default. Command dispatch and lifecycle controls require explicit options. Even read-only routes expose operational state, so protect the entire mounted surface; use stricter Servekit endpoint policy for mutating routes. HTTP controls are pod-local, and stop routes do not wait for commands already in flight.

The standalone opshttp.ReadinessCheck(runtime) adapter remains available only for standalone Servekit services that do not use an Opskit registry.

See Composition with Opskit and Servekit and the optional opshttp examples for route inventory, endpoint policy, and error mappings.

Documentation

Examples

Runnable programs live in examples/, which includes a guided tour of the example set.

Recommended reading order:

  1. examples/basic
  2. examples/loop-worker
  3. examples/readiness
  4. examples/commands
  5. examples/opskit-checks
  6. examples/opskit-command
  7. examples/retry-policy
  8. examples/concurrency-limits
  9. examples/failure-policy
  10. examples/multi-worker
  11. examples/testing
  12. examples/observability-slog
  13. examples/observability-otel
  14. examples/managed-service
  15. examples/production-composition

Optional Workerkit-specific HTTP controls are demonstrated separately in examples/opshttp-basic, examples/opshttp-commands, and examples/admin-lifecycle.

API Reference

The canonical symbol-level API documentation should live in Go doc comments so it stays accurate in editors and Go tooling. The repository-level companion is docs/api.md, which groups the exported surface into a human-oriented map.

Development

This repository uses make for local verification:

make verify
make build-examples
make test-race
make govulncheck

make verify checks formatting, runs go vet, runs tests, builds the runnable examples, and verifies that go.mod and go.sum are tidy. make build-examples is available when you only want to compile the runnable examples.

CI runs verification and race tests on the supported Go versions. Release tags are gated by those jobs plus govulncheck before publishing.

Issues and Scope

Workerkit is maintained as a small bootstrap library for worker lifecycle, readiness, command dispatch, retry policy, observability, and optional operations HTTP integration.

Bug reports, documentation fixes, and compatibility issues are welcome. Large feature additions are evaluated conservatively because Workerkit is intentionally not a workflow engine, job queue, scheduler, distributed orchestrator, or application framework.

Maintenance

Workerkit is a small open source library maintained on a best-effort basis.

The active development line lives on main, and that is the only line actively maintained unless explicitly noted otherwise. The minimum supported Go version is declared in go.mod, and the Go versions currently verified in CI are listed in .github/workflows/ci.yaml.

Compatibility-impacting changes should be called out explicitly in release notes or release descriptions. Long-lived maintenance branches and backports are not planned unless explicitly noted.

License

MIT

About

Production-oriented Go runtime for domain workers: lifecycle, readiness, commands, retries, limits, failure policy, status, and observability.

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Contributors