durablepg

package module
v0.0.0-...-9433fcb Latest Latest
Warning

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

Go to latest
Published: Mar 26, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

durablepg

Go Reference Go Report Card

durablepg is a durable workflow engine for Go backed by PostgreSQL. Define workflows in code that survive process crashes, restarts, and deployments—state is automatically checkpointed at each step.

Table of Contents

Features

  • Code-First Workflows: Define workflows as Go code using a fluent builder API.
  • Automatic Durability: Every step result is persisted; workflows resume exactly where they left off.
  • PostgreSQL-Native: Uses PostgreSQL for persistence, locking, and pub/sub (no external message queue required).
  • Event-Driven: Workflows can wait for external events with WaitEvent.
  • Timed Operations: Built-in support for Sleep with precise scheduling.
  • Retry with Backoff: Automatic exponential backoff on step failures.
  • Idempotency Keys: Built-in idempotency key support to prevent duplicate workflow runs.
  • Horizontal Scaling: Multiple workers can run concurrently with FOR UPDATE SKIP LOCKED coordination.
  • LISTEN/NOTIFY Wakeups: Hint-only notifications with polling fallback for reliability.
  • UUIDv7 Support: Automatic PostgreSQL 18 uuidv7() run IDs when available.
  • Partitioned Event Log: Event tables are automatically partitioned for efficient cleanup.

Installation

go get github.com/akashsharma95/durablepg

Quick Start

1. Initialize the Engine
package main

import (
    "context"
    "os"
    "time"

    durablepg "github.com/akashsharma95/durablepg"
    "github.com/jackc/pgx/v5/pgxpool"
)

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

    // Connect to PostgreSQL
    pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
    if err != nil {
        panic(err)
    }
    defer pool.Close()

    // Create the engine
    engine, err := durablepg.New(durablepg.Config{DB: pool})
    if err != nil {
        panic(err)
    }

    // Initialize schema and default partitions
    if err := engine.Init(ctx); err != nil {
        panic(err)
    }

    // Register workflows
    engine.RegisterWorkflow("signup", func(wf *durablepg.Builder) {
        wf.Step("create_user", func(ctx context.Context, sc *durablepg.StepContext) (any, error) {
            var in struct {
                Email string `json:"email"`
            }
            if err := sc.DecodeInput(&in); err != nil {
                return nil, err
            }
            userID := createUser(ctx, in.Email)
            return map[string]any{"user_id": userID}, nil
        })

        wf.WaitEvent("signup.confirmed", 30*time.Minute)

        wf.Step("activate", func(ctx context.Context, sc *durablepg.StepContext) (any, error) {
            var createResult struct {
                UserID string `json:"user_id"`
            }
            if err := sc.StepResult("create_user", &createResult); err != nil {
                return nil, err
            }
            activateUser(ctx, createResult.UserID)
            return map[string]any{"active": true}, nil
        })
    })

    // Start the worker (runs until context is cancelled)
    go engine.StartWorker(ctx)

    // Enqueue a workflow run
    runID, err := engine.Run(ctx, "signup", map[string]any{
        "email": "user@example.com",
    }, durablepg.WithDeduplicationKey("signup:user@example.com"))
    if err != nil {
        panic(err)
    }
    println("Started workflow:", runID)

    // Block forever (or handle shutdown signal)
    select {}
}
2. Define a Workflow
engine.RegisterWorkflow("signup", func(b *durablepg.Builder) {
    // Step 1: Create user account
    b.Step("create_user", func(ctx context.Context, sc *durablepg.StepContext) (any, error) {
        var in struct {
            Email string `json:"email"`
        }
        if err := sc.DecodeInput(&in); err != nil {
            return nil, err
        }
        // Idempotent side effect: create user in database
        userID := createUser(ctx, in.Email)
        return map[string]any{"user_id": userID}, nil
    })

    // Step 2: Wait for email confirmation (external event)
    b.WaitEvent("signup.confirmed", 30*time.Minute)

    // Step 3: Activate the account
    b.Step("activate", func(ctx context.Context, sc *durablepg.StepContext) (any, error) {
        // Access results from previous steps
        var createResult struct {
            UserID string `json:"user_id"`
        }
        if err := sc.StepResult("create_user", &createResult); err != nil {
            return nil, err
        }
        activateUser(ctx, createResult.UserID)
        return map[string]any{"active": true}, nil
    })
})
3. Emit Events
// When user confirms email (e.g., from HTTP handler)
err := engine.EmitEvent(ctx, "signup.confirmed:user@example.com", map[string]any{
    "confirmed_at": time.Now(),
})

