Help us shape the future of Go! Take the 2026 Go Developer Survey

deploystate

package
v1.0.14 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package deploystate is the pluggable backend for forge's reconcile state: what forge last shipped to a target, what it last OBSERVED running there, and the per-environment policy that says whether forge is allowed to do anything about the difference.

Why this is in pkg/ and not internal/

pkg/ is forge's public import surface, and Go's internal/ rule is what makes that boundary the plug point: no other module may import forge's internal/ tree. An operator of many forge projects — Reliant, or anyone else — implements Store against an interface it can actually import, and forge's own loop is one caller among several. Put the interface in internal/ and there is no pluggable backend; there is only a fork.

The constraint that keeps this honest

IF IT DOES NOT WORK AGAINST A DIRECTORY OF FILES, IT IS IN THE WRONG REPO. Local is not the fallback, it is the acceptance test. The moment a method needs a transaction, or a fleet-wide query like "every environment across every project due for a pass", the file implementation becomes a lie and what has actually shipped is a client for a mandatory server with a stub attached.

So this interface has five methods and none of them is a scheduler. Fleet-scale concerns — sharding, leases, fences, placement — belong to whoever is DRIVING many instances of forge's per-project loop, and they are already solved there (control-plane's internal/reconcilestore holds the lease and the fence). Forge's loop only ever reconciles one project's environments, and one project's state is a directory.

The one thing files genuinely cannot offer is compare-and-swap: [Put] is last-writer-wins. That is not a gap papered over, it is the boundary drawn correctly — a CAS token on this interface would exist solely to serve a fleet backend, and the fleet backend already fences its writes one layer up, where it holds the lease that makes a fence meaningful. See Store for the full argument.

What lives here

  • Record — one target's desired and observed halves, side by side.
  • State — five values, and StateUnknown is the zero value.
  • Policy — observe / converge / pinned, defaulting to observe.
  • Store — the interface.
  • Local — files, under .forge/state. No server, no network.
  • Remote — HTTP+JSON against an endpoint. Proof the seam is real.

Index

Constants

View Source
const DefaultStabilityWindow = 30 * time.Second

DefaultStabilityWindow is how long a target must hold its matching, serving shape before Record.Evaluate will call it converged.

Thirty seconds is chosen to outlast the failure it exists to catch: a container that starts, passes nothing, and is restarted. A crash loop with a backoff longer than this window will still be caught by the serving check on the next pass — the window is not the only defence, it is the one that stops a single optimistic snapshot from being reported as a successful promotion.

View Source
const DirRel = ".forge/state"

DirRel is where a Local store keeps its files, relative to the project root. The same directory forge's existing deploy-state files live in, covered by the existing `.forge/` .gitignore rule.

Variables

View Source
var ErrNotFound = errors.New("deploystate: no record for this key")

ErrNotFound is returned by Store.Get when no record exists for a key. Match it with errors.Is.

A sentinel rather than a (nil, nil) return because the callers that matter treat the two cases differently: "never deployed" is a normal starting condition, while a nil record handed to code expecting one is a panic three frames later.

Functions

This section is empty.

Types

type Action

type Action int

Action is what a reconcile pass concluded it should DO. Separate from State, which is what it concluded is TRUE, because the same truth yields different actions under different policy — and conflating them is how a reporting tool grows a write path nobody reviewed.

const (
	// ActionNone means nothing to do: the target is converged, or it is
	// unknown and there is nothing to act on.
	ActionNone Action = iota
	// ActionReport means drift exists and forge will say so and stop.
	ActionReport
	// ActionConverge means drift exists and policy permits correcting it.
	ActionConverge
	// ActionWait means the target is matching but has not yet held its
	// shape past the stability window. Distinct from None: a caller
	// arming a rollback timer needs to know the clock is still running.
	ActionWait
)

func (Action) String

func (a Action) String() string

String renders the action for logs.

type Decision

type Decision struct {
	Key    Key
	State  State
	Policy Policy
	Action Action

	// Drifted lists the OWNED fields that do not match. Empty under
	// every action but report and converge.
	Drifted []Field

	// Reason is one line explaining the action, for the log line a human
	// reads when they ask why forge did or did not do something.
	Reason string
}

Decision is one pass's conclusion about one target.

func Decide

func Decide(rec Record, policy Policy, now time.Time, window time.Duration) Decision

Decide computes the action for one record under one policy.

