catalog

package
v0.0.6 Latest Latest
Warning

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

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

Documentation

Overview

Package catalog is the control plane for database-per-tenant fabriq (catalog mode; spec 2026-07-03, D1/D2): a small, strongly-consistent registry mapping each tenant to the cluster and database that own its source of truth.

The catalog exists ONLY in catalog mode. Hash-sharded and single-shard deployments keep fabriq's zero-catalog property: minting a tenant is its first write. In catalog mode that trade is reversed on purpose — dedicated databases require provisioning, and provisioning requires a place to record it.

The catalog is deliberately tiny: Get on the request path (cached by the shard directory), Put with optimistic concurrency for the provisioning state machine, List for the sweeper and the fleet migration roller.

Index

Constants

View Source
const (
	DegradedMetaKey   = "catalog"
	DegradedMetaValue = "degraded"
)

DegradedMetaKey / DegradedMetaValue are the fabriqerr Meta.Detail entry that flags a routing answer as served from a replica while the primary was unreachable. Failover stamps it on a replica NotFound, and the directory stamps it on any error it derives from a replica-sourced entry (MarkDegradedDetail); IsDegraded recognises it. The directory's cache predicate excludes degraded answers so a lagged replica can never pin a tenant for a TTL.

Variables

This section is empty.

Functions

func IsDegraded

func IsDegraded(err error) bool

IsDegraded reports whether err is a replica-sourced answer served while the primary was unreachable — the directory must not negative-cache it.

func MarkDegradedDetail

func MarkDegradedDetail(detail map[string]string) map[string]string

MarkDegradedDetail stamps the degraded marker onto a Meta.Detail map, allocating one if nil, and returns it. Callers building a routing error from a replica-sourced entry use it so the directory will not cache the answer.

func ValidateEntry

func ValidateEntry(e Entry) error

ValidateEntry checks the invariants every store enforces before writing.

Types

type Catalog

type Catalog interface {
	// Get returns a tenant's entry, or CodeNotFound.
	Get(ctx context.Context, tenantID string) (Entry, error)

	// Put writes an entry with optimistic concurrency on UpdatedAt:
	// a zero UpdatedAt creates (CodeAlreadyExists if present); a non-zero
	// UpdatedAt updates iff it matches the stored token
	// (CodeVersionConflict otherwise). Invalid entries are CodeInvalidInput.
	Put(ctx context.Context, e Entry) (Entry, error)

	// List pages through every entry in stable (tenant id) order.
	List(ctx context.Context, cursor Cursor, limit int) ([]Entry, Cursor, error)
}

Catalog is the control-plane port. Implementations: the Postgres control database (adapters/postgres) and fabriqtest.FakeCatalog. Both are gated by the contract suite in catalogtest.

type Cursor

type Cursor string

Cursor is an opaque List pagination token ("" = first page).

type Entry

type Entry struct {
	TenantID  string `json:"tenantId"`
	ClusterID string `json:"clusterId"`
	Database  string `json:"database"`
	State     State  `json:"state"`
	// Version is the fabriq migration version the tenant database was last
	// observed at (grove migration versions, e.g. "202607030030"). The
	// router fails closed when it is below the binary's floor.
	Version string `json:"version"`
	// Schema is the tenant's Postgres schema in schema-per-tenant
	// consolidation mode (e.g. "tenant_acme"); empty in database mode. When
	// set it selects the tenant's namespace via search_path within the
	// shared consolidation database named by Database.
	Schema string `json:"schema"`
	// UpdatedAt is the optimistic-concurrency token: Put succeeds only when
	// it matches the stored row (zero UpdatedAt = create, must not exist).
	// The store stamps a fresh UpdatedAt on every successful write.
	UpdatedAt time.Time `json:"updatedAt"`
	// FromReplica is a transient routing-provenance flag: true when this entry
	// was served by a read replica during a primary outage (set only by
	// Failover, never persisted or serialized). The directory treats any answer
	// it derives from a replica-sourced entry — a version-gate or not-active
	// CodeUnavailable — as degraded (non-cacheable), because a lagged replica
	// could report a stale-old version or a superseded state.
	FromReplica bool `json:"-"`
}

Entry is one tenant's catalog row.

func (Entry) ShardID

func (e Entry) ShardID() string

ShardID derives the routing shard id for this entry: one tenant database = one shard, addressed as "{clusterId}/{database}".

type Failover

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

Failover is a high-availability catalog.Catalog for catalog mode's routing read path (spec 2026-07-03 HA, H1-H3): it reads the PRIMARY first and falls through to read-only REPLICAS only when the primary is unreachable. Writes and any read that the primary answers definitively never touch a replica, so steady-state staleness is zero.

It is injected into shard.CatalogDirectory in place of the bare primary. Every other catalog consumer (provisioning, adminapi, the sweeper scan, the reconciler elector) keeps the concrete primary store — those want the authoritative write node, not a possibly-lagged replica.

func NewFailover

func NewFailover(primary Catalog, replicas ...Catalog) *Failover

NewFailover wraps a primary with zero or more read replicas. With no replicas it is a transparent pass-through to the primary.

func (*Failover) Get

func (f *Failover) Get(ctx context.Context, tenantID string) (Entry, error)

Get reads the primary first; on a transport failure it walks the replicas.

func (*Failover) List

func (f *Failover) List(ctx context.Context, cursor Cursor, limit int) ([]Entry, Cursor, error)

List falls through to replicas on any primary error. Unlike Get, List has no definitive business answer to distinguish (no NotFound), so every primary error is a transport failure — replica-fallback needs no primaryDefinitive gate here. Only Get is wired to the routing directory in v1; List fallback is kept for interface coherence.

func (*Failover) Put

func (f *Failover) Put(ctx context.Context, e Entry) (Entry, error)

Put is primary-only: fabriq never writes to a replica (H4, no split-brain).

func (*Failover) ReadStats

func (f *Failover) ReadStats() (primary, replica, failover int64)

ReadStats returns cumulative routing-read counters (primary-served, replica-served, and failover events). Used by the observability poller.

type State

type State string

State is a tenant's lifecycle state in the catalog.

const (
	// StatePending — row exists, nothing provisioned yet.
	StatePending State = "pending"
	// StateCreating — CREATE DATABASE issued.
	StateCreating State = "creating"
	// StateMigrating — database exists, migration chain running.
	StateMigrating State = "migrating"
	// StateActive — routable; the only state the directory serves.
	StateActive State = "active"
	// StateSuspended — deliberately routed off (offboarding, incident).
	StateSuspended State = "suspended"
	// StateFailed — provisioning failed; listable for operators, never
	// routable. The provisioner may resume it.
	StateFailed State = "failed"
)

func (State) Valid

func (s State) Valid() bool

Valid reports whether s is a known lifecycle state.

Directories

Path Synopsis
Package catalogtest is the catalog contract suite: the behaviors every catalog.Catalog implementation must exhibit, run against fabriqtest.FakeCatalog (unit) and the Postgres control store (integration) — the conformance-kit discipline: drift between fake and adapter is a failing test.
Package catalogtest is the catalog contract suite: the behaviors every catalog.Catalog implementation must exhibit, run against fabriqtest.FakeCatalog (unit) and the Postgres control store (integration) — the conformance-kit discipline: drift between fake and adapter is a failing test.

Jump to

Keyboard shortcuts

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