Documentation
¶
Index ¶
- Constants
- Variables
- func QueryTimeToTTL(queryTime time.Duration) time.Duration
- type Cache
- type Entry
- type LocalCache
- func (l *LocalCache) Close() error
- func (l *LocalCache) Invalidate(_ context.Context, namespaces []Namespace) (uint64, error)
- func (l *LocalCache) InvalidateTenant(_ context.Context, id tenant.ID) error
- func (l *LocalCache) Lookup(_ context.Context, id tenant.ID, sha string, deps []Namespace) (Entry, Snapshot, error)
- func (l *LocalCache) Prune(served func(tenant.ID) bool)
- func (l *LocalCache) Set(_ context.Context, snap Snapshot, value []byte, ttl time.Duration) error
- func (l *LocalCache) Wait()
- type Namespace
- type RedisCache
- func (r *RedisCache) Close() error
- func (r *RedisCache) Invalidate(ctx context.Context, namespaces []Namespace) (uint64, error)
- func (r *RedisCache) InvalidateTenant(ctx context.Context, id tenant.ID) error
- func (r *RedisCache) Lookup(ctx context.Context, id tenant.ID, sha string, deps []Namespace) (Entry, Snapshot, error)
- func (r *RedisCache) Set(ctx context.Context, snap Snapshot, value []byte, ttl time.Duration) error
- type RedisConfig
- type Snapshot
- type VersionManager
- func (vm *VersionManager) BumpNamespace(ns Namespace)
- func (vm *VersionManager) BumpTable(id tenant.ID, table string)
- func (vm *VersionManager) BumpTenant(id tenant.ID)
- func (vm *VersionManager) Prune(keep func(tenant.ID) bool)
- func (vm *VersionManager) QueryKey(id tenant.ID, sha string, deps []Namespace) string
Constants ¶
const ( RedisStandalone = "standalone" RedisCluster = "cluster" RedisSentinel = "sentinel" )
Redis deployment modes for RedisConfig.Mode.
const ( DefaultRedisKeyPrefix = "wh" DefaultRedisTimeout = 100 * time.Millisecond DefaultRedisDialTimeout = time.Second DefaultRedisMaxValueBytes = 1 << 20 DefaultRedisCompressMinBytes = 1 << 10 DefaultRedisVersionTTL = 7 * 24 * time.Hour DefaultRedisPendingMax = 100_000 )
Defaults for RedisConfig's zero values.
Variables ¶
var ErrForeignDependency = errors.New("cache: dependency names another tenant")
ErrForeignDependency is a Lookup whose dependencies name a tenant other than the one it is for: a cached result is one tenant's, and so is every version it is filed under.
Functions ¶
Types ¶
type Cache ¶
type Cache interface {
// Lookup reads the entry for sha under tenant id at deps' current
// versions, and returns the snapshot of those versions for the Set that
// fills it on a miss. sha is the caller's key for the SQL and params;
// deps are the namespaces the result reads (one for a structured query,
// none yet for a pipe), each of tenant id — any other is
// ErrForeignDependency. An error is a miss with a zero Snapshot.
Lookup(ctx context.Context, id tenant.ID, sha string, deps []Namespace) (Entry, Snapshot, error)
// Set stores value under snap, the Snapshot a Lookup returned before any
// input of the value was chosen. It returns an error only when the backend failed;
// a value the cache declines to keep — too large, refused admission, a
// non-positive ttl, or a zero snap — is not an error.
Set(ctx context.Context, snap Snapshot, value []byte, ttl time.Duration) error
// Invalidate bumps the version for each namespace, orphaning every cached query
// that depends on it. A namespace with an empty Scope bumps the whole table
// (every scope); a non-empty Scope bumps just that scope plus the whole-table
// view. A bump reaches the namespace's tenant alone: the same table under
// another tenant keeps its versions. Returns the number of namespaces processed.
Invalidate(ctx context.Context, namespaces []Namespace) (uint64, error)
// InvalidateTenant orphans every cached result of one tenant in one step
// — every table and scope, bumped or not, and every pipe result — for a
// tenant that comes back after an absence from the invalidation fan-out
// (its settings folder rejected or removed, #583 story 6), stale by every
// insert it missed, or that moved to another ClickHouse address or
// database, whose cached results were read from other tables.
InvalidateTenant(ctx context.Context, id tenant.ID) error
// Close releases resources.
Close() error
}
Cache provides versioned query-result storage with TTL support.
Every entry is one tenant's and folds that tenant's version, so InvalidateTenant orphans all of it — a result with no dependencies (a pipe) included. A backend that cannot be reached is a miss on Lookup and a no-op on Set; the caller runs its query either way.
type LocalCache ¶
type LocalCache struct {
// contains filtered or unexported fields
}
LocalCache is an L1 in-process cache backed by Ristretto: one pool for every tenant (#583 story 8) — a heavier tenant holds more of it — with the tenant leading every key, so no entry is shared across tenants.
func NewLocal ¶
func NewLocal(maxCost int64) (*LocalCache, error)
NewLocal creates a new Ristretto-backed local cache.
func (*LocalCache) Close ¶
func (l *LocalCache) Close() error
func (*LocalCache) Invalidate ¶
Invalidate bumps the version for each namespace, instantly orphaning every cached query that depends on it. An empty Scope bumps the whole table (every scope at once); a non-empty Scope bumps just that scope plus the whole-table view. Returns the number of namespaces processed.
This bumps exactly what it's given. A whole-table bump already subsumes every per-scope bump for the same table (every key that folds a scope version folds the table version too), so a caller that knows a whole-table bump is coming should drop the now-redundant scope entries itself — the ingest worker does this as it builds the batch, where it already loops once and knows it's a single table.
func (*LocalCache) InvalidateTenant ¶ added in v0.2.0
InvalidateTenant orphans every cached result of tenant id, pipe results included: its version index is dropped, nothing enumerated (see VersionManager.BumpTenant).
func (*LocalCache) Lookup ¶ added in v0.2.0
func (l *LocalCache) Lookup(_ context.Context, id tenant.ID, sha string, deps []Namespace) (Entry, Snapshot, error)
Lookup reads a cached query RESULT by its sha (hash of SQL+params) and the namespaces it depends on, and snapshots the key at their current versions. Used by BOTH structured queries (which pass one Namespace) and pipes (none yet).
func (*LocalCache) Prune ¶ added in v0.2.0
func (l *LocalCache) Prune(served func(tenant.ID) bool)
Prune drops the version index of every tenant served rejects, orphaning its entries as InvalidateTenant would, so a tenant removed or rejected at a reload stops holding memory (#262). The entries themselves go with their TTL or Ristretto's eviction.
func (*LocalCache) Set ¶
Set stores a query result under the key its Lookup snapshotted. Admission is asynchronous (see Wait), and Ristretto may still decline the value.
func (*LocalCache) Wait ¶
func (l *LocalCache) Wait()
Wait blocks until all buffered writes have been applied. Exposed for testing; production callers rarely need this.
type Namespace ¶
Namespace is one (tenant, table, scope) a cached result depends on. The tenant leads every key built from it, so the same table under two tenants is two namespaces, versioned and bumped apart (#583 story 8). Table and Scope are raw names: the cache escapes them where it builds a key (keyenc), so no caller escapes and no separator in a name can run two fields together.
type RedisCache ¶ added in v0.2.0
type RedisCache struct {
// contains filtered or unexported fields
}
RedisCache is a Cache shared by every process pointed at one Redis — or Valkey, Dragonfly, ElastiCache, MemoryDB: it uses only GET, SET and MGET, no scripts and no client tracking.
Versions are random tokens, one per tenant, per table and per scope, under the tenant's hash tag; a bump sets a fresh one. A value carries the tokens it was computed under and is a hit only while they are all still current, so a lost token (eviction, expiry, a restart without persistence) can only cause misses. Restoring a snapshot is not a loss but a rollback — a restart after a crash that reloads the server's last save included, which stock Redis and Valkey make by default: the old tokens return with their values. A lookup is one round trip. The server failing or timing out is a miss, a skipped fill and a deferred invalidation — never a failed query.
func NewRedis ¶ added in v0.2.0
func NewRedis(cfg RedisConfig) (*RedisCache, error)
NewRedis builds a RedisCache. A malformed cfg is an error; a server that cannot be reached within the dial timeout is not — the cache starts bypassed and keeps dialing in the background.
func (*RedisCache) Close ¶ added in v0.2.0
func (r *RedisCache) Close() error
Close stops the background loops, retries the pending bumps past the breaker every drainMinBackoff for up to closeDrainBudget, and closes the connection. Bumps still undelivered are lost: the entries they would orphan are served until their TTL.
func (*RedisCache) Invalidate ¶ added in v0.2.0
Invalidate sets a fresh token for every token the namespaces' writes reach, in pipelined batches of drainBatch, one round trip each. Bumps the server does not take are kept and retried until it does, and reported as an error meanwhile.
func (*RedisCache) InvalidateTenant ¶ added in v0.2.0
InvalidateTenant sets a fresh tenant token, orphaning every entry of id.
func (*RedisCache) Lookup ¶ added in v0.2.0
func (r *RedisCache) Lookup(ctx context.Context, id tenant.ID, sha string, deps []Namespace) (Entry, Snapshot, error)
Lookup reads the tokens deps fold and the entry for sha in one pipelined round trip. A token that does not exist yet is created, never read as a value, so its first use is a miss.
type RedisConfig ¶ added in v0.2.0
type RedisConfig struct {
Addrs []string // host:port; several are seeds (cluster) or sentinels
Mode string // RedisStandalone (""), RedisCluster or RedisSentinel
SentinelMaster string // the master set name, Mode RedisSentinel
Username string
Password string
DB int // standalone and sentinel only
TLS *tls.Config // nil: plaintext
KeyPrefix string // leads every key; separates deployments sharing a server
Timeout time.Duration // per operation
DialTimeout time.Duration
MaxValueBytes int // largest value stored, after compression
CompressMinBytes int // zstd-compress values at least this large
VersionTTL time.Duration // a token's lifetime from its last bump, jittered ±10%; reads do not extend it
PendingMax int // undelivered bumps kept before collapsing to tenant bumps
BreakerThreshold int // consecutive failures that open the breaker
BreakerOpenFor time.Duration // how long it stays open before a probe
// contains filtered or unexported fields
}
RedisConfig configures a RedisCache. A zero field takes its Default* value, except CompressMinBytes, where 0 means never compress.
type Snapshot ¶ added in v0.2.0
type Snapshot struct {
// contains filtered or unexported fields
}
Snapshot is the dependency versions a Lookup observed. A caller takes it before choosing any input a bump invalidates — the tenant's connection as well as the rows its query reads — and Set files the result under it, so a bump that lands after the Lookup orphans the fill rather than re-homing what was read before the bump under the post-bump versions (#382). The zero Snapshot makes Set a no-op.
type VersionManager ¶
type VersionManager struct {
// contains filtered or unexported fields
}
VersionManager is the invalidation index: one version per tenant, per (tenant, table) and per (tenant, table, scope), in maps keyed by name alone, never by another version (#262). A bump overwrites a version in place, so the index holds one entry per live tenant, table and scope however often each is bumped, and forgetting a tenant releases all of it.
An entry's key (`QueryKey`) folds all three versions of each dependency, which gives the lattice: a table bump orphans every scope, a scope bump that scope and the whole-table view, and a tenant bump everything of the tenant's.
func NewVersionManager ¶
func NewVersionManager() *VersionManager
NewVersionManager initializes the thread-safe version store.
func (*VersionManager) BumpNamespace ¶
func (vm *VersionManager) BumpNamespace(ns Namespace)
BumpNamespace advances one (tenant, table, scope) namespace plus the table's whole-table (empty-scope) view, since a write to a named scope also changes the whole-table result; other scopes' cached queries stay valid.
func (*VersionManager) BumpTable ¶
func (vm *VersionManager) BumpTable(id tenant.ID, table string)
BumpTable advances a tenant's table version, orphaning every namespace — and every cached query — that depends on the table, in one step (the whole-table nuke). The table's scope versions are dropped with it: every key they were folded into also folds the old table version. The same table under another tenant is untouched.
func (*VersionManager) BumpTenant ¶ added in v0.2.0
func (vm *VersionManager) BumpTenant(id tenant.ID)
BumpTenant orphans every cached query of a tenant, whatever its deps, in one step (the whole-tenant nuke), by dropping the tenant's index: the next key built for it gets a fresh generation, which no cached entry folds. Nothing has to be enumerated, a table no bump ever keyed is orphaned like the rest, and the index the tenant held is released. Other tenants are untouched.
func (*VersionManager) Prune ¶ added in v0.2.0
func (vm *VersionManager) Prune(keep func(tenant.ID) bool)
Prune drops the index of every tenant keep rejects, as BumpTenant would, so a tenant that stops being served stops holding memory; one served again starts over at a fresh generation.
func (*VersionManager) QueryKey ¶
QueryKey builds the queries-table key for tenant id's result that depends on deps: the query's sha (hash of SQL+params) folded with the tenant's version and, for every dependency, its tenant's, table's and scope's versions, so a bump of the tenant or of any dependency misses the key — a result with no deps (a pipe) is orphaned by BumpTenant too. A structured query passes one Namespace, a pipe none yet (#343). Deps are sorted so their order never changes the key, and every version is read under one lock, so the key is one consistent snapshot. The key nests two levels: the escaped sha and the '.'-joined tenant and dependency segments, separated by '|', which no escaped field or '.' join ever holds.
The first key built for a tenant creates its index at a fresh generation.