The policy gate is a SINGLE branch and it comes before any converge path, so there is exactly one place in this package where the decision to change something is made. A converge action that could be reached by two routes is one that will eventually be reached by a route nobody checked the policy on.

func DecideEnv

func DecideEnv(ctx context.Context, store Store, env string, now time.Time, window time.Duration) ([]Decision, error)

DecideEnv reads the environment's policy and decides for every record in it.

THE POLICY READ HAPPENS HERE, ON EVERY CALL, and nothing in this package caches it. That is the mechanism behind "opting out must be instant": a worker calling DecideEnv each pass re-reads the policy each pass, so setting an environment to pinned takes effect on the next pass — no deploy, no restart, no artifact rebuild. A policy read once at construction, or compiled into a deploy artifact, would be correct in every test and wrong at the only moment anyone cares about it.

A policy read that FAILS aborts the whole environment rather than falling back to a default. Falling back would mean a backend outage silently re-enables convergence on an environment an operator pinned, which inverts the guarantee the pinned setting exists to give.

type Desired

type Desired struct {
	// Digest is the canonical image digest declared, when the
	// declaration pins one. Empty when the target is pinned by mutable
	// tag instead, which is a legitimate and common case and must not be
	// reported as drift — see [Record.Evaluate].
	Digest string `json:"digest,omitempty"`

	// Image and Tag are the human-facing reference, carried for
	// reporting (a non-cluster provider has no cluster to ask, so the
	// deployed tag has to be remembered somewhere).
	Image string `json:"image,omitempty"`
	Tag   string `json:"tag,omitempty"`

	// Replicas is the declared scale, or nil when the tier has no
	// replica concept. Nil and a pointer to 0 mean different things:
	// "not applicable" versus "deliberately scaled to nothing".
	Replicas *int `json:"replicas,omitempty"`

	// DeclaredAt is when this declaration was recorded.
	DeclaredAt time.Time `json:"declared_at,omitzero"`
}

Desired is what the declaration asks for. Only the fields a Field can name — this is the comparison half of a Record, not a copy of the whole spec.

type Field

type Field string

Field names a comparable property of a target. NOT every difference is drift, and this type is how forge says which differences are its business.

The failure this prevents is specific and common: an autoscaler moves a replica count, forge reports drift, the engineer confirms it is correct behaviour, and after the third such report nobody reads the drift badge again. A signal people have learned to ignore is worse than no signal, because it occupies the place where a real one would have gone.

So ownership is per-target and explicit. A target under an HPA lists FieldDigest and not FieldReplicas; a target forge fully owns lists both. Anything not listed is the world's, and a difference there is recorded and never reported as drift.

const (
	// FieldDigest is the content-addressed identity of what is running.
	FieldDigest Field = "digest"
	// FieldReplicas is the scale. Frequently NOT forge's to own — an
	// autoscaler owning this field is the motivating example for this
	// whole type.
	FieldReplicas Field = "replicas"
)

type Key

type Key struct {
	// Env is the environment name (dev, staging, prod).
	Env string `json:"env"`
	// Provider is the deploy provider id (k8s, compose, external, ...).
	Provider string `json:"provider"`
	// Service is the service or frontend name.
	Service string `json:"service"`
}

Key identifies one target within one project. Three segments, matching the on-disk layout that already exists (.forge/state/<provider>-<env>-<service>.json), so adopting this package does not orphan any state a user already has.

func (Key) String

func (k Key) String() string

String renders the key for logs and error messages.

func (Key) Valid

func (k Key) Valid() bool

Valid reports whether every segment is populated. A key with an empty segment would collide with another key on disk after sanitization, so it is refused at the boundary rather than silently overwriting.

type Local

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

Local implements Store over a directory. This is the default backend, and it is also the acceptance test for the interface: an OSS user with no server at all gets drift detection, five states, and policy out of it.

On-disk layout

.forge/state/reconcile-<env>-<provider>-<service>.json   one Record
.forge/state/policy-<env>.json                           one Policy

A NEW filename prefix rather than reusing the existing `<provider>-<env>-<service>.json`, and that is deliberate. Those files are written and read by forge's non-cluster providers to remember the tag they last shipped (the next deploy's ${LAST_TAG}). Widening that file's schema in place would mean an older forge reading a newer file, and a provider that misparses it loses the only record of what it deployed.

So the two coexist, and Local READS the legacy file when it has no record of its own (see Local.Get). A project that has been deploying for months does not report "never deployed" on the day it turns reconciliation on.

