Skip to content

Commit 0803d82

Browse files
committed
feat(cluster): validate Calvin read-sets against write versions at apply
Each participant now checks its own vShard's slice of the transaction's LSN-versioned read-set against the local write-version index immediately before applying, and reports whether it was still current on the executor response. The check is observation-only: apply always commits regardless of outcome. A node-global counter tracks how often a not-current read-set is observed, giving tests and metrics visibility into staleness without reaching into the per-core write-version index.
1 parent 444811a commit 0803d82

41 files changed

Lines changed: 501 additions & 27 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

nodedb-physical/src/physical_plan/meta.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use std::collections::BTreeMap;
66

7-
use nodedb_types::calvin::PassiveReadKey;
7+
use nodedb_types::calvin::{PassiveReadKey, VersionedReadEntry};
88
use nodedb_types::timeseries::continuous_agg::ContinuousAggregateDef;
99
use nodedb_types::{TenantId, Value};
1010

@@ -303,19 +303,19 @@ pub enum MetaOp {
303303
/// the wall clock independently. Wire-additive: defaults to 0 on decode
304304
/// of older entries.
305305
epoch_system_ms: i64,
306-
/// Whether THIS node is the leader of the data-group owning this
307-
/// vshard, stamped by the dispatching scheduler at dispatch time.
308-
///
309-
/// OLLP determinism: the optimistic-lock verification (`actual !=
310-
/// predicted`) and the resulting `OllpRetryRequired` are LEADER-ONLY.
311-
/// Followers trust the leader's verified decision and apply the carried
312-
/// `ollp_predicted_surrogates` set verbatim, so every replica mutates
313-
/// the identical surrogate set (Calvin determinism). This flag is a
314-
/// per-node, non-replicated dispatch property — the scheduler on each
315-
/// node stamps its OWN role; it is never part of the replicated log.
316-
/// Wire-additive: defaults to `false` on decode of older entries (safe:
317-
/// a follower-style apply against the carried predicted set).
306+
/// Whether THIS node is the leader of the data-group owning this vshard,
307+
/// stamped by the dispatching scheduler. OLLP verification (`actual !=
308+
/// predicted` → `OllpRetryRequired`) is leader-only; followers apply the
309+
/// carried `ollp_predicted_surrogates` verbatim so every replica mutates
310+
/// the identical set (Calvin determinism). Per-node, never replicated.
311+
/// Wire-additive: defaults to `false` on decode of older entries.
318312
is_group_leader: bool,
313+
/// The transaction's LSN-versioned read-set from the replicated `TxClass`.
314+
/// Each participant checks its own vShard's reads at apply and records
315+
/// whether they were still current, without gating apply. Wire-additive:
316+
/// defaults to empty on decode of older entries.
317+
#[serde(default)]
318+
versioned_reads: Vec<VersionedReadEntry>,
319319
},
320320

321321
/// Calvin dependent-read executor: passive participant reads keys and

nodedb/src/bridge/dispatch.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ mod tests {
579579
payload: Payload::from_vec(b"result".to_vec()),
580580
watermark_lsn: Lsn::new(42),
581581
error_code: None,
582+
read_set_valid: None,
582583
},
583584
})
584585
.unwrap();
@@ -631,6 +632,7 @@ mod tests {
631632
payload: Payload::empty(),
632633
watermark_lsn: Lsn::ZERO,
633634
error_code: None,
635+
read_set_valid: None,
634636
},
635637
})
636638
.unwrap();

nodedb/src/bridge/envelope.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,16 @@ pub struct Response {
245245

246246
/// Error code if status is not Ok.
247247
pub error_code: Option<ErrorCode>,
248+
249+
/// Whether this response's originating transaction found its slice of the
250+
/// versioned read-set still current against the local write versions at
251+
/// apply time. `Some(true)` = still current (or no reads observed for this
252+
/// slice); `Some(false)` = at least one read was superseded; `None` = the
253+
/// response did not carry a read-set check (reads, control ops, and every
254+
/// non-transaction response). Populated only by the transaction-apply path.
255+
/// Reporting only — the apply commits regardless; a `Some(false)` here does
256+
/// not abort or alter the write.
257+
pub read_set_valid: Option<bool>,
248258
}
249259

250260
pub use nodedb_physical::physical_plan::PhysicalPlan;
@@ -480,6 +490,7 @@ mod tests {
480490
payload: Payload::from_vec(b"result".to_vec()),
481491
watermark_lsn: Lsn::new(42),
482492
error_code: None,
493+
read_set_valid: None,
483494
};
484495
assert_eq!(resp.status, Status::Ok);
485496
assert_eq!(resp.watermark_lsn, Lsn::new(42));
@@ -496,6 +507,7 @@ mod tests {
496507
payload: Payload::empty(),
497508
watermark_lsn: Lsn::ZERO,
498509
error_code: Some(ErrorCode::DeadlineExceeded),
510+
read_set_valid: None,
499511
};
500512
assert_eq!(resp.error_code, Some(ErrorCode::DeadlineExceeded));
501513
}