Architecture

High-Level Overview
┌─────────────────────────────────────────────────────────────────────────────┐
│                              Application Layer                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌──────────────┐     ┌──────────────┐     ┌──────────────────────────┐    │
│   │   Workflow   │     │   Workflow   │     │       Engine             │    │
│   │  Definition  │────▶│   Builder    │────▶│  (Coordinator)           │    │
│   │  (User Code) │     │              │     │                          │    │
│   └──────────────┘     └──────────────┘     │  - Register workflows    │    │
│                                              │  - Enqueue runs          │   │
│                                              │  - Emit events           │   │
│                                              │  - Manage workers        │   │
│                                              └───────────┬──────────────┘   │
│                                                          │                  │
│                                              ┌───────────▼──────────────┐   │
│                                              │        Worker            │   │
│                                              │   (Background Loop)      │   │
│                                              │                          │   │
│                                              │  - Poll for ready runs   │   │
│                                              │  - Claim via leases      │   │
│                                              │  - Execute steps         │   │
│                                              │  - Checkpoint results    │   │
│                                              └───────────┬──────────────┘   │
│                                                          │                  │
└──────────────────────────────────────────────────────────┼──────────────────┘
                                                           │
┌──────────────────────────────────────────────────────────▼───────────────────┐
│                            PostgreSQL Layer                                  │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   ┌─────────────────┐  ┌──────────────────┐  ┌────────────────────────────┐  │
│   │  workflow_runs  │  │ step_checkpoints │  │       event_log            │  │
│   ├─────────────────┤  ├──────────────────┤  │     (Partitioned)          │  │
│   │ id              │  │ run_id           │  ├────────────────────────────┤  │
│   │ workflow_name   │  │ step_key         │  │ id                         │  │
│   │ state           │  │ result_json      │  │ event_key                  │  │
│   │ step_index      │  │ created_at       │  │ payload_json               │  │
│   │ input_json      │  └──────────────────┘  │ created_at                 │  │
│   │ next_run_at     │                        └────────────────────────────┘  │
│   │ lease_owner     │                                                        │
│   │ lease_until     │          ┌──────────────────────────────────┐          │
│   │ retries         │          │      LISTEN/NOTIFY Channel       │          │
│   │ ...             │          │   (durablepg_workflow_ready)     │          │
│   └─────────────────┘          └──────────────────────────────────┘          │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
Component Details
Engine (engine.go)

The Engine is the central coordinator responsible for:

Responsibility Description
Workflow Registration Maintains a registry mapping workflow names to their compiled operation lists
Run Enqueueing Inserts new workflow runs into workflow_runs table and triggers NOTIFY
Event Emission Writes events to event_log and wakes waiting workflows
Worker Management Initializes and coordinates background workers
Schema Management Provides methods to create/migrate database schema
type Engine struct {
    db         *pgxpool.Pool
    workflows  map[string]*compiledWorkflow  // name -> operations
    config     Config
    workerID   string                         // unique per instance
}
Worker (worker.go)

The Worker is a background goroutine that drives workflow execution:

Responsibility Description
Polling Periodically queries for ready workflows or those with expired leases
Claiming Uses FOR UPDATE SKIP LOCKED to claim workflows atomically
Leasing Sets lease_owner and lease_until to prevent other workers from claiming
Step Execution Runs step functions, persisting results to step_checkpoints
State Transitions Updates workflow state based on execution results
LISTEN Handling Subscribes to PostgreSQL notifications for immediate wakeup

Worker Loop:

┌─────────────────────────────────────────────────────────────────┐
│                        Worker Loop                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────┐    ┌──────────┐    ┌───────────┐    ┌──────────┐  │
│   │  LISTEN │───▶│  Claim   │───▶│  Execute  │───▶│  Update  │  │
│   │   or    │    │   Runs   │    │   Steps   │    │  State   │  │
│   │  Poll   │    │          │    │           │    │          │  │
│   └─────────┘    └──────────┘    └───────────┘    └──────────┘  │
│        │                                                  │     │
│        └──────────────────────────────────────────────────┘     │
│                         (repeat)                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
Builder (builder.go)

The Builder provides a fluent API for defining workflow operations:

type Builder struct {
    operations []operation
}

