dwarf

package module
v0.10.5 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: Apache-2.0 Imports: 1 Imported by: 0

README

License Apache 2 Go Reference Test Discord

Dwarf is a standalone, embeddable workflow-orchestration engine for Go.

You describe a workflow as a graph of tasks; dwarf runs it — dispatching one task at a time per step, persisting state between steps in a SQL database, and handling the hard parts of durable orchestration: parallel fan-out/fan-in, conditional routing, retries with backoff, timed sleeps, subgraphs, and human-in-the-loop pauses.

Dwarf has no built-in transport. It doesn't know how your tasks are reached (a local function call, an RPC, a message bus) or where your graphs live. You wire it to your world through a few small dependency interfaces, and it handles scheduling, state, durability, and recovery.

g := workflow.NewGraph("Greet")
g.SetEndpoint("Hello", "http://example/hello") // node "Hello" dispatches to this endpoint URL
g.AddTransition("Hello", workflow.END)

proxy := engine.NewTestProxy()
proxy.HandleGraph("http://example/greet", g)
proxy.HandleTask("http://example/hello", func(ctx context.Context, f *workflow.Flow) error {
    f.SetString("greeting", "hello "+f.GetString("name"))
    return nil
})

eng := dwarf.NewEngineUnderTest(t.Name()) // SQLite in-memory, auto-dropped
defer eng.Shutdown(ctx)
eng.SetHost(proxy) // TestProxy implements the Host interface
eng.Startup(ctx)

_, out, _ := eng.Run(ctx, "http://example/greet", map[string]any{"name": "ada"}, nil) // Run returns (flowKey, outcome, err)
fmt.Println(out.State.GetString("greeting")) // hello ada

Why dwarf

  • Durable by construction. Every step is checkpointed to SQL. A crashed worker's in-flight step is recovered by lease expiry; a flow can be inspected, resumed, forked, or continued days later.
  • Parallelism that merges cleanly. Static and dynamic (forEach) fan-out run branches concurrently; fan-in merges their state with per-field reducers (append, add, union, merge, …).
  • Human-in-the-loop. A task can Interrupt to park the flow for external input and Resume later — approvals, manual review, async callbacks.
  • Fair and prioritized. Two-level scheduling: strict priority bands across the cluster, weighted fairness within a band so one tenant can't starve another.
  • Scales horizontally. Run many replicas against sharded databases; they coordinate entirely through the databases they already share, with no message bus to run between them.
  • OTEL-native observability. Structured logs (slog), 33 dwarf_* instruments, and distributed tracing, all through standard providers you inject.
  • Four SQL dialects. PostgreSQL, MySQL/MariaDB, SQL Server, and SQLite (testing / single-instance).
  • Free, and cheap at scale. Apache 2.0 — no license tier, no vendor, no per-operation pricing. A single 4-vCPU engine host sustains ~9,400 steps/sbillions of steps a month — and the whole deployment is your own replicas plus a few ordinary SQL instances. No broker, no control plane, nothing else to run or pay for, so the bill is a handful of cloud instances rather than a metered service.

Dwarf depends only on sequel (SQL), plus the OpenTelemetry API.

Install

go get github.com/microbus-io/dwarf

Requires Go 1.26+.

Packages

Import Role Who imports it
github.com/microbus-io/dwarf Thin convenience: NewEngine() The host process
github.com/microbus-io/dwarf/engine The engine: lifecycle, operations, config, the Host interface The host process only
github.com/microbus-io/dwarf/workflow Pure types: Graph, Flow, FlowOptions, reducers, error helpers Any code that defines tasks or graphs

The split matters: dwarf/workflow is a lightweight type package. Code that defines tasks and graphs imports only dwarf/workflow, never the engine, so the engine's heavy dependencies (SQL drivers, the scheduler) stay out of those builds.

The host model

The engine reaches the outside world through a single Host interface, registered once with SetHost. Two methods, both required — and that is the whole contract, single-replica or not: the engine sends nothing between replicas, so there is no transport for you to wire.

type Host interface {
    // Required. Fetch a workflow graph by name (called at Create; the graph is then frozen on the flow,
    // and on subgraph spawn).
    LoadGraph(ctx context.Context, workflowURL string) (*workflow.Graph, error)

    // Required. Execute one task. The Flow carrier arrives with its input state populated; write outputs.
    ExecuteTask(ctx context.Context, taskURL string, flow *workflow.Flow) error
}

A standalone host backs LoadGraph with an in-memory registry / file / database, and ExecuteTask with a local function table or an RPC client. A bus-based host (for example a microservice mesh) bridges them to its transport. The engine never learns how tasks are reached.