nodedb/src/control/cluster/calvin/scheduler/driver/core/dispatch.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ impl Scheduler {
212212
plans,
213213
epoch_system_ms: txn.epoch_system_ms,
214214
is_group_leader: self.is_group_leader(),
215+
// The replicated read-set travels to the apply core so each
216+
// participant can check, at apply, whether its slice of the reads was
217+
// still current. Empty for pure-write / autocommit transactions.
218+
versioned_reads: txn.tx_class.versioned_reads.as_slice().to_vec(),
215219
});
216220

217221
// no-determinism: request deadline is ephemeral, not written to WAL

nodedb/src/control/cluster/calvin/scheduler/driver/core/scheduler.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,17 @@ impl Scheduler {
318318
}
319319

320320
if response.status == crate::bridge::envelope::Status::Ok {
321+
// Observe whether the applying participant reported its slice of the
322+
// transaction's reads as no longer current against the local write
323+
// versions. Observation only: the apply already committed and nothing
324+
// is aborted here — the count is a node-global signal for tests and
325+
// metrics. `None` means no read-set was checked (fast path / no reads).
326+
if response.read_set_valid == Some(false) {
327+
self.shared
328+
.read_set_validation_failures
329+
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
330+
}
331+
321332
// Deposit the FULL applied Response (affected-count + watermark +
322333
// any RETURNING rows) into the local sidecar BEFORE proposing the
323334
// replicated CompletionAck. The ack fires the coordinator's

nodedb/src/control/planner/calvin/submit.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ fn synthetic_returning_response(payload_bytes: Vec<u8>) -> Response {
6363
payload: Payload::from_vec(payload_bytes),
6464
watermark_lsn: Lsn::ZERO,
6565
error_code: None,
66+
read_set_valid: None,
6667
}
6768
}
6869

nodedb/src/control/request_tracker.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ mod tests {
121121
payload: Payload::empty(),
122122
watermark_lsn: Lsn::ZERO,
123123
error_code: None,
124+
read_set_valid: None,
124125
}
125126
}
126127

@@ -133,6 +134,7 @@ mod tests {
133134
payload: Payload::from_vec(data.as_bytes().to_vec()),
134135
watermark_lsn: Lsn::ZERO,
135136
error_code: None,
137+
read_set_valid: None,
136138
}
137139
}
138140

nodedb/src/control/server/broadcast.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ pub async fn broadcast_to_all_cores_txn(
103103
payload: crate::bridge::envelope::Payload::from_vec(outcome.merged_array),
104104
watermark_lsn: outcome.watermark_lsn,
105105
error_code: None,
106+
read_set_valid: None,
106107
})
107108
}
108109

@@ -209,6 +210,7 @@ pub async fn broadcast_count_to_all_cores(
209210
payload: crate::bridge::envelope::Payload::from_vec(payload),
210211
watermark_lsn: max_lsn,
211212
error_code: None,
213+
read_set_valid: None,
212214
})
213215
}
214216

nodedb/src/control/server/dispatch_utils.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ mod collect_budget_tests {
618618
payload: Payload::from_vec(array_payload(n)),
619619
watermark_lsn: Lsn::ZERO,
620620
error_code: None,
621+
read_set_valid: None,
621622
}
622623
}
623624

@@ -630,6 +631,7 @@ mod collect_budget_tests {
630631
payload: Payload::from_vec(array_payload(n)),
631632
watermark_lsn: Lsn::ZERO,
632633
error_code: None,
634+
read_set_valid: None,
633635
}
634636
}
635637

@@ -643,6 +645,7 @@ mod collect_budget_tests {
643645
payload: Payload::from_vec(vec![0u8; bytes]),
644646
watermark_lsn: Lsn::ZERO,
645647
error_code: None,
648+
read_set_valid: None,
646649
}
647650
}
648651

@@ -655,6 +658,7 @@ mod collect_budget_tests {
655658
payload: Payload::from_vec(vec![0u8; bytes]),
656659
watermark_lsn: Lsn::ZERO,
657660
error_code: None,
661+
read_set_valid: None,
658662
}
659663
}
660664

nodedb/src/control/server/exchange/gather.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,5 +448,6 @@ pub(super) fn outcome_to_response(merged_array: Vec<u8>, watermark_lsn: Lsn) ->
448448
payload: crate::bridge::envelope::Payload::from_vec(merged_array),
449449
watermark_lsn,
450450
error_code: None,
451+
read_set_valid: None,
451452
}
452453
}

0 commit comments

Comments
 (0)