type operation struct {
    kind       opKind           // opStep, opSleep, opWaitEvent
    key        string           // unique identifier
    stepFn     StepFunc         // for opStep
    duration   time.Duration    // for opSleep
    timeout    time.Duration    // for opWaitEvent
}

Operation Types:

Operation Description
Step(key, fn) Execute a function and persist its result
Sleep(duration) Pause execution for a specified duration
WaitEvent(key, timeout) Wait for an external event or timeout
StepContext (types.go)

StepContext is passed to every step function, providing access to:

type StepContext struct {
    RunID    WorkflowID
    Workflow string
    Input    json.RawMessage
}

// Methods:
func (sc *StepContext) DecodeInput(dest any) error      // Unmarshal workflow input
func (sc *StepContext) StepResult(key string, dest any) error
func (sc *StepContext) WorkflowID() WorkflowID
Data Flow
┌──────────────────────────────────────────────────────────────────────────────┐
│                            Workflow Execution Flow                           │
└──────────────────────────────────────────────────────────────────────────────┘

1. ENQUEUE
   ─────────────────────────────────────────────────────────────────────────────
   Application                         Engine                        PostgreSQL
       │                                  │                               │
       │  Enqueue("signup", input)        │                               │
       │─────────────────────────────────▶│                               │
       │                                  │  INSERT workflow_runs         │
       │                                  │  (state='ready')              │
       │                                  │──────────────────────────────▶│
       │                                  │                               │
       │                                  │  NOTIFY durablepg_ready       │
       │                                  │──────────────────────────────▶│
       │                                  │                               │
       │  ◀───────── runID ───────────────│                               │


2. CLAIM & EXECUTE
   ─────────────────────────────────────────────────────────────────────────────
   Worker                                                            PostgreSQL
       │                                                                  │
       │  SELECT ... FOR UPDATE SKIP LOCKED                               │
       │  WHERE state='ready' AND next_run_at <= now()                    │
       │─────────────────────────────────────────────────────────────────▶│
       │                                                                  │
       │  UPDATE: state='leased', lease_owner=workerID, lease_until=...   │
       │─────────────────────────────────────────────────────────────────▶│
       │                                                                  │
       │                                                                  │
       │  ┌────────────────────────────────────────────────────────────┐  │
       │  │                   For each operation:                      │  │
       │  │                                                            │  │
       │  │   [Step]       Check step_checkpoints for cached result    │  │
       │  │                If miss: execute fn, INSERT checkpoint      │  │
       │  │                Advance step_index                          │  │
       │  │                                                            │  │
       │  │   [Sleep]      UPDATE next_run_at = now() + duration       │  │
       │  │                SET state = 'ready', release lease          │  │
       │  │                (worker exits, resumes later)               │  │
       │  │                                                            │  │
       │  │   [WaitEvent]  SET state = 'waiting_event'                 │  │
       │  │                SET wait_key = event_key                    │  │
       │  │                SET wait_deadline = now() + timeout         │  │
       │  │                (worker exits, waits for event)             │  │
       │  └────────────────────────────────────────────────────────────┘  │
       │                                                                  │
       │  All operations complete: UPDATE state='completed'               │
       │─────────────────────────────────────────────────────────────────▶│


3. EVENT EMISSION
   ─────────────────────────────────────────────────────────────────────────────
   Application                         Engine                        PostgreSQL
       │                                  │                               │
       │  EmitEvent("signup.confirmed",   │                               │
       │            payload)              │                               │
       │─────────────────────────────────▶│                               │
       │                                  │  INSERT event_log             │
       │                                  │──────────────────────────────▶│
       │                                  │                               │
       │                                  │  UPDATE workflow_runs         │
       │                                  │  SET state='ready'            │
       │                                  │  WHERE wait_key LIKE 'signup.%'
       │                                  │──────────────────────────────▶│
       │                                  │                               │
       │                                  │  NOTIFY durablepg_ready       │
       │                                  │──────────────────────────────▶│
