Skip to main content

nodedb_cluster/metadata_group/
entry.rs

1// SPDX-License-Identifier: BUSL-1.1
2
3//! The canonical wire-type for every entry proposed to the metadata Raft group.
4
5use serde::{Deserialize, Serialize};
6
7use nodedb_types::Hlc;
8
9use crate::metadata_group::compensation::Compensation;
10use crate::metadata_group::descriptors::{DescriptorId, DescriptorLease};
11use crate::metadata_group::migration_state::{MigrationCheckpointPayload, MigrationPhaseTag};
12
13/// An entry in the replicated metadata log.
14///
15/// Every mutation to cluster-wide state — DDL, topology, routing,
16/// descriptor leases, cluster version bumps — is encoded as one of
17/// these variants, proposed against the metadata Raft group, and
18/// applied on every node by a
19/// [`crate::metadata_group::applier::MetadataApplier`].
20///
21/// The `CatalogDdl` variant is the single wire shape for every DDL
22/// mutation. Its `payload` is an opaque, host-serialized
23/// `nodedb::control::catalog_entry::CatalogEntry` value — the
24/// `nodedb-cluster` crate is deliberately ignorant of the host's
25/// per-DDL-object struct shapes. This keeps the cluster crate
26/// layering-clean and makes adding new DDL object types on the
27/// host side a zero-wire-change operation.
28#[derive(
29    Debug,
30    Clone,
31    PartialEq,
32    Eq,
33    Serialize,
34    Deserialize,
35    zerompk::ToMessagePack,
36    zerompk::FromMessagePack,
37)]
38pub enum MetadataEntry {
39    /// Single generic DDL entry carrying an opaque host-side payload.
40    /// Produced by every pgwire DDL handler via
41    /// `nodedb::control::metadata_proposer::propose_catalog_entry`.
42    CatalogDdl {
43        payload: Vec<u8>,
44    },
45
46    /// DDL entry with attached audit context. Produced by pgwire DDL
47    /// handlers that have the authenticated identity + raw statement
48    /// text bound at the call site (every `CREATE`, `ALTER`, `DROP`,
49    /// `GRANT`, `REVOKE` path). Applied identically to `CatalogDdl`
50    /// on every node; additionally, the production applier fsync-
51    /// appends an audit record to the audit segment WAL with the
52    /// authenticated user, HLC at commit, descriptor versions before
53    /// + after, and the raw SQL — exactly what J.4 requires.
54    ///
55    /// Carries its own payload so legacy proposers (internal lease
56    /// and descriptor-drain flows that have no SQL text) can keep
57    /// using the plain `CatalogDdl` variant without synthesizing
58    /// fake audit context.
59    CatalogDdlAudited {
60        payload: Vec<u8>,
61        /// Authenticated user id at propose time.
62        auth_user_id: String,
63        /// Authenticated username at propose time.
64        auth_user_name: String,
65        /// Raw SQL statement as the client sent it. Not parsed here —
66        /// the cluster crate is opaque to SQL syntax. Persisted on
67        /// every replica so post-hoc audit queries don't depend on
68        /// the proposing node still being alive.
69        sql_text: String,
70    },
71
72    /// Atomic batch of metadata entries proposed by a transactional
73    /// DDL session (`BEGIN; CREATE ...; CREATE ...; COMMIT;`). The
74    /// applier unpacks and applies each sub-entry in order at a
75    /// single raft log index, so either all commit or none do.
76    Batch {
77        entries: Vec<MetadataEntry>,
78    },
79
80    /// A catalog DDL proposal fenced by the replicated preparation owner.
81    /// Appliers execute `entry` only while `token` is the current owner;
82    /// superseded proposals are deterministic no-ops so they cannot wedge the
83    /// Raft apply watermark.
84    DdlPrepared {
85        token: u64,
86        entry: Box<MetadataEntry>,
87    },
88
89    /// Acquire the replicated global descriptor-preparation lease. The first
90    /// unexpired owner wins; contenders observe the winner after this entry is
91    /// applied and retry after its matching release or expiry.
92    DdlPrepareAcquire {
93        token: u64,
94    },
95    /// Release the descriptor-preparation lease iff `token` still owns it.
96    DdlPrepareRelease {
97        token: u64,
98    },
99
100    // ── Topology / routing ─────────────────────────────────────────────
101    TopologyChange(TopologyChange),
102    RoutingChange(RoutingChange),
103
104    // ── Cluster version ────────────────────────────────────────────────
105    ClusterVersionBump {
106        from: u16,
107        to: u16,
108    },
109
110    // ── Descriptor leases ──────────────────────────────────────────────
111    DescriptorLeaseGrant(DescriptorLease),
112    DescriptorLeaseRelease {
113        node_id: u64,
114        descriptor_ids: Vec<DescriptorId>,
115    },
116
117    // ── Descriptor lease drain ────────────────────────────────────────
118    /// Begin draining leases on a descriptor. While a drain entry
119    /// is active, any `acquire_descriptor_lease` at
120    /// `version <= up_to_version` must be rejected cluster-wide so
121    /// the in-flight DDL that bumps the version can make progress.
122    ///
123    /// `expires_at` is the HLC at which this drain entry is
124    /// considered stale and ignored by `is_draining` checks on
125    /// read. Acts as a TTL that prevents a crashed proposer from
126    /// leaving an orphaned drain that blocks the cluster forever.
127    DescriptorDrainStart {
128        descriptor_id: DescriptorId,
129        up_to_version: u64,
130        expires_at: Hlc,
131    },
132    /// End draining on a descriptor. Emitted explicitly on drain
133    /// timeout so the cluster can make progress. On the happy
134    /// path (successful `Put*` apply), the host-side applier
135    /// clears drain implicitly — this variant is the escape
136    /// hatch for the failure path.
137    DescriptorDrainEnd {
138        descriptor_id: DescriptorId,
139    },
140
141    /// Cluster-wide CA trust mutation (L.4). Proposed by
142    /// `nodedb rotate-ca --stage` (to add a new CA) and
143    /// `nodedb rotate-ca --finalize --remove <fp>` (to drop an old
144    /// CA). Applied on every node by `MetadataCommitApplier`: writes
145    /// or deletes `data_dir/tls/ca.d/<fp_hex>.crt` and triggers a
146    /// live rebuild of the rustls server + client configs so the
147    /// new trust set takes effect without restart.
148    ///
149    /// `add_ca_cert` and `remove_ca_fingerprint` are independent:
150    /// the `--stage` form sets `add_ca_cert = Some(new_ca_der)` +
151    /// `remove_ca_fingerprint = None`; `--finalize` flips both. A
152    /// single entry carrying both performs the cutover atomically
153    /// once the operator has confirmed every node has reissued.
154    CaTrustChange {
155        /// DER-encoded CA certificate to add to the trust set. `None`
156        /// when this entry only removes.
157        add_ca_cert: Option<Vec<u8>>,
158        /// SHA-256 fingerprint of the CA to remove from the trust set.
159        /// `None` when this entry only adds.
160        remove_ca_fingerprint: Option<[u8; 32]>,
161    },
162
163    // ── Sync producer identity ────────────────────────────────────────
164    /// Replicate a new Lite client registration to every node.
165    ///
166    /// On apply every node writes the same registration row verbatim and
167    /// advances its producer-id allocator HWM to at least `producer_id`, so
168    /// a future leader never reissues the same id after a failover. The
169    /// `producer_id` and initial `epoch` are assigned by the leader before
170    /// proposing, so all replicas store identical state.
171    ///
172    /// Applied host-side only; the cluster cache tracks the applied index.
173    SyncProducerRegister {
174        lite_id: String,
175        producer_id: u64,
176        tenant_id: u64,
177        epoch: u64,
178        created_ms: i64,
179    },
180
181    /// Replicate a fencing epoch advance to every node.
182    ///
183    /// Applied with max-wins semantics: each node sets
184    /// `current_epoch = max(stored_epoch, new_epoch)` so out-of-order
185    /// or duplicate delivery cannot roll the epoch backwards. A fence
186    /// entry with no prior register row is a no-op on that node
187    /// (tolerated for reordering/missing idempotency).
188    ///
189    /// Applied host-side only; the cluster cache tracks the applied index.
190    SyncProducerFence {
191        lite_id: String,
192        new_epoch: u64,
193    },
194
195    // ── Surrogate identity ────────────────────────────────────────────
196    /// Advance the cluster-wide surrogate high-watermark to `hwm`.
197    ///
198    /// Proposed by the metadata-group leader whenever the local
199    /// `SurrogateRegistry` flush threshold trips (every 1024
200    /// allocations or 200 ms, whichever comes first). Applied on
201    /// every node by the host-side `MetadataCommitApplier` which
202    /// calls `SurrogateRegistry::restore_hwm(hwm)` — idempotent and
203    /// monotonic, so out-of-order replay or duplicate delivery are
204    /// both safe. Followers must never allocate surrogates locally;
205    /// they only advance their in-memory HWM via these log entries.
206    SurrogateAlloc {
207        hwm: u32,
208    },
209
210    /// Reserve a disjoint batch of surrogates for one node (HiLo).
211    ///
212    /// The carved `[start, end)` range is **not** carried in the entry:
213    /// it is computed AT APPLY by advancing the cluster-wide surrogate
214    /// global watermark on every node. Because every node applies the
215    /// metadata log in the same order against an identical watermark,
216    /// all nodes deterministically agree on which range this reservation
217    /// carves — guaranteeing globally-unique, disjoint surrogate batches
218    /// without any node ever minting a value another node also minted.
219    ///
220    /// `node_id` + `request_id` identify the requesting node and the
221    /// specific in-flight reservation so the owning node's host-side
222    /// applier can (a) install the carved batch locally and (b) fire the
223    /// completion signal that unblocks the waiting allocation path. Only
224    /// the owning node installs the batch; every other node merely
225    /// advances the watermark so future reservations stay disjoint.
226    SurrogateReserve {
227        node_id: u64,
228        request_id: u64,
229        batch_size: u32,
230    },
231
232    /// Join-token lifecycle transition (L.4). Proposed by the
233    /// bootstrap-listener handler on every state change so that all
234    /// Raft peers can enforce single-use token semantics even after a
235    /// crash-restart cycle.
236    ///
237    /// `token_hash` is the SHA-256 of the token hex string — the raw
238    /// token is never stored in the log. `transition` encodes the
239    /// direction: `Register` for first issuance, `BeginInFlight` when
240    /// a joiner presents the token, `MarkConsumed` when the bundle is
241    /// delivered, `RevertInFlight` when the dead-man timer fires, and
242    /// `MarkExpired` / `MarkAborted` for the terminal states.
243    JoinTokenTransition {
244        token_hash: [u8; 32],
245        transition: JoinTokenTransitionKind,
246        /// Unix-ms timestamp at the time of the proposal.
247        ts_ms: u64,
248    },
249
250    /// Crash-safe migration phase checkpoint. Persisted on every phase
251    /// transition; on coordinator restart, recovery scans the
252    /// `MigrationStateTable` and resumes from the latest committed
253    /// checkpoint. Apply is idempotent on `(migration_id, phase, attempt)`
254    /// — duplicate delivery is a no-op. CRC32C mismatch is fatal.
255    MigrationCheckpoint {
256        /// Hyphenated UUID string (zerompk does not serialize uuid::Uuid).
257        migration_id: String,
258        phase: MigrationPhaseTag,
259        attempt: u32,
260        payload: MigrationCheckpointPayload,
261        crc32c: u32,
262        ts_ms: u64,
263    },
264
265    /// Replicated migration abort with ordered compensations. Each
266    /// compensation is applied in order; any failure is fatal (no
267    /// warn-and-continue). On success, the migration's row in
268    /// `MigrationStateTable` is deleted.
269    MigrationAbort {
270        migration_id: String,
271        reason: String,
272        compensations: Vec<Compensation>,
273    },
274}
275
276/// The direction of a join-token lifecycle transition.
277#[derive(
278    Debug,
279    Clone,
280    PartialEq,
281    Eq,
282    Serialize,
283    Deserialize,
284    zerompk::ToMessagePack,
285    zerompk::FromMessagePack,
286)]
287pub enum JoinTokenTransitionKind {
288    /// New token registered (Issued state). Carries expiry so all nodes
289    /// can enforce the TTL independently.
290    Register { expires_at_ms: u64 },
291    /// Joiner presented the token; transitioning Issued → InFlight.
292    BeginInFlight { node_addr: String },
293    /// Bundle delivered; transitioning InFlight → Consumed.
294    MarkConsumed { node_addr: String },
295    /// Dead-man timer fired; transitioning InFlight → Issued.
296    RevertInFlight,
297    /// Token TTL elapsed without consumption.
298    MarkExpired,
299    /// Explicitly invalidated by an operator.
300    MarkAborted,
301}
302
303/// Topology mutations proposed through the metadata group.
304#[derive(
305    Debug,
306    Clone,
307    PartialEq,
308    Eq,
309    Serialize,
310    Deserialize,
311    zerompk::ToMessagePack,
312    zerompk::FromMessagePack,
313)]
314pub enum TopologyChange {
315    Join { node_id: u64, addr: String },
316    Leave { node_id: u64 },
317    PromoteToVoter { node_id: u64 },
318    StartDecommission { node_id: u64 },
319    FinishDecommission { node_id: u64 },
320}
321
322/// Routing-table mutations proposed through the metadata group.
323#[derive(
324    Debug,
325    Clone,
326    PartialEq,
327    Eq,
328    Serialize,
329    Deserialize,
330    zerompk::ToMessagePack,
331    zerompk::FromMessagePack,
332)]
333pub enum RoutingChange {
334    /// Move a vShard to a new raft group leaseholder.
335    ReassignVShard {
336        vshard_id: u32,
337        new_group_id: u64,
338        new_leaseholder_node_id: u64,
339    },
340    /// Record a leadership transfer within an existing group.
341    LeadershipTransfer {
342        group_id: u64,
343        new_leader_node_id: u64,
344    },
345    /// Remove a node from a Raft group's member and learner sets.
346    ///
347    /// Used by the decommission flow to strip a draining node out of
348    /// every group it belongs to. Proposing this is only safe once
349    /// `safety::check_can_decommission` has confirmed the group will
350    /// still satisfy the configured replication factor.
351    RemoveMember { group_id: u64, node_id: u64 },
352    /// Set the explicit placement (intended voter set) for a Raft group.
353    ///
354    /// Records the target voter membership authored by the placement
355    /// subsystem and replicated to all nodes via the metadata Raft group.
356    /// Consumers compare this against the current `members` to decide
357    /// whether a learner is eligible for promotion.
358    SetPlacement { group_id: u64, placement: Vec<u64> },
359}