func NewLocal

func NewLocal(projectDir string) *Local

NewLocal returns a Local store rooted at a project directory (the one holding forge.yaml). The state directory is created lazily on first write, so a project that never reconciles never grows the tree.

func (*Local) Get

func (l *Local) Get(ctx context.Context, key Key) (Record, error)

Get returns the record for one key.

The legacy fallback is the reason this is not four lines. When no reconcile record exists, Local reads the older deploy-state file and presents it as a DESIRED-ONLY record: forge did ship this, here is what it shipped, and the observed half is explicitly unmeasured. The resulting record evaluates to StateUnknown, which is the honest answer — nobody has looked yet — rather than the green a populated desired half might otherwise suggest.

func (*Local) List

func (l *Local) List(ctx context.Context, env string) ([]Record, error)

List returns every reconcile record for one environment.

It reads only the reconcile-prefixed files — NOT the legacy ones. A list is a report of what reconciliation knows, and synthesizing never-observed entries from deploy history would fill it with rows whose every column is unknown. Get's fallback exists to answer a question about a specific target; List answering the same way would be noise.

func (*Local) Policy

func (l *Local) Policy(ctx context.Context, env string) (Policy, error)

Policy reads the per-environment policy from disk on EVERY call — no cache, no memo. That is the feature: an engineer who writes `{"policy":"pinned"}` into .forge/state/policy-prod.json mid-incident has changed forge's behaviour for the very next pass, with no deploy, no restart, and no artifact rebuild.

A missing file is not an error. It returns PolicyObserve, which is also the zero value, so the absence of an opinion means "change nothing" through every path at once.

func (*Local) PolicyPath

func (l *Local) PolicyPath(env string) string

PolicyPath is the file an engineer edits to change an environment's policy by hand. Exported because an error message that says "this environment is pinned" without saying WHERE to unpin it sends the reader to grep, and the whole value of an instant opt-out is lost if finding the switch takes longer than the incident.

func (*Local) Put

func (l *Local) Put(ctx context.Context, rec Record) error

Put writes a record. The key on the record is authoritative.

func (*Local) SetPolicy

func (l *Local) SetPolicy(ctx context.Context, env string, p Policy) error

SetPolicy records the policy for one environment.

type Observation

type Observation struct {
	// Digest is what is RUNNING. Empty means the observer could not
	// determine it.
	Digest string `json:"digest,omitempty"`

	// Replicas is the observed scale, nil when not applicable.
	Replicas *int `json:"replicas,omitempty"`

	// Ready is how many of those replicas are passing readiness. Nil
	// when not applicable. This is the field that separates "applied" —
	// which a deploy's own return value already told you — from
	// "working", which only a read can.
	Ready *int `json:"ready,omitempty"`

	// Serving is the observer's verdict on whether the target is doing
	// its job. FALSE IS THE ZERO VALUE, so an Observation that nobody
	// filled in cannot read as serving. Meaningful only when Measured is
	// true.
	Serving bool `json:"serving"`

	// Measured distinguishes "looked, and here is what I found" from "did
	// not look". Without it, a zero Observation is indistinguishable
	// from a target that is genuinely absent and unhealthy, and those
	// call for opposite actions: investigate versus go and look.
	Measured bool `json:"measured"`

	// Detail is one line of human-readable context. REQUIRED whenever
	// the observation is anything other than a clean serving read — an
	// unknown with no reason is indistinguishable from an observer that
	// silently failed.
	Detail string `json:"detail,omitempty"`

	// ObservedAt is when the read happened, wall-clock UTC. A caller
	// comparing declared against observed needs to know how stale the
	// answer is; without it a cached observation reads as a fresh one.
	ObservedAt time.Time `json:"observed_at,omitzero"`
}

Observation is what was actually found running. Mirrors deploytarget.Observed's facts without importing it: forge's internal package may not be imported from pkg/, and an operator implementing a Store should not have to depend on forge's provider tree to do it.

type Policy

type Policy int

Policy is what forge is permitted to do about a difference between what an environment declares and what is actually running.

Three values, and the default is deliberately the timid one

Policy is an int enum precisely so its ZERO VALUE is PolicyObserve. A Policy that arrives by any path — decoded from a file that predates this field, built as a struct literal in a test, returned by a backend that has not been taught about policy yet — reads as "report, change nothing" until something deliberately says otherwise. The failure mode of a forgotten field must be inaction.