Concurrency Model
┌─────────────────────────────────────────────────────────────────────────────┐
│                        Multi-Worker Concurrency                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   ┌──────────────┐    ┌──────────────┐    ┌──────────────┐                  │
│   │   Worker 1   │    │   Worker 2   │    │   Worker 3   │                  │
│   │  (Server A)  │    │  (Server B)  │    │  (Server B)  │                  │
│   └──────┬───────┘    └──────┬───────┘    └──────┬───────┘                  │
│          │                   │                   │                          │
│          │    FOR UPDATE SKIP LOCKED             │                          │
│          ▼                   ▼                   ▼                          │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                                                                     │   │
│   │   workflow_runs table                                               │   │
│   │   ┌─────────────────────────────────────────────────────────────┐   │   │
│   │   │ run_1 │ leased │ worker_1 │ ████████████                    │   │   │
│   │   │ run_2 │ leased │ worker_2 │ ████████                        │   │   │
│   │   │ run_3 │ leased │ worker_3 │ ██████████████                  │   │   │
│   │   │ run_4 │ ready  │   NULL   │ (available for claiming)        │   │   │
│   │   │ run_5 │ ready  │   NULL   │ (available for claiming)        │   │   │
│   │   └─────────────────────────────────────────────────────────────┘   │   │
│   │                                                                     │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│   Key guarantees:                                                           │
│   - Each run is processed by exactly one worker at a time                   │
│   - SKIP LOCKED prevents contention and blocking                            │
│   - Expired leases are reclaimed by any worker                              │
│   - No external coordinator required                                        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
Retry & Backoff Strategy
┌─────────────────────────────────────────────────────────────────────────────┐
│                         Exponential Backoff                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   Step fails ──▶ Calculate backoff ──▶ Schedule retry ──▶ Release lease     │
│                                                                             │
│   Backoff = min(BaseBackoff * 2^retries, MaxBackoff)                        │
│                                                                             │
│   Example (BaseBackoff=1s, MaxBackoff=1h):                                  │
│                                                                             │
│   Retry 0:  1s                                                              │
│   Retry 1:  2s                                                              │
│   Retry 2:  4s                                                              │
│   Retry 3:  8s                                                              │
│   Retry 4:  16s                                                             │
│   Retry 5:  32s                                                             │
│   Retry 6:  64s                                                             │
│   Retry 7:  128s                                                            │
│   Retry 8:  256s                                                            │
│   Retry 9:  512s                                                            │
│   Retry 10: 1024s → capped at 1h                                            │
│   ...                                                                       │
│   Retry N:  → state='failed' (after MaxRetries)                             │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

API Reference

Engine
Method Description
New(cfg Config) (*Engine, error) Create a new engine instance.
Init(ctx) error Apply schema and create default future partitions.
ApplySchema(ctx) error Create/migrate database tables.
EnsurePartitions(ctx, monthsAhead) error Ask PostgreSQL to create future event-log partitions.
EventLogMonthlyPartitionsSQL(start, months) (string, error) Generate DDL for event log partitions.
RegisterWorkflow(name, build) Register a workflow using a builder function.
Register(wf Workflow) Register a workflow definition.
StartWorker(ctx) Start the background worker (blocks until ctx cancelled).
Run(ctx, name, input, ...opts) (string, error) Preferred alias for starting a workflow run.
Enqueue(ctx, name, input, ...opts) (string, error) Start a new workflow run.
EmitEvent(ctx, key, payload) error Emit an event that may wake waiting workflows.
Workflow Interface
type Workflow interface {
    Name() string           // Unique workflow identifier
    Build(b *Builder)       // Define workflow operations
}
Builder Methods
Method Description
Step(key string, fn StepFunc) Add a durable step that persists its result.
Sleep(duration time.Duration) Pause workflow execution for a duration.
WaitEvent(keyPrefix string, timeout time.Duration) Wait for an external event or timeout.
StepFunc Type
type StepFunc func(ctx context.Context, sc *StepContext) (any, error)
StepContext Methods
Method Description
DecodeInput(dest any) error Unmarshal workflow input into dest.
StepResult(key string, dest any) error Get result from a previous step.
WorkflowID() WorkflowID Get the current workflow run ID.
Enqueue Options
Option Description
WithWorkflowID(id WorkflowID) Provide a deterministic workflow run ID.
WithDeduplicationKey(key string) Preferred alias for deduplicating workflow runs.
WithIdempotencyKey(key string) Prevent duplicate runs with same key.
WithScheduledAt(t time.Time) Schedule workflow to start at a future time.

Configuration

