placement

package
v0.3.5 Latest Latest
Warning

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

Go to latest
Published: Nov 7, 2026 License: GPL-3.0 Imports: 12 Imported by: 0

README

Durable placement primitive

This opt-in package has no app wiring and changes no standalone, Plex, or local transcode behavior. It requires a database opened by internal/store, which registers/translates its component-prefixed migration for SQLite and PostgreSQL 14. Constructors explicitly receive the database, options, and retirement verifier. There is no process singleton or background worker.

Trusted control-plane integration:

  1. Create a service with a positive ledger quota, maximum lease (1 second to 1 hour), and transaction/lock timeout (positive, at most 1 minute). Use consistent options across coordinators.
  2. SetPool(scope, role, capacity) persists a resource budget. Zero denies admission; absent scopes never imply unlimited capacity. Share a scope across processes using the same physical GPU or other resource. Workers must also enforce their own physical limits. This is admission accounting, not bandwidth shaping.
  3. An authenticated membership/policy reconciler calls SetTarget(target, roles, requiredScopesByRole). Empty roles disable admission. Each enabled role needs 1–16 explicit, distinct scope names. Startup must not blindly replace a live membership incarnation. Policy updates never clear existing claims, including claims from an old boot. Policy changes govern new assignments, not automatic revocation of existing work. Capability freshness, drain, and protocol policy are the reconciler's responsibility; this package is not a policy evaluator.
  4. Reserve supplies stable session/controller identities, controller boot, target node/boot, role, operation key, expected generation, required claims, and bounded lease. All configured scopes are mandatory. Persist the returned full fence in transport DTOs, worker execution state, and output ownership. Generation zero means a previously unused session. Reassignment increments the durable generation only after retirement proof freed the previous claims.
  5. Retry an uncertain reservation with the same operation and immutable payload. Canonicalized scope ordering does not change identity. Changing any other request field under the current key returns ErrConflict. A replay never reserves again or inherits later lease extensions. It returns the initial assignment/deadline and fresh authority/retired state; retired replay cannot start work. Old-generation requests remain fenced after replacement.
  6. Renew(fence, absoluteDeadline) monotonically extends live authority within the maximum lease. The absolute deadline makes repeated calls idempotent; expired authority cannot be revived. Lookup returns the current deadline, immutable initial deadline, and fresh DB-clock authority state. Claims in a Record describe the original allocation even after retirement; accounting rows, rather than this historical field, determine capacity use.
  7. Timeouts and database errors are UnknownOutcome, including ambiguous commits. Reconcile via Lookup and same-operation retry. A deadline is authority loss, not encoder retirement. Status/stop/retirement transport credentials must stay usable after expiry while start/media authority is denied. No time-based reclaim, unreachable-node reclaim, or startup cleanup exists.
  8. Retire(fence, evidence) invokes the injected verifier outside transactions and then checks the full fence under the transaction lock before release. Missing verifier fails closed. Verification must establish authenticated exact-generation process retirement, or trusted operator attestation of external host fencing. Permanent node loss can therefore strand capacity until an operator proves fencing. Network silence and elapsed time are insufficient. A verifier is a trust boundary, not an implementation provided here.

All mutations (and consistent Lookup) use one component-wide transaction lock; PostgreSQL uses the existing advisory-lock facility, and SQLite obtains a real write lock before reads, including with deferred in-memory transactions. The context/timeout bounds lock contention. This correctness-first serialization limits throughput; there is no speculative lock sharding. Queries touch only the bounded placement ledger and indexed scope accounting, not the media library.

Retirement deletes accounting rows but retains one bounded tombstone per logical session, with generation floor, bounded identities, operation hash, and at most 16 historical claims. LedgerUsage exposes total/retired rows. Quota exhaustion is ErrQuota. There is intentionally no TTL deletion: arbitrarily delayed old requests cannot be safely fenced after forgetting their generation floor. Raise quota consistently, reuse logical session identities with generation CAS, or replace/archive an entire cluster namespace only after externally establishing quiescence. Never simply delete tombstones to admit new work.

