Documentation
¶
Overview ¶
Package shard routes a tenant's source-of-truth operations to the Postgres shard that holds it. A tenant lives entirely on one shard (ADR 0007), so routing is a directory lookup, never a distributed transaction or a scatter-gather read.
The router is engine-neutral: it speaks only the fabriq capability ports (command.Store, query.RelationalQuerier/VectorQuerier/TSQuerier/SpatialQuerier) and implements those same ports by delegating to the resolved shard. The facade, executor and every call site are therefore unchanged — sharding is one more adapter behind a port. Single-Postgres deployments use the degenerate one-shard Set (Single), for which routing is a no-op.
Index ¶
- Variables
- type Analytics
- func (a *Analytics) Query(ctx context.Context, q query.AnalyticsQuery, into any) error
- func (a *Analytics) QueryRaw(ctx context.Context, into any, sql string, args ...any) error
- func (a *Analytics) Track(ctx context.Context, events []query.AnalyticsEvent) error
- func (a *Analytics) UpsertInsightFacts(ctx context.Context, facts []insights.Fact) error
- type AutoscaleConfig
- type CatalogDirOption
- type Dialer
- type Directory
- func Cached(inner Directory, ttl time.Duration) Directory
- func CatalogDirectory(cat catalog.Catalog, ttl time.Duration, opts ...CatalogDirOption) Directory
- func CatalogDirectoryWithClock(cat catalog.Catalog, ttl time.Duration, now func() time.Time) Directory
- func HashDirectory(ids ...string) Directory
- func PinnedDirectory(pins map[string]string, fallback Directory) Directory
- type Documents
- func (d *Documents) ApplyUpdate(ctx context.Context, docID string, update []byte) error
- func (d *Documents) ApplyUpdateWithSeq(ctx context.Context, docID string, update []byte) (int64, error)
- func (d *Documents) Compact(ctx context.Context, docID string) error
- func (d *Documents) Snapshot(ctx context.Context, docID string) (document.Materialized, error)
- func (d *Documents) Sync(ctx context.Context, docID string, stateVector []byte) ([]byte, error)
- type DynamicSet
- type Live
- func (l *Live) After(ctx context.Context, q livequery.LiveQuery, after livequery.Cursor, limit int) ([]livequery.Row, error)
- func (l *Live) Members(ctx context.Context, q livequery.LiveQuery) ([]string, error)
- func (l *Live) Snapshot(ctx context.Context, q livequery.LiveQuery, limit int) ([]livequery.Row, error)
- type LiveSource
- type PoolManager
- type PoolManagerConfig
- type ProjectionStateStore
- type Provider
- type Relational
- func (r *Relational) Get(ctx context.Context, entity, id string, into any) error
- func (r *Relational) GetMany(ctx context.Context, entity string, ids []string, into any) error
- func (r *Relational) List(ctx context.Context, entity string, q query.ListQuery, into any) error
- func (r *Relational) Query(ctx context.Context, into any, sql string, args ...any) error
- type ReplaySource
- type Router
- type ScaleEvent
- type SchemaRouter
- type Set
- func (s *Set) Acquire(ctx context.Context) (Shard, context.Context, func(), error)
- func (s *Set) AcquireFor(ctx context.Context, tenantID string) (Shard, context.Context, func(), error)
- func (s *Set) All() []Shard
- func (s *Set) For(ctx context.Context) (Shard, error)
- func (s *Set) ForTenant(ctx context.Context, tenantID string) (Shard, error)
- func (s *Set) IDs() []string
- func (s *Set) Len() int
- func (s *Set) ResolveID(ctx context.Context, tenantID string) (string, error)
- type Shard
- type Spatial
- func (s *Spatial) Covering(ctx context.Context, q query.CoverQuery, into any) error
- func (s *Spatial) Delete(ctx context.Context, entity, id string) error
- func (s *Spatial) Get(ctx context.Context, entity, id string) (geom query.Geometry, meta map[string]any, ok bool, err error)
- func (s *Spatial) Upsert(ctx context.Context, entity, id string, geom query.Geometry, ...) error
- func (s *Spatial) Within(ctx context.Context, q query.SpatialQuery, into any) error
- type Store
- type Timeseries
- type Vector
- func (v *Vector) Delete(ctx context.Context, entity, id string) error
- func (v *Vector) DeleteByMeta(ctx context.Context, entity string, filter map[string]string) error
- func (v *Vector) Get(ctx context.Context, entity, id string) ([]float32, error)
- func (v *Vector) Similar(ctx context.Context, q query.VectorQuery, into any) error
- func (v *Vector) Upsert(ctx context.Context, entity, id string, embedding []float32, ...) error
Constants ¶
This section is empty.
Variables ¶
var ( DirGrow = scaleGrow DirShrink = scaleShrink )
DirGrow / DirShrink expose scale directions to other packages (wiring, tests). The underlying type stays unexported; only the values escape.
Functions ¶
This section is empty.
Types ¶
type Analytics ¶ added in v0.0.6
type Analytics struct {
// contains filtered or unexported fields
}
Analytics routes query.AnalyticsQuerier (per-tenant customer-facing analytics: event ingest, cube query, raw SQL escape hatch).
func NewAnalytics ¶ added in v0.0.6
NewAnalytics wraps a Set as a routing query.AnalyticsQuerier.
func (*Analytics) QueryRaw ¶ added in v0.0.6
QueryRaw routes the raw-SQL escape hatch to the tenant's shard.
func (*Analytics) Track ¶ added in v0.0.6
Track routes a customer-analytics event batch to the tenant's shard.
func (*Analytics) UpsertInsightFacts ¶ added in v0.0.6
UpsertInsightFacts implements insights.FactSink by routing a proj:insights fact batch to the ctx tenant's shard: Acquire resolves the shard from the tenant already stamped on ctx (the consumer's handle stamps tenant+scope before calling the sink), so this lands on the same shard — and through the same inTenantTx/RLS containment — as every other per-tenant analytics call the Track/Query/QueryRaw methods above make. sh.Analytics is typed query.AnalyticsQuerier on the Shard struct, but the concrete adapter (*postgres.InsightsAdapter) also implements insights.FactSink; the type assertion recovers that second facet.
type AutoscaleConfig ¶ added in v0.0.6
type AutoscaleConfig struct {
Min, Max int // hard floor / ceiling
Interval time.Duration // controller tick (default 5s)
ConnBudget int // total server-side conns; 0 = no budget clamp
PerShardConns int // conns per shard pool (default 4)
HeapSoftLimit uint64 // heap-in-use bytes above which growth freezes; 0 = off
GrowFactor float64 // default 1.5
ShrinkStep int // default 1
ConfirmTicks int // consecutive same-direction ticks before acting (default 3)
CooldownTicks int // ticks suppressed after a change (default 3)
MissRatioHigh float64 // grow above this (default 0.20)
EvictRateHigh float64 // evictions/acquires above this (default 0.10)
OpenFillLow float64 // open/cap below this = oversized (default 0.75)
HeldUtilLow float64 // held/open below this = idle (default 0.50)
HeapShrinkMult float64 // heap > softLimit*this ⇒ force shrink (default 1.10)
}
AutoscaleConfig fully parameterizes the pool autoscaler. The user-facing config (fabriq.AdaptivePoolConfig) sets the load-bearing few; the policy knobs default in withDefaults so tests and callers can omit them.
type CatalogDirOption ¶ added in v0.0.6
type CatalogDirOption func(*catalogDirectory)
CatalogDirOption configures a CatalogDirectory.
func WithCatalogClock ¶ added in v0.0.6
func WithCatalogClock(now func() time.Time) CatalogDirOption
WithCatalogClock injects the clock (TTL tests).
func WithMinVersion ¶ added in v0.0.6
func WithMinVersion(v string) CatalogDirOption
WithMinVersion fails routing CLOSED for tenants whose database is recorded below the binary's migration floor (spec D7): serving a schema older than the code expects corrupts, so the tenant gets a typed CodeUnavailable until the fleet roller catches it up.
type Dialer ¶ added in v0.0.6
Dialer opens the adapter stack for one tenant database. close tears the stack down when the pool manager evicts the shard.
type Directory ¶
Directory resolves a tenant to its shard id. Implementations range from a constant (one shard) to a cached, versioned table (many shards) — the router does not care which.
func Cached ¶
Cached memoizes a directory's tenant→shard answers for ttl, so the hot path does not pay a lookup per operation. Placement is stable (a tenant's shard does not change without an explicit move), so a short TTL is purely a freshness bound for future catalog-backed directories; the hash directory is constant and caching it is free insurance.
func CatalogDirectory ¶ added in v0.0.6
CatalogDirectory routes tenants by the db-per-tenant control plane (spec 2026-07-03, D1/D2): each ACTIVE catalog entry resolves to its own shard, addressed "{clusterId}/{database}". Non-active states resolve to typed errors the gateway maps to honest statuses instead of 500s:
unknown tenant → CodeNotFound pending/creating/migrating → CodeUnavailable (provisioning) suspended / failed → CodeUnavailable
Both positive and negative answers are cached for ttl. Placement is stable, so the TTL is a freshness bound: a suspension or a just-provisioned tenant takes effect within one TTL — and repeated lookups of unknown tenants cannot storm the control database.
func CatalogDirectoryWithClock ¶ added in v0.0.6
func CatalogDirectoryWithClock(cat catalog.Catalog, ttl time.Duration, now func() time.Time) Directory
CatalogDirectoryWithClock is retained for tests predating the option form.
func HashDirectory ¶
HashDirectory places tenants across a fixed set of shards by hashing the tenant id — stateless and deterministic, so every replica routes a tenant the same way without coordination. It is the simplest directory: it needs no catalog table, but changing the shard set re-places tenants (and so requires a data move). A catalog-backed directory with explicit placement (ADR 0007) is the drop-in upgrade when tenants must be moved individually.
func PinnedDirectory ¶ added in v0.0.6
PinnedDirectory routes explicitly pinned tenants to their pinned shard and delegates every other tenant to fallback. It is the config-driven override for residency / high-value tenants that must live on a specific shard without replacing the whole directory: an exact tenant-id match wins, the hash placement stays in charge of the rest. Pins are copied, so later caller mutations do not change routing. PinnedDirectory does not know the shard set — a pin naming an unknown shard is rejected by Config.Validate at Open time, and Set.ForTenant fails loudly for any directory that names a shard the set does not hold.
type Documents ¶ added in v0.0.6
type Documents struct {
// contains filtered or unexported fields
}
Documents routes document.Store by the ctx tenant — the catalog-mode document plane, where each tenant's CRDT tables live in its own database (lifting ADR 0007 step 2's primary-only restriction).
func NewDocuments ¶ added in v0.0.6
NewDocuments builds the document-plane router.
func (*Documents) ApplyUpdate ¶ added in v0.0.6
ApplyUpdate implements document.Store.
func (*Documents) ApplyUpdateWithSeq ¶ added in v0.0.6
func (d *Documents) ApplyUpdateWithSeq(ctx context.Context, docID string, update []byte) (int64, error)
ApplyUpdateWithSeq routes the seq-returning apply (the live fan-out decorator needs the log seq for gap detection).
type DynamicSet ¶ added in v0.0.6
type DynamicSet struct {
// contains filtered or unexported fields
}
DynamicSet is the catalog-mode Router: the directory resolves the ctx tenant to its dedicated shard id and the provider keeps that shard's pools alive.
func NewDynamicSet ¶ added in v0.0.6
func NewDynamicSet(dir Directory, provider Provider) *DynamicSet
NewDynamicSet builds the catalog-mode router.
func (*DynamicSet) Acquire ¶ added in v0.0.6
Acquire implements Router (ctx unchanged — schema stamping, if any, is the SchemaRouter decorator's job).
func (*DynamicSet) AcquireFor ¶ added in v0.0.6
func (d *DynamicSet) AcquireFor(ctx context.Context, tenantID string) (Shard, context.Context, func(), error)
AcquireFor implements Router (ctx unchanged).
type Live ¶ added in v0.0.6
type Live struct {
// contains filtered or unexported fields
}
Live is the catalog-mode live-query router: it resolves the ctx tenant to its shard and serves snapshots/refills from that shard's own database. Mirrors the Documents/Store/Relational routers.
type LiveSource ¶ added in v0.0.6
type LiveSource interface {
Snapshot(ctx context.Context, q livequery.LiveQuery, limit int) ([]livequery.Row, error)
After(ctx context.Context, q livequery.LiveQuery, after livequery.Cursor, limit int) ([]livequery.Row, error)
Members(ctx context.Context, q livequery.LiveQuery) ([]string, error)
}
LiveSource is the shard's live-query snapshot/refill oracle (Postgres is the exact-top-N truth). Static single-shard deployments serve it from the primary; catalog mode fills Shard.Live per tenant database.
type PoolManager ¶ added in v0.0.6
type PoolManager struct {
// contains filtered or unexported fields
}
PoolManager implements Provider: lazy dial on first touch (singleflight), refcounted release, LRU idle-first eviction at the cap, and a per-shard dial breaker so a down database cannot be dial-stormed.
func NewPoolManager ¶ added in v0.0.6
func NewPoolManager(dial Dialer, cfg PoolManagerConfig) *PoolManager
NewPoolManager builds a PoolManager over the dialer.
func (*PoolManager) Cap ¶ added in v0.0.6
func (p *PoolManager) Cap() int
Cap reports the current effective MaxActive (static or adaptive).
func (*PoolManager) CloseAll ¶ added in v0.0.6
func (p *PoolManager) CloseAll() error
CloseAll tears down every open shard (process shutdown). Held shards are closed too — pgx pool Close blocks until in-flight connections return.
func (*PoolManager) Stats ¶ added in v0.0.6
func (p *PoolManager) Stats() (open, held int)
Stats reports live pool-manager counters (observability).
type PoolManagerConfig ¶ added in v0.0.6
type PoolManagerConfig struct {
// MaxActive caps concurrently-open shards (LRU-evicted, idle first).
// Zero falls back to 128.
MaxActive int
// AcquireTimeout bounds how long Acquire waits for capacity when every
// open shard is busy. Zero falls back to 5s.
AcquireTimeout time.Duration
// DialBackoff is the base negative-cache window after a failed dial
// (doubled per consecutive failure, capped at 32x). Zero = 2s.
DialBackoff time.Duration
// Adaptive, when non-nil, enables autoscaling of MaxActive. The static
// MaxActive is the default (nil).
Adaptive *AutoscaleConfig
// OnScale is called after each cap change (best-effort; must not block).
OnScale func(ScaleEvent)
// contains filtered or unexported fields
}
PoolManagerConfig tunes the dynamic pool lifecycle.
type ProjectionStateStore ¶ added in v0.0.6
type ProjectionStateStore interface {
projection.StateRepo
// SetApplied records the applied version (projection.AppliedRecorder).
SetApplied(ctx context.Context, tenantID, proj, aggregate, aggID string, version int64) error
// Tenants lists every tenant this shard has bookkeeping for.
Tenants(ctx context.Context) ([]string, error)
}
ProjectionStateStore is the per-shard projection bookkeeping the engines, rebuilder and WaitForProjection route to (satisfied by the Postgres adapter's StateRepo).
type Provider ¶ added in v0.0.6
Provider owns shard lifecycles in catalog mode: Acquire returns a live shard for the id (dialing it on first touch), and the release func MUST be called when the operation completes so eviction can make progress.
type Relational ¶
type Relational struct {
// contains filtered or unexported fields
}
Relational routes query.RelationalQuerier (reads of the source of truth).
func NewRelational ¶
func NewRelational(set Router) *Relational
NewRelational wraps a Set as a routing query.RelationalQuerier.
type ReplaySource ¶ added in v0.0.6
type ReplaySource interface {
SnapshotEntities(ctx context.Context, tenantID string, fn func(env event.Envelope) error) error
AggregateVersions(ctx context.Context, tenantID, entity string) (map[string]int64, error)
Repair(ctx context.Context, tenantID string, d projection.Drift) error
}
ReplaySource is the shard's event-truth surface for blue-green rebuilds and the drift reconciler: snapshot the tenant's aggregates as events, read their current versions, and republish a repair event onto the tenant's outbox. Satisfied by the Postgres adapter; static deployments leave Shard.Replay nil (the worker plane routes via its concrete adapter map), catalog mode fills it per tenant database.
type Router ¶ added in v0.0.6
type Router interface {
Acquire(ctx context.Context) (Shard, context.Context, func(), error)
AcquireFor(ctx context.Context, tenantID string) (Shard, context.Context, func(), error)
}
Router resolves the ctx tenant to a live shard. Acquire pairs the shard with the routing context to use downstream and a release func: the static Set releases nothing and returns ctx unchanged (its pools live for the process), while catalog-mode DynamicSet refcounts pooled shards so the pool manager never closes a shard mid-operation. The returned context is the seam through which schema-per-tenant mode stamps search_path (SchemaRouter); every other Router returns the input ctx verbatim.
type ScaleEvent ¶ added in v0.0.6
ScaleEvent is emitted after each cap change (OnScale hook).
type SchemaRouter ¶ added in v0.0.6
type SchemaRouter struct {
// contains filtered or unexported fields
}
SchemaRouter wraps a Router and stamps the tenant's consolidation-mode schema (pathctx) onto the routing context, so the adapter's per-tx stamp sets search_path to the tenant's schema within the shared consolidation database. It is wired only in schema isolation (Config.Catalog.Isolation == "schema"); the shard it wraps (a DynamicSet over a consolidation database) is shared by many tenants, and this decorator is the one seam that distinguishes them per request.
The schema is derived deterministically from the tenant id (pathctx.SchemaForTenant), matching what the provisioner persisted in Entry.Schema — so routing needs no extra catalog lookup on the hot path.
func NewSchemaRouter ¶ added in v0.0.6
func NewSchemaRouter(inner Router) *SchemaRouter
NewSchemaRouter builds the schema-stamping decorator over inner.
func (*SchemaRouter) Acquire ¶ added in v0.0.6
Acquire implements Router: resolve the ctx tenant, then stamp its schema.
func (*SchemaRouter) AcquireFor ¶ added in v0.0.6
func (r *SchemaRouter) AcquireFor(ctx context.Context, tenantID string) (Shard, context.Context, func(), error)
AcquireFor implements Router: acquire the consolidation shard through the inner router, then return a context carrying the tenant's search_path.
type Set ¶
type Set struct {
// contains filtered or unexported fields
}
Set is the tenant -> shard routing table: a directory plus the shards it can name.
func New ¶
New builds a routing Set from a directory and its shards. Every shard needs a non-empty, unique id; the set needs at least one.
func Single ¶
Single builds the degenerate one-shard Set: every tenant routes to sh. This is what single-Postgres deployments use, so the routing layer is a no-op until a second shard is configured. An empty sh.ID defaults to "0".
func (*Set) Acquire ¶ added in v0.0.6
Acquire implements Router on the static Set (no-op release, ctx unchanged).
func (*Set) AcquireFor ¶ added in v0.0.6
func (s *Set) AcquireFor(ctx context.Context, tenantID string) (Shard, context.Context, func(), error)
AcquireFor implements Router on the static Set (no-op release, ctx unchanged).
func (*Set) All ¶
All returns the shards, ordered by id — for the worker plane to start a per-shard relay / reconciler against each.
func (*Set) For ¶
For resolves the shard owning the ctx tenant. It requires a tenant (the same precondition every source-of-truth port already enforces) and fails loudly if the directory names a shard the set does not hold.
func (*Set) ForTenant ¶
ForTenant resolves the shard owning an explicit tenant id — the seam the worker plane uses to route by the tenant carried in a method argument (projection bookkeeping, snapshot, reconcile) rather than on ctx.
type Shard ¶
type Shard struct {
ID string
Store command.Store
Relational query.RelationalQuerier
Vector query.VectorQuerier
Timeseries query.TSQuerier
Spatial query.SpatialQuerier
// Analytics is the shard's per-tenant customer-facing analytics port
// (Task 4+). Static deployments and catalog mode both fill it with the
// postgres insights adapter; nil until Config.Insights.Enabled routes it
// through the shard set (f.Analytics() degrades to
// notConfiguredAnalytics{} until then).
Analytics query.AnalyticsQuerier
// Documents is the shard's CRDT document plane. Static deployments
// leave it nil (the primary's doc store serves, ADR 0007 step 2);
// catalog mode fills it per tenant database.
Documents document.Store
// Maintenance is the shard's single-pass worker surface for the
// catalog-mode sweeper. Static deployments leave it nil (the worker
// plane runs its own boot-time loops).
Maintenance sweep.Maintainer
// Projection is the shard's projection bookkeeping surface
// (projection_applied/_state stay co-located with the aggregates they
// track). Static deployments leave it nil (the worker plane holds
// concrete adapters); catalog mode fills it per tenant database.
Projection ProjectionStateStore
// Replay is the shard's event-truth surface for rebuilds/reconcile.
// Static deployments leave it nil; catalog mode fills it per tenant DB.
Replay ReplaySource
// Live is the shard's live-query snapshot/refill oracle. Static
// deployments leave it nil (the primary's LiveStore serves, wired
// directly into Ports.Live); catalog mode fills it per tenant DB.
Live LiveSource
}
Shard is one tenant-home: the source-of-truth ports for the tenants that live on it. In production a single Postgres adapter satisfies all five; tests supply stubs.
type Spatial ¶
type Spatial struct {
// contains filtered or unexported fields
}
Spatial routes query.SpatialQuerier (geometry upsert/search/delete).
func NewSpatial ¶
NewSpatial wraps a Set as a routing query.SpatialQuerier.
func (*Spatial) Covering ¶ added in v0.0.6
Covering routes a containment search to the tenant's shard, provided that shard's spatial adapter implements query.SpatialCoverer. Returns an error when the underlying adapter is radius-only.
func (*Spatial) Get ¶ added in v0.0.5
func (s *Spatial) Get(ctx context.Context, entity, id string) (geom query.Geometry, meta map[string]any, ok bool, err error)
Get routes a single-geometry read to the tenant's shard.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store routes command.Store (the write path) by tenant.
func (*Store) InTenantTx ¶
func (s *Store) InTenantTx(ctx context.Context, fn func(ctx context.Context, tx command.Tx) error) error
InTenantTx routes the write transaction to the tenant's shard. The whole transaction (aggregate row + event + outbox) runs there, so atomicity is preserved without any distributed transaction.
type Timeseries ¶
type Timeseries struct {
// contains filtered or unexported fields
}
Timeseries routes query.TSQuerier (telemetry ingest/range reads).
func NewTimeseries ¶
func NewTimeseries(set Router) *Timeseries
NewTimeseries wraps a Set as a routing query.TSQuerier.
func (*Timeseries) Range ¶
func (t *Timeseries) Range(ctx context.Context, q query.RangeQuery, into any) error
Range routes a time-window read.
type Vector ¶
type Vector struct {
// contains filtered or unexported fields
}
Vector routes query.VectorQuerier (embedding upsert/search).
func (*Vector) DeleteByMeta ¶ added in v0.0.3
DeleteByMeta routes a metadata-filtered delete to the tenant's shard.