type Config struct {
    // DB is the PostgreSQL connection pool (required)
    DB *pgxpool.Pool

    // Schema is the PostgreSQL schema name
    // Default: "durable"
    Schema string

    // Queue is the default queue name for new runs
    // Default: "default"
    Queue string

    // MaxConcurrency caps in-flight workflow executions per engine
    // Default: runtime.GOMAXPROCS(0)
    MaxConcurrency int

    // PollInterval is how often workers check for ready workflows
    // Default: 250ms
    PollInterval time.Duration

    // LeaseTTL is how long a worker holds a workflow lease
    // Default: 30s
    LeaseTTL time.Duration

    // HeartbeatInterval controls how often leased runs refresh ownership
    // Default: 10s
    HeartbeatInterval time.Duration
}

Database Schema

Tables
workflow_runs

Primary table storing workflow instances and their execution state.

Column Type Description
id UUID Primary key (UUIDv7 when available).
workflow_name TEXT Registered workflow name.
state TEXT Current state (see below).
step_index INT Current operation index.
input_json JSONB Workflow input data.
idempotency_key TEXT Optional deduplication key.
next_run_at TIMESTAMPTZ When to resume execution.
lease_owner TEXT Worker ID holding the lease.
lease_until TIMESTAMPTZ Lease expiration time.
wait_key TEXT Event key being waited on.
wait_deadline TIMESTAMPTZ Event wait timeout.
retries INT Number of retry attempts.
created_at TIMESTAMPTZ Creation timestamp.
updated_at TIMESTAMPTZ Last update timestamp.
step_checkpoints

Stores persisted results of completed steps.

Column Type Description
run_id UUID Foreign key to workflow_runs.
step_key TEXT Step identifier.
result_json JSONB Serialized step result.
created_at TIMESTAMPTZ Checkpoint timestamp.
event_log (Partitioned)

Stores emitted events for workflows waiting on external signals.

Column Type Description
id UUID Primary key.
event_key TEXT Event identifier.
payload_json JSONB Event payload.
created_at TIMESTAMPTZ Emission timestamp.

Partitioning: Monthly range partitions on created_at for efficient cleanup.

Workflow States
State Description
ready Workflow is queued and ready to execute.
leased A worker has claimed the workflow and is executing.
waiting_event Workflow is paused, waiting for an external event.
completed Workflow finished all operations successfully.
failed Workflow exceeded maximum retry attempts.
State Transitions
                    ┌──────────────────────────────────────────┐
                    │                                          │
                    ▼                                          │
              ┌──────────┐                                     │
  Enqueue ───▶│  ready   │◀────────────────────────────────────┤
              └────┬─────┘                                     │
                   │                                           │
                   │ Worker claims                             │
                   ▼                                           │
              ┌──────────┐                                     │
              │  leased  │─────────────────────────────────────┤
              └────┬─────┘                                     │
                   │                                           │
        ┌──────────┼──────────┬──────────────┐                │
        │          │          │              │                │
        │ step ok  │ Sleep    │ WaitEvent    │ step fails     │
        │          │          │              │ (retry)        │
        ▼          ▼          ▼              │                │
   ┌──────────┐   next    ┌─────────────┐    │                │
   │completed │   run_at  │waiting_event│    │                │
   └──────────┘    │      └──────┬──────┘    │                │
                   │             │           │                │
                   │      Event  │           │                │
                   │      arrives│           │                │
                   │             │           │                │
                   └─────────────┴───────────┘
                             │
                             │ MaxRetries exceeded
                             ▼
                       ┌──────────┐
                       │  failed  │
                       └──────────┘

Workflow Lifecycle

1. Registration

When you call engine.RegisterWorkflow(name, build):

  • The Builder is invoked to collect all operations.
  • Operations are compiled into an ordered list.
  • The workflow is stored in the engine's registry.
2. Enqueueing

When you call engine.Run(ctx, "name", input):

  • A new row is inserted into workflow_runs with state='ready'.
  • NOTIFY is sent to wake any listening workers immediately.
  • The run ID is returned to the caller.
3. Claiming

Workers poll or listen for ready workflows:

  • Query: SELECT ... WHERE state='ready' AND next_run_at <= now() FOR UPDATE SKIP LOCKED.
  • Update: SET state='leased', lease_owner=workerID, lease_until=now()+leaseDuration.
  • Multiple workers can run concurrently without conflicts.
4. Execution

For each operation in the workflow:

Step:

  1. Check step_checkpoints for existing result.
  2. If cached: use cached value, advance to next operation.
  3. If not cached: execute function, persist result, advance.

