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 t
(&self, collection: &str, pk_bytes: &[u8])
| 97 | /// on the registry write-lock so the registry hwm and the |
| 98 | /// persisted PK row cannot diverge under concurrent assigners. |
| 99 | pub fn assign(&self, collection: &str, pk_bytes: &[u8]) -> crate::Result<Surrogate> { |
| 100 | let catalog = match self.credential_store.catalog().as_ref() { |
| 101 | Some(c) => c, |
| 102 | None => return Ok(Surrogate::ZERO), |
| 103 | }; |
| 104 | |
| 105 | // Fast-path: existing binding. Done under a read lock — most |
| 106 | // production calls land here once the per-collection working |
| 107 | // set has been observed. |
| 108 | if let Some(s) = catalog.get_surrogate_for_pk(DatabaseId::DEFAULT, collection, pk_bytes)? { |
| 109 | return Ok(s); |
| 110 | } |
| 111 | |
| 112 | // Slow path: allocate + persist + maybe flush. The write lock |
| 113 | // guards the (allocate, write-pk-row) pair so two concurrent |
| 114 | // assigners can't both observe "missing", both allocate, and |
| 115 | // both write — the second would silently overwrite the |
| 116 | // first's binding with a different surrogate. |
| 117 | let registry = self.registry.write().map_err(|_| crate::Error::Internal { |
| 118 | detail: "surrogate registry lock poisoned".into(), |
| 119 | })?; |
| 120 | // Re-check inside the lock: another assigner may have raced |
| 121 | // us between the read above and the lock acquisition. |
| 122 | if let Some(s) = catalog.get_surrogate_for_pk(DatabaseId::DEFAULT, collection, pk_bytes)? { |
| 123 | return Ok(s); |
| 124 | } |
| 125 | let surrogate = registry.alloc_one()?; |
| 126 | catalog.put_surrogate(DatabaseId::DEFAULT, collection, pk_bytes, surrogate)?; |
| 127 | // Emit a durable WAL bind before the lock releases. Order is |
| 128 | // load-bearing: a crash between catalog write and bind append |
| 129 | // is invisible (the catalog row is already on disk via redb's |
| 130 | // own WAL); a crash before the catalog write leaves nothing |
| 131 | // to recover; a crash between bind append and lock release is |
| 132 | // recovered by replaying the bind into the catalog (idempotent |
| 133 | // via the two-table overwrite). |
| 134 | self.wal_appender |
| 135 | .record_bind_to_wal(surrogate.as_u32(), collection, pk_bytes)?; |
| 136 | |
| 137 | // Flush trigger: durably checkpoint the new hwm if either the |
| 138 | // ops or elapsed-time threshold has tripped. Both writes are |
| 139 | // idempotent so a crash between them on a re-run replays |
| 140 | // cleanly. |
| 141 | if registry.should_flush() { |
| 142 | let raft_shared = self.shared.get().and_then(|w| w.upgrade()); |
| 143 | let combined = CombinedPersist { |
| 144 | catalog, |
| 145 | wal_appender: self.wal_appender.as_ref(), |
| 146 | raft_shared: raft_shared.as_deref(), |
| 147 | }; |
| 148 | registry.flush(&combined)?; |
| 149 | } |
| 150 | |
| 151 | Ok(surrogate) |
| 152 | } |
| 153 | |
| 154 | /// Read-only lookup: return the surrogate previously bound to |
| 155 | /// `(collection, pk_bytes)` without ever allocating or writing. |