The transport and workers must enforce these fences and execution deadlines; this package alone does not stop FFmpeg, validate retirement evidence, provide clock synchronization, or authorize peers. Database clock sets authority. Remote execution needs its own conservative clock-skew/expiry enforcement rule. No leadership epoch is used as an execution generation.

Focused gate: GOMAXPROCS=2 CGO_ENABLED=0 go test -p 1 ./internal/placement. store.OpenTest automatically uses isolated PostgreSQL schemas when ETHER_TEST_POSTGRES is set. PostgreSQL 14 is included in the existing CI matrix.

Documentation

Overview

Package placement is a durable admission primitive, not a scheduler or an execution engine. Only trusted control-plane callers may configure policy or submit retirement attestations. Workers must separately enforce fences before starting/publishing/stopping work. Lease expiry revokes authority, never proves process retirement, and never frees capacity.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Claim

type Claim struct {
	Scope string
	Units int64
}

type Controller

type Controller struct{ ID, Boot string }

type Error

type Error string
const (
	ErrInvalid     Error = "placement: invalid request"
	ErrFenced      Error = "placement: stale fence or generation"
	ErrConflict    Error = "placement: operation payload conflict"
	ErrCapacity    Error = "placement: capacity exhausted"
	ErrPolicy      Error = "placement: missing or ineligible target/scope policy"
	ErrRetirement  Error = "placement: retirement proof required"
	ErrQuotaPolicy Error = "placement: fleet ledger quota differs; coordinate deployment policy"
	ErrQuota       Error = "placement: session ledger quota exhausted"
)
const ErrReceiptUnknown Error = "placement: exact retirement receipt unknown"

func (Error) Error

func (e Error) Error() string

type Fence

type Fence struct {
	Session    string
	Generation int64
	Controller Controller
	Target     Target
}

type History

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

History shares its explicitly supplied placement Service's DB and transaction policy. It owns no workers or goroutines. The namespace is trusted host config, never a caller request field; the DB also pins a single namespace for this ledger.

func NewHistory

func NewHistory(service *Service, configuredCluster string, verifier ReceiptVerifier) (*History, error)

func (*History) Lookup

Lookup returns exact persisted trusted evidence or unknown. It neither infers retirement from a generation floor nor supplies current execution authority.

func (*History) Record

func (h *History) Record(ctx context.Context, r RetirementReceipt) error

Record publishes evidence only for an already retired current exact fence. It cannot release claims. If replacement wins first, it fails closed. Prefer RetireWithReceipt to avoid that publication/replacement gap. Immutable replay of an existing trusted receipt succeeds after replacement without re-verifying discarded local payloads. Conflicting replay is always rejected.

func (*History) RetireWithReceipt

func (h *History) RetireWithReceipt(ctx context.Context, r RetirementReceipt, evidence string) error

RetireWithReceipt invokes BOTH trusted verifiers before taking placement:all, then atomically rechecks the full current fence and persists the receipt with the normal Retire claim release. An existing immutable receipt is read-only reconciliation and needs no fresh proof. UnknownOutcome requires exact Lookup; never discard local ownership on an unconfirmed result. No quota/floor is freed.

type HistoryKey

type HistoryKey struct {
	Cluster string
	Fence   Fence
}

HistoryKey identifies one exact generation in the configured cluster. A higher generation or a missing row is never an exact historical retirement receipt.

type Options

type Options struct {
	// AuthorityReady gates creation and extension, never reads or retirement.
	AuthorityReady func() bool
	MaxSessions    int // hard ledger bound, including terminal generation floors
	MaxLease       time.Duration
	LockTimeout    time.Duration
}

type QuotaStatus

