1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
// SPDX-License-Identifier: BUSL-1.1
//! The read/allocate/bind operations on [`super::SurrogateAssigner`]:
//! `assign`, `assign_fresh`, `assign_anonymous`, `bind`, and `lookup`.
use nodedb_types::{DatabaseId, TenantId};
use nodedb_types::Surrogate;
use super::types::SurrogateAssigner;
impl SurrogateAssigner {
/// Resolve `(collection, pk_bytes)` to a stable surrogate.
///
/// - If the credential store has no catalog (in-memory test fixture),
/// returns `Surrogate::ZERO`. Production state always wires a
/// redb-backed `CredentialStore::open` so this branch never fires.
/// - If a binding already exists, return it (no allocation, no flush).
/// - Else: allocate one surrogate, persist the binding, and check
/// the registry's flush threshold; flush durably if tripped.
///
/// Allocation + catalog write happen inside one critical section
/// on the registry write-lock so the registry hwm and the
/// persisted PK row cannot diverge under concurrent assigners.
pub fn assign(
&self,
database_id: DatabaseId,
tenant_id: TenantId,
collection: &str,
pk_bytes: &[u8],
) -> crate::Result<Surrogate> {
let catalog = self.credential_store.catalog();
// Fast-path: existing binding. Done under a read lock — most
// production calls land here once the per-collection working
// set has been observed.
if let Some(s) =
catalog.get_surrogate_for_pk(database_id, tenant_id, collection, pk_bytes)?
{
return Ok(s);
}
// Slow path: allocate + persist + maybe flush. The write lock
// guards the (allocate, write-pk-row) pair so two concurrent
// assigners can't both observe "missing", both allocate, and
// both write — the second would silently overwrite the
// first's binding with a different surrogate.
//
// In cluster mode the allocation source is the node's reserved
// batch (`try_alloc_reserved`); when the batch is empty the
// background refiller normally has the next batch ready, so the
// lock-free draw simply succeeds. The synchronous `ensure_batch`
// refill remains only as a rare safety net — see
// `cluster_reserve` for the full hot-path contract. It MUST run
// WITHOUT the registry write lock held (the applier installs the
// batch under a read guard; holding the write lock across the
// wait would deadlock), so we drop the lock, refill, and retry.
loop {
let registry = self.registry_write()?;
// Re-check inside the lock: another assigner may have raced
// us between the read above and the lock acquisition.
if let Some(s) =
catalog.get_surrogate_for_pk(database_id, tenant_id, collection, pk_bytes)?
{
return Ok(s);
}
let surrogate = match self.alloc_locked(®istry)? {
Some(s) => {
// Proactive top-up: if the batch is running low, nudge
// the background refiller so the next reservation lands
// before the pool drains — keeping the blocking Raft
// round-trip OFF this latency-critical insert path.
self.nudge_refill_if_low(®istry);
s
}
None => {
// Cluster mode, empty batch: the background refiller
// hasn't caught up. Release the lock, nudge it, and fall
// back to a synchronous reservation as a rare safety net
// so liveness is preserved even if the refiller stalled.
drop(registry);
self.refill_notify.notify_one();
self.ensure_batch()?;
continue;
}
};
catalog.put_surrogate(database_id, tenant_id, collection, pk_bytes, surrogate)?;
// Emit a durable WAL bind before the lock releases. Order is
// load-bearing: a crash between catalog write and bind append
// is invisible (the catalog row is already on disk via redb's
// own WAL); a crash before the catalog write leaves nothing
// to recover; a crash between bind append and lock release is
// recovered by replaying the bind into the catalog (idempotent
// via the two-table overwrite).
self.wal_appender.record_bind_to_wal(
database_id,
tenant_id,
surrogate.as_u32(),
collection,
pk_bytes,
)?;
self.maybe_flush(®istry, catalog)?;
return Ok(surrogate);
}
}
/// Allocate a FRESH surrogate for a row with no content primary key — a
/// collection whose primary key is the auto-generated `_rowid` (no
/// `PRIMARY KEY` was declared). Unlike [`assign`](Self::assign), there is
/// no fast-path lookup: every call allocates a new value, so N rows get N
/// distinct surrogates instead of collapsing onto the binding for an empty
/// key.
///
/// The surrogate is self-bound (pk = its own decimal string). The Data
/// Plane sets the row's `_rowid` field equal to this surrogate, so the
/// self-binding makes a later `WHERE _rowid = N` point lookup resolve back
/// to it, and reuses the same durable bind/flush machinery as `assign` so
/// the hwm advance is persisted and Raft-proposed identically.
pub fn assign_fresh(
&self,
database_id: DatabaseId,
tenant_id: TenantId,
collection: &str,
) -> crate::Result<Surrogate> {
let catalog = self.credential_store.catalog();
// Allocate + self-bind + maybe-flush under the registry write lock,
// with the same empty-batch refill fallback as the `assign` slow path.
loop {
let registry = self.registry_write()?;
let surrogate = match self.alloc_locked(®istry)? {
Some(s) => {
self.nudge_refill_if_low(®istry);
s
}
None => {
drop(registry);
self.refill_notify.notify_one();
self.ensure_batch()?;
continue;
}
};
// Self-bind: pk is the surrogate's own decimal string, matching the
// `_rowid` value the Data Plane writes (surrogate as i64) once run
// through `sql_value_to_string`, so `WHERE _rowid = N` resolves.
let pk = surrogate.as_u32().to_string();
let pk_bytes = pk.as_bytes();
catalog.put_surrogate(database_id, tenant_id, collection, pk_bytes, surrogate)?;
self.wal_appender.record_bind_to_wal(
database_id,
tenant_id,
surrogate.as_u32(),
collection,
pk_bytes,
)?;
self.maybe_flush(®istry, catalog)?;
return Ok(surrogate);
}
}
/// Read-only lookup: return the surrogate previously bound to
/// `(collection, pk_bytes)` without ever allocating or writing.
/// Used by point-read/update/delete planning where a missing
/// binding means the row does not exist (semantic no-op).
///
/// When the credential store has no catalog (in-memory test
/// fixture), returns `Some(Surrogate::ZERO)` — mirroring the
/// `Surrogate::ZERO` allocation `assign` performs in the same
/// catalog-less mode, so a write/read pair against an unwired
/// catalog still resolves to the same identity.
pub fn lookup(
&self,
database_id: DatabaseId,
tenant_id: TenantId,
collection: &str,
pk_bytes: &[u8],
) -> crate::Result<Option<Surrogate>> {
let catalog = self.credential_store.catalog();
catalog.get_surrogate_for_pk(database_id, tenant_id, collection, pk_bytes)
}
/// Bind `(collection, pk_bytes)` to a *carried* surrogate without ever
/// allocating, resolving concurrent carried values **first-wins** and
/// returning the *authoritative* surrogate the caller must use.
///
/// Used on the Raft apply path: a coordinator assigned the surrogate at
/// plan time, embedded it in the plan, and carried it on the wire; the
/// owner installs that identity rather than drawing a fresh (divergent)
/// one from its own allocator. Because two different non-owner
/// coordinators can each assign a *different* surrogate (from disjoint
/// HiLo batches) for the *same* key, the owner must resolve this
/// deterministically: the FIRST binding wins and every later carried
/// value is discarded. The returned `Surrogate` is the authoritative one
/// (the already-bound value when one exists, else the carried value just
/// persisted) and MUST be used as the storage key by the caller —
/// otherwise the owner would create duplicate rows under different
/// surrogates for the same key.
///
/// - No catalog (in-memory test fixture): returns `Ok(surrogate)` — the
/// carried value is authoritative, nothing to persist (mirrors
/// `assign`'s catalog-less branch).
/// - Binding already exists: returns `Ok(existing)` — first-wins, never
/// overwrites, discards the carried value even if it differs.
/// - Otherwise: persist the binding + emit the durable WAL bind under the
/// registry write lock (same order as `assign`), `restore_hwm` so the
/// global watermark stays ahead of the carried value, and return the
/// now-bound `surrogate`.
///
/// Replay/retry is idempotent: re-applying the same entry finds the
/// existing binding in the pre-check and returns it without writing.
///
/// Crucially this never touches `alloc_locked`/`maybe_flush`: the
/// allocator counter must NOT advance on a bind — that would burn a
/// surrogate and diverge from the coordinator.
pub fn bind(
&self,
database_id: DatabaseId,
tenant_id: TenantId,
collection: &str,
pk_bytes: &[u8],
surrogate: Surrogate,
) -> crate::Result<Surrogate> {
let catalog = self.credential_store.catalog();
// First-wins pre-check under a read lock: if any binding is already
// installed (replay, retry, or a competing coordinator's carried
// value applied first) it is authoritative — return it, never
// overwrite, discard the carried value even if it differs.
if let Some(existing) =
catalog.get_surrogate_for_pk(database_id, tenant_id, collection, pk_bytes)?
{
return Ok(existing);
}
// Hold the registry write lock across (re-check, persist binding,
// WAL bind, hwm advance) so it is one critical section — same lock
// discipline as `assign`, which also serializes on this write lock.
// `restore_hwm` itself is atomic on the counter; we call it through
// the held guard rather than re-locking (which would deadlock on
// this std `RwLock`).
let registry = self.registry_write()?;
// Re-check under the lock (TOCTOU): a concurrent `assign`/`bind` on
// this node could have written between the pre-check and the lock.
// First-wins still applies — return the existing value.
if let Some(existing) =
catalog.get_surrogate_for_pk(database_id, tenant_id, collection, pk_bytes)?
{
return Ok(existing);
}
catalog.put_surrogate(database_id, tenant_id, collection, pk_bytes, surrogate)?;
self.wal_appender.record_bind_to_wal(
database_id,
tenant_id,
surrogate.as_u32(),
collection,
pk_bytes,
)?;
// Advance the local watermark past the carried value so a later
// LOCAL `assign`/`assign_anonymous` on this node can never re-issue
// it. Idempotent and monotonic — never lowers, never advances the
// allocator's draw position (only the hwm floor).
registry
.restore_hwm(surrogate.as_u32())
.map_err(|e| crate::Error::Internal {
detail: format!("surrogate bind restore_hwm failed: {e}"),
})?;
Ok(surrogate)
}
/// Allocate a fresh surrogate for an entity that has no user-facing
/// primary key (e.g. headless vector inserts). The surrogate is
/// self-keyed in the catalog (`pk_bytes = surrogate.as_u32().to_be_bytes()`)
/// so the binding round-trips homogeneously with named-PK rows: a
/// later lookup via the self-bytes returns the same surrogate, and
/// the reverse lookup returns the self-bytes back. Keeps the
/// catalog single-shaped — no special-case "unbound" rows.
pub fn assign_anonymous(
&self,
database_id: DatabaseId,
tenant_id: TenantId,
collection: &str,
) -> crate::Result<Surrogate> {
let catalog = self.credential_store.catalog();
loop {
let registry = self.registry_write()?;
let surrogate = match self.alloc_locked(®istry)? {
Some(s) => {
self.nudge_refill_if_low(®istry);
s
}
None => {
drop(registry);
self.refill_notify.notify_one();
self.ensure_batch()?;
continue;
}
};
let self_bytes = surrogate.as_u32().to_be_bytes();
catalog.put_surrogate(database_id, tenant_id, collection, &self_bytes, surrogate)?;
self.wal_appender.record_bind_to_wal(
database_id,
tenant_id,
surrogate.as_u32(),
collection,
&self_bytes,
)?;
self.maybe_flush(®istry, catalog)?;
return Ok(surrogate);
}
}
}
#[cfg(test)]
mod tests {
use super::super::types::SurrogateAssigner;
use nodedb_types::{DatabaseId, Surrogate, TenantId};
use std::sync::{Arc, RwLock};
use crate::control::security::credential::CredentialStore;
use crate::control::surrogate::registry::SurrogateRegistry;
use crate::control::surrogate::wal_appender::{NoopWalAppender, SurrogateWalAppender};
fn open_test() -> (tempfile::TempDir, Arc<SurrogateAssigner>) {
let dir = tempfile::tempdir().unwrap();
let credentials = Arc::new(CredentialStore::open(&dir.path().join("system.redb")).unwrap());
let reg = Arc::new(RwLock::new(SurrogateRegistry::new()));
let wal: Arc<dyn SurrogateWalAppender> = Arc::new(NoopWalAppender);
let a = Arc::new(SurrogateAssigner::new(reg, credentials, wal));
(dir, a)
}
const T0: TenantId = TenantId::new(0);
#[test]
fn assign_is_idempotent_for_same_pk() {
let (_dir, a) = open_test();
let s1 = a
.assign(DatabaseId::DEFAULT, T0, "users", b"alice")
.unwrap();
let s2 = a
.assign(DatabaseId::DEFAULT, T0, "users", b"alice")
.unwrap();
assert_eq!(s1, s2);
assert_eq!(s1, Surrogate::new(1));
}
#[test]
fn assign_distinct_tenants_do_not_collide() {
let (_dir, a) = open_test();
let t1 = TenantId::new(1);
let t2 = TenantId::new(2);
let s1 = a
.assign(DatabaseId::DEFAULT, t1, "users", b"alice")
.unwrap();
let s2 = a
.assign(DatabaseId::DEFAULT, t2, "users", b"alice")
.unwrap();
assert_ne!(s1, s2);
}
#[test]
fn assign_distinct_pks_returns_distinct_surrogates() {
let (_dir, a) = open_test();
let s1 = a
.assign(DatabaseId::DEFAULT, T0, "users", b"alice")
.unwrap();
let s2 = a.assign(DatabaseId::DEFAULT, T0, "users", b"bob").unwrap();
assert_ne!(s1, s2);
}
#[test]
fn assign_writes_reverse_binding() {
let (_dir, a) = open_test();
let s = a
.assign(DatabaseId::DEFAULT, T0, "users", b"alice")
.unwrap();
let cat = a.credential_store.catalog();
assert_eq!(
cat.get_pk_for_surrogate(DatabaseId::DEFAULT, T0, "users", s)
.unwrap(),
Some(b"alice".to_vec())
);
}
#[test]
fn assign_persists_hwm_at_flush_threshold() {
let (_dir, a) = open_test();
// Allocate just up to and across the 1024 ops threshold.
let n = crate::control::surrogate::registry::FLUSH_OPS_THRESHOLD as usize;
for i in 0..n {
let pk = format!("u{i}");
let _ = a
.assign(DatabaseId::DEFAULT, T0, "users", pk.as_bytes())
.unwrap();
}
// Either threshold (1024 ops or 200 ms elapsed) may fire
// first; assert only that the catalog persisted *some*
// checkpoint inside the (0, n] band.
let cat = a.credential_store.catalog();
let persisted = cat.get_surrogate_hwm().unwrap();
assert!(persisted > 0 && persisted <= n as u32);
}
}