Sleep:

  1. Update next_run_at = now() + duration.
  2. Set state = 'ready'.
  3. Release lease, exit (workflow resumes later).

WaitEvent:

  1. Set state = 'waiting_event'.
  2. Set wait_key and wait_deadline.
  3. Release lease, exit (workflow resumes when event arrives or timeout).
5. Completion

When all operations complete:

  • Update state = 'completed'.
  • Workflow is finished.

Durability Guarantees

At-Least-Once Execution

Every step is guaranteed to complete at least once:

  • Results are persisted to step_checkpoints after successful execution.
  • If a worker crashes before persisting, the step will re-execute on retry.
  • Once persisted, the step will never re-execute (result is replayed).
Exactly-Once Semantics

To achieve exactly-once semantics, make your steps idempotent:

b.Step("charge-payment", func(ctx context.Context, sc *durablepg.StepContext) (any, error) {
    // Use workflow run ID as idempotency key with external services
    return paymentProvider.Charge(ctx, ChargeRequest{
        IdempotencyKey: string(sc.WorkflowID()) + "-charge",
        Amount:         100,
    })
})
Failure Recovery
Failure Mode Recovery Mechanism
Worker crash during step Lease expires, another worker reclaims.
Worker crash after checkpoint Cached result replayed, step skipped.
Database connection lost Worker reconnects, resumes polling.
PostgreSQL failover Connection pool handles reconnection.

Best Practices

Step Idempotency

Steps should be idempotent whenever possible. While the engine guarantees a step won't re-execute after its result is persisted, a crash during step execution (before persistence) will cause a retry.

Event Key Patterns

Use structured event keys for targeted delivery:

// In workflow definition - include unique identifier
b.WaitEvent("order.paid:"+orderID, 24*time.Hour)

// When emitting - match the exact key
engine.EmitEvent(ctx, "order.paid:ORD-12345", payload)
Error Handling

Return errors from steps to trigger automatic retry with backoff:

b.Step("call-api", func(ctx context.Context, sc *durablepg.StepContext) (any, error) {
    resp, err := httpClient.Do(req)
    if err != nil {
        return nil, err // Will retry with exponential backoff
    }
    if resp.StatusCode >= 500 {
        return nil, fmt.Errorf("server error: %d", resp.StatusCode)
    }
    return parseResponse(resp)
})
Graceful Shutdown

Handle shutdown signals to allow in-progress steps to complete:

ctx, cancel := context.WithCancel(context.Background())

go func() {
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
    <-sigCh
    cancel()
}()

engine.StartWorker(ctx) // Returns when ctx is cancelled
Event Log Partitioning

Pre-create monthly partitions for high-volume workloads:

// On startup or via cron job
ddl, _ := engine.EventLogMonthlyPartitionsSQL(time.Now(), 6) // Next 6 months
pool.Exec(ctx, ddl)

Performance

The repository includes real PostgreSQL integration benchmarks in benchmark_test.go. These are not microbenchmarks of isolated functions; they exercise the actual durable execution path.

What Is Measured
Benchmark What it measures Why it matters
BenchmarkEnqueue Producer-side workflow creation throughput Cost of the durable enqueue path (INSERT + RETURNING + NOTIFY)
BenchmarkE2ESingleStep End-to-end throughput for a single-step workflow Cost of claim, checkpoint, step advance, and completion
BenchmarkE2EEventWakeLatency Wait-for-event resume latency Cost of event wake-up, state transition, and completion
Latest Sample Results

These numbers were measured on March 15, 2026 on:

  • Machine: Apple M1 Pro
  • OS/Arch: darwin/arm64
  • PostgreSQL: 17 (Docker)
  • Go benchmark flags: -benchmem -count=1
BenchmarkEnqueue-8                         2266    442979 ns/op      2257 runs/s      3105 B/op     65 allocs/op
BenchmarkE2ESingleStep/workers_1-8         112  10330762 ns/op        96.79 workflows/s 16079 B/op   259 allocs/op
BenchmarkE2ESingleStep/workers_8-8         648   1626729 ns/op       614.6 workflows/s  12027 B/op   215 allocs/op
BenchmarkE2EEventWakeLatency-8              69  17163561 ns/op      7524 wake-us/op   34067 B/op   543 allocs/op
Observations
  • Enqueue throughput is about 2.26k runs/s on a local Postgres instance.
  • End-to-end single-step execution scales from 96.8 workflows/s with one worker to 614.6 workflows/s with eight workers.
  • Event-driven wake-up latency is about 7.5ms for the wake path and about 17.2ms for the full wait-and-resume cycle.
  • Allocation pressure is currently much higher on the event path than on simple enqueue. The most obvious optimization targets are JSON churn and the number of SQL round-trips in the wait/event flow.