type QuotaStatus struct {
	Capacity  int  `json:"capacity"`
	Used      int  `json:"used"`
	Retired   int  `json:"retired"`
	Exhausted bool `json:"exhausted"`
	Matches   bool `json:"matches"`
}

type ReceiptVerifier

type ReceiptVerifier interface {
	VerifyReceipt(context.Context, RetirementReceipt) error
}

ReceiptVerifier is a trusted host dependency, not a wire decoder. It MUST bind Key.Cluster to the actual configured cluster authority, authenticate the full fence, request hash and source owner/PlayID/revision/size, and independently establish exact encoder AND local input AND source-owner retirement. Source proof includes backend Done+nil Err and joined producers, not merely zero holds. NeverStarted requires an irreversible exact negative admission tombstone. Accepted renewal history must be worker-accepted, not just source-extended. Digests alone, silence, expiry and missing status are never proof. Verification runs outside placement locks; callers must retain ownership through publication.

type Record

type Record struct {
	InitialDeadline time.Time // immutable Reserve result; replay never inherits a renewal
	Fence
	Operation          string
	Role               config.NodeRole
	Deadline           time.Time
	Retired, Authority bool
	Claims             []Claim // immutable requested allocation, including after retirement
}

Record is the current durable assignment. Authority is a fresh DB-clock observation, not a permit to execute indefinitely. Deadline is unchanged on Reserve replay. Retired assignments must never be started, even on replay.

type RenewalSummary

type RenewalSummary struct {
	Operation, PreviousVersion uint64
	PreviousDeadline, Deadline time.Time
}

RenewalSummary preserves only the last worker-accepted CAS, never source-only extension evidence. It grants no authority and cannot be replayed as a renewal.

type Request

type Request struct {
	Session, Operation string
	ExpectedGeneration int64
	Controller         Controller
	Target             Target
	Role               config.NodeRole
	Claims             []Claim // immutable requested allocation, including after retirement
	Lease              time.Duration
}

type RetirementReceipt

type RetirementReceipt struct {
	Key                                   HistoryKey
	Operation                             string // placement reservation operation
	RequestHash                           [32]byte
	NeverStarted                          bool
	Source                                Target
	PlayID, Revision                      string
	Size                                  int64
	EncoderProof, InputProof, SourceProof [32]byte
	InitialDeadline                       time.Time
	Renewable                             bool
	Renewal                               *RenewalSummary
}

RetirementReceipt contains bounded identifiers and SHA-256 digests, never raw credentials, capabilities, URLs or proof payloads. RequestHash commits the exact immutable worker Start encoding. NeverStarted is reserved for authenticated stop-before-start proof whose admission barrier was sealed before attestation; it is not inferred from missing records. All three proofs remain mandatory.

type RetirementVerifier

type RetirementVerifier interface {
	VerifyRetired(context.Context, Fence, string) error
}

RetirementVerifier validates authenticated exact-fence retirement, or a trusted operator attestation that the old execution host has been externally fenced. Network silence, node absence, and elapsed deadlines are NOT sufficient proof. It runs outside transactions; the full fence is rechecked afterwards.

type Service

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

func New

func New(db *sql.DB, o Options, verifier RetirementVerifier) (*Service, error)

func (*Service) EnsureLedgerQuota

func (s *Service) EnsureLedgerQuota(ctx context.Context) error

EnsureLedgerQuota seeds or verifies the fleet's durable admission ceiling. It never raises an existing policy. Every fresh Reserve checks this same row under the placement transaction lock; stale coordinator settings fail closed.

func (*Service) LedgerStatus

func (s *Service) LedgerStatus(ctx context.Context) (QuotaStatus, error)

func (*Service) LedgerUsage

func (s *Service) LedgerUsage(ctx context.Context) (used, retired int, err error)

LedgerUsage exposes quota pressure. Retired rows ARE compact tombstones: claims are removed, but the fixed-size fence, generation and operation digest survive forever. There is intentionally no TTL deletion API: discarding a floor would allow an arbitrarily delayed generation-zero replay to resurrect old work. Raise MaxSessions consistently across coordinators or archive/replace the whole cluster namespace under externally established quiescence when quota is reached.

