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
// SPDX-License-Identifier: BUSL-1.1
//! CRDT delta-apply handler: validate + materialize an applied Loro delta,
//! for both the non-sync (SQL / native client) and sync (peer) paths.
//!
//! Split out of `crdt.rs` to keep that file within the file-size limit. The
//! one-document-per-delta contract enforced here is the crux: cross-engine
//! identity assigns exactly one Control-Plane surrogate per delta, so the Data
//! Plane can materialize only the single frame-declared row. A delta whose
//! write-set names any other or additional row must be rejected loudly rather
//! than materialized partially — silently dropping the extra rows is the
//! data-loss bug this guard closes.
use tracing::{debug, warn};
use nodedb_types::Surrogate;
use nodedb_types::sync::violation::ViolationType;
use nodedb_types::sync::wire::{AckStatus, SyncProvenance};
use crate::bridge::envelope::{ErrorCode, Response};
use crate::data::executor::sync_gate::SyncAdmit;
use crate::engine::crdt::tenant_state::ValidatedApplyOutcome;
use crate::data::executor::core_loop::CoreLoop;
use crate::data::executor::task::ExecutionTask;
/// Parameters for [`CoreLoop::execute_crdt_apply`].
pub(in crate::data::executor) struct CrdtApplyParams<'a> {
pub collection: &'a str,
pub document_id: &'a str,
pub delta: &'a [u8],
pub surrogate: Surrogate,
pub peer_id: u64,
pub provenance: Option<&'a SyncProvenance>,
pub constraint_version_required: u64,
}
impl CoreLoop {
/// Enforce the one-document-per-delta contract: every row a validated delta
/// wrote must be exactly the frame-declared `(collection, document_id)`.
///
/// Cross-engine identity assigns exactly one Control-Plane surrogate per
/// delta, so the Data Plane can materialize only that single row. A delta
/// whose write-set names any other or additional row — a client that
/// coalesced N document upserts into one delta, or tagged the frame with a
/// synthetic id matching no written row — cannot be materialized without a
/// surrogate per extra row; materializing just one would silently drop the
/// rest. Returns a human-readable detail naming the offending rows so the
/// caller surfaces the violation instead of losing data.
fn single_document_write_set(
collection: &str,
document_id: &str,
write_set: &[(String, String)],
) -> Result<(), String> {
let foreign: Vec<String> = write_set
.iter()
.filter(|(coll, row)| coll != collection || row != document_id)
.map(|(coll, row)| format!("{coll}/{row}"))
.collect();
if foreign.is_empty() {
Ok(())
} else {
Err(format!(
"delta for {collection}/{document_id} wrote {} row(s) outside its frame \
target: [{}]; a delta must carry exactly one document (cross-engine \
identity binds one surrogate per delta)",
foreign.len(),
foreign.join(", ")
))
}
}
pub(in crate::data::executor) fn execute_crdt_apply(
&mut self,
task: &ExecutionTask,
params: CrdtApplyParams<'_>,
) -> Response {
let CrdtApplyParams {
collection,
document_id,
delta,
surrogate,
peer_id,
provenance,
constraint_version_required,
} = params;
let tenant_id = task.request.tenant_id;
let Some(prov) = provenance else {
// Non-sync path (SQL / native client): validate + apply, no gate.
// There is no client to reject here, so the validated outcome is
// only observed for its DLQ side effect and logged.
// Borrow the engine in a nested block so the &mut borrow is dropped
// before the sparse write below takes &self. On a Clean apply we
// read the merged row back and encode it while the borrow is live,
// carrying the materialized bytes out.
let materialized = {
let engine = match self.get_crdt_engine(task.request.database_id, tenant_id) {
Ok(e) => e,
Err(e) => {
warn!(core = self.core_id, error = %e, "failed to create CRDT engine");
return self.response_error(
task,
ErrorCode::Internal {
detail: e.to_string(),
},
);
}
};
let outcome = engine.apply_committed_delta_validated(
collection,
delta,
surrogate,
document_id,
peer_id,
);
match outcome {
ValidatedApplyOutcome::Clean { write_set } => {
// Enforce the one-document-per-delta contract before
// materializing: a delta that wrote rows other than the
// frame target has no surrogate for those rows, so
// materializing only `document_id` would silently drop
// the rest.
match Self::single_document_write_set(collection, document_id, &write_set) {
Ok(()) => {
if surrogate != Surrogate::ZERO {
Ok(Self::encode_crdt_row(engine, collection, document_id))
} else {
Ok(None)
}
}
Err(detail) => Err(detail),
}
}
ValidatedApplyOutcome::Rejected(vt) => {
debug!(core = self.core_id, %collection, reason = %vt, "crdt apply violated constraint (DLQ)");
Ok(None)
}
ValidatedApplyOutcome::Malformed => {
warn!(core = self.core_id, %collection, "crdt apply skipped malformed delta");
Ok(None)
}
}
};
// engine borrow dropped here. The Loro import already happened, so
// the checkpoint must capture it regardless of the outcome below.
self.checkpoint_coordinator.mark_dirty("crdt", 1);
// Materialize into the sparse document store so DocumentScan /
// ShapeSnapshot see the synced document — unless the delta violated
// the one-document-per-delta contract, in which case reject loudly
// rather than materialize a partial row.
match materialized {
Ok(Some(bytes)) => {
self.materialize_synced_document(
task,
tenant_id.as_u64(),
collection,
surrogate,
&bytes,
);
}
Ok(None) => {}
Err(detail) => {
warn!(
core = self.core_id,
%collection,
%document_id,
detail = %detail,
"crdt apply rejected: multi-document delta violates one-document-per-delta contract"
);
return self.response_error(
task,
ErrorCode::RejectedConstraint {
constraint: "crdt_single_document_delta".to_string(),
detail,
},
);
}
}
return self.response_ok(task);
};
// Sync path: run the idempotency gate before touching the engine.
// Call sync_admit first (exclusive &mut self borrow, no engine borrow).
let admit = self.sync_admit(prov);
// Snapshot the current HWM for Duplicate / Fenced / Gap responses
// before any engine borrow.
let current_hwm = self.sync_hwm_value(prov.producer_id, prov.stream_id);
let (status, applied_seq, reject) = match admit {
SyncAdmit::Apply => {
// Borrow the engine in a nested block so the &mut borrow is
// dropped before sync_commit takes &mut self for sync_hwm.
// The validated apply never fails: a violation is DLQ'd and a
// corrupt blob is a no-op, so the HWM always advances and the
// stream cannot wedge.
//
// Before validating, fence the delta against the constraint
// version it was admitted against. `SetConstraints` rides the
// same per-vshard data Raft log as this `CrdtApply`, so at
// this log index every replica has applied the identical log
// prefix and therefore has the identical installed
// `constraint_versions[collection]` — the gate decision is
// deterministic across replicas, no divergence.
enum GateOutcome {
Pending { installed: u64 },
Applied(ValidatedApplyOutcome),
}
let (outcome, materialized) = {
let engine = match self.get_crdt_engine(task.request.database_id, tenant_id) {
Ok(e) => e,
Err(e) => {
warn!(core = self.core_id, error = %e, "failed to create CRDT engine");
return self.response_error(
task,
ErrorCode::Internal {
detail: e.to_string(),
},
);
}
};
let installed = engine.installed_constraint_version(collection);
if constraint_version_required > installed {
(GateOutcome::Pending { installed }, None)
} else {
let applied = engine.apply_committed_delta_validated(
collection,
delta,
surrogate,
document_id,
peer_id,
);
// On a Clean apply, read the merged row back and encode
// it while the engine borrow is still live so the bytes
// can be materialized into the sparse store below.
let mat = if matches!(applied, ValidatedApplyOutcome::Clean { .. })
&& surrogate != Surrogate::ZERO
{
Self::encode_crdt_row(engine, collection, document_id)
} else {
None
};
(GateOutcome::Applied(applied), mat)
}
};
// engine borrow is dropped here; mark_dirty / sync_commit take
// &mut self, and the sparse materialize takes &self.
let reject = match outcome {
GateOutcome::Pending { installed } => {
// Create-race: the constraints this delta was admitted
// against are not yet installed on THIS replica (the
// reconcile loop delivers SetConstraints
// asynchronously). Do NOT import an unvalidated delta
// — that is exactly the hole this fence closes.
// Carry a retryable reject; the client re-pushes once
// the install catches up. This is NOT a dead letter,
// so it is not DLQ'd.
debug!(
core = self.core_id,
%collection,
required = constraint_version_required,
installed,
"crdt apply fenced: constraint version pending (retryable)"
);
Some(ViolationType::ConstraintVersionPending {
collection: collection.to_string(),
required: constraint_version_required,
installed,
})
}
GateOutcome::Applied(ValidatedApplyOutcome::Clean { write_set }) => {
self.checkpoint_coordinator.mark_dirty("crdt", 1);
// Enforce the one-document-per-delta sync contract. A
// delta that coalesced multiple documents (or targeted
// a synthetic frame id that matches no written row)
// cannot be materialized past its single surrogate;
// reject it loudly so the client re-pushes one delta per
// document instead of silently losing rows.
match Self::single_document_write_set(collection, document_id, &write_set) {
Ok(()) => None,
Err(detail) => {
warn!(
core = self.core_id,
%collection,
%document_id,
detail = %detail,
"crdt sync apply rejected: multi-document delta violates one-document-per-delta contract"
);
Some(ViolationType::ConstraintViolation { detail })
}
}
}
GateOutcome::Applied(ValidatedApplyOutcome::Rejected(vt)) => {
self.checkpoint_coordinator.mark_dirty("crdt", 1);
Some(vt)
}
GateOutcome::Applied(ValidatedApplyOutcome::Malformed) => {
warn!(core = self.core_id, %collection, "crdt apply skipped malformed delta");
None
}
};
// Materialize the merged document into the sparse store so
// DocumentScan / ShapeSnapshot see the synced write. `materialized`
// is Some only on a Clean apply with an assigned surrogate, and a
// contract-violating delta (`reject` set) must not surface a
// partial row.
if reject.is_none()
&& let Some(bytes) = materialized
{
self.materialize_synced_document(
task,
tenant_id.as_u64(),
collection,
surrogate,
&bytes,
);
}
// Advance the HWM unconditionally after apply — a rejected,
// fenced, or malformed delta must not wedge the sync stream.
self.sync_commit(prov);
(AckStatus::Applied, prov.seq, reject)
}
SyncAdmit::Duplicate => (AckStatus::Duplicate, current_hwm, None),
SyncAdmit::Fenced => (AckStatus::Fenced, current_hwm, None),
SyncAdmit::Gap { expected } => (AckStatus::Gap { expected }, current_hwm, None),
};
self.sync_ack_response_ext(task, status, applied_seq, reject)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ws(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(c, r)| (c.to_string(), r.to_string()))
.collect()
}
#[test]
fn single_matching_row_is_accepted() {
assert!(CoreLoop::single_document_write_set("users", "a", &ws(&[("users", "a")])).is_ok());
}
#[test]
fn empty_write_set_is_accepted() {
// A delete / no-op delta wrote no rows — nothing to materialize, no
// contract violation.
assert!(CoreLoop::single_document_write_set("users", "a", &ws(&[])).is_ok());
}
#[test]
fn additional_row_is_rejected() {
// Frame targets "a" but the delta also wrote "b": the extra row has no
// surrogate and would be silently dropped.
let err = CoreLoop::single_document_write_set(
"users",
"a",
&ws(&[("users", "a"), ("users", "b")]),
)
.expect_err("multi-row delta must be rejected");
assert!(
err.contains("users/b"),
"detail names the offending row: {err}"
);
}
#[test]
fn synthetic_frame_id_matching_no_written_row_is_rejected() {
// The batch-coalesced bug: frame id "5_ops" matches no real written
// row, so every written row is "foreign" and the delta is rejected
// instead of materializing zero rows.
let err = CoreLoop::single_document_write_set(
"entries",
"5_ops",
&ws(&[("entries", "u1"), ("entries", "u2")]),
)
.expect_err("synthetic frame id must be rejected");
assert!(err.contains("entries/u1") && err.contains("entries/u2"));
}
#[test]
fn foreign_collection_is_rejected() {
let err = CoreLoop::single_document_write_set("users", "a", &ws(&[("orders", "a")]))
.expect_err("row in a different collection must be rejected");
assert!(err.contains("orders/a"));
}
}