These numbers are useful for relative comparisons between changes. They are not a portable SLA because they depend heavily on CPU, storage, network, and PostgreSQL configuration.

Running the Benchmarks

The benchmark harness uses pgtestdb, which creates isolated test databases on top of an existing PostgreSQL instance. It does not start PostgreSQL for you.

Start a disposable local PostgreSQL 17 instance:

docker run --rm --name durablepg-bench-pg \
  -e POSTGRES_PASSWORD=postgres \
  -e POSTGRES_USER=postgres \
  -e POSTGRES_DB=postgres \
  -p 55432:5432 \
  postgres:17

Then run the benchmark suite:

DURABLEPG_TEST_DATABASE_URL='postgres://postgres:postgres@127.0.0.1:55432/postgres?sslmode=disable' \
go test -run '^$' -bench . -benchmem -count=1

Requirements

  • PostgreSQL: 17+ (18+ recommended for native uuidv7()).
  • Go: 1.25+.
  • Dependencies: github.com/jackc/pgx/v5.

License

Apache License 2.0

Documentation

Overview

Package durablepg provides a durable workflow engine backed by PostgreSQL.

Quick start:

engine, _ := durablepg.New(durablepg.Config{DB: pool})
_ = engine.Init(ctx)
engine.RegisterWorkflow("my_workflow", func(wf *durablepg.Builder) {})
go engine.StartWorker(ctx)
_, _ = engine.Run(ctx, "my_workflow", map[string]any{"user_id": "123"})

Index

Constants

This section is empty.

Variables

View Source
var ErrStepResultNotFound = errors.New("durablepg: step result not found")

Functions

This section is empty.

Types

type Builder

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

Builder records a workflow definition.

func (*Builder) Sleep

func (b *Builder) Sleep(d time.Duration)

Sleep pauses execution until duration elapses.

func (*Builder) Step

func (b *Builder) Step(name string, fn StepFunc, opts ...StepOption)

Step appends a durable function step.

func (*Builder) WaitEvent

func (b *Builder) WaitEvent(key string, timeout time.Duration)

WaitEvent pauses execution until key is emitted or timeout is reached.

type Config

type Config struct {
	DB                *pgxpool.Pool
	Schema            string
	Queue             string
	MaxConcurrency    int
	PollInterval      time.Duration
	LeaseTTL          time.Duration
	HeartbeatInterval time.Duration
}

Config configures Engine.

type Engine

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

Engine executes durable workflows using PostgreSQL.

func New

func New(cfg Config) (*Engine, error)

New creates a workflow engine.

func (*Engine) ApplySchema

func (e *Engine) ApplySchema(ctx context.Context) error

ApplySchema creates all durable workflow tables and indexes.

func (*Engine) CreateEventLogMonthlyPartitions

func (e *Engine) CreateEventLogMonthlyPartitions(ctx context.Context, from time.Time, months int) error

CreateEventLogMonthlyPartitions executes DDL for monthly event_log partitions.

func (*Engine) EmitEvent

func (e *Engine) EmitEvent(ctx context.Context, key string, payload any) error

EmitEvent records an event and wakes waiting workflows.

func (*Engine) Enqueue

func (e *Engine) Enqueue(ctx context.Context, name string, input any, opts ...EnqueueOption) (WorkflowID, error)

Enqueue schedules a workflow run.

func (*Engine) EnsureEventLogMonthlyPartitions deprecated

func (e *Engine) EnsureEventLogMonthlyPartitions(ctx context.Context, from time.Time, months int) error

EnsureEventLogMonthlyPartitions is kept for compatibility.

Deprecated: use EventLogMonthlyPartitionsSQL or CreateEventLogMonthlyPartitions.

func (*Engine) EnsurePartitions

func (e *Engine) EnsurePartitions(ctx context.Context, monthsAhead int) error

EnsurePartitions calls the SQL function to create monthly event_log partitions. monthsAhead must be between 0 and 120 (10 years).

func (*Engine) EventLogMonthlyPartitionsSQL

func (e *Engine) EventLogMonthlyPartitionsSQL(from time.Time, months int) (string, error)

