engine

package
v1.0.0-rc.1 Latest Latest
Warning

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

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

Documentation

Overview

Package engine orchestrates branch lifecycle as sagas over the registry, cow planner, and runtime driver. The CLI (P1) and branchd (P2) both embed it.

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidName = errors.New("invalid branch name")

ErrInvalidName rejects branch names that cannot be used across runtimes (docker container names, k8s pod names — RFC 1123 after the pgbranch-br- prefix). The API maps it to 400.

Functions

This section is empty.

Types

type Action

type Action struct {
	Kind   ActionKind `json:"kind"`
	Target string     `json:"target"`
	Reason string     `json:"reason"`
}

Action is one intended convergence step. Target is the branch name, container id, layer volume or volume name the action operates on; Reason is a human-readable justification. ReconcilePlan is a list of these.

type ActionKind

type ActionKind string

ActionKind enumerates the convergence steps a reconcile pass can take. The values double as the {action} label on pgbranch_reconcile_actions_total.

const (
	// ActionReap destroys a branch whose TTL has passed.
	ActionReap ActionKind = "reap"
	// ActionFailStuck fails a branch wedged in creating/resetting past the
	// stuck timeout and cleans its half-built resources.
	ActionFailStuck ActionKind = "fail_stuck"
	// ActionRemoveOrphanContainer removes a managed container/pod with no live
	// registry row.
	ActionRemoveOrphanContainer ActionKind = "remove_orphan_container"
	// ActionGCLayer removes a frozen layer (volume + row) whose refcount is 0.
	ActionGCLayer ActionKind = "gc_layer"
	// ActionGCVolume removes a managed volume owned by no live branch/source.
	ActionGCVolume ActionKind = "gc_volume"
)

type DiffResult added in v0.3.0

type DiffResult struct {
	SchemaDiff string       `json:"schema_diff"`
	Tables     []TableDelta `json:"tables"`
}

DiffResult is what changed in a branch relative to its base: a unified schema diff (pg_dump --schema-only of base vs branch; empty = identical) and per-table row-estimate deltas.

type Engine

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

func New

func New(reg *registry.Registry, drv runtime.Driver, defaultImage string, opts ...Option) *Engine

New builds an engine on the default OverlayFS backend.

func NewWithPlanner

func NewWithPlanner(reg *registry.Registry, drv runtime.Driver, defaultImage string, planner cow.Planner, opts ...Option) *Engine

NewWithPlanner selects the copy-on-write backend (branchd --cow).

func (*Engine) AddSource

func (e *Engine) AddSource(ctx context.Context, s *registry.Source, password string) error

AddSource registers a source and seeds it from the given live Postgres.

func (*Engine) ApplyReconcile

func (e *Engine) ApplyReconcile(ctx context.Context, now time.Time, stuckTimeout time.Duration) (ReconcilePlan, error)

ApplyReconcile computes a plan and executes it, returning the actions taken. It re-checks every destructive action against the live registry immediately before acting (safety: a branch may have been provisioned, a layer referenced, a volume claimed between planning and applying) and only ever touches pgbranch-managed resources. Best-effort: an action that fails is recorded as an error but does not abort the pass.

func (*Engine) BranchUsage

func (e *Engine) BranchUsage(ctx context.Context, name string) (int64, error)

BranchUsage measures a branch's copy-on-write layer in bytes (the branch's own writes, not the shared source data). Overlay: `du -sb` on the rw volume; zfs: the clone's `used` property (space unique to the clone). It is a helper-container roundtrip — cheap, but not free.

func (*Engine) CreateBranch

func (e *Engine) CreateBranch(ctx context.Context, name, sourceName string, ttl time.Duration) (_ *registry.Branch, err error)

CreateBranch is a saga: every step registers a compensation that runs (in reverse order) if a later step fails. No orphans, ever. ttl 0 means the branch never expires.

func (*Engine) CreateBranchFrom

func (e *Engine) CreateBranchFrom(ctx context.Context, name, parentName string, ttl time.Duration) (_ *registry.Branch, err error)

CreateBranchFrom creates a branch whose base is another (ready) branch's current state — branch-from-branch.

Overlay backend: a freeze saga. The parent's rw volume cannot be shared writable, so it is frozen into an immutable layer:

CHECKPOINT parent -> stop parent -> fresh parent rw volume ->
restart parent on [frozen rw, …its old chain…, source] (wait ready) ->
start child on the same chain -> commit (layer row + parent rw swap,
atomic) -> child ready.

The parent gets a new container (and so possibly a new host port; the wire router resolves live, so dbname@parent connections just reconnect). On any failure before the commit the parent is restored to its original rw volume and chain and restarted; if even that fails it is marked failed — never half-frozen. The layer row is committed only after both restarts succeeded.

ZFS backend: block-level CoW — snapshot the parent's clone and clone that. No freeze, no stop, no layer rows.

CSI backend: the child's PVC is a clone of the parent's PVC. No freeze or layer rows either, but the parent is briefly stopped around the clone for crash consistency (see provisionCSI).

