Documentation
¶
Overview ¶
Package db provides Postgres connection bootstrapping and schema migration for the Wardyn control plane. Postgres is the ONLY required dependency.
Index ¶
- Constants
- Variables
- func AdvisoryLock(ctx context.Context, pool *pgxpool.Pool, key int64) (release func(), err error)
- func AdvisoryLockKeyed(ctx context.Context, pool *pgxpool.Pool, class, obj int32, wait time.Duration) (release func(), err error)
- func AppClockAgeMicros(t, now time.Time) int64
- func AppClockAgeSQL(placeholder string) string
- func AuditChainCanary(ctx context.Context, pool *pgxpool.Pool) error
- func AuditChainLockTimeoutSQL() string
- func AuditDDLBypassRoutes(ctx context.Context, pool *pgxpool.Pool) ([]string, error)
- func AuditDDLProtected(ctx context.Context, pool *pgxpool.Pool) (bool, error)
- func Connect(ctx context.Context, dsn string) (*pgxpool.Pool, error)
- func LockHeld(ctx context.Context) error
- func LockRefused(err error) bool
- func Migrate(ctx context.Context, pool *pgxpool.Pool) error
- func MigrateAllowingUnknown(ctx context.Context, pool *pgxpool.Pool) error
- func MigrateConn(ctx context.Context, conn *pgxpool.Conn) error
- func TryAdvisoryLock(ctx context.Context, pool *pgxpool.Pool, key int64) (release func(), ok bool, err error)
- func TryAdvisoryLockConn(ctx context.Context, pool *pgxpool.Pool, key int64) (conn *pgxpool.Conn, release func(), ok bool, err error)
- func TryAdvisoryLockDedicated(ctx context.Context, pool *pgxpool.Pool, key int64) (conn *pgx.Conn, release func(), ok bool, err error)
- type AuditAppendPosture
- type LocalLocker
- type LockKey
- type Locker
- type PGLocker
- type SweepTicks
- type SweeperLeader
- func (l *SweeperLeader) Current(ctx context.Context, epoch int64) (bool, error)
- func (l *SweeperLeader) Go(ctx context.Context, fn func(context.Context))
- func (l *SweeperLeader) Info(ctx context.Context) (SweeperLeaderInfo, error)
- func (l *SweeperLeader) Join() (ctx context.Context, epoch int64, end func(), ok bool)
- func (l *SweeperLeader) Run(ctx context.Context)
- type SweeperLeaderInfo
Constants ¶
const ( // RunOpLockClass serializes the operations that act on one run's proxy and // broker credentials from a row they read earlier (a revive, the lease // sweep's stop). obj = the run id. RunOpLockClass int32 = 0x57524F50 // ASCII "WROP" // ADORunTokenLockClass serializes the mint and revoke of one run's Azure // DevOps run token. Reserved for ha-l2.4, which takes it; named here so the // order below is one list. ADORunTokenLockClass int32 = 0x57415254 // ASCII "WART" // AWSSSOLockClass is the AWS SSO refresh single-flight: obj = the // secret-store namespace the credential lives in. AWSSSOLockClass int32 = 0x57415753 // ASCII "WAWS" // ADOSignInLockClass is the Entra sign-in refresh single-flight (Azure // DevOps and Azure AI Foundry): obj = person and provider row. ADOSignInLockClass int32 = 0x5741444F // ASCII "WADO" // SiteConfigLockClass serializes the read-modify-write writers of the one // site-config document. SiteConfigLockClass int32 = 0x57534346 // ASCII "WSCF" // CapEnforcementLockClass serializes writers of the capability-enforcement // document. CapEnforcementLockClass int32 = 0x57434150 // ASCII "WCAP" // AuditSweepLockClass admits one audit-chain verify sweep at a time. It is // taken last and only ever tried, never waited for. AuditSweepLockClass int32 = 0x57415357 // ASCII "WASW" )
The classid half of each ordered lock's two-argument key. Distinct from every other class in this package (LoginSupersedeLockClass, SecretRowLockClass, PushPathListLockClass) and from the one-argument keys, which carry a different objsubid.
const AuditChainLockKey int64 = 0x5741524459_434841 // ASCII "WARDYCHA"
AuditChainLockKey serializes appends to the audit_events hash chain. Transaction-scoped, never session-scoped: released by the commit that makes the new row visible, so the next writer's head read can't miss it. Since 0056_audit_chain_serialize.sql the trigger takes it too and allocates seq inside the trigger under this lock, so position and chain link are decided together (0047 alone let two racing writers invert chain order against seq order, since seq was assigned before the trigger ran). In-tree insert paths still take it before their INSERT; re-entrant within a transaction, so the trigger's own acquisition is free for them. ponytail: ONE lock for the whole chain — a chain has exactly one head, and write volume is nowhere near contention. Upgrade only if that changes: per-partition chains with a key per partition.
const AuditPartitionLockKey int64 = 0x5741524459_415054 // ASCII "WARDYAPT"
AuditPartitionLockKey serializes the creators of audit_events partitions: two replicas booting at once, or a boot beside the daily sweeper, would otherwise both create the same month. audit_ensure_partitions takes it inside the database as a transaction lock; the literal in 0111_audit_partitioned.sql must equal this. It is deliberately NOT AuditChainLockKey: creating a month must not queue behind (or hold up) an append.
const AuditPartitionMonthsAhead = 12
AuditPartitionMonthsAhead is how many months past the current one audit_ensure_partitions keeps created. A constant, not a setting: the function itself refuses anything outside 1..24, and the boot and the daily sweeper both pass this.
const BootKeyLockKey int64 = 0x5741524459_424B59 // ASCII "WARDYBKY"
BootKeyLockKey serializes the CREATE path of cmd/wardynd's boot keys across WARDYN_HA replicas: without it, two booting against an empty store each generate a key and the loser serves one nobody else holds.
const DefaultPoolMaxConns = 10
DefaultPoolMaxConns is the pool size when the DSN leaves pool_max_conns unset, or the CPU count if that is larger. pgx's own default is the larger of 4 and the CPU count, but on a 4-CPU host the single-instance lock, the sweeper leader and the ground-truth rotator held three pooled connections for the process lifetime, so the reaper's tick lock took the fourth and waited forever for a fifth to prune, and every request after it hung. Since 0.8.7 those three hold connections of their own, outside the pool (TryAdvisoryLockDedicated); the default stays 10.
const GovernanceAssignmentLockClass int32 = 0x57474153 // ASCII "WGAS"
GovernanceAssignmentLockClass is the classid of the TRANSACTION-scoped two-argument lock keyed to one governance assignment's natural key (hashtext of subject_type and subject). The assignment upsert and the approval of a held assignment change both take it, so a create that was absent when it was proposed cannot be overwritten by an upsert that landed in between.
const GovernanceGraphLockClass int32 = 0x57474750 // ASCII "WGGP"
GovernanceGraphLockClass is the classid of the TRANSACTION-scoped two-argument lock store.PG.WriteGovernanceProfile and DeleteGovernanceProfile take (second key 0): one write to the governance profile graph at a time, so the cycle and depth checks they make are the ones the stored graph keeps.
const GovernanceTargetLockClass int32 = 0x57474754 // ASCII "WGGT"
GovernanceTargetLockClass is the classid of the TRANSACTION-scoped two-argument lock keyed to one target of a held governance change (hashtext of the kind and its natural key), for the kinds whose row may be absent: a capability grant, a restriction, the enforcement map, a role mapping. The direct write and the approval of a held change to that target both take it, so a write that landed between the approval's staleness check and its own write cannot be overwritten.
const GroundTruthRotatorLockKey int64 = 0x5741524459_475452 // ASCII "WARDYGTR"
GroundTruthRotatorLockKey elects the ground-truth token rotator's leader. Unlike ReaperAdvisoryLockKey, acquired ONCE and held for the process lifetime; other replicas back off and retry.
HONEST CEILING: at most one steady-state leader, not mutual exclusion — an advisory lock dies with its SESSION, not the process, so a Postgres restart/failover/idle-timeout can release it under a still-running leader and a standby takes over unaware. Harmless only because every write is an atomic rename of a stateless token; work needing genuine fencing must not reuse this key.
const KeyDomainAssignmentLockClass int32 = 0x574B4441 // ASCII "WKDA"
KeyDomainAssignmentLockClass is the classid of the TRANSACTION-scoped two-argument lock (second key 0) every key-domain assignment write takes, the direct write and the approval of a held one alike: one write at a time, so the membership check a set makes (two groups of one person in different domains) sees every assignment written before it, including another group's.
const LoginSupersedeLockClass int32 = 0x574C474E // ASCII "WLGN"
LoginSupersedeLockClass is the classid half of a TWO-argument advisory lock key (objid = one person's actor string folded to int32, store.LoginLocker). Serializes ONE person's concurrent sign-in launches: without it, two launches' independent supersede/insert/supersede statements can each read the other as not-yet-existing, leaving two live sandboxes each holding a captured AWS SSO session. Taken BLOCKING: the loser must run AFTER the winner, not skip the supersede. A separate key space from db.go's int64 keys — the one- and two-argument lock forms carry different objsubid, so they cannot collide.
const PushPathListLockClass int32 = 0x57505054 // ASCII "WPPT"
PushPathListLockClass is the classid of the TRANSACTION-scoped two-argument lock store.PG.RecordPushPathList takes, keyed to one run: that run's list inserts serialize, so the per-run cap it counts is the cap it enforces.
const ReaperAdvisoryLockKey int64 = 0x5741524459_524541 // ASCII "WARDYREA"
ReaperAdvisoryLockKey makes the lifecycle reap tick single-flight. Unlike migrateAdvisoryLockKey, only ever taken with TryAdvisoryLock: a losing replica SKIPS the tick, since a queued second reap is duplicate work.
const RunCapLockClass int32 = 0x57525243 // ASCII "WRRC"
RunCapLockClass is the classid of the TRANSACTION-scoped two-argument lock store.PG.CreateRunUnderCap takes (second key 0): one capped run insert at a time across replicas, so the count it checks is the cap it enforces.
const RunOutputLockClass int32 = 0x57524F55 // ASCII "WROU"
RunOutputLockClass is the classid of the TRANSACTION-scoped two-argument lock store.PG's run_outputs writers, readers and erasure take, keyed to one run: an erasure's tombstone and a writer's tombstone check are serialized, so no writer commits a row the erasure has already ruled out. A leaf: taken last inside the one transaction and never held while taking another lock, so it sits outside LockOrder.
const SecretConvertLockKey int64 = 0x5741524459_454E56 // ASCII "WARDYENV"
SecretConvertLockKey makes the boot conversion of legacy (v0) secrets rows to envelope v1 single-writer (secretstore/pg's ConvertV0): a second replica booting at the same moment waits, then finds nothing left to convert.
const SecretRekeyLockKey int64 = 0x5741524459_524B59 // ASCII "WARDYRKY"
SecretRekeyLockKey serializes `wardynd -rotate-age-key` maintenance mode: two concurrent rekeys would each re-encrypt from an old key the other already replaced, so the second is refused.
HONEST CEILING: does NOT detect a running wardynd (that's SingleInstanceLockKey). "Stop the daemon first" is an operator procedure, not enforced here — a live daemon would write ciphertext under an already-retired key.
const SecretRowLockClass int32 = 0x57534543 // ASCII "WSEC"
SecretRowLockClass is the classid of the TRANSACTION-scoped two-argument lock secretstore/pg takes around a store-mode Put, keyed to one (owner, name) row: one writer per row at a time across replicas. Taken with pg_advisory_xact_lock inside the Put's own transaction, released by the commit that writes the row — not through AdvisoryLockKeyed.
const SingleInstanceLockKey int64 = 0x5741524459_494E53 // ASCII "WARDYINS"
SingleInstanceLockKey is the RUNTIME half of the one-replica safety control (the Helm chart's `replicas > 1` render refusal is the other half). Taken once at boot, held for the process lifetime: a second instance refuses to start.
SECURITY: not modesty. wardynd keeps per-process state a second instance can't see — sharpest in the secret-masking registry (internal/secretmask), an in-memory map that FAILS OPEN: a recording uploaded to the wrong instance is persisted VERBATIM with live credentials in cleartext under a `success` audit event. The chart's refusal is render-time only; `kubectl scale` or an HPA defeats it silently.
HONEST CEILING, same mechanism as GroundTruthRotatorLockKey: at most one steady-state instance, not a fence.
const SweeperLeaderLockKey int64 = 0x5741524459_53574C // ASCII "WARDYSWL"
SweeperLeaderLockKey elects the one replica that runs the sweepers which must run once (approvals, recordings, credentials, the always-egress reconcile and the run pause). Acquired ONCE and held for the process lifetime by SweeperLeader; followers retry on a backoff and take over when the leader's session ends.
HONEST CEILING, as for GroundTruthRotatorLockKey: an advisory lock dies with its SESSION, so a Postgres failover can release it under a still-running leader. Unlike the rotator, this lock IS used for work that needs fencing, so each acquisition bumps the durable epoch in sweeper_leader (0110) and a multi-step operation re-checks its epoch (SweeperLeader.Current) before it writes.
const TerminalSandboxSweepLockKey int64 = 0x5741524459_545353 // ASCII "WARDYTSS"
TerminalSandboxSweepLockKey makes the terminal-sandbox sweep tick single-flight across control planes (#710), the same shape as ReaperAdvisoryLockKey and for the same reason SingleInstanceLockKey alone is not enough: that lock is at most one steady-state instance, not mutual exclusion (its own HONEST CEILING) — a deployment booted with WARDYN_HA skips the claim entirely, and a Postgres restart/failover can release its session under a still-running daemon while a second one boots and claims it. Either way, two tickers running at once would both re-run teardown for the same aged KILLED run, doubling its run.kill rows and calling the runner twice. Always taken with TryAdvisoryLock: a control plane that loses simply skips the tick, since a queued second sweep of the same page is pure duplicate work.
Variables ¶
var ( // ErrLockBusy: the lock is held elsewhere and the wait budget ran out (or, // for TryLock's caller, ok is false and this is not returned). ErrLockBusy = errors.New("db: the lock is held elsewhere") // ErrLockNoCapacity: every connection the lock pool may hold is in use. ErrLockNoCapacity = errors.New("db: the lock pool has no free connection") ErrLockUnavailable = errors.New("db: the lock could not be taken") // ErrLockOrder: a lock was taken out of LockOrder. ErrLockOrder = errors.New("db: lock taken out of order") // ErrLockLost is the cause of the context a hold hands out, once the // connection holding the lock is gone. ErrLockLost = errors.New("db: the lock was lost with its connection") )
Lock failures. A refusal (ErrLockBusy, ErrLockNoCapacity, ErrLockUnavailable) means the guarded work did not run and may be retried; ErrLockOrder is a programming error.
var AuditChainLockTimeout = 5 * time.Second
AuditChainLockTimeout bounds how long ANY writer waits for AuditChainLockKey. Since 0056 the trigger takes it on every audit_events insert, including from outside this repo, so a stray open transaction (an operator's psql session) can hold up every audit write with no Wardyn bug involved. Previously an unbounded request-path wait pinned a goroutine and a pool connection per stuck write, exhausting the pool at pool_max_conns=3.
5s is measured: a legitimate wait is other writers queueing for one nextval+head-read+sha256+insert each (low single-digit ms), so 5s absorbs a queue in the thousands, and stays under AuditSpool.Drain's 15s bound so the request path yields before the background drain does.
A write that loses the race is not dropped: on the request path it's spooled locally for the drain to replay; on the broker's mint transaction the audit insert shares the tx with the credential, so a timeout fail-closed refuses the MINT rather than issue an unaudited credential.
lock_timeout, not a context deadline: fires only on a lock wait (never a slow-but-progressing statement), server-side, SET LOCAL-scoped, reports SQLSTATE 55P03, and (measured on Postgres 17) also bounds the trigger's own acquisition, covering a writer that never takes the lock explicitly.
var ErrAdvisoryLockNoCapacity = errors.New("db: pool cannot spare a connection for an advisory lock")
ErrAdvisoryLockNoCapacity is AdvisoryLockKeyed's one STRUCTURAL refusal: the pool can't spare advisoryLockFreeConnsNeeded connections (every call at pool_max_conns=1; transiently on a busy larger pool). The caller proceeds unlocked and audits it; every other error is a wait or a database fault, and refuses.
var LockOrder = []int32{ RunOpLockClass, ADORunTokenLockClass, AWSSSOLockClass, ADOSignInLockClass, SiteConfigLockClass, CapEnforcementLockClass, AuditSweepLockClass, }
LockOrder is the one documented order. A lock may be taken while holding others only if its class appears LATER in this list than every class already held (the same key again is re-entrant and always allowed).
run operation, then the Azure DevOps run-token mint/revoke, then the AWS SSO refresh, then the Azure DevOps sign-in refresh (the order the credential erase takes its two), then the document locks, and the audit sweep last.
var LockPoolAcquireWait = 2 * time.Second
LockPoolAcquireWait bounds the wait for one of the LockPoolMaxConns slots.
var LockPoolMaxConns = 8
LockPoolMaxConns bounds the connections one process holds for locks at once; a lock beyond it waits LockPoolAcquireWait and is then refused. Nested locks share their caller's connection, so this bounds concurrent guarded operations, not locks.
var LockWait = 30 * time.Second
LockWait is the TOTAL budget a blocking lock waits before it refuses. Generous: a refresh holder's own worst case is two token calls and a retry delay, about 21 seconds, and a caller whose token already expired has nothing else to do but wait for it.
var LockWatchInterval = time.Second
LockWatchInterval is how often a hold checks that its connection still holds every lock it is meant to.
var LoginSupersedeLockWait = 5 * time.Second
LoginSupersedeLockWait is the TOTAL budget one caller spends trying to take a keyed lock before being REFUSED (retry) rather than let through unlocked; only ErrAdvisoryLockNoCapacity proceeds unlocked. Matches AuditChainLockTimeout: enough to absorb a deep queue of a rival launch's handful of indexed statements.
Functions ¶
func AdvisoryLock ¶ added in v0.8.0
AdvisoryLock is TryAdvisoryLock's BLOCKING sibling: waits for session-level advisory lock key until ctx ends, erroring rather than proceeding unlocked. Same release contract and pool_max_conns >= 2 need.
func AdvisoryLockKeyed ¶ added in v0.8.0
func AdvisoryLockKeyed(ctx context.Context, pool *pgxpool.Pool, class, obj int32, wait time.Duration) (release func(), err error)
AdvisoryLockKeyed takes the SESSION-level TWO-argument advisory lock (class, obj) on a connection borrowed from pool, waiting up to wait in TOTAL. TryAdvisoryLock's blocking sibling, for work that must be SERIALIZED rather than skipped. ErrAdvisoryLockNoCapacity is the one error a caller may treat as "proceed unlocked"; every other one means the work was not serialized and must not run.
SET LOCAL lock_timeout scopes server-side to a short transaction, but the LOCK itself is session-level and outlives that commit — the caller's guarded work is several independent statements after this returns, which a transaction-scoped lock could not cover.
Call the returned release (deferred, every path) to unlock and free the in-process slot — skipping it strands both for the life of the process.
func AppClockAgeMicros ¶ added in v0.7.0
AppClockAgeMicros returns how long ago the app-clock instant t was, as of the app-clock instant now, in microseconds, clamped to [0, maxAppClockAge]. Both arguments must come from the SAME clock: a duration between two readings of one clock carries no skew, so it's safe to hand to a different clock — passing a value read back from the DATABASE as t would reintroduce the skew.
Clamped at zero so a value stamped in the future reads as "now" rather than a negative interval. The ZERO TIME is NOT special-cased here: it means different things to different callers, so each states its own answer.
func AppClockAgeSQL ¶ added in v0.7.0
AppClockAgeSQL renders an app-measured age, in MICROSECONDS, as an interval to subtract from the database's own clock. Microseconds as a bigint (not a float or pgx interval) matches timestamptz's own resolution exactly.
func AuditChainCanary ¶ added in v0.7.0
auditChainCanary appends ONE synthetic audit row inside a transaction it always rolls back, and asserts it came out chained: row_hash set, prev_hash equal to the head read under the same lock. It is the FUNCTIONAL half of the boot check — catalog shape (trigger present, enabled, shipped function) is not enough: 0057 shipped a trigger that was all three and did not work (a SECURITY DEFINER pinned to the wrong search_path), so Migrate reported success while every audit insert then failed or silently linked to the wrong table's head. Never committed, so there is no synthetic row in anybody's audit log — cost is one burned seq per boot, which the sweep tolerates.
A chain that demonstrably does not chain refuses the boot, since serving over it writes a log the verify sweep will report broken for as long as it runs. A canary that could not be RUN (chain lock busy, statement cancelled) reports at ERROR and lets the boot continue — those are bounded, transient and self-clearing.
AuditChainCanary runs the same check against an arbitrary pool, for the caller that needs it on a pool Migrate never touched: with a split migrate/app DSN, real audit writes go through a different role with different search_path/privileges, so a canary that only ever ran as the migrator would prove nothing about the pool that actually writes audit rows. Same function, not a second copy, so the two boot paths can't disagree about what a working chain is.
func AuditChainLockTimeoutSQL ¶ added in v0.7.0
func AuditChainLockTimeoutSQL() string
AuditChainLockTimeoutSQL applies AuditChainLockTimeout to the current transaction. SET takes no bind parameters — the value is a formatted-in integer, never caller input. SET LOCAL reverts at commit/rollback.
func AuditDDLBypassRoutes ¶ added in v0.7.0
AuditDDLBypassRoutes names EVERY route by which pool's role can reach past audit_events' append-only guard. An empty slice means protected (what AuditDDLProtected checks for). Named routes let the boot log tell an operator which one to close, since the fix differs per route and a bare bool would mislead. Fails safe: a missing table, or any error, is a bypass.
func AuditDDLProtected ¶
AuditDDLProtected reports whether pool's role is UNABLE to bypass the audit_events append-only triggers: not a superuser or MEMBER (via the grant chain, not just ownership) of the owner role, holds no TRIGGER privilege, and cannot SET session_replication_role (silences every simply-enabled trigger, no DDL needed). Fails safe on any ambiguity (missing table, error). Delegates to AuditDDLBypassRoutes so no second query can disagree.
SECURITY: TRIGGER privilege alone — without owning or superusing the table — lets a role add its own BEFORE INSERT trigger and rewrite any field (prev_hash/row_hash included) before the chain trigger hashes the forgery; ensureAuditTriggers guards the trigger SHAPE, this only checks whether the privilege to create one is held.
All four legs test MEMBERSHIP, not a role attribute: reading rolsuper off current_user directly misses the managed-Postgres shape (GRANT admin role TO app role, NOINHERIT) that can SET ROLE to superuser while every direct-attribute check reports protected.
Deliberately NOT modeled: pg_write_all_data (write rights, but append-only triggers still fire) and host-level escalation (a compromised host survives no claim here anyway).
func Connect ¶
Connect opens a pgxpool to dsn and performs a lightweight liveness check. Returns the pool; caller owns Close().
func LockHeld ¶ added in v0.8.6
LockHeld asks the database, on the hold's own connection, whether every lock ctx carries is still held: nil only when it is, ErrLockLost when it is not or the session cannot be asked. It answers whether or not ctx has ended, and that is its use: a hold's context ended first by its parent keeps that cause for good, so a lock lost afterwards never shows in it. Work that must not act once another replica may hold the lock asks here immediately before and after acting. A session-level lock lost with its session is never held by that session again, so a nil after acting means the lock was held throughout. A local hold cannot be lost and is always held.
func LockRefused ¶ added in v0.8.6
LockRefused reports whether err is a refusal the caller answers with a retry rather than a fault of its own.
func Migrate ¶
Migrate applies all migrations in internal/db/migrations/*.sql in lexical order. Each migration runs inside its own transaction; already-applied filenames are skipped. Idempotent. It REFUSES a database that records an applied migration this binary does not ship: see unknownAppliedMigrations.
func MigrateAllowingUnknown ¶ added in v0.7.13
MigrateAllowingUnknown is Migrate with the unknown-migration refusal turned into a WARN — the break-glass behind WARDYN_ALLOW_UNKNOWN_MIGRATIONS.
func MigrateConn ¶ added in v0.8.6
MigrateConn is Migrate on a connection the caller already holds, for the one caller that must keep every statement of the migration on the session that owns a lock it took first (wardynd -migrate-only). Same refusals and the same migration lock as Migrate; the caller still owns conn and releases it.
func TryAdvisoryLock ¶ added in v0.5.0
func TryAdvisoryLock(ctx context.Context, pool *pgxpool.Pool, key int64) (release func(), ok bool, err error)
TryAdvisoryLock takes session-level advisory lock key on a connection borrowed from pool WITHOUT waiting, reporting ok=false if held elsewhere. Call the returned release (deferred) or strand a pooled conn permanently. Only for BOUNDED work: the conn is unavailable to everyone else until release, so the caller's own queries need a second conn — requires pool_max_conns >= 2 (a 1-conn pool self-deadlocks).
func TryAdvisoryLockConn ¶ added in v0.8.6
func TryAdvisoryLockConn(ctx context.Context, pool *pgxpool.Pool, key int64) (conn *pgxpool.Conn, release func(), ok bool, err error)
TryAdvisoryLockConn is TryAdvisoryLock that also hands back the connection holding the lock, for the caller that must run its own statements on that same session (wardynd -migrate-only). The conn is only valid until release is called; the caller must not Release it itself.
func TryAdvisoryLockDedicated ¶ added in v0.8.7
func TryAdvisoryLockDedicated(ctx context.Context, pool *pgxpool.Pool, key int64) (conn *pgx.Conn, release func(), ok bool, err error)
TryAdvisoryLockDedicated is TryAdvisoryLockConn for a lock held for the PROCESS LIFETIME: the session is dialled for this lock from pool's own connection settings and never comes out of the pool, so a long hold cannot starve the queries the pool serves (pgxpool.Acquire blocks on an empty pool rather than erroring). Each hold is one more server connection on top of pool_max_conns.
conn is for the holder's own statements on that session and is not safe for concurrent use; it is valid until release, which unlocks and then closes it within dedicatedLockReleaseWait. Unlock comes first so the lock is free when release returns, not when the server notices the socket is gone.
Types ¶
type AuditAppendPosture ¶ added in v0.8.6
type AuditAppendPosture struct {
// Role is current_user on the pool asked.
Role string
// CanAppend is EXECUTE on audit_append. Every audit write goes through that function, so a role
// without it cannot write one audit row.
CanAppend bool
// DirectInsert is INSERT on audit_events itself. Always true for the owner (a single-DSN
// install); the chain trigger still refuses a row audit_append did not allocate, which binds a
// well-behaved or mistaken writer, not a hostile owner.
DirectInsert bool
// PublicExecute names the audit functions any role may execute (a NULL ACL means the PUBLIC
// default). 0111 revokes it; an owner's later REPLACE or an ALTER DEFAULT PRIVILEGES can bring
// it back.
PublicExecute []string
}
AuditAppendPosture is what the serving pool's role can do to the audit log, as the catalog says it.
func AuditAppendPostureOf ¶ added in v0.8.6
AuditAppendPostureOf reads the AuditAppendPosture of pool's role. Read-only; it fails closed (a missing function reads as "cannot append").
type LocalLocker ¶ added in v0.8.6
type LocalLocker struct {
// contains filtered or unexported fields
}
LocalLocker is the in-process Locker for a store with no database: the same order, nesting and refusal rules, serializing within one process only.
func NewLocalLocker ¶ added in v0.8.6
func NewLocalLocker() *LocalLocker
NewLocalLocker returns an empty LocalLocker.
type LockKey ¶ added in v0.8.6
type LockKey struct{ Class, Obj int32 }
LockKey names one lock: its class (one of the *LockClass constants) and the object it guards, folded to 32 bits.
func HeldLocks ¶ added in v0.8.6
HeldLocks lists the locks ctx carries, outermost first (tests, diagnostics).
func NewLockKey ¶ added in v0.8.6
NewLockKey keys class to parts, which are joined unambiguously and folded with crc32. A collision is harmless: it only serializes two unrelated objects. No parts is the class's single document lock.
type Locker ¶ added in v0.8.6
type Locker interface {
// Lock waits up to wait for k. The returned context is ctx's, carrying the
// hold, and is cancelled when the lock is lost or released; the guarded
// work must use it.
Lock(ctx context.Context, k LockKey, wait time.Duration) (context.Context, func(), error)
// TryLock takes k only if it is free: ok is false, with a nil error, when
// it is held elsewhere.
TryLock(ctx context.Context, k LockKey) (context.Context, func(), bool, error)
}
Locker takes the ordered locks. PG-backed lockers serialize across replicas; the local one serializes within a process (a store with no database).
type PGLocker ¶ added in v0.8.6
type PGLocker struct {
// contains filtered or unexported fields
}
PGLocker is the Postgres-backed Locker.
func LockerFor ¶ added in v0.8.6
LockerFor returns the process's one PGLocker for pool, created on first use from the pool's own connection settings. Every store.PG over one pool shares it, so LockPoolMaxConns bounds the process, not each copy of the store.
func NewPGLocker ¶ added in v0.8.6
NewPGLocker is a locker of its own with size connection slots (tests).
type SweepTicks ¶ added in v0.8.6
type SweepTicks struct {
// contains filtered or unexported fields
}
SweepTicks is the Postgres-backed sweephealth.Store: one sweep_ticks row per sweep, shared by every replica (migration 0120).
func NewSweepTicks ¶ added in v0.8.6
func NewSweepTicks(pool *pgxpool.Pool) *SweepTicks
NewSweepTicks returns the shared tick record on pool.
func (*SweepTicks) RecordTick ¶ added in v0.8.6
func (s *SweepTicks) RecordTick(ctx context.Context, sweep, replica string, success bool, at time.Time) error
RecordTick writes an attempt, or a success, for sweep. Both columns move with GREATEST, so a write older than the stored time (a replica with a slow clock, or a delayed write) leaves it where it was. A success also counts as an attempt, so a row whose attempt write was lost still has both. replica follows the newest attempt.
func (*SweepTicks) Ticks ¶ added in v0.8.6
func (s *SweepTicks) Ticks(ctx context.Context) (map[string]sweephealth.Tick, error)
Ticks reads every sweep's record.
type SweeperLeader ¶ added in v0.8.6
type SweeperLeader struct {
// contains filtered or unexported fields
}
SweeperLeader elects and monitors the sweeper leader. Build one with NewSweeperLeader, start Run once, and gate each sweeper with Go or Join.
func NewSweeperLeader ¶ added in v0.8.6
func NewSweeperLeader(pool *pgxpool.Pool, holder string) *SweeperLeader
NewSweeperLeader returns a leader for pool. holder names this process in sweeper_leader.holder (forensics and status only; the lock decides).
func (*SweeperLeader) Current ¶ added in v0.8.6
Current reports whether epoch is still the durable one: false once another term has acquired the lease, which is when a stale leader must stop writing.
func (*SweeperLeader) Go ¶ added in v0.8.6
func (l *SweeperLeader) Go(ctx context.Context, fn func(context.Context))
Go runs fn once per term this process leads, with the term's context, and returns when ctx ends. fn is expected to loop until its context is cancelled (the periodic sweepers) or to do its work once and return (a boot reconcile): either way it is not started again inside the same term.
func (*SweeperLeader) Info ¶ added in v0.8.6
func (l *SweeperLeader) Info(ctx context.Context) (SweeperLeaderInfo, error)
Info reads the durable leader record. The row is seeded by 0110, so a missing row is an error.
func (*SweeperLeader) Join ¶ added in v0.8.6
func (l *SweeperLeader) Join() (ctx context.Context, epoch int64, end func(), ok bool)
Join joins the current term as one unit of leader work. ok is false on a follower. ctx ends when the lease is lost, and end must be called when the work is done: the leader waits for every end before it lets go of the lock. epoch is the term's, for Current.
func (*SweeperLeader) Run ¶ added in v0.8.6
func (l *SweeperLeader) Run(ctx context.Context)
Run elects until ctx ends: take the lock, bump the epoch, lead, and on loss stop the sweeps and try again.
type SweeperLeaderInfo ¶ added in v0.8.6
type SweeperLeaderInfo struct {
Epoch int64
Holder string
AcquiredAt time.Time
// Self is true when this process holds the lease right now.
Self bool
}
SweeperLeaderInfo is the durable record of the current leader, for the status reporting that tells a follower who leads.