That is not timidity for its own sake. Silent correction during an incident, while an engineer is deliberately holding a thing together by hand, is the single behaviour that makes people rip these systems out — and they are right to, because at that moment the reconciler is not enforcing an invariant, it is fighting the only person who understands the situation. Converge is opt-in, per environment, forever.

const (
	// PolicyObserve reports drift and changes nothing. The zero value,
	// and the default for an environment that has never said otherwise.
	PolicyObserve Policy = iota

	// PolicyConverge permits forge to correct drift it detects. Opt-in.
	PolicyConverge

	// PolicyPinned refuses ALL changes, including ones a human asked
	// for through a normal deploy.
	//
	// Distinct from observe, and the distinction is the whole reason
	// there are three values rather than a bool. Observe says "I will
	// not act on my own initiative"; pinned says "nothing may change
	// this environment right now", which is what you set during a
	// freeze, an incident, or a migration you are driving by hand.
	// Collapsing them would mean the only way to stop a deploy is to
	// also stop drift REPORTING — losing the signal at the exact moment
	// it is most wanted.
	PolicyPinned
)

func ParsePolicy

func ParsePolicy(s string) (Policy, error)

ParsePolicy decodes the string form. The EMPTY STRING IS NOT AN ERROR: it decodes to PolicyObserve, which is what makes a policy file that predates this field, or a backend that returns nothing, land on the safe value rather than failing a pass.

An unrecognized non-empty value IS an error. "converg" must not silently become observe — a user who typed a value meant to change behaviour, and a typo that reads as the default is indistinguishable from the setting having been applied.

func (Policy) AllowsChange

func (p Policy) AllowsChange() bool

AllowsChange reports whether ANY write to the target is permitted — an ordinary deploy as well as a reconcile. Only pinned says no.

func (Policy) AllowsConverge

func (p Policy) AllowsConverge() bool

AllowsConverge reports whether forge may correct drift under this policy. A method rather than a `== PolicyConverge` at each call site, so that adding a fourth value later cannot silently leave a caller comparing against a stale set.

func (Policy) MarshalJSON

func (p Policy) MarshalJSON() ([]byte, error)

MarshalJSON emits the string form, so a policy file is something a human can read and edit in an incident without a decoder ring.

func (Policy) String

func (p Policy) String() string

String renders the policy for logs, files and JSON.

func (*Policy) UnmarshalJSON

func (p *Policy) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts the string form, including "" and JSON null, both of which yield PolicyObserve.

type Record

type Record struct {
	Key Key `json:"key"`

	Desired  Desired     `json:"desired"`
	Observed Observation `json:"observed"`

	// Owned lists the fields forge is responsible for. EMPTY MEANS
	// NOTHING IS OWNED, which is deliberate: a Record that predates this
	// field, or that a backend forgot to populate, reports no drift
	// rather than reporting every difference as drift. The direction of
	// the default matters — over-reporting is what destroys trust in the
	// signal, and under-reporting is visible as a badge that never moves.
	Owned []Field `json:"owned,omitempty"`

	// StableSince is when the target most recently ENTERED its current
	// matching, serving shape. Nil whenever it is not in one.
	//
	// This field is why "converged" means something. A clean apply is
	// not a successful promotion: the apply succeeds, the pod starts,
	// the pod crashes, and a reconciler that reported success at apply
	// time has already told everyone the promotion worked. Requiring the
	// shape to HOLD for a window is what makes the report honest, and it
	// is the same clock an automatic rollback would arm itself against.
	StableSince *time.Time `json:"stable_since,omitempty"`
}

Record is one target's two halves side by side, plus the bookkeeping that turns a comparison into a trustworthy verdict.

func (Record) DriftedFields

func (r Record) DriftedFields() []Field

DriftedFields returns the owned fields whose observed value does not match the declared one. Empty when there is no drift — including the case where forge owns nothing, which is the safe default.

A field the observer could not determine is NOT drift. An image pinned by mutable tag yields an empty desired digest and an observer that cannot resolve a digest yields an empty observed one; calling either case drift would flag a config that is both legal and common, which is exactly the noise that makes people stop reading the badge.

func (Record) Evaluate

func (r Record) Evaluate(now time.Time, window time.Duration) State

Evaluate computes the verdict for this record as of `now`, given a stability window.