func (*Service) Lookup

func (s *Service) Lookup(ctx context.Context, session string) (Record, error)

Lookup reconciles unknown outcomes. Its snapshot and authority observation are serialized with mutations, so a mixed old fence/new claims record is impossible.

func (*Service) RaiseLedgerQuota

func (s *Service) RaiseLedgerQuota(ctx context.Context, expected int) error

RaiseLedgerQuota is an explicit monotonic compare-and-swap operation for a trusted operator. Updating deployment JSON alone cannot raise the fleet policy. Old coordinators keep existing-work renewal/retirement but cannot allocate new generations until configured with the newly committed ceiling.

func (*Service) RemoveTarget

func (s *Service) RemoveTarget(ctx context.Context, target Target, evidence string) error

RemoveTarget is an explicit trusted operator attestation that an exact boot has been permanently removed/fenced. It is never called by liveness pruning. That incarnation cannot be admitted again. A future boot is independent.

func (*Service) Renew

func (s *Service) Renew(ctx context.Context, f Fence, deadline time.Time) error

Renew uses an absolute deadline, so retries cannot extend the requested lease. It cannot revive expired authority. A stale renewal never touches replacements.

func (*Service) Reserve

func (s *Service) Reserve(ctx context.Context, request Request) (Record, error)

Reserve atomically admits all scopes or none. ExpectedGeneration=0 creates a new logical session; replacement requires the current retired generation. Idempotency keys are scoped to a session generation transition. Old replay is fenced by ExpectedGeneration after a replacement, including after compaction.

func (*Service) Retire

func (s *Service) Retire(ctx context.Context, f Fence, evidence string) error

Retire is the ONLY capacity release operation, including after timeout or permanent node loss. Proof verification runs before taking the component lock; an exact-fence recheck prevents stale proof from releasing a replacement.

func (*Service) RetireDisplaced

func (s *Service) RetireDisplaced(ctx context.Context, f Fence, evidence string) error

RetireDisplaced releases only an exactly verified former execution. Silence, missing membership and lease expiry are never accepted as process exit proof.

func (*Service) SetPool

func (s *Service) SetPool(ctx context.Context, scope string, role config.NodeRole, capacity int64) error

SetPool persists a role-specific resource budget. Zero closes admission; absent budgets fail closed. Use the same scope for a physical resource shared by multiple nodes. Reducing capacity below outstanding claims is rejected.

func (*Service) SetTarget

func (s *Service) SetTarget(ctx context.Context, target Target, roles config.NodeRoles, scopes map[config.NodeRole][]string) error

SetTarget persists the currently admitted boot and explicit enabled roles. It never releases reservations from any boot. An empty role list closes new admission (unlike the config default, explicit policy cannot imply unlimited). Only the authenticated membership/control-plane reconciler may call this; worker startup must not blindly overwrite another live incarnation.

func (*Service) Supersede

func (s *Service) Supersede(ctx context.Context, failed Fence, request Request, seal func(context.Context, *sql.Tx, Record) error) (Record, error)

Supersede revokes an execution without asserting its process has retired. Old capacity remains charged independently of the replacement. The trusted controller callback seals its operation and installs its route in this same transaction. An unknown outcome must retry the identical request.

type Target

type Target struct{ NodeID, Boot string }

type UnknownOutcome

type UnknownOutcome struct{ Err error }

UnknownOutcome means a database operation did not produce a confirmed result. Reconcile using Lookup and retry the SAME immutable operation. Never assume failure means that no reservation/execution exists.

func (*UnknownOutcome) Error

func (e *UnknownOutcome) Error() string

func (*UnknownOutcome) Unwrap

func (e *UnknownOutcome) Unwrap() error

Jump to

Keyboard shortcuts

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