Production wiring

Each Set* returns an error, so misconfiguration fails loudly at wiring time:

eng := dwarf.NewEngine()
eng.SetShard(engine.ShardSpec{Index: 1, DSN: "postgres://user:pass@db-a.internal:5432/dwarf", VirtualCPUs: 8})
eng.SetShard(engine.ShardSpec{Index: 2, DSN: "postgres://user:pass@db-b.internal:5432/dwarf", VirtualCPUs: 8})
eng.SetWorkers(64)
eng.SetHost(host)
eng.SetLogger(slog.Default())
eng.SetMeterProvider(otel.GetMeterProvider())
eng.SetTracerProvider(otel.GetTracerProvider())

err := eng.Startup(ctx)
if err != nil {
    log.Fatal(err)
}
defer eng.Shutdown(ctx)

flowKey, err := eng.Create(ctx, "checkout", initialState, &workflow.FlowOptions{
    Priority:    10,
    FairnessKey: tenantID,
    Baggage:     actorClaims,
})
outcome, err := eng.Await(ctx, flowKey) // Create returns a running flow; there is no separate start call

The live Set* methods (SetMaxOpenConns, SetTimeBudget, SetDefaultPriority, SetRefillInterval) may be called after Startup for hot reconfiguration; the rest are construction-time only.

Database support

Engine Use Notes
PostgreSQL 13+ Recommended for production MVCC, no gap locks; fan-out runs deadlock-free at any concurrency
SQL Server Production Enable READ_COMMITTED_SNAPSHOT for non-blocking reads
MySQL / MariaDB Production, expect tuning Prefer READ-COMMITTED isolation to drop gap locks
SQLite Testing & single-instance dev only Used automatically by NewEngineUnderTest; do not run in production

See docs/deployment.md for tuning, sharding, and connection-pool guidance.

Documentation

Full guides live in docs/, split by who they are written for — start at the documentation index for the recommended reading order.

Building workflows:

Running it in production:

  • Operating dwarf — what the engine repairs itself, what needs you, what to watch
  • Production checklist — the pre-flight list before you go live
  • Runbook — symptom-indexed incident response
  • Deployment — database choice, sharding, config, multi-replica
  • Observability — logs, metrics, tracing
  • Upgrading — redeploying your app, upgrading dwarf itself, graph evolution
  • Resharding — adding, retiring and moving shards
  • Backup and restore — what to back up, and what a restore does to flows
  • Data handling — what is stored, what is searchable, retention
  • Benchmarks — throughput & latency per dialect and shard count, and how to run them
  • Cloud benchmarks — the sizing formula, measured against managed PostgreSQL

API reference: pkg.go.dev/github.com/microbus-io/dwarf.

License

Apache License 2.0. See LICENSE.

Documentation

Overview

Package dwarf is a standalone, embeddable workflow-orchestration engine.

Dwarf executes workflow graphs: it dispatches tasks, manages state between steps, and handles fan-out/fan-in, retries, sleeps, conditional routing, subgraphs, and human-in-the-loop interrupts. It is library code with no built-in transport: a host application wires it to its own task execution, graph storage, and observability through a small set of injected dependency interfaces (see the engine package). It depends only on a SQL database (via sequel).

This root package is a thin convenience: NewEngine returns an *engine.Engine. The real API lives in two sub-packages:

  • github.com/microbus-io/dwarf/engine - the engine: Startup/Shutdown, the Create/Run/Await operations, configuration, and the dependency interfaces. Import this only in the process that hosts the engine.
  • github.com/microbus-io/dwarf/workflow - the pure types: Graph, Flow, FlowOptions, FlowOutcome, and reducers. Import this in code that defines tasks and graphs; it has no heavy dependencies.

A 30-second taste, using the in-process test harness:

proxy := engine.NewTestProxy()
g := workflow.NewGraph("Greet")
g.SetEndpoint("Hello", "http://example/hello") // node "Hello" dispatches to this endpoint URL
g.AddTransition("Hello", workflow.END)
proxy.HandleGraph("http://example/greet", g)
proxy.HandleTask("http://example/hello", func(ctx context.Context, f *workflow.Flow) error {
	f.SetString("greeting", "hello "+f.GetString("name"))
	return nil
})

eng := dwarf.NewEngineUnderTest(t.Name()) // SQLite in-memory, auto-dropped at test end
defer eng.Shutdown(ctx)
eng.SetHost(proxy)
eng.Startup(ctx)