The order of the checks is the design, so it is spelled out:

  1. NOT MEASURED WINS OVER EVERYTHING. An unobservable target is unknown even if its desired half looks perfect, because the desired half is a wish and wishes are free.
  2. NOT SERVING BEATS MATCHING. Degraded outranks converged: an exact match onto a crash-looping workload is degraded. This is the check that stops "the apply succeeded" from being reported as success.
  3. DRIFT IS COMPUTED OVER OWNED FIELDS ONLY. A replica count that moved under an autoscaler forge does not own is recorded and not reported.
  4. MATCHING BUT YOUNG IS CONVERGING, NOT CONVERGED.

func (Record) Owns

func (r Record) Owns(f Field) bool

Owns reports whether forge claims this field for this target.

func (Record) WithStability

func (r Record) WithStability(prev *Record, now time.Time) Record

WithStability returns a copy of r whose StableSince reflects a fresh observation, carrying the previous record's clock forward when the target has held its shape.

THE CARRY-FORWARD IS THE WHOLE POINT. A writer that stamped StableSince=now on every pass would reset the clock every time it looked, so nothing would ever age past the window and no target would ever be reported converged. A writer that never stamped it would call a target converged the instant it first matched, which is the crash- loop bug. The clock advances only on a genuine transition INTO the good shape, and is cleared on any transition out of it.

prev may be nil (first observation of this target).

type Remote

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

Remote implements Store against an HTTP+JSON endpoint. Reliant is the first implementor of the server side; the protocol is written down here so it is not.

Why this exists in forge rather than only in Reliant

An interface with one implementation is not a seam, it is a type declaration with extra steps. Shipping the second implementation is what proves the boundary holds — and it did its job: writing this is what established that the interface needed no transaction, no lease and no cursor, because nothing here wanted one.

Why HTTP+JSON and not a generated client

The same reason forge chose REST over a vendored SDK twice already. A generated client drags a protocol runtime into forge/pkg, which every forge APP inherits — and the shapes on the wire here are five verbs over two flat structs. `net/http` covers it with no new module.

The protocol

GET    {base}/v1/state/{env}/{provider}/{service}   -> 200 Record | 404
PUT    {base}/v1/state/{env}/{provider}/{service}   <- Record, 204
GET    {base}/v1/state/{env}                        -> 200 {"records":[...]}
GET    {base}/v1/policy/{env}                       -> 200 {"policy":"..."} | 404
PUT    {base}/v1/policy/{env}                       <- {"policy":"..."}, 204

Every path segment is URL-escaped, so a service name with a slash cannot reach a sibling route.

func NewRemote

func NewRemote(baseURL string, opts ...RemoteOption) *Remote

NewRemote builds a Remote against a base URL ("https://host/api").

The default client carries a 30s timeout. A Store call sits inside a reconcile pass that holds a lease with its own TTL, and a request with no timeout outlives the lease that authorized it — at which point the write it eventually makes is a stale write wearing a fresh timestamp, which is the failure the whole fence design upstream exists to catch. Better to fail the pass and let the next one hold a real lease.

func (*Remote) Get

func (r *Remote) Get(ctx context.Context, key Key) (Record, error)

Get fetches one record. A 404 becomes ErrNotFound so callers branch on the same sentinel regardless of which backend they hold.

func (*Remote) List

func (r *Remote) List(ctx context.Context, env string) ([]Record, error)

List returns every record for one environment.

func (*Remote) Policy

func (r *Remote) Policy(ctx context.Context, env string) (Policy, error)

Policy reads the environment's policy.

A 404 yields PolicyObserve, not an error — matching Local exactly. The direction is what matters: a backend that has never heard of this environment must not be able to produce a converge verdict.

func (*Remote) Put

func (r *Remote) Put(ctx context.Context, rec Record) error

Put writes one record.

func (*Remote) SetPolicy

func (r *Remote) SetPolicy(ctx context.Context, env string, p Policy) error

SetPolicy writes the environment's policy.

type RemoteOption

type RemoteOption func(*Remote)

RemoteOption configures a Remote.

func WithBearerToken

func WithBearerToken(token string) RemoteOption

WithBearerToken sets an Authorization header on every request.

func WithHTTPClient

func WithHTTPClient(c *http.Client) RemoteOption

WithHTTPClient supplies the client to use. Callers with their own retry, tracing or auth transport pass it here rather than having forge invent a second policy alongside theirs.

type State

type State int

State is the verdict for one target: five values, not two.

A boolean "in sync" collapses three genuinely different situations — still settling, actively wrong, and not measured — into one "no", and every consumer then re-derives the distinction from fields the boolean was supposed to save it from reading.