EventLogMonthlyPartitionsSQL returns DDL to create monthly event_log partitions.

func (*Engine) Init

func (e *Engine) Init(ctx context.Context) error

Init applies the schema and creates event_log partitions for the next 12 months.

func (*Engine) Migrate deprecated

func (e *Engine) Migrate(ctx context.Context) error

Migrate is kept for compatibility.

Deprecated: use ApplySchema.

func (*Engine) Register

func (e *Engine) Register(wf Workflow)

Register compiles and stores a workflow definition.

func (*Engine) RegisterWorkflow

func (e *Engine) RegisterWorkflow(name string, build func(*Builder))

RegisterWorkflow registers a workflow using a name and builder function.

func (*Engine) Run

func (e *Engine) Run(ctx context.Context, name string, input any, opts ...RunOption) (WorkflowID, error)

Run starts a workflow run and returns its workflow ID.

func (*Engine) RunWorkflow

func (e *Engine) RunWorkflow(ctx context.Context, name string, input any, opts ...RunOption) (WorkflowID, error)

RunWorkflow is a compatibility alias for Run.

func (*Engine) SchemaSQL

func (e *Engine) SchemaSQL() string

SchemaSQL returns the schema DDL used by ApplySchema.

func (*Engine) StartWorker

func (e *Engine) StartWorker(ctx context.Context) error

StartWorker starts polling, claiming, and executing workflows until ctx is canceled.

type EnqueueOption

type EnqueueOption func(*enqueueOptions)

EnqueueOption configures Enqueue.

func WithIdempotencyKey

func WithIdempotencyKey(key string) EnqueueOption

WithIdempotencyKey deduplicates enqueues per workflow name.

func WithMaxAttempts

func WithMaxAttempts(n int) EnqueueOption

WithMaxAttempts sets max run attempts.

func WithQueue

func WithQueue(queue string) EnqueueOption

WithQueue routes the run to a specific queue.

func WithRunID

func WithRunID(id WorkflowID) EnqueueOption

WithRunID provides a deterministic run ID.

func WithScheduledAt

func WithScheduledAt(at time.Time) EnqueueOption

WithScheduledAt sets the first execution timestamp.

type RunOption

type RunOption = EnqueueOption

RunOption is the preferred name for options passed to Run.

func WithDeduplicationKey

func WithDeduplicationKey(key string) RunOption

WithDeduplicationKey is the preferred alias for WithIdempotencyKey.

func WithWorkflowID

func WithWorkflowID(id WorkflowID) RunOption

WithWorkflowID provides a deterministic workflow run ID.

type StepContext

type StepContext struct {
	RunID    WorkflowID
	Workflow string
	Input    json.RawMessage
	// contains filtered or unexported fields
}

StepContext exposes workflow input and completed step values.

func (*StepContext) DecodeInput

func (sc *StepContext) DecodeInput(dst any) error

DecodeInput decodes workflow input into dst.

func (*StepContext) RawValue

func (sc *StepContext) RawValue(step string) (json.RawMessage, bool)

RawValue returns the raw JSON for a prior step.

func (*StepContext) StepResult

func (sc *StepContext) StepResult(step string, dst any) error

StepResult decodes a prior step value and returns an error when it is absent.

func (*StepContext) Value

func (sc *StepContext) Value(step string, dst any) (bool, error)

Value decodes a prior step value by step name.

func (*StepContext) WorkflowID

func (sc *StepContext) WorkflowID() WorkflowID

WorkflowID returns the current workflow run ID.

func (*StepContext) WorkflowName

func (sc *StepContext) WorkflowName() string

WorkflowName returns the workflow definition name.

type StepFunc

type StepFunc func(ctx context.Context, sc *StepContext) (any, error)

StepFunc runs one durable step. It must be idempotent.

type StepOption

type StepOption func(*stepOptions)

StepOption configures one workflow step.

func WithStepTimeout

func WithStepTimeout(timeout time.Duration) StepOption

WithStepTimeout bounds step execution time.

type Workflow

type Workflow interface {
	Name() string
	Build(*Builder)
}

Workflow defines a code-first durable workflow.

func DefineWorkflow

func DefineWorkflow(name string, build func(*Builder)) Workflow

DefineWorkflow creates a workflow from a name and builder function.

type WorkflowID

type WorkflowID string

WorkflowID uniquely identifies a workflow run.

Jump to

Keyboard shortcuts

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