postgres

package
v0.18.0 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// TTL is the lease duration for each EID lock row.
	TTL time.Duration `yaml:"ttl"`
	// AcquireBackoff is the initial wait between retry attempts when a lock is
	// contended. Successive waits grow exponentially and are jittered, so this is
	// the floor rather than a fixed poll interval.
	AcquireBackoff time.Duration `yaml:"acquireBackoff"`
	// AcquireMaxBackoff caps the exponential growth of AcquireBackoff, so a long
	// wait keeps checking at a steady rate instead of drifting towards the
	// deadline.
	AcquireMaxBackoff time.Duration `yaml:"acquireMaxBackoff"`
	// AcquireDeadline is the total time allowed to acquire all EID locks. It is
	// the entire budget for waiting out contention: callers are expected not to
	// wrap AcquireLocks in a retry loop of their own, since that would multiply
	// this deadline by their own attempt count.
	AcquireDeadline time.Duration `yaml:"acquireDeadline"`
	// Heartbeat is the interval at which held leases are renewed (~TTL/3).
	Heartbeat time.Duration `yaml:"heartbeat"`
	// Owner identifies this replica and is required: it scopes every lease
	// query, so two replicas sharing one owner value share their leases.
	// Defaults to the FSC node ID when empty; locker construction fails when
	// both this field and the node ID are empty or blank.
	Owner string `yaml:"owner"`
}

Config holds Postgres lease-table locking settings.

type Locker

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

Locker implements locker.Locker using a SQL lease table. Acquire and renew queries use Postgres-specific features (TIMESTAMPTZ, ON CONFLICT DO UPDATE … RETURNING, ::interval casts).

func New

func New(db *sql.DB, table string, cfg Config, replicaID id.ReplicaIDProvider) (*Locker, error)

New creates a Postgres-backed distributed Locker. The table is created if it does not exist. db must be a *sql.DB connected to Postgres.

cfg.Owner defaults to the identifier reported by replicaID (the FSC node ID in production). It returns an error wrapping errs.ErrLockerOwnerRequired when the resolved owner is empty or blank — including when replicaID is nil or reports an empty ID — because an owner shared by several replicas disables mutual exclusion across all of them. That check runs before the table is created, so a misconfigured node fails without touching the database.

func (*Locker) AcquireLocks

func (p *Locker) AcquireLocks(ctx context.Context, anchor string, eIDs ...string) error

AcquireLocks claims a lease on every enrollment ID in eIDs for anchor, across all replicas sharing the table.

Implementation: the IDs are deduplicated and sorted (dedup.AndSort) for the same deadlock-free ordering the in-memory locker relies on. It then retries tryAcquireAll — a single atomic upsert that succeeds only if it can claim every ID — until AcquireDeadline passes (or ctx is cancelled), backing off between attempts with exponential growth and jitter. On success it records the held IDs under anchor and starts a background heartbeat that renews the leases before they expire, so a long-running audit keeps its locks while a crashed replica's leases expire and become claimable by others. Any partial state is released on the give-up/cancel paths, except over an anchor that already holds a live session, whose leases are left alone.

A failure caused by another holder joins ErrLockContention, and additionally ErrLockAcquireTimeout once the waiting budget is spent, which tells callers this locker already waited and the attempt should not simply be repeated (see auditor.Service.acquireLocksWithRetry). AcquireDeadline is that budget.

An empty eIDs set is a successful acquisition of nothing: there is no lease to take, so no session is opened and AssertLocksHeld has nothing to verify.

Re-acquiring under a live anchor may keep or shrink its set, never grow it: see the Locker contract for why widening is refused with ErrLockSetWidened, and releaseDropped for what shrinking has to clean up.

func (*Locker) AssertLocksHeld

func (p *Locker) AssertLocksHeld(ctx context.Context, anchor string) error

AssertLocksHeld verifies this replica still holds every lease it acquired for anchor. It compares the number of IDs recorded locally at acquisition time against the count of matching, non-expired, owner-scoped rows in the table. A mismatch means a lease expired and may have been taken over by another replica, so it returns ErrLockNotHeld. Callers use this after long-running work to confirm their locks were not silently lost.

An anchor with no recorded session holds no leases, so there is nothing that could have been lost and the assertion succeeds. Reporting ErrLockNotHeld instead conflated "lost the locks I took" with "never took any", which failed two legitimate flows under this backend while both succeeded under the in-memory locker: a request whose inputs and outputs yield no enrollment IDs at all, and an auditor that validates and appends without calling Audit (so never acquires locks) — see the dvp and nft auditor views.

A session that failed renewal is not affected: heartbeatLoop leaves the session in place when it gives up, so its recorded IDs are still counted here and the mismatch is still reported.

func (*Locker) ReleaseLocks

func (p *Locker) ReleaseLocks(ctx context.Context, anchor string)

ReleaseLocks releases all leases held under anchor: it stops the background heartbeat for that anchor and deletes the corresponding rows from the table.

Jump to

Keyboard shortcuts

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