func (*Engine) DestroyBranch

func (e *Engine) DestroyBranch(ctx context.Context, name string) (err error)

func (*Engine) DiffBranch added in v0.3.0

func (e *Engine) DiffBranch(ctx context.Context, name string) (_ *DiffResult, err error)

DiffBranch reports what changed in a ready branch relative to its base. It provisions an internal throwaway branch ("diff-<6 hex>") from the target's OWN base — the recorded source volume/generation and frozen-layer chain, not the source's current generation — then runs pg_dump --schema-only and a row-estimate query inside both instances over the local socket (no credentials involved, so rotated branch passwords don't matter) and diffs host-side. The throwaway is a normal registry row (TTL'd, so the reaper cleans strays if branchd dies mid-diff) and is destroyed before returning, success or not. Expect a few seconds of wall time: a full branch provision plus two dumps.

func (*Engine) PlanReconcile

func (e *Engine) PlanReconcile(ctx context.Context, now time.Time, stuckTimeout time.Duration) (ReconcilePlan, error)

PlanReconcile computes the convergence plan WITHOUT mutating anything: it is the read-only half of reconcile, backing pgb doctor and GET /v1/reconcile/plan. now and stuckTimeout drive the TTL-reap and stuck-row detection; the rest is pure registry-vs-reality drift.

func (*Engine) ReapExpired

func (e *Engine) ReapExpired(ctx context.Context, now time.Time) (destroyed []string, err error)

ReapExpired destroys every ready/failed branch whose TTL has passed and returns the names destroyed. Retained as a thin primitive over the unified reconcile loop (see reconcile.go) for callers that only want the TTL pass; now is injected for testability. The reconcile loop in branchd folds this in.

func (*Engine) Reconcile

func (e *Engine) Reconcile(ctx context.Context, now time.Time, stuckTimeout time.Duration, logf func(format string, args ...any)) (ReconcilePlan, error)

Reconcile converges the registry with reality in one pass: reaps TTL-expired branches, fails branches stuck in creating/resetting past stuckTimeout, removes orphaned managed containers, and GCs dangling layers/volumes. It is the unified loop body branchd runs on a ticker (and once at startup); the CLI/REST doctor (plan) and gc (apply) call PlanReconcile/ApplyReconcile directly. logf (nil = silent) receives a one-line summary per pass.

func (*Engine) RefreshSource

func (e *Engine) RefreshSource(ctx context.Context, name, password string) error

RefreshSource re-seeds a source into a fresh generation volume. Existing branches keep the volume they were created from; only new branches see the new generation. The previous generation's volume is GC'd once no live branch references it. A failed seed leaves the current generation intact.

func (*Engine) RemoveSource

func (e *Engine) RemoveSource(ctx context.Context, name string) error

RemoveSource deletes a source's volume, its orphaned frozen layers, and the registry rows. Refused while any live branch still uses the source or (defensively) while any layer is still referenced.

func (*Engine) ResetBranch

func (e *Engine) ResetBranch(ctx context.Context, name string) (_ *registry.Branch, err error)

ResetBranch throws away a ready branch's writes and reprovisions it from its recorded source volume on the same registry row (ready -> resetting -> ready; new container id and host port).

func (*Engine) RunReconcile

func (e *Engine) RunReconcile(ctx context.Context, interval, stuckTimeout time.Duration, logf func(format string, args ...any))

RunReconcile runs Reconcile on a ticker until ctx is done; branchd's single background loop. It runs one pass immediately so startup drift converges without waiting a full interval.

type Option added in v0.3.0

type Option func(*Engine)

Option configures optional engine behavior at construction time.

func WithCredentialRotation added in v0.3.0

func WithCredentialRotation() Option

WithCredentialRotation turns on per-branch credential rotation: every branch create and reset generates a fresh password, applies it inside the branch and stores it on the branch row (returned by the API as `password`).

func WithMetrics

func WithMetrics(m *metrics.Metrics) Option

WithMetrics attaches a metrics sink the engine uses to observe saga durations/errors, masking duration, in-flight ops and reaper/reconcile counters. nil is accepted (every metric call is nil-safe).

type ReconcilePlan

type ReconcilePlan struct {
	Actions []Action `json:"actions"`
}

ReconcilePlan is the set of convergence steps a pass intends (or, after apply, took). It is computed read-only and can be reported (pgb doctor / GET /v1/reconcile/plan) or applied (pgb gc / POST /v1/reconcile). Drift reports true when the plan is non-empty.

func (ReconcilePlan) Drift

func (p ReconcilePlan) Drift() bool

Drift reports whether the plan found anything to converge.

type TableDelta added in v0.3.0

type TableDelta struct {
	Table      string `json:"table"`
	BaseRows   int64  `json:"base_rows"`
	BranchRows int64  `json:"branch_rows"`
	Delta      int64  `json:"delta"`
}

TableDelta is one table's row-estimate comparison between a branch and its base. Counts come from pg_class.reltuples — planner estimates, not exact counts (fresh never-analyzed tables report 0).

Jump to

Keyboard shortcuts

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