StateUnknown is the ZERO VALUE, and that is load-bearing. An opaque `External` target runs a command forge cannot read back; there is genuinely nothing to observe, and the honest answer is "I do not know". Making it the zero value means a Record built by any path — a backend that failed halfway, a test literal, a file written by an older forge — reads as unknown rather than as green. A dashboard that shows green for something nobody measured is worse than one that shows nothing, because it is confidently wrong and people stop reading it.

const (
	// StateUnknown means nothing measured this target. The zero value.
	StateUnknown State = iota

	// StateConverging means the target matches what was declared but has
	// not yet been stable long enough to trust. See [Record.StableSince].
	StateConverging

	// StateConverged means the target matches AND has held that shape
	// past the stability window.
	StateConverged

	// StateDiverged means an owned field does not match the declaration.
	// This is drift.
	StateDiverged

	// StateDegraded means the target is not serving — regardless of
	// whether it matches. A perfect apply onto a crash-looping pod is
	// degraded, not converged.
	StateDegraded
)

func (State) MarshalJSON

func (s State) MarshalJSON() ([]byte, error)

MarshalJSON emits the string form.

func (State) String

func (s State) String() string

String renders the state for logs and JSON. Unknown renders as "unknown" rather than "" so an unset value is visible as a value.

func (*State) UnmarshalJSON

func (s *State) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts the string form; "" and null decode to unknown.

type Store

type Store interface {
	// Get returns the record for one target, or ErrNotFound.
	Get(ctx context.Context, key Key) (Record, error)

	// Put writes a record. Last-writer-wins; see the type comment.
	Put(ctx context.Context, rec Record) error

	// List returns every record for one environment, in a stable order
	// (provider then service) so that CLI output and diffs do not
	// reshuffle between runs for no reason.
	//
	// Scoped to ONE environment on purpose. An unscoped "list
	// everything" would be the first method a fleet backend implements
	// with a paginated cursor and an owner filter, and at that point the
	// local implementation is a toy stapled to a real one.
	List(ctx context.Context, env string) ([]Record, error)

	// Policy returns the reconcile policy for one environment. A missing
	// policy is NOT an error: it returns PolicyObserve, because the
	// absence of an opinion must mean "change nothing", never "converge".
	Policy(ctx context.Context, env string) (Policy, error)

	// SetPolicy records the reconcile policy for one environment.
	SetPolicy(ctx context.Context, env string, p Policy) error
}

Store is the pluggable backend. Five methods, and the count is a constraint rather than a coincidence.

Every method must be honestly implementable by a directory of files

That is the acceptance test this interface was designed against, and Local is the proof. It is not about supporting an offline mode as a courtesy; it is the thing that keeps forge's half of reconciliation from quietly becoming a client for a mandatory server.

The pressure is real and it is specific. Each of these would be natural to add, and each is refused:

  • ListDue(shard, count) — "which environments need a pass". A directory cannot shard, and more importantly SHOULD NOT: forge's loop reconciles ONE project, so it knows its environments from the config it already loaded. Sharding exists only for a fleet, and a fleet driver already has it (control-plane's reconcilestore does exactly this, over Postgres, with a lease and a fence).
  • Transaction / WithTx — multi-key atomicity. Files cannot, and the only caller who would need it is one coordinating writers it does not control.
  • AcquireLease / Fence — mutual exclusion between writers. A single project's loop has one writer. A fleet has many, and fences them one layer up where the lease actually lives.

The one honest gap: Put is last-writer-wins

There is no compare-and-swap, and files are the reason. A CAS token on this interface would be unimplementable over a directory without lockfiles whose staleness semantics are a worse problem than the one they solve — a crashed writer's lock is indistinguishable from a live one's, and a local tool that hangs waiting for a lock nobody holds is a bug report.

This is stated rather than papered over because the alternative is a method the local backend satisfies by ignoring its own argument, which is how an interface starts lying. Concurrent writes to one project's state are outside forge's model (one loop, one project); a backend that genuinely has concurrent writers fences them where it holds the lease, which is the layer that can.

Policy is a read, every pass

Store.Policy is separate from Store.Get and is meant to be called on each pass rather than cached. Opting out of convergence must be INSTANT and must not require a deploy: an engineer who sets an environment to pinned mid-incident needs the next pass to honour it, not the next release. A policy baked into an artifact, or read once at start-up, fails exactly when it is needed.

Jump to

Keyboard shortcuts

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