Documentation
¶
Overview ¶
Package store implements per-(org, namespace) SQLite shards with optional consensus replication.
Layout on disk — hanzoai/namespace's, shared with every other store in the estate rather than one of tasks' own:
<rootDir>/orgs/<org>/<namespace>.db <rootDir>/orgs/<org>/projects/<project>/<namespace>.db <rootDir>/orgs/_platform/<namespace>.db the root (unscoped) tenant
A shard is created on first use. Namespaces are not declared up front.
A Principal that narrows to a USER has no place in this layout and is refused — see ErrUserLeg.
Schema (single table; key/value blob):
CREATE TABLE kv( key TEXT PRIMARY KEY, value BLOB NOT NULL, upd INTEGER NOT NULL );
Encryption:
Every shard is encrypted at rest, keyed by hanzoai/cek from the process master and the namespace that owns it. There is no plaintext posture and no key material beside the file: a shard is born encrypted or it does not exist. A deployment with no master mints a random one that dies with the process, which is the honest shape of a keyless run.
Replication:
A Replicator may be attached via WithReplicator. Every put/del is wrapped in a replication.Frame and Propose'd before the local apply commits. On Accept the frame is also dispatched to peers via the driver-internal Subscribe path.
Index ¶
- Constants
- Variables
- func CopyFile(dst, src string) (int64, error)
- func IsCrossNamespacePrefix(prefix string) bool
- func NsFromKey(key string) (kind, ns, rest string, ok bool)
- func SplitPrefix(prefix string) (kind, ns, suffix string)
- func ValidName(name string) error
- type Manager
- func (m *Manager) Close() error
- func (m *Manager) Encrypted() bool
- func (m *Manager) Get(ctx context.Context, p Principal, ns string) (*Shard, error)
- func (m *Manager) ListPrincipals(ctx context.Context) ([]Principal, error)
- func (m *Manager) ListShards(ctx context.Context, p Principal) ([]*Shard, error)
- func (m *Manager) OpenShardCount() int
- func (m *Manager) Replicator() replication.Replicator
- func (m *Manager) RootDir() string
- func (m *Manager) ShardPath(p Principal, ns string) string
- func (m *Manager) WithReplicator(r replication.Replicator)
- type Principal
- type Shard
- func (s *Shard) Checkpoint() error
- func (s *Shard) Close() error
- func (s *Shard) Del(ctx context.Context, key string) error
- func (s *Shard) Get(ctx context.Context, key string) ([]byte, bool, error)
- func (s *Shard) List(ctx context.Context, prefix string, fn func(key string, value []byte) error) error
- func (s *Shard) Namespace() string
- func (s *Shard) Path() string
- func (s *Shard) Principal() Principal
- func (s *Shard) Put(ctx context.Context, key string, value []byte) error
Constants ¶
const Depth = 3
Depth is the number of legs in a Principal: org, project, user.
const MasterKeyLen = cek.KeyLen
MasterKeyLen is the required length of the store's root key.
const Sentinel = "_"
Sentinel is the path segment standing for an unset leg of a Principal. It is not a legal id, so a Principal with an unset leg can never be confused with one that names a tenant of that name.
Variables ¶
var CrossNamespaceKinds = map[string]bool{ "ns": true, "nx": true, }
CrossNamespaceKinds lists the kinds whose List(prefix) operations must enumerate every shard under the org. Anything else routes to a single shard.
var ErrClosed = errors.New("store: manager closed")
ErrClosed is returned when a method runs after Close.
var ErrUserLeg = errors.New("store: the shared namespace layout has no place for a user-scoped shard; scope to the org or its project")
ErrUserLeg reports that a Principal narrows to a USER, which the shared namespace layout has no place for.
hanzoai/namespace names an org, an org's project, and the deployment itself. namespace.Key returns an error for a user namespace on purpose — "this layout has no place for them, and inventing one silently is how a second convention starts". Tasks used to write <root>/<org>/<project>/<user>/, a layout of its own; it now shares the estate's, and the user leg is the one part of its tenancy that does not survive the move.
It is refused at the door rather than only when the file is keyed, so a user-scoped shard fails the same way in dev as in production instead of working plaintext and dying the day a master key is set.
var IdleEvictAfter = 10 * time.Minute
IdleEvictAfter sets how long an open shard may sit unused before the manager closes it. Mutable for tests.
var SweepEvery = time.Minute
SweepEvery is how often idle shards are evicted and resident shards sealed. Mutable for tests.
Functions ¶
func IsCrossNamespacePrefix ¶
IsCrossNamespacePrefix reports whether prefix is a bare kind/ scan that must fan out across shards.
func NsFromKey ¶
nsFromKey parses the canonical key layout to derive the namespace segment. Returns ("", false) for keys that are themselves the namespace registry (`ns/<name>`); the caller treats those specially.
Layout reference:
ns/<name> wf/<ns>/<workflowId>/<runId> wfh/<ns>/<workflowId>/<runId>/<eventId> sc/<ns>/<scheduleId> bt/<ns>/<batchId> dp/<ns>/<deploymentName> nx/<ns>/<endpointName> id/<ns>/<email> sa/<ns>/<attrName> idem/<ns>/<workflowId>/<requestId>
func SplitPrefix ¶
SplitPrefix returns (kind, ns, suffix) for a list prefix. ns may be empty when IsCrossNamespacePrefix(prefix) is true.
func ValidName ¶ added in v1.52.4
ValidName rejects anything that would escape or alias a shard directory or file, or collide with the unset-leg sentinel. Every name that becomes a path segment — a principal's legs and a namespace alike — passes through here, so there is one rule for all of them.
The rule is hanzoai/namespace's own segment rule, asked rather than restated: a name must already BE a legal namespace segment, exactly as written. That settles what a name cannot be by construction instead of by denylist — no separator so no path, no dot so no ".." and no hidden name, no trailing space, no encoded form of any of those — and it holds for the namespace leg too, which cek renders straight into a filename.
"Exactly as written" is the strict part, and it is deliberate. The namespace constructor case-folds, so it would ACCEPT "Acme" and quietly store it as "acme"; a leg that only becomes legal after folding has two spellings, and the second one names the same file under a different key. Legs arrive already folded (see Org), so anything that would change here did not come through the door.
Types ¶
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the on-disk shard layout and the open-shard cache.
It holds no key. The master lives in hanzoai/cek — one key per process, for every database the process opens — because threading it through every caller would not make it less global, only harder to see.
func New ¶
New opens the manager rooted at rootDir. The directory is created on demand; errors are returned only for unrecoverable IO problems.
master is the 32-byte root key every shard is derived from, resolved from KMS by whoever runs this. It is installed into hanzoai/cek, which is where the process keeps it.
A nil master does NOT mean plaintext — there is no such posture any more. It means "I have no key to state":
- Embedded in a process that already installed one (cloud resolves it through credz at boot), that key is used. One deployment, one key — an embedded engine minting a second would write files its host could not read.
- Otherwise a random master is minted for this process. Nothing it writes outlives the run, by construction, which is the honest shape of a deployment that was given no key and the reason a developer needs no configuration at all.
func (*Manager) Encrypted ¶ added in v1.52.4
Encrypted reports whether shards are opened at rest encrypted. They always are; it answers whether this process holds a key to open any at all.
func (*Manager) Get ¶
Get returns the open shard for (principal, ns), creating it on disk if needed. The returned Shard is safe for concurrent use.
func (*Manager) ListPrincipals ¶ added in v1.52.4
ListPrincipals enumerates every tenant that owns at least one shard. Used by the cron sweeper, which must see EVERY tenant's schedules from the root engine.
It walks the namespace layout: orgs/_platform is the root tenant, every other orgs/<slug> is an org, and each of those may hold projects/<slug>. The slug it reads back is ALREADY the folded storage identity, so the Principal it builds renders straight back to the directory it came from (see Org for why folding twice would not).
func (*Manager) ListShards ¶
ListShards enumerates every namespace shard the principal owns. Used by cross-namespace operations like ListNamespaces().
func (*Manager) OpenShardCount ¶
OpenShardCount reports the number of resident shards (for /v1/tasks/cluster).
func (*Manager) Replicator ¶
func (m *Manager) Replicator() replication.Replicator
Replicator returns the currently-installed driver, or nil.
func (*Manager) ShardPath ¶
ShardPath returns the on-disk file for (principal, ns), or "" for a principal the layout cannot name.
func (*Manager) WithReplicator ¶
func (m *Manager) WithReplicator(r replication.Replicator)
WithReplicator installs r as the consensus driver for every shard opened from now on, and re-installs it on already-open shards.
type Principal ¶ added in v1.52.4
Principal is the tenant that owns a shard: the org, optionally narrowed to a project and to a user. It is the SAME value in both places tenancy is decided, so the two can never disagree:
- the directory the shard's file lives in (String), and
- the key-encryption key that file's DEK is wrapped under (KEK).
Legs narrow left to right and each unset leg is written as Sentinel, so the encoding is fixed-width and injective: acme/_/z (an org's user, no project) and acme/z/_ (an org's project) are distinct paths and derive distinct keys. The zero Principal is the root — the unscoped embedded / dev tenant.
The org is what a tenant IS; project and user narrow it and are each independently optional, because IAM mints identities that carry a user without a project. Nothing may be set without an org.
func Org ¶ added in v1.52.4
Org returns the principal naming an org.
It is THE DOOR: the raw IAM org a caller holds is folded here, once, through namespace.Sanitize — the one injective slugger — so everything downstream carries the STORAGE identity. That matters because a Principal is both the routing key on a replication frame and the name of the directory its shard lives in: folding again at the open would re-suffix a slug that already carries a disambiguation suffix, and land the tenant in a different, empty database than the one ListPrincipals just read off disk.
A name Sanitize REFUSES — it carries whitespace, a control or a format rune, the class no injective fold survives — is kept RAW rather than folded, so that Valid rejects it. Folding it would produce the empty string, and an empty org leg is the ROOT principal: a name too hostile to store would have become the deployment's own platform tenant. Fail-closed, loudly.
func OrgProject ¶ added in v1.52.9
OrgProject is the door for a principal narrowed to one of an org's projects. Both legs are folded, by the same rule, in the same place.
func ParsePrincipal ¶ added in v1.52.4
ParsePrincipal is the inverse of String.
func (Principal) Namespace ¶ added in v1.52.9
Namespace is the entity hanzoai/namespace names this shard's tenant, and therefore both where its file lives and what its key is derived from — one name, two renderings, so the two cannot drift apart:
root (zero) → system → orgs/_platform/<ns>.db
{Org} → org/<org> → orgs/<org>/<ns>.db
{Org, Project} → org/<org>/<proj> → orgs/<org>/projects/<proj>/<ns>.db
anything with User → ErrUserLeg
The legs are already the folded storage identity (see Org), so this VALIDATES them and does not fold again — namespace.Sanitize is injective but not idempotent, and a second fold would rename a tenant whose first fold carried a disambiguation suffix.
func (Principal) String ¶ added in v1.52.4
String is the canonical encoding: the three legs joined by "/", each unset leg written as Sentinel. It is BOTH the shard's directory (relative to the store root) and the routing key carried on a replication frame — one encoding, so a frame always lands in the directory its principal names.
type Shard ¶
type Shard struct {
// contains filtered or unexported fields
}
Shard owns one SQLite file. WAL mode + foreign keys + 5s busy timeout. One writer; readers share the cache. Replicator hooks fire from put/del before local commit so the cluster sees the mutation first.
func (*Shard) Checkpoint ¶
Checkpoint makes the on-disk file fully self-contained: it truncates the WAL and, for an encrypted shard the pure-Go codec envelope backs, seals the committed pages back into the ciphertext file. The seal is a successful no-op for a shard that persists per commit (the live codec and plaintext paths), so callers never ask which one they hold. Used by the migration tool before a copy and by the manager's sweep, which is what bounds an envelope-backed shard's exposure to an unclean exit.
func (*Shard) List ¶
func (s *Shard) List(ctx context.Context, prefix string, fn func(key string, value []byte) error) error
List walks every kv row whose key starts with prefix in lexicographic order.