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 ¶
- Variables
- type Action
- type ActionKind
- type DiffOption
- type DiffResult
- type Engine
- func (e *Engine) AddSource(ctx context.Context, s *registry.Source, password string) error
- func (e *Engine) ApplyReconcile(ctx context.Context, now time.Time, stuckTimeout time.Duration) (ReconcilePlan, error)
- func (e *Engine) BranchUsage(ctx context.Context, name string) (int64, error)
- func (e *Engine) CreateBranch(ctx context.Context, name, sourceName string, ttl time.Duration) (_ *registry.Branch, err error)
- func (e *Engine) CreateBranchFrom(ctx context.Context, name, parentName string, ttl time.Duration) (_ *registry.Branch, err error)
- func (e *Engine) DestroyBranch(ctx context.Context, name string) (err error)
- func (e *Engine) DiffBranch(ctx context.Context, name string, opts ...DiffOption) (_ *DiffResult, err error)
- func (e *Engine) PlanReconcile(ctx context.Context, now time.Time, stuckTimeout time.Duration) (ReconcilePlan, error)
- func (e *Engine) ReapExpired(ctx context.Context, now time.Time) (destroyed []string, err error)
- func (e *Engine) Reconcile(ctx context.Context, now time.Time, stuckTimeout time.Duration, ...) (ReconcilePlan, error)
- func (e *Engine) RefreshSource(ctx context.Context, name, password string) error
- func (e *Engine) RemoveSource(ctx context.Context, name string) error
- func (e *Engine) ResetBranch(ctx context.Context, name string) (_ *registry.Branch, err error)
- func (e *Engine) RunReconcile(ctx context.Context, interval, stuckTimeout time.Duration, ...)
- type Option
- type ReconcilePlan
- type TableDelta
Constants ¶
This section is empty.
Variables ¶
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.
var ErrQuotaExceeded = errors.New("branch quota exceeded")
ErrQuotaExceeded is returned by the create paths when --max-branches is set and the live-branch count is already at the cap. The API maps it to 403.
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 DiffOption ¶
type DiffOption func(*diffOptions)
DiffOption tunes DiffBranch.
func WithDataSample ¶
func WithDataSample(n int) DiffOption
WithDataSample turns on bounded data sampling: for each table whose branch row-estimate exceeds its base estimate, DiffBranch returns up to n branch-only rows (matched by primary key) in TableDelta.SampleRows. A non-positive n uses the default cap (20). Tables without a primary key are skipped. Off by default.
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 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) 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 ¶
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 (*Engine) DiffBranch ¶ added in v0.3.0
func (e *Engine) DiffBranch(ctx context.Context, name string, opts ...DiffOption) (_ *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 ¶
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 ¶
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 ¶
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 ¶
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 WithMaxBranches ¶
WithMaxBranches caps the number of live (non-destroyed) branches. The create paths return ErrQuotaExceeded once the cap is reached. 0 (the default) is unlimited. branchd --max-branches / PGBRANCH_MAX_BRANCHES.
func WithMetrics ¶
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).
func WithTTLPolicy ¶
WithTTLPolicy sets the create-time TTL policy: defaultTTL is used when a create requests no TTL (0 = no default, never expires); maxTTL caps any requested TTL (0 = no cap). branchd --default-ttl / --max-ttl. The policy is applied in the engine create path so both API- and ghook-created branches inherit it.
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"`
// SampleRows is a bounded set of branch-only rows (present on the branch,
// absent on the base, matched by primary key) — populated only when the
// diff is requested with data sampling (engine.WithDataSample) and only for
// tables whose branch row-estimate exceeds the base estimate. Tables with
// no primary key are skipped (sampling needs a stable key to diff by).
SampleRows []map[string]any `json:"sample_rows,omitempty"`
}
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).