_, out, _ := eng.Run(ctx, "http://example/greet", map[string]any{"name": "ada"}, nil)
fmt.Println(out.State["greeting"]) // hello ada

See the docs/ directory in the repository for guides on graphs, tasks, scheduling, observability, and deployment.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewEngine

func NewEngine() *engine.Engine

NewEngine creates a new workflow engine with default settings.

func NewEngineUnderTest added in v0.10.0

func NewEngineUnderTest(testName string) *engine.Engine

NewEngineUnderTest creates a workflow engine wired for testing: isolated, auto-dropped databases keyed by the given test name. The caller owns teardown (defer eng.Shutdown(ctx)). See engine.NewEngineUnderTest.

Types

This section is empty.

Directories

Path Synopsis
The bench command drives the dwarf engine through its public API against a real database and measures step/flow/MB throughput and latency at a sweep of closed-loop concurrencies.
The bench command drives the dwarf engine through its public API against a real database and measures step/flow/MB throughput and latency at a sweep of closed-loop concurrencies.
Package engine is the dwarf workflow-orchestration engine.
Package engine is the dwarf workflow-orchestration engine.
internal
candidates
Package candidates holds the per-replica bounded set of step candidates produced by the engine's refillers.
Package candidates holds the per-replica bounded set of step candidates produced by the engine's refillers.
claimstracker
Package claimstracker records which steps a replica has a claim CAS in flight on, so a sibling worker that pops the same candidate can skip it instead of paying a round trip to lose the CAS.
Package claimstracker records which steps a replica has a claim CAS in flight on, so a sibling worker that pops the same candidate can skip it instead of paying a round trip to lose the CAS.
database
Package database owns the engine's sharded SQL connections: it opens and migrates every shard, routes by 1-based shard index, fans an operation out over all shards, and closes them.
Package database owns the engine's sharded SQL connections: it opens and migrates every shard, routes by 1-based shard index, fans an operation out over all shards, and closes them.
enginetest
Package enginetest holds the engine test helpers that are shared across package boundaries - by the white-box tests that stay in package engine AND by the black-box tests in the fixtures package.
Package enginetest holds the engine test helpers that are shared across package boundaries - by the white-box tests that stay in package engine AND by the black-box tests in the fixtures package.
faninmap
Package faninmap derives the fan-out -> fan-in convergence map of a workflow graph: which node a given fan-out source's branches converge on.
Package faninmap derives the fan-out -> fan-in convergence map of a workflow graph: which node a given fan-out source's branches converge on.
keys
Package keys encodes and decodes the engine's composite flow and step keys.
Package keys encodes and decodes the engine's composite flow and step keys.
latch
Package latch parks callers on a key until something reports that key done.
Package latch parks callers on a key until something reports that key done.
lru
Package lru provides a small thread-safe LRU cache with a per-entry TTL.
Package lru provides a small thread-safe LRU cache with a per-entry TTL.
peers
Package peers reports the fleet sharing one shard's database, from that shard's replica registry.
Package peers reports the fleet sharing one shard's database, from that shard's replica registry.
pipeline
Package pipeline runs one shard's supply cycle: it looks at what is due, asks the planner what it may serve, fetches that, and pushes it to the candidate cache the workers drain.
Package pipeline runs one shard's supply cycle: it looks at what is due, asks the planner what it may serve, fetches that, and pushes it to the candidate cache the workers drain.
piston
Package piston supplies step candidates from one shard.
Package piston supplies step candidates from one shard.
planner
Package planner decides which work a shard may dispatch, given what every shard last reported.
Package planner decides which work a shard may dispatch, given what every shard last reported.
staterefs
Package staterefs stores a large carried state field once instead of copying it into every step that merely passes it along.
Package staterefs stores a large carried state field once instead of copying it into every step that merely passes it along.
turnstile
Package turnstile bounds how many callers may hold a resource at once AND decides which waiter is admitted next: the lowest priority band, and within a band the job that started earliest.
Package turnstile bounds how many callers may hold a resource at once AND decides which waiter is admitted next: the lowest priority band, and within a band the job that started earliest.
workers
Package workers holds the demand side of dispatch: a CREW of goroutines that pop step candidates from the cache and hand each to one callback.
Package workers holds the demand side of dispatch: a CREW of goroutines that pop step candidates from the cache and hand each to one callback.
Package workflow holds the pure data types of the dwarf workflow engine: the building blocks a host uses to define workflows and the carriers it reads and writes when running tasks.
Package workflow holds the pure data types of the dwarf workflow engine: the building blocks a host uses to define workflows and the carriers it reads and writes when running tasks.

Jump to

Keyboard shortcuts

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