pgstore

package
v0.6.1 Latest Latest
Warning

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

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

Documentation

Overview

Package pgstore is the Postgres backend for running several halo-server and halo-shadow replicas (ADR-0021): versioned forward-only migrations, the shared record logs and KV behind internal/storage, the hash-chained audit table, fleet reports, rollout documents, leader leases with fencing tokens, and halo-shadow's pair table and global budget. It speaks database/sql over the pgx stdlib driver and stores opaque bytes: the packages that own the types encode them.

Index

Constants

View Source
const DefaultMaxConns = 16

DefaultMaxConns is the pool size when Options.MaxConns is 0.

Variables

View Source
var ErrAuditChanged = errors.New("pgstore: an audit entry changed while it was being redacted")

ErrAuditChanged: a body to redact is no longer the one the caller read.

View Source
var ErrFenced = storage.ErrFenced

ErrFenced means a fenced write was refused: the writer no longer holds the lease whose token it was opened with (another replica leads now). It is storage.ErrFenced, so callers outside pgstore can test for it.

View Source
var ErrInsecureTLS = errors.New("pgstore: database URL does not verify the server's certificate")

ErrInsecureTLS is CheckTLS's refusal.

View Source
var ErrNotEmpty = errors.New("pgstore: target database is not empty")

ErrNotEmpty means an import's target already holds state.

View Source
var ErrSchema = errors.New("pgstore: schema mismatch")

ErrSchema means the database schema does not match this binary.

View Source
var ErrServiceTokenLimit = errors.New("service token limit reached")

ErrServiceTokenLimit is PutServiceToken's refusal at the active-token cap.

Functions

func CheckTLS

func CheckTLS(dsn string) error

CheckTLS refuses a URL whose connection to a non-loopback host is not TLS with a verified certificate and host name (sslmode=verify-full): sslmode disable, allow, prefer and require (no certificate check), and verify-ca (no host-name check) all leave the connection open to a man-in-the-middle on the network. Loopback hosts and Unix sockets never leave the machine and are accepted.

func HolderID

func HolderID() string

HolderID is a replica-unique lease holder id: the host (pod) name and a random suffix, so two processes on one host never share a lease.

func SchemaVersion

func SchemaVersion() int

SchemaVersion is the newest migration this binary knows.

Types

type AuditLink struct {
	Seq        uint64
	Hash, Prev string
	Entry      []byte
	At         time.Time
}

AuditLink is one stored audit entry: its chain position and encoded body.

type BudgetCounter

type BudgetCounter struct {
	Budget, Key string
	Bucket      int64 // bucket start, unix seconds
	USD         float64
	Tokens      int64
}

BudgetCounter is one bucket of one budget counter.

type DB

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

DB is a Postgres handle shared by every store of one process.

func Open

func Open(ctx context.Context, dsn string) (*DB, error)

Open connects to dsn with default Options and checks the connection.

func OpenWith

func OpenWith(ctx context.Context, dsn string, o Options) (*DB, error)

OpenWith connects to dsn and checks the connection.

func (*DB) Acquire

func (d *DB) Acquire(ctx context.Context, name, holder string, ttl time.Duration) (int64, bool, error)

Acquire implements LeaseStore on halo_leases.

func (*DB) Add

func (d *DB) Add(ctx context.Context, ns, key string, val []byte, exp time.Time) (bool, error)

Add implements storage.KV: the upsert only overwrites an expired row, and Postgres's row lock on conflict makes concurrent Adds of one key race-free.

func (*DB) AppendAudit

func (d *DB) AppendAudit(ctx context.Context, chain func(headSeq uint64, headHash string) (AuditLink, error)) error

AppendAudit appends one entry to the hash chain. The chain head row is locked (SELECT ... FOR UPDATE) for the whole transaction, so concurrent writers on any replica append strictly one after another; chain receives the head and returns the new link, which must extend it.

func (*DB) AppendPair

func (d *DB) AppendPair(ctx context.Context, id string, at time.Time, row []byte) error

AppendPair stores one encoded pair row (idempotent on id).

func (*DB) ApplyBudget

func (d *DB) ApplyBudget(ctx context.Context, batch string, incs []BudgetCounter) (duplicate bool, err error)

ApplyBudget adds incs to the counters unless batch was already applied, in one transaction: the batch id's primary key decides, so of two replicas receiving the same retry exactly one counts it. The upserts run in (budget, key, bucket) order, so two transactions touching the same rows lock them in the same order and cannot deadlock. Nothing is pruned here: see PruneBudgets.

