Control plane: gRPC (tonic/prost). Data plane values: a Corium-defined tagged
binary encoding carried in protobuf bytes fields. Segments never travel over
gRPC — peers read the blob store directly.
The sortable segment encoding (data-model.md) is also the wire encoding for
single values; composite payloads (tx-data, query args, results) use a
length-prefixed tagged variant of the same tag space extended with container
tags (list, vector, map, set) and an interning table per message for keywords
and repeated strings. One corium-protocol::codec module owns both variants;
round-trip and cross-variant property tests keep them honest.
Rationale (ADR-0006): protobuf handles framing, streaming, auth, and versioning where it is strong; EDN's open value set lives in one codec we control, rather than being contorted into protobuf messages.
service Transactor {
rpc Transact(TransactRequest) returns (TransactResponse); // tx-data bytes → tempids, basis, tx-data
rpc Subscribe(SubscribeRequest) returns (stream TxReport); // declares client basis; server backfills then streams
rpc Sync(SyncRequest) returns (SyncResponse); // wait for basis ≥ t
rpc Status(StatusRequest) returns (StatusResponse); // basis, index-basis, lease info, stats
}
Subscribe is the peer's lifeline: tx-reports, index-basis announcements,
and heartbeats are multiplexed on this stream. The handshake advertises
the server's heartbeat interval; a stream silent for three intervals is
presumed dead and dropped even when the transport has not noticed.
Disconnect ⇒ peer reconnects, rotating through its endpoint preference
list (an HA standby rejects the subscription with a standby
FAILED_PRECONDITION until it holds the lease; peers with storage
credentials can also rediscover the holder's advertised endpoint from the
root record) and resubscribes from its basis; the transactor backfills
from the log if the gap is large.index-basis-t of the immutable snapshot the peer just loaded. The root
selects a complete snapshot, and the subscription supplies the gap through
the handshake basis, so concurrent index publication does not require a
cross-service transaction.format-version roots with a clear upgrade error.TransactRequest.expected_basis_t. When present, the
transactor rejects a stale request before transaction preparation or durable
append; peer-local read/modify/write adapters use this fence.0xA6 — class:8 ‖ epoch:4 ‖ vtype:1 ‖ body — and the versioned schema payload that carries protection
classes and per-attribute protection timelines. The tag is 0xA6 rather
than the 0xA0 the sortable encoding uses, because 0xA0 is LIST in the
composite tag space. A pre-v3 client never receives one: values reach a thin
client through the boundary EDN, which renders an unhydrated value as
#corium/redacted {:class … :type …}, a tagged element any EDN reader
parses. Sending sealed values on the wire arrives with the peer server's
deferred --seal-through mode.schema_basis_t and
schema_generation to the handshake and schema_generation_after to tx
reports. The handshake schema is the snapshot effective at the subscriber's
from_basis_t, not the server's newest schema. Reports with
t > from_basis_t advance data and schema together. A subscriber from basis
0 receives the pre-basis schema seed in the handshake, never as a t = 0
report. These semantics require a protocol-version bump so an older peer
cannot silently install a current schema before replaying older data.The current peer connects to an ordered transactor endpoint list. The proposed transactor fleet design replaces that deployment contract with one fleet endpoint while preserving the database field as the authoritative request target.
The SDK will duplicate a canonical database routing key in gRPC metadata so an
L7 load balancer can apply advisory consistent-hash affinity. Any transactor
ingress may receive the request; owner-dependent work is executed locally or
forwarded once to the lease holder. Structured NotOwner details replace
parsing standby/deposed message text. Affinity never grants ownership.
Transparent retry after a request reaches the owner additionally requires a durable transaction request ID and result deduplication. Until that protocol exists, an in-flight connection loss remains ambiguous exactly as it is today.
CreateDatabase, DeleteDatabase, ForkDatabase, ListDatabases,
GcDeletedDatabases, index controls, and GetBackupInfo. Most are thin
wrappers over root-store operations plus transactor bootstrap datoms.
ForkDatabase creates a new database duplicating an existing one at a
transaction basis by copying the log prefix; the fork replays it and publishes
indexes of its own. GetBackupInfo briefly serializes with commit to return a
definite current basis and the underlying storage connection; the backup
client then leaves the transactor and reads the bounded native log range
directly.
Schema migration adds one administrative call:
rpc AlterSchema(AlterSchemaRequest) returns (AlterSchemaResponse);
There is no PlanSchemaUpdate. Planning is a pure function of a desired schema
and one immutable database value, and a peer already holds that value, so it
plans locally against its own Db and needs only Inspect. Adding a
round trip would buy nothing and would let the plan disagree with the value the
operator is looking at.
AlterSchema carries the desired schema as normalized EDN attribute maps —
not the plan — alongside the plan digest, installed-schema fingerprint,
observed basis, --prune mode, allowances, acknowledgements, and tool version.
The transactor recomputes the logical plan and its digest from that schema and
the schema installed under its own writer queue, and refuses unless the digest
matches. A digest it could not re-derive would be an opaque token rather than a
precondition. Advisory counts are not part of the digest, so data drift that
preserves every precondition does not invalidate a plan. The response reports
the resulting basis, schema generation, the idents installed, and whether
anything changed at all. Catalog never depends on the operator service.
The schema generation is derived per database value rather than carried on the
wire: peers apply schema datoms in tx reports and converge on it. Carrying it
explicitly — schema_basis_t and schema_generation on the handshake,
schema_generation_after on tx reports, and the schema generation plus
per-attribute AVET readiness basis on published-root and status metadata — is
still proposed, and is what a protocol-version bump would gate so an older peer
cannot silently install a current schema before replaying older data. The
forced backfill that bypasses interval/tail pacing belongs with it, as does the
rule that readiness is never reported ahead of the root that proves coverage.
Job submission, planning, approval, cancellation, and watching; schedules; and
fleet, database, storage, and key inspection — with a JSON/HTTP gateway over the
same surface for the UI and for scripting. It is a separate service from
Catalog on purpose: the transactor's latency belongs to the commit pipeline,
so it is called by the operator service rather than extended into it. See
operator-service.md.
For languages without the peer library; queries run server-side on a hosted peer:
service PeerServer {
rpc Query(QueryRequest) returns (stream QueryResultChunk);
rpc Pull(PullRequest) returns (PullResponse);
rpc Transact(TransactRequest) returns (TransactResponse); // proxied
rpc Datoms(DatomsRequest) returns (stream DatomChunk);
rpc TxRange(TxRangeRequest) returns (stream TxChunk);
rpc DbStats(DbStatsRequest) returns (DbStatsResponse);
rpc Subscribe(SubscribeRequest) returns (stream TxReport); // relayed
}
Requests name a db view as
{db-name, as-of?, since?, history?, as-of-instant?, since-instant?} so thin
clients get the full time model, by basis or by wall clock. Result streams are chunked with a
server-enforced fuel/deadline per query. This service definition plus the
codec spec is the public thin-client protocol; a conformance doc and test
vectors ship with it so third parties can write clients.
Authenticator trait; static tokens in v1).corium-protocol::authz (optional per-surface enforcement, external
identity providers, and per-principal view decisions). Guards are wired into
the transactor and peer gRPC services; a filtered decision is rejected on
surfaces that cannot enforce it yet. See auth.md and
ADR-0012.#corium/sealed {:class :epoch :vtype :body},
which the transactor validates against its schema without holding any key. A
peer server hydrates per request with its own key set; forwarding sealed
values to the thin client (--seal-through) is deferred.The same service traits have an in-process implementation over channels
(corium-peer talks to corium-transactor directly). Tests and the
simulator run the identical pipeline code both ways; only the transport
differs. This is the mechanism that lets us build "full topology" logic from
day one while running single-process until M4.
Can you improve this documentation? These fine people already did:
Casey Marshall & ClaudeEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |