1use super::{CheckpointAttempt, CheckpointWatermark, Deserialize, NodeId, Serialize};
4#[cfg(feature = "cluster")]
5use super::{Duration, BARRIER_ENDPOINT_VERSION, MAX_BARRIER_ENDPOINT_BYTES};
6
7#[cfg(feature = "cluster")]
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub(super) struct BarrierProcessIdentity {
13 pub(super) node_id: u64,
14 pub(super) boot_incarnation: uuid::Uuid,
15 pub(super) process_term: u64,
16}
17
18#[cfg(feature = "cluster")]
19impl BarrierProcessIdentity {
20 pub(super) fn from_process_lease(lease: &super::super::ProcessLease) -> Result<Self, String> {
21 lease
22 .validate(lease.node)
23 .map_err(|error| error.to_string())?;
24 Ok(Self {
25 node_id: lease.node.0,
26 boot_incarnation: lease.owner,
27 process_term: lease.term,
28 })
29 }
30
31 pub(super) const fn is_canonical(self) -> bool {
32 self.node_id != 0 && !self.boot_incarnation.is_nil() && self.process_term != 0
33 }
34
35 pub(super) fn matches_participant(
36 self,
37 participant: &crate::checkpoint::CheckpointParticipant,
38 ) -> bool {
39 self.node_id == participant.node_id && self.boot_incarnation == participant.boot_incarnation
40 }
41}
42
43#[cfg(feature = "cluster")]
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub(super) struct BarrierEndpointRecord {
47 version: u8,
48 address: String,
49 process: BarrierProcessIdentity,
50}
51
52#[cfg(feature = "cluster")]
53impl BarrierEndpointRecord {
54 pub(super) fn new(address: String, process: BarrierProcessIdentity) -> Result<Self, String> {
55 let record = Self {
56 version: BARRIER_ENDPOINT_VERSION,
57 address,
58 process,
59 };
60 record.validate()?;
61 Ok(record)
62 }
63
64 pub(super) fn validate(&self) -> Result<(), String> {
65 if self.version != BARRIER_ENDPOINT_VERSION {
66 return Err(format!(
67 "unsupported cluster control endpoint version {}",
68 self.version
69 ));
70 }
71 if self.address.is_empty() || self.address.len() > MAX_BARRIER_ENDPOINT_BYTES / 2 {
72 return Err("cluster control endpoint address is empty or oversized".into());
73 }
74 if !self.process.is_canonical() {
75 return Err("cluster control endpoint process identity is not canonical".into());
76 }
77 Ok(())
78 }
79
80 pub(super) fn encode(&self) -> Result<String, String> {
81 let encoded = serde_json::to_string(self).map_err(|error| error.to_string())?;
82 if encoded.len() > MAX_BARRIER_ENDPOINT_BYTES {
83 return Err("cluster control endpoint advertisement is oversized".into());
84 }
85 Ok(encoded)
86 }
87}
88
89#[cfg(feature = "cluster")]
90#[derive(Debug, Clone, Copy)]
91pub(super) struct ExpectedBarrierProcess {
92 node_id: u64,
93 boot_incarnation: uuid::Uuid,
94 process_term: Option<u64>,
95}
96
97#[cfg(feature = "cluster")]
98impl ExpectedBarrierProcess {
99 pub(super) const fn participant(node_id: u64, boot_incarnation: uuid::Uuid) -> Self {
100 Self {
101 node_id,
102 boot_incarnation,
103 process_term: None,
104 }
105 }
106
107 pub(super) const fn exact(process: &crate::checkpoint::LeaderProofOwner) -> Self {
108 Self {
109 node_id: process.node_id,
110 boot_incarnation: process.boot_id,
111 process_term: Some(process.process_term),
112 }
113 }
114
115 pub(super) fn matches(self, actual: BarrierProcessIdentity) -> bool {
116 self.node_id == actual.node_id
117 && self.boot_incarnation == actual.boot_incarnation
118 && self
119 .process_term
120 .is_none_or(|term| term == actual.process_term)
121 }
122}
123
124#[cfg(feature = "cluster")]
125pub(super) fn decode_barrier_endpoint(
126 raw: &str,
127) -> Result<(String, Option<BarrierProcessIdentity>), String> {
128 if raw.len() > MAX_BARRIER_ENDPOINT_BYTES {
129 return Err("cluster control endpoint advertisement is oversized".into());
130 }
131 if !raw.trim_start().starts_with('{') {
132 if raw.is_empty() {
133 return Err("cluster control endpoint address is empty".into());
134 }
135 return Ok((raw.to_string(), None));
136 }
137 let record: BarrierEndpointRecord = serde_json::from_str(raw)
138 .map_err(|error| format!("invalid cluster control endpoint advertisement: {error}"))?;
139 record.validate()?;
140 Ok((record.address, Some(record.process)))
141}
142
143#[cfg(feature = "cluster")]
144pub(super) fn encode_barrier_endpoint(
145 address: &str,
146 process: Option<BarrierProcessIdentity>,
147) -> Result<String, String> {
148 if address.is_empty() || address.len() > MAX_BARRIER_ENDPOINT_BYTES / 2 {
149 return Err("cluster control endpoint address is empty or oversized".into());
150 }
151 process.map_or_else(
152 || Ok(address.to_string()),
153 |process| BarrierEndpointRecord::new(address.to_string(), process)?.encode(),
154 )
155}
156
157#[cfg(feature = "cluster")]
161pub(super) const PHASE_RPC_TIMEOUT: Duration = Duration::from_secs(3);
162
163#[cfg(feature = "cluster")]
164pub(super) const PREPARE_RETRY_INITIAL_BACKOFF: Duration = Duration::from_millis(10);
165
166#[cfg(feature = "cluster")]
167pub(super) const PREPARE_RETRY_MAX_BACKOFF: Duration = Duration::from_millis(250);
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171pub enum Phase {
172 Prepare,
175 Aligned,
179 Commit,
181 Abort,
183}
184
185pub(super) const fn is_terminal_phase(phase: Phase) -> bool {
186 matches!(phase, Phase::Commit | Phase::Abort)
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191pub struct BarrierAnnouncement {
192 pub epoch: u64,
194 pub checkpoint_id: u64,
196 pub assignment_fence: Option<super::super::CheckpointAssignmentFence>,
199 pub leader_proof: Option<super::super::LeaderProof>,
204 pub phase: Phase,
206 pub flags: u64,
208}
209
210pub(super) fn announcement_attempt(ann: &BarrierAnnouncement) -> CheckpointAttempt {
211 CheckpointAttempt::new(ann.epoch, ann.checkpoint_id)
212}
213
214pub(super) fn validate_announcement_attempt(ann: &BarrierAnnouncement) -> Result<(), String> {
215 if announcement_attempt(ann).is_canonical() {
216 Ok(())
217 } else {
218 Err(format!(
219 "barrier announcement must use one nonzero canonical checkpoint ID; received epoch {} and checkpoint ID {}",
220 ann.epoch, ann.checkpoint_id
221 ))
222 }
223}
224
225pub(super) fn validate_ack_attempt(ack: &BarrierAck) -> Result<(), String> {
226 if CheckpointAttempt::new(ack.epoch, ack.checkpoint_id).is_canonical() {
227 Ok(())
228 } else {
229 Err(format!(
230 "barrier acknowledgement must use one nonzero canonical checkpoint ID; received epoch {} and checkpoint ID {}",
231 ack.epoch, ack.checkpoint_id
232 ))
233 }
234}
235
236#[cfg(feature = "cluster")]
237pub(super) fn validate_wire_checkpoint_attempt(
238 epoch: u64,
239 checkpoint_id: u64,
240) -> Result<(), tonic::Status> {
241 if CheckpointAttempt::new(epoch, checkpoint_id).is_canonical() {
242 Ok(())
243 } else {
244 Err(tonic::Status::invalid_argument(
245 "Barrier request must use one nonzero canonical checkpoint ID",
246 ))
247 }
248}
249
250pub(super) fn same_announcement_identity(
251 left: &BarrierAnnouncement,
252 right: &BarrierAnnouncement,
253) -> bool {
254 announcement_attempt(left) == announcement_attempt(right)
255 && left.assignment_fence == right.assignment_fence
256 && left.leader_proof == right.leader_proof
257 && left.flags == right.flags
258}
259
260pub(super) fn merge_history_exact(
264 current: BarrierAnnouncement,
265 incoming: BarrierAnnouncement,
266 history: &str,
267) -> Result<BarrierAnnouncement, String> {
268 if current.flags != incoming.flags {
269 return Err(format!(
270 "conflicting {history} barrier flags for exact attempt ({}, {})",
271 current.epoch, current.checkpoint_id
272 ));
273 }
274 if current.phase == incoming.phase && current != incoming {
275 return Err(format!(
276 "conflicting {history} {:?} payloads for exact attempt ({}, {})",
277 current.phase, current.epoch, current.checkpoint_id
278 ));
279 }
280 if is_terminal_phase(current.phase) && is_terminal_phase(incoming.phase) {
281 if current.phase != incoming.phase {
282 return Err(format!(
283 "conflicting {history} terminal phases for exact attempt ({}, {})",
284 current.epoch, current.checkpoint_id
285 ));
286 }
287 return Ok(current);
288 }
289 if is_terminal_phase(current.phase) {
290 return Ok(current);
291 }
292 if is_terminal_phase(incoming.phase) {
293 return Ok(incoming);
294 }
295 if !same_announcement_identity(¤t, &incoming) {
296 return Err(format!(
297 "conflicting {history} barrier certificates for exact attempt ({}, {})",
298 current.epoch, current.checkpoint_id
299 ));
300 }
301 if current.phase == Phase::Aligned && incoming.phase == Phase::Prepare {
302 Ok(current)
303 } else {
304 Ok(incoming)
305 }
306}
307
308#[cfg(feature = "cluster")]
310pub(super) fn merge_direct_announcement(
311 current: BarrierAnnouncement,
312 incoming: BarrierAnnouncement,
313) -> Result<BarrierAnnouncement, String> {
314 validate_announcement_attempt(¤t)?;
315 validate_announcement_attempt(&incoming)?;
316 match incoming.checkpoint_id.cmp(¤t.checkpoint_id) {
317 std::cmp::Ordering::Greater => Ok(incoming),
318 std::cmp::Ordering::Less => Ok(current),
319 std::cmp::Ordering::Equal => merge_history_exact(current, incoming, "direct"),
320 }
321}
322
323pub(super) fn merge_observed_announcement(
327 grpc: BarrierAnnouncement,
328 durable: BarrierAnnouncement,
329) -> Result<BarrierAnnouncement, String> {
330 validate_announcement_attempt(&grpc)?;
331 validate_announcement_attempt(&durable)?;
332 match durable.checkpoint_id.cmp(&grpc.checkpoint_id) {
333 std::cmp::Ordering::Greater => Ok(durable),
334 std::cmp::Ordering::Less => Ok(grpc),
335 std::cmp::Ordering::Equal => {
336 if grpc.phase == durable.phase && !is_terminal_phase(grpc.phase) && grpc != durable {
337 Err(format!(
338 "conflicting direct and durable {:?} payloads for exact attempt ({}, {})",
339 grpc.phase, grpc.epoch, grpc.checkpoint_id
340 ))
341 } else if is_terminal_phase(durable.phase) {
342 Ok(durable)
343 } else if is_terminal_phase(grpc.phase) {
344 Ok(grpc)
345 } else if !same_announcement_identity(&grpc, &durable) {
346 Err(format!(
347 "conflicting direct and durable certificates for exact attempt ({}, {})",
348 grpc.epoch, grpc.checkpoint_id
349 ))
350 } else if grpc.phase == Phase::Aligned && durable.phase == Phase::Prepare {
351 Ok(grpc)
352 } else {
353 Ok(durable)
354 }
355 }
356 }
357}
358
359pub(super) fn merge_scanned_announcement(
362 current: BarrierAnnouncement,
363 incoming: BarrierAnnouncement,
364) -> Result<BarrierAnnouncement, String> {
365 validate_announcement_attempt(¤t)?;
366 validate_announcement_attempt(&incoming)?;
367 match incoming.checkpoint_id.cmp(¤t.checkpoint_id) {
368 std::cmp::Ordering::Greater => Ok(incoming),
369 std::cmp::Ordering::Less => Ok(current),
370 std::cmp::Ordering::Equal => merge_history_exact(current, incoming, "durable"),
371 }
372}
373
374pub(super) fn validate_publication_order(
375 current: &BarrierAnnouncement,
376 incoming: &BarrierAnnouncement,
377) -> Result<(), String> {
378 validate_announcement_attempt(current)?;
379 validate_announcement_attempt(incoming)?;
380 match incoming.checkpoint_id.cmp(¤t.checkpoint_id) {
381 std::cmp::Ordering::Greater => Ok(()),
382 std::cmp::Ordering::Less => Err(format!(
383 "stale barrier publication ({}, {}) cannot replace newer admitted attempt ({}, {})",
384 incoming.epoch, incoming.checkpoint_id, current.epoch, current.checkpoint_id
385 )),
386 std::cmp::Ordering::Equal if current == incoming => Ok(()),
387 std::cmp::Ordering::Equal => {
388 let merged =
389 merge_history_exact(current.clone(), incoming.clone(), "local publication")?;
390 if merged == *incoming {
391 Ok(())
392 } else {
393 Err(format!(
394 "barrier publication cannot regress exact attempt ({}, {}) from {:?} to {:?}",
395 current.epoch, current.checkpoint_id, current.phase, incoming.phase
396 ))
397 }
398 }
399 }
400}
401
402pub(super) fn validate_scanned_announcements(
403 mut announcements: Vec<BarrierAnnouncement>,
404) -> Result<Option<BarrierAnnouncement>, String> {
405 for announcement in &announcements {
406 validate_announcement_attempt(announcement)?;
407 }
408 announcements.sort_unstable_by_key(|announcement| {
411 (
412 announcement.checkpoint_id,
413 is_terminal_phase(announcement.phase),
414 )
415 });
416 announcements
417 .into_iter()
418 .try_fold(None, |highest, announcement| {
419 Ok(Some(match highest {
420 None => announcement,
421 Some(current) => merge_scanned_announcement(current, announcement)?,
422 }))
423 })
424}
425
426#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
428pub enum BarrierAckDisposition {
429 Prepared,
432 PreparedWithReplay,
434 Failed,
436 Captured,
439 CapturedWithReplay,
441}
442
443impl BarrierAckDisposition {
444 pub(super) const fn precedence(self) -> u8 {
445 match self {
446 Self::Prepared => 0,
447 Self::PreparedWithReplay => 1,
448 Self::Captured => 2,
449 Self::CapturedWithReplay => 3,
450 Self::Failed => 4,
451 }
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457pub struct BarrierAck {
458 pub epoch: u64,
460 pub checkpoint_id: u64,
462 pub assignment_digest: Option<[u8; 32]>,
464 pub flags: u64,
466 pub disposition: BarrierAckDisposition,
468 pub error: Option<String>,
470 pub watermark: CheckpointWatermark,
473}
474
475#[derive(Debug, Clone, PartialEq, Eq)]
477pub enum QuorumOutcome {
478 Reached {
480 acks: Vec<NodeId>,
482 follower_watermark: CheckpointWatermark,
484 handoff_replay_pending: bool,
486 },
487 TimedOut {
489 got: Vec<NodeId>,
491 missing: Vec<NodeId>,
493 },
494 Failed {
496 failures: Vec<(NodeId, String)>,
498 },
499}