func (*DB) AuditEntries

func (d *DB) AuditEntries(ctx context.Context) ([][]byte, error)

AuditEntries returns every stored entry body, oldest first. ponytail: reads the whole chain like the file backend; page by seq if it gets big.

func (*DB) AuditHead

func (d *DB) AuditHead(ctx context.Context) (uint64, string, error)

AuditHead returns the chain head (seq 0 and "" when empty).

func (*DB) BudgetCounters

func (d *DB) BudgetCounters(ctx context.Context, keys [][2]string) ([]BudgetCounter, error)

BudgetCounters returns the buckets of the given (budget, key) counters, or of every counter when keys is nil.

func (*DB) CheckSchema

func (d *DB) CheckSchema(ctx context.Context) error

CheckSchema returns ErrSchema unless this binary can run against the database: at exactly its schema version, or newer when every newer migration still accepts it (a rolling upgrade's old pods, or a binary rollback). Behind means run `halo-server migrate` (or --auto-migrate).

func (*DB) ClaimPending

func (d *DB) ClaimPending(ctx context.Context, dedupe, requestID string, holder func(id string) (known, pending bool)) (bool, error)

ClaimPending makes requestID the one pending request for dedupe; ok=false when another pending request holds it. The primary key decides between concurrent claims. holder reports what the caller's request log says about the current holder: a known request that is no longer pending (decided) is replaced at once; an unknown one only once its claim is orphanClaimAge old. Replacement is a compare-and-swap on the holder's id.

func (*DB) Close

func (d *DB) Close() error

Close closes the pool.

func (*DB) CountAuthFailure added in v0.4.0

func (d *DB) CountAuthFailure(ctx context.Context, key string, window time.Duration) (int64, error)

CountAuthFailure adds one to key's failure count and returns it, in one upsert: a live row (younger than window on the database clock) is incremented, an expired or absent one restarts at 1 with a new window.

func (*DB) Counts

func (d *DB) Counts(ctx context.Context) (map[string]int64, error)

Counts returns row counts: "journal/<stream>" per log stream, "audit", "reports" and "docs/<kind>".

func (*DB) Delete

func (d *DB) Delete(ctx context.Context, ns, key string) error

Delete implements storage.KV.

func (*DB) DeleteReportsWhere added in v0.6.0

func (d *DB) DeleteReportsWhere(ctx context.Context, match func(report []byte) bool, dry bool) (latest, history int64, err error)

DeleteReportsWhere deletes every latest report and history row whose body match selects (ADR-0040 erasure) and returns how many of each it deleted (with dry, would delete). ponytail: reads every report; erasure is a rare admin action.

func (*DB) Docs

func (d *DB) Docs(ctx context.Context, kind string) ([][]byte, error)

Docs returns every document of kind, by name.

func (*DB) EachAudit

func (d *DB) EachAudit(ctx context.Context, from uint64, fn func(seq uint64, entry []byte) bool) error

EachAudit streams stored audit entries with seq >= from, in seq order, until fn returns false.

func (*DB) EachAuditDesc

func (d *DB) EachAuditDesc(ctx context.Context, fn func(seq uint64, entry []byte) bool) error

EachAuditDesc streams stored audit entries newest first, in pages of auditDescPage keyed on seq (ORDER BY seq DESC LIMIT), until fn returns false: a tail read touches only the rows it returns, not the whole table.

func (*DB) Errors

func (d *DB) Errors() int64

Errors is the number of failed statements since Open.

func (*DB) Fenced

func (d *DB) Fenced(f Fence) *Fenced

Fenced returns d's view fenced by f.

func (*DB) Get

func (d *DB) Get(ctx context.Context, ns, key string) ([]byte, bool, error)

Get implements storage.KV.

func (*DB) GetDoc

func (d *DB) GetDoc(ctx context.Context, kind, name string) ([]byte, bool, error)

GetDoc returns document kind/name; ok is false when there is none.

func (*DB) Import

func (d *DB) Import(ctx context.Context, fn func(*Importer) error) error

Import runs fn in one transaction that it first checks is starting from an empty database (every table Counts covers). fn returning an error, or a failed commit, leaves the database as it was.

func (*DB) Leased

func (d *DB) Leased(ctx context.Context, name string, ttl time.Duration, fn func(ctx context.Context, view *Fenced))

Leased runs fn on exactly one replica at a time: every replica calls it with the same name, the one holding lease name runs fn with a context that ends when the lease is lost, and the document writes of the view fn gets are fenced by that lease. Leased returns when ctx ends. ttl 0 = the default (15s).

func (*DB) Migrate

func (d *DB) Migrate(ctx context.Context) (applied []int, err error)

Migrate applies every pending migration in one transaction, under an advisory lock so concurrent replicas apply each exactly once, and returns the versions it applied. A database newer than this binary is fine when its newer migrations still accept this binary (see minBinaryHeader), and an error otherwise.

func (*DB) OpenFencedLog

func (d *DB) OpenFencedLog(ctx context.Context, name string, replay func([]byte), f Fence) (storage.Log, error)

OpenFencedLog is OpenLog whose appends fail with ErrFenced once f's lease has moved on, so a deposed leader cannot write behind the new one.

func (*DB) OpenLog

func (d *DB) OpenLog(ctx context.Context, name string, replay func([]byte)) (storage.Log, error)

OpenLog implements storage.Opener: it replays the stream and tails it.

func (*DB) Pairs

func (d *DB) Pairs(ctx context.Context, fn func(row []byte) error) error

Pairs calls fn with every stored pair row, oldest first.

func (*DB) Ping

func (d *DB) Ping(ctx context.Context) error

Ping checks the database is reachable (readiness).

func (*DB) PruneAudit added in v0.6.0

func (d *DB) PruneAudit(ctx context.Context, through uint64) (int64, error)

PruneAudit deletes entries with seq <= through (ADR-0040 retention). The head row is untouched, so appends continue the chain.

func (*DB) PruneBudgets

func (d *DB) PruneBudgets(ctx context.Context, retention time.Duration) (int64, error)

PruneBudgets drops what no budget can still count, judged on the database clock and nothing else: buckets that started more than retention ago (the caller passes the longest window policy allows), and batch ids older than budgetBatchMemory. It never consults a replica's policy, so a replica on a stale policy cannot delete the counters of a budget it does not know yet; a removed budget's rows simply age out. Safe on any replica, any number of times.

func (*DB) PrunePairs

func (d *DB) PrunePairs(ctx context.Context, cutoff time.Time) (int, error)

PrunePairs deletes pairs older than cutoff.

func (*DB) PruneReports added in v0.6.0

func (d *DB) PruneReports(ctx context.Context, cutoff time.Time) (int64, error)

PruneReports deletes reporters whose report was last seen (the lastSeen the server stamped, its clock) before cutoff, with their history (ADR-0040).

func (*DB) Put

func (d *DB) Put(ctx context.Context, ns, key string, val []byte, exp time.Time) error

Put implements storage.KV.

func (*DB) PutDoc

func (d *DB) PutDoc(ctx context.Context, kind, name string, body []byte) error

PutDoc stores document kind/name.

func (*DB) PutReport

func (d *DB) PutReport(ctx context.Context, key, hostname string, report []byte, keep int) error

PutReport upserts the latest report for key and appends it to key's history, keeping the newest keep entries.

func (*DB) PutRevocation added in v0.4.0

func (d *DB) PutRevocation(ctx context.Context, r Revocation) error

PutRevocation records r in one upsert that never weakens a live row: the epoch and the expiry only move forward (GREATEST), and the record follows the larger epoch. Expired rows are pruned.

func (*DB) PutServiceToken added in v0.5.0

func (d *DB) PutServiceToken(ctx context.Context, t ServiceToken, maxActive int, now time.Time) error

PutServiceToken inserts a new token unless maxActive tokens (not revoked, expiring after now) already exist (ErrServiceTokenLimit): the count and the insert are one transaction under an advisory lock, so concurrent creations on any replicas never exceed it. An existing id is an error (ids are random: a collision is a bug, never an overwrite). Long-expired rows are pruned.

func (*DB) Raise

func (d *DB) Raise(ctx context.Context, ns, key string, at int64, exp time.Time) error

Raise implements storage.KV with one upsert: a live row keeps the greater of its value and at, and the later expiry.

func (*DB) RedactAudit added in v0.6.0

func (d *DB) RedactAudit(ctx context.Context, bodies map[uint64][2][]byte) error

RedactAudit replaces the stored bodies of existing entries (ADR-0040 erasure): seq -> {body read, redacted body}. Each row is swapped only if it still holds the body read (compare and swap), all or none, so two erasures racing cannot undo each other. seq, hash and prev never change.

func (*DB) Release

func (d *DB) Release(ctx context.Context, name, holder string) error

Release implements LeaseStore.

func (*DB) ReleasePending

func (d *DB) ReleasePending(ctx context.Context, dedupe, requestID string) error

ReleasePending drops dedupe's claim if requestID still holds it.

func (*DB) ReportHistory

func (d *DB) ReportHistory(ctx context.Context, key string, limit int) ([][]byte, error)

ReportHistory returns up to limit reports for key, newest first.

func (*DB) Reports

func (d *DB) Reports(ctx context.Context) ([][]byte, error)

Reports returns the latest report of every reporter.

func (*DB) Reserve

func (d *DB) Reserve(ctx context.Context, scope, id string, amount, limit float64, ttl time.Duration) (bool, error)

Reserve books amount against scope's budget of limit USD, globally across replicas: one conditional UPDATE (spent+reserved+amount <= limit) decides. Reservations older than ttl (a worker that died before settling) are first reaped and charged in full, since their upstream calls may have been paid.

func (*DB) Revocations added in v0.4.0

func (d *DB) Revocations(ctx context.Context) ([]Revocation, error)

Revocations returns every live revocation, ordered by kind and key.

func (*DB) RevocationsFor added in v0.4.0

func (d *DB) RevocationsFor(ctx context.Context, jtis, subjects []string) ([]Revocation, error)

RevocationsFor returns the live revocations of the given jtis and subjects: one indexed query, for the per-request check.

func (*DB) RevokeServiceToken added in v0.5.0

func (d *DB) RevokeServiceToken(ctx context.Context, id string, record []byte) (ServiceToken, bool, error)

RevokeServiceToken marks id revoked and replaces its record, unless it is already revoked (the first revocation's record stands). It returns the row as stored and whether it exists.

func (*DB) ServiceToken added in v0.5.0

func (d *DB) ServiceToken(ctx context.Context, id string) (ServiceToken, bool, error)

ServiceToken returns the row with id: one primary-key lookup, for the per-request check.

func (*DB) ServiceTokens added in v0.5.0

func (d *DB) ServiceTokens(ctx context.Context) ([]ServiceToken, error)

ServiceTokens lists every row, oldest expiry first.

func (*DB) Settle

func (d *DB) Settle(ctx context.Context, scope, id string, amount, charged float64) error

Settle swaps reservation id for the charged amount. If the reservation was already reaped (and charged in full) only the excess is added.

func (*DB) Spent

func (d *DB) Spent(ctx context.Context, scope string) (float64, error)

Spent returns scope's settled spend.

func (*DB) Take

func (d *DB) Take(ctx context.Context, ns, key string) ([]byte, bool, error)

Take implements storage.KV: DELETE ... RETURNING is atomic, so of concurrent Takes exactly one sees the row.

func (*DB) TryLock

func (d *DB) TryLock(ctx context.Context, ns, key, owner string, ttl time.Duration) (bool, error)

TryLock implements storage.KV: the owner is the value, the expiry is computed by the database (now() + ttl), and the upsert only takes a row that is expired or already the owner's.

func (*DB) Unlock

func (d *DB) Unlock(ctx context.Context, ns, key, owner string) error

Unlock implements storage.KV: it deletes only owner's row.

func (*DB) WriteMetrics

func (d *DB) WriteMetrics(w io.Writer)

WriteMetrics writes the database metrics in Prometheus text format.

type Elector

type Elector struct {
	Store  LeaseStore
	Name   string // lease name, e.g. "controller"
	Holder string // unique per replica (pod name + random suffix)
	// TTL is the lease length (default 15s); Renew the renewal period
	// (default TTL/3). A leader that has not renewed by TTL-Renew after its
	// last successful renewal started steps down, so it is gone before the
	// lease can expire on the database clock.
	TTL, Renew time.Duration
	Log        *slog.Logger
	Now        func() time.Time // test seam; default time.Now
	// contains filtered or unexported fields
}

Elector runs a singleton loop on exactly one replica. Every replica runs an Elector on the same lease; the one holding it runs lead, and stops it (cancels its context and waits) when the lease is lost or cannot be renewed in time, before any other replica can take the lease over.

func (*Elector) Leader

func (e *Elector) Leader() bool

Leader reports whether this replica currently leads.

func (*Elector) Run

func (e *Elector) Run(ctx context.Context, lead func(ctx context.Context, token int64))

Run campaigns until ctx ends. lead runs while this replica leads, with a context cancelled on step-down and the lease's fencing token; if it returns on its own it is restarted on the next round while the lease is still held. On exit Run stops lead and releases the lease so a peer takes over at once.

func (*Elector) Token

func (e *Elector) Token() int64

Token is the fencing token of the current leadership (0 when not leading).

func (*Elector) WriteMetrics

func (e *Elector) WriteMetrics(w io.Writer)

WriteMetrics writes halo_leader (1 when this replica leads) and leadership changes.

type Fence

type Fence struct {
	Name  string
	Token int64
}

Fence ties a log's appends to a leader lease: an append commits only while lease Name is held with fencing token Token.

type Fenced

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

Fenced is the view of d a leader writes through: its logs (OpenLog) and document writes (PutDoc) commit only while f's lease is still held at f's token, so a deposed leader finishing a tick cannot overwrite its successor.

func (*Fenced) GetDoc

func (v *Fenced) GetDoc(ctx context.Context, kind, name string) ([]byte, bool, error)

GetDoc reads like DB.GetDoc.

func (*Fenced) OpenLog

func (v *Fenced) OpenLog(ctx context.Context, name string, replay func([]byte)) (storage.Log, error)

OpenLog implements storage.Opener with fenced appends.

func (*Fenced) PutDoc

func (v *Fenced) PutDoc(ctx context.Context, kind, name string, body []byte) error

PutDoc is DB.PutDoc, refused with ErrFenced once the lease has moved on.

type Importer

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

Importer writes a whole data-dir import inside one transaction: either all of it commits, after the caller verified it through the same transaction, or none of it does and the import can simply be run again.

func (*Importer) AddBudget

func (im *Importer) AddBudget(incs []BudgetCounter) error

AddBudget adds counter buckets.

func (*Importer) AppendAudit

func (im *Importer) AppendAudit(l AuditLink) error

AppendAudit appends an already-chained entry; it must extend the head.

func (*Importer) AppendJournal

func (im *Importer) AppendJournal(stream string, body []byte) error

AppendJournal appends one record to stream.

func (*Importer) Counts

func (im *Importer) Counts() (map[string]int64, error)

Counts is DB.Counts inside the import transaction.

func (*Importer) EachAudit

func (im *Importer) EachAudit(fn func(seq uint64, entry []byte) bool) error

EachAudit streams the imported chain (seq order) for verification.

func (*Importer) PutDoc

func (im *Importer) PutDoc(kind, name string, body []byte) error

PutDoc stores document kind/name.

func (*Importer) PutReport

func (im *Importer) PutReport(key, hostname string, report []byte) error

PutReport stores a reporter's latest report (history starts with it).

func (*Importer) PutRevocation added in v0.4.0

func (im *Importer) PutRevocation(r Revocation) error

PutRevocation stores a revocation as DB.PutRevocation does (no pruning).

func (*Importer) PutServiceToken added in v0.5.0

func (im *Importer) PutServiceToken(t ServiceToken) error

PutServiceToken stores a token as DB.PutServiceToken does (no pruning).

type LeaseStore

type LeaseStore interface {
	// Acquire takes or renews lease name for holder for ttl. ok is false while
	// another holder's lease is unexpired. token is the fencing token: it is
	// unchanged across renewals and grows whenever the lease is (re)taken.
	Acquire(ctx context.Context, name, holder string, ttl time.Duration) (token int64, ok bool, err error)
	// Release expires holder's lease now so another replica can take it.
	Release(ctx context.Context, name, holder string) error
}

LeaseStore grants named leases. Expiry is judged on the store's clock (for Postgres, the database's now()), never on a replica's.

type Options

type Options struct {
	// MaxConns caps the pool (default DefaultMaxConns). Every replica holds
	// up to this many connections: size it against the server's max_connections.
	MaxConns int
	// InsecureTLS accepts a connection to a non-loopback host without
	// verified TLS (sslmode below verify-full). Off by default: the database
	// holds the audit chain, device token hashes and the kill list.
	InsecureTLS bool
}

Options tunes Open.

type Revocation added in v0.4.0

type Revocation struct {
	Kind, Key string
	Epoch     int64
	Expires   time.Time
	Record    []byte
}

Revocation is one credential revocation row (ADR-0026). Kind is "jti" or "subject"; Epoch is a subject's revoked-before time (unix seconds, 0 for a jti); Record is opaque to this package.

type ServiceToken added in v0.5.0

type ServiceToken struct {
	ID      string
	Hash    []byte
	Expires time.Time
	Revoked bool
	Record  []byte
}

ServiceToken is one service-account API token row (ADR-0036). Hash is the SHA-256 of the token; the token itself is never stored. Record is opaque to this package.

Directories

Path Synopsis
Package pgtest opens throwaway Postgres schemas for integration tests.
Package pgtest opens throwaway Postgres schemas for integration tests.

Jump to

Keyboard shortcuts

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