1use std::collections::HashSet;
16use std::fmt;
17use std::time::{Duration, Instant};
18
19use prost::Message;
20
21use crate::meta;
22
23pub const MAX_WRITE_CONCERN_REPLICAS: usize = 4096;
24pub const MAX_WRITE_CONCERN_POLICY_BYTES: usize = 1024;
25const MAX_RESULT_BYTES: usize = 3 * MAX_WRITE_CONCERN_REPLICAS * 280 + 8192;
26mod evidence;
27pub use evidence::{
28 WriteAcknowledgement, WriteConcernEvidence, WriteDurability, WriteOperationEvidence,
29};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33#[non_exhaustive]
34pub enum WriteConcern {
35 #[default]
36 Local,
37 Count(u32),
38 Majority,
39}
40
41impl WriteConcern {
42 pub fn required_acknowledgements(
43 self,
44 replica_count: usize,
45 ) -> Result<u32, WriteConcernPolicyError> {
46 let majority = replica_count / 2 + 1;
47 match self {
48 Self::Local | Self::Count(1) => Ok(1),
49 Self::Count(0) => Err(invalid("acknowledgement count must be positive")),
50 Self::Count(count) if replica_count != 0 && count as usize <= majority => Ok(count),
51 Self::Count(_) if replica_count == 0 => Err(invalid(
52 "nonlocal concern requires explicit replica placement",
53 )),
54 Self::Count(_) => Err(invalid("acknowledgement count exceeds majority")),
55 Self::Majority if replica_count == 0 => Err(invalid(
56 "majority concern requires explicit replica placement",
57 )),
58 Self::Majority => u32::try_from(majority)
59 .map_err(|_| invalid("replica count exceeds supported range")),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)]
67pub struct WriteConcernOptions {
68 #[builder(default)]
69 concern: WriteConcern,
70 #[builder(default = Duration::from_secs(5))]
71 timeout: Duration,
72 #[builder(default)]
75 require_other_region: bool,
76 #[builder(default)]
79 strict_acknowledgements: bool,
80 #[builder(default)]
83 minimum_durability: WriteDurability,
84 #[builder(default)]
86 require_evidence: bool,
87}
88
89impl Default for WriteConcernOptions {
90 fn default() -> Self {
91 Self::builder().build()
92 }
93}
94
95impl WriteConcernOptions {
96 pub fn minimum_durability(&self) -> WriteDurability {
97 self.minimum_durability
98 }
99 pub fn with_minimum_durability(mut self, durability: WriteDurability) -> Self {
100 self.minimum_durability = durability;
101 self
102 }
103 pub fn require_evidence(&self) -> bool {
104 self.require_evidence || self.minimum_durability != WriteDurability::MemoryApplied
105 }
106 pub fn with_require_evidence(mut self, required: bool) -> Self {
107 self.require_evidence = required;
108 self
109 }
110 pub fn concern(&self) -> WriteConcern {
111 self.concern
112 }
113 pub fn timeout(&self) -> Duration {
114 self.timeout
115 }
116 pub fn require_other_region(&self) -> bool {
118 self.require_other_region
119 }
120 pub fn with_require_other_region(mut self, required: bool) -> Self {
122 self.require_other_region = required;
123 self
124 }
125 pub fn strict_acknowledgements(&self) -> bool {
128 self.strict_acknowledgements
129 }
130 pub fn with_strict_acknowledgements(mut self, strict: bool) -> Self {
133 self.strict_acknowledgements = strict;
134 self
135 }
136 pub fn validate(&self) -> Result<(), WriteConcernPolicyError> {
137 let ms = self.timeout.as_millis();
138 if ms == 0 || ms > u64::MAX as u128 || Instant::now().checked_add(self.timeout).is_none() {
139 return Err(invalid(
140 "write concern timeout must be representable and at least one millisecond",
141 ));
142 }
143 if self.concern == WriteConcern::Count(0) {
144 return Err(invalid("acknowledgement count must be positive"));
145 }
146 Ok(())
147 }
148
149 pub fn encode(&self) -> Result<Vec<u8>, WriteConcernPolicyError> {
150 self.validate()?;
151 let (level, acknowledgements) = match self.concern {
152 WriteConcern::Local => (meta::WriteConcernLevel::WriteConcernLocal, 0),
153 WriteConcern::Count(n) => (meta::WriteConcernLevel::WriteConcernCount, n),
154 WriteConcern::Majority => (meta::WriteConcernLevel::WriteConcernMajority, 0),
155 };
156 Ok(meta::WriteConcernPolicy {
157 level: level as i32,
158 acknowledgements,
159 timeout_ms: self.timeout.as_millis() as u64,
160 require_other_region: self.require_other_region,
161 strict_acknowledgements: self.strict_acknowledgements,
162 minimum_durability: self.minimum_durability as i32,
163 require_evidence: self.require_evidence,
164 }
165 .encode_to_vec())
166 }
167
168 pub fn decode(bytes: &[u8]) -> Result<Self, WriteConcernPolicyError> {
171 preflight_policy(bytes)?;
172 let policy = meta::WriteConcernPolicy::decode(bytes).map_err(|e| invalid(e.to_string()))?;
173 let concern = match meta::WriteConcernLevel::try_from(policy.level) {
174 Ok(meta::WriteConcernLevel::WriteConcernLocal) if policy.acknowledgements == 0 => {
175 WriteConcern::Local
176 }
177 Ok(meta::WriteConcernLevel::WriteConcernCount) => {
178 WriteConcern::Count(policy.acknowledgements)
179 }
180 Ok(meta::WriteConcernLevel::WriteConcernMajority) if policy.acknowledgements == 0 => {
181 WriteConcern::Majority
182 }
183 _ => return Err(invalid("invalid acknowledgement policy")),
184 };
185 let options = Self {
186 concern,
187 timeout: Duration::from_millis(policy.timeout_ms),
188 require_other_region: policy.require_other_region,
189 strict_acknowledgements: policy.strict_acknowledgements,
190 minimum_durability: WriteDurability::decode(policy.minimum_durability)?,
191 require_evidence: policy.require_evidence,
192 };
193 options.validate()?;
194 Ok(options)
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
199#[non_exhaustive]
200pub enum WriteConcernPolicyError {
201 Invalid(String),
202 Unsupported(String),
203}
204
205impl fmt::Display for WriteConcernPolicyError {
206 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207 match self {
208 Self::Invalid(s) | Self::Unsupported(s) => f.write_str(s),
209 }
210 }
211}
212impl std::error::Error for WriteConcernPolicyError {}
213fn invalid(s: impl Into<String>) -> WriteConcernPolicyError {
214 WriteConcernPolicyError::Invalid(s.into())
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct WriteConcernReceipt {
224 receiving_node: String,
225 replica_count: u32,
226 requested: u32,
227 required: u32,
228 acknowledged_nodes: Vec<String>,
229 receiving_region: Option<Box<str>>,
230 outside_region_acknowledged_nodes: Vec<String>,
231 evidence: Option<Box<WriteConcernEvidence>>,
232 requested_durability: WriteDurability,
233}
234
235impl WriteConcernReceipt {
236 pub fn requested_durability(&self) -> WriteDurability {
237 self.requested_durability
238 }
239 pub fn with_requested_durability(
240 mut self,
241 durability: WriteDurability,
242 ) -> Result<Self, WriteConcernPolicyError> {
243 self.requested_durability = durability;
244 self.validate()?;
245 Ok(self)
246 }
247 pub fn evidence(&self) -> Option<&WriteConcernEvidence> {
250 self.evidence.as_deref()
251 }
252 pub fn with_evidence(
253 mut self,
254 evidence: WriteConcernEvidence,
255 ) -> Result<Self, WriteConcernPolicyError> {
256 self.evidence = Some(Box::new(evidence));
257 self.validate()?;
258 Ok(self)
259 }
260 pub fn new(
261 receiving_node: String,
262 replica_count: u32,
263 requested: u32,
264 required: u32,
265 acknowledged_nodes: Vec<String>,
266 ) -> Result<Self, WriteConcernPolicyError> {
267 let receipt = Self {
268 receiving_node,
269 replica_count,
270 requested,
271 required,
272 acknowledged_nodes,
273 receiving_region: None,
274 outside_region_acknowledged_nodes: Vec::new(),
275 evidence: None,
276 requested_durability: WriteDurability::MemoryApplied,
277 };
278 receipt.validate()?;
279 Ok(receipt)
280 }
281 pub fn receiving_node(&self) -> &str {
282 &self.receiving_node
283 }
284 pub fn replica_count(&self) -> Option<u32> {
285 (self.replica_count != 0).then_some(self.replica_count)
286 }
287 pub fn requested_acknowledgements(&self) -> u32 {
288 self.requested
289 }
290 pub fn required_acknowledgements(&self) -> u32 {
291 self.required
292 }
293 pub fn acknowledged_nodes(&self) -> &[String] {
294 &self.acknowledged_nodes
295 }
296 pub fn was_overridden(&self) -> bool {
297 self.required < self.requested
298 }
299
300 pub fn with_region_progress(
302 mut self,
303 receiving_region: String,
304 outside_region_acknowledged_nodes: Vec<String>,
305 ) -> Result<Self, WriteConcernPolicyError> {
306 self.receiving_region = Some(receiving_region.into_boxed_str());
307 self.outside_region_acknowledged_nodes = outside_region_acknowledged_nodes;
308 self.validate()?;
309 Ok(self)
310 }
311 pub fn receiving_region(&self) -> Option<&str> {
313 self.receiving_region.as_deref()
314 }
315 pub fn outside_region_acknowledged_nodes(&self) -> &[String] {
318 &self.outside_region_acknowledged_nodes
319 }
320 pub fn other_region_satisfied(&self) -> bool {
322 self.receiving_region.is_some() && !self.outside_region_acknowledged_nodes.is_empty()
323 }
324 pub fn validate_for(
327 &self,
328 options: &WriteConcernOptions,
329 satisfied: bool,
330 ) -> Result<(), WriteConcernPolicyError> {
331 self.validate()?;
332 let expected = options
333 .concern()
334 .required_acknowledgements(self.replica_count as usize)?;
335 if self.requested != expected {
336 return Err(invalid(
337 "write concern reply changed the requested threshold",
338 ));
339 }
340 if options.strict_acknowledgements() && self.required != self.requested {
341 return Err(invalid(
342 "write concern reply lowered a strict acknowledgement threshold",
343 ));
344 }
345 if options.require_other_region()
346 && (self.receiving_region.is_none() || satisfied && !self.other_region_satisfied())
347 {
348 return Err(invalid(
349 "write concern reply lacks required region progress",
350 ));
351 }
352 if satisfied && self.acknowledged_nodes.len() < self.required as usize {
353 return Err(invalid("receipt lacks required acknowledgements"));
354 }
355 match &self.evidence {
356 None if options.require_evidence() => {
357 return Err(invalid("write concern reply omitted required evidence"))
358 }
359 Some(evidence)
360 if satisfied
361 && evidence
362 .acknowledgements()
363 .iter()
364 .any(|ack| ack.durability() < options.minimum_durability()) =>
365 {
366 return Err(invalid("write concern reply lowered requested durability"));
367 }
368 _ => {}
369 }
370 if self.evidence.is_some() && self.requested_durability != options.minimum_durability() {
371 return Err(invalid("receipt changed requested durability"));
372 }
373 Ok(())
374 }
375
376 fn validate(&self) -> Result<(), WriteConcernPolicyError> {
377 if self.requested_durability != WriteDurability::MemoryApplied && self.evidence.is_none() {
378 return Err(invalid("durability policy lacks evidence"));
379 }
380 if let Some(evidence) = &self.evidence {
381 evidence.validate_receipt(self)?;
382 }
383 let valid_id = |id: &str| !id.is_empty() && id.len() <= 255;
384 let acknowledged: HashSet<_> = self.acknowledged_nodes.iter().collect();
385 let outside: HashSet<_> = self.outside_region_acknowledged_nodes.iter().collect();
386 if !valid_id(&self.receiving_node)
387 || self.requested == 0
388 || self.required == 0
389 || self.required > self.requested
390 || self.replica_count as usize > MAX_WRITE_CONCERN_REPLICAS
391 || (self.replica_count == 0 && self.requested != 1)
392 || (self.replica_count > 0 && self.requested > self.replica_count / 2 + 1)
393 || self.acknowledged_nodes.len() > MAX_WRITE_CONCERN_REPLICAS
394 || (self.replica_count == 0 && self.acknowledged_nodes.len() != 1)
395 || (self.replica_count > 0
396 && self.acknowledged_nodes.len() > self.replica_count as usize)
397 || self.acknowledged_nodes.iter().any(|id| !valid_id(id))
398 || acknowledged.len() != self.acknowledged_nodes.len()
399 || !self
400 .acknowledged_nodes
401 .iter()
402 .any(|id| id == &self.receiving_node)
403 || self
404 .receiving_region
405 .as_ref()
406 .is_some_and(|region| !valid_id(region))
407 || self.receiving_region.is_some() && self.replica_count == 0
408 || self.receiving_region.is_none() && !self.outside_region_acknowledged_nodes.is_empty()
409 || self.outside_region_acknowledged_nodes.len() > self.acknowledged_nodes.len()
410 || outside.len() != self.outside_region_acknowledged_nodes.len()
411 || self
412 .outside_region_acknowledged_nodes
413 .iter()
414 .any(|id| id == &self.receiving_node || !acknowledged.contains(id))
415 {
416 return Err(invalid("invalid write concern receipt"));
417 }
418 Ok(())
419 }
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
423#[non_exhaustive]
424pub enum WriteConcernOutcome {
425 Satisfied(WriteConcernReceipt),
426 TimedOut(WriteConcernReceipt),
427 DurabilityFailed {
428 receipt: WriteConcernReceipt,
429 error: String,
430 },
431 InvalidPolicy(String),
432 Unsupported(String),
433 Backpressure,
434 UnknownOutcome(String),
435}
436
437impl WriteConcernOutcome {
438 pub fn encode(&self) -> Vec<u8> {
439 let mut result = meta::WriteConcernResult::default();
440 let (status, receipt) = match self {
441 Self::Satisfied(receipt) => (
442 meta::WriteConcernStatus::WriteConcernSatisfied,
443 Some(receipt),
444 ),
445 Self::TimedOut(receipt) => (
446 meta::WriteConcernStatus::WriteConcernTimedOut,
447 Some(receipt),
448 ),
449 Self::DurabilityFailed { receipt, error } => {
450 result.error = error.clone();
451 (
452 meta::WriteConcernStatus::WriteConcernDurabilityFailed,
453 Some(receipt),
454 )
455 }
456 Self::InvalidPolicy(error) => {
457 result.error = error.clone();
458 (meta::WriteConcernStatus::WriteConcernInvalid, None)
459 }
460 Self::Unsupported(error) => {
461 result.error = error.clone();
462 (meta::WriteConcernStatus::WriteConcernUnsupported, None)
463 }
464 Self::Backpressure => (meta::WriteConcernStatus::WriteConcernBackpressure, None),
465 Self::UnknownOutcome(error) => {
466 result.error = error.clone();
467 (meta::WriteConcernStatus::WriteConcernUnknownOutcome, None)
468 }
469 };
470 result.status = status as i32;
471 if result.error.len() > 4096 {
472 let mut limit = 4096;
473 while !result.error.is_char_boundary(limit) {
474 limit -= 1;
475 }
476 result.error.truncate(limit);
477 }
478 if let Some(receipt) = receipt {
479 result.receiving_node = receipt.receiving_node.clone();
480 result.replica_count = receipt.replica_count;
481 result.requested_acknowledgements = receipt.requested;
482 result.required_acknowledgements = receipt.required;
483 result.acknowledged_nodes = receipt.acknowledged_nodes.clone();
484 result.receiving_region = receipt.receiving_region.as_deref().map(str::to_owned);
485 result.outside_region_acknowledged_nodes =
486 receipt.outside_region_acknowledged_nodes.clone();
487 result.evidence = receipt
488 .evidence
489 .as_deref()
490 .map(WriteConcernEvidence::encode);
491 result.requested_durability = receipt.requested_durability as i32;
492 }
493 result.encode_to_vec()
494 }
495 pub fn decode(bytes: &[u8]) -> Result<Self, WriteConcernPolicyError> {
496 preflight_result(bytes)?;
497 let result = meta::WriteConcernResult::decode(bytes).map_err(|e| invalid(e.to_string()))?;
498 match meta::WriteConcernStatus::try_from(result.status) {
499 Ok(
500 meta::WriteConcernStatus::WriteConcernSatisfied
501 | meta::WriteConcernStatus::WriteConcernTimedOut
502 | meta::WriteConcernStatus::WriteConcernDurabilityFailed,
503 ) => {
504 let mut receipt = WriteConcernReceipt::new(
505 result.receiving_node,
506 result.replica_count,
507 result.requested_acknowledgements,
508 result.required_acknowledgements,
509 result.acknowledged_nodes,
510 )?;
511 if let Some(region) = result.receiving_region {
512 receipt = receipt
513 .with_region_progress(region, result.outside_region_acknowledged_nodes)?;
514 } else if !result.outside_region_acknowledged_nodes.is_empty() {
515 return Err(invalid("region progress lacks receiving region"));
516 }
517 if let Some(evidence) = result.evidence {
518 receipt = receipt.with_evidence(WriteConcernEvidence::decode(evidence)?)?;
519 }
520 receipt = receipt.with_requested_durability(WriteDurability::decode(
521 result.requested_durability,
522 )?)?;
523 if result.status == meta::WriteConcernStatus::WriteConcernSatisfied as i32 {
524 if receipt.evidence().is_some_and(|evidence| {
525 evidence
526 .acknowledgements()
527 .iter()
528 .any(|ack| ack.durability() < receipt.requested_durability())
529 }) {
530 return Err(invalid("success receipt lacks its advertised durability"));
531 }
532 if receipt.acknowledged_nodes.len() < receipt.required as usize {
533 return Err(invalid("receipt lacks required acknowledgements"));
534 }
535 if receipt.receiving_region.is_some() && !receipt.other_region_satisfied() {
536 return Err(invalid("receipt lacks required region acknowledgement"));
537 }
538 Ok(Self::Satisfied(receipt))
539 } else if result.status
540 == meta::WriteConcernStatus::WriteConcernDurabilityFailed as i32
541 {
542 Ok(Self::DurabilityFailed {
543 receipt,
544 error: result.error,
545 })
546 } else {
547 Ok(Self::TimedOut(receipt))
548 }
549 }
550 Ok(meta::WriteConcernStatus::WriteConcernInvalid) => {
551 Ok(Self::InvalidPolicy(result.error))
552 }
553 Ok(meta::WriteConcernStatus::WriteConcernUnsupported) => {
554 Ok(Self::Unsupported(result.error))
555 }
556 Ok(meta::WriteConcernStatus::WriteConcernBackpressure) => Ok(Self::Backpressure),
557 Ok(meta::WriteConcernStatus::WriteConcernUnknownOutcome) => {
558 Ok(Self::UnknownOutcome(result.error))
559 }
560 _ => Err(invalid("unknown write concern outcome")),
561 }
562 }
563}
564
565fn varint(bytes: &[u8], pos: &mut usize) -> Result<u64, WriteConcernPolicyError> {
566 let mut value = 0;
567 for shift in (0..70).step_by(7) {
568 let byte = *bytes.get(*pos).ok_or_else(|| invalid("truncated policy"))?;
569 *pos += 1;
570 if shift == 63 && byte > 1 {
571 return Err(invalid("overflowing policy varint"));
572 }
573 value |= u64::from(byte & 127) << shift;
574 if byte & 128 == 0 {
575 return Ok(value);
576 }
577 }
578 Err(invalid("overflowing policy varint"))
579}
580
581pub fn preflight_policy(bytes: &[u8]) -> Result<(), WriteConcernPolicyError> {
582 if bytes.len() > MAX_WRITE_CONCERN_POLICY_BYTES {
583 return Err(invalid("write concern policy exceeds byte limit"));
584 }
585 let mut pos = 0;
586 let mut seen = [false; 8];
587 while pos < bytes.len() {
588 let tag = varint(bytes, &mut pos)?;
589 let field = tag >> 3;
590 if !(1..=7).contains(&field) {
591 return Err(WriteConcernPolicyError::Unsupported(
592 "unsupported write concern policy field".into(),
593 ));
594 }
595 if tag & 7 != 0 || seen[field as usize] {
596 return Err(invalid("invalid or duplicate policy field"));
597 }
598 seen[field as usize] = true;
599 let value = varint(bytes, &mut pos)?;
600 if field == 4 && value > 1 {
601 return Err(invalid("invalid region requirement boolean"));
602 }
603 if field == 5 && value > 1 {
604 return Err(invalid("invalid strict acknowledgement boolean"));
605 }
606 if field == 6 && value > 2 {
607 return Err(invalid("invalid write durability"));
608 }
609 if field == 7 && value > 1 {
610 return Err(invalid("invalid evidence requirement boolean"));
611 }
612 }
613 Ok(())
614}
615
616pub fn preflight_result(bytes: &[u8]) -> Result<(), WriteConcernPolicyError> {
617 if bytes.len() > MAX_RESULT_BYTES {
618 return Err(invalid("write concern result exceeds byte limit"));
619 }
620 let mut pos = 0;
621 let mut nodes = 0usize;
622 let mut outside_nodes = 0usize;
623 let mut evidence_seen = false;
624 while pos < bytes.len() {
625 let tag = varint(bytes, &mut pos)?;
626 let field = tag >> 3;
627 if field == 0 {
628 return Err(invalid("invalid receipt field"));
629 }
630 match tag & 7 {
631 0 => {
632 varint(bytes, &mut pos)?;
633 }
634 1 => {
635 pos = pos
636 .checked_add(8)
637 .ok_or_else(|| invalid("receipt size overflow"))?;
638 }
639 5 => {
640 pos = pos
641 .checked_add(4)
642 .ok_or_else(|| invalid("receipt size overflow"))?;
643 }
644 2 => {
645 let len = usize::try_from(varint(bytes, &mut pos)?)
646 .map_err(|_| invalid("receipt size overflow"))?;
647 if field == 6 {
648 nodes += 1;
649 if nodes > MAX_WRITE_CONCERN_REPLICAS || len > 255 {
650 return Err(invalid("receipt acknowledgement limit"));
651 }
652 }
653 if field == 9 {
654 outside_nodes += 1;
655 if outside_nodes > MAX_WRITE_CONCERN_REPLICAS || len > 255 {
656 return Err(invalid("receipt region acknowledgement limit"));
657 }
658 }
659 if (field == 2 || field == 8) && len > 255 || field == 7 && len > 4096 {
660 return Err(invalid("receipt string limit"));
661 }
662 if field == 10 {
663 if evidence_seen {
664 return Err(invalid("duplicate evidence field"));
665 }
666 evidence_seen = true;
667 evidence::preflight(
668 bytes
669 .get(pos..pos.saturating_add(len))
670 .ok_or_else(|| invalid("truncated evidence"))?,
671 )?;
672 }
673 pos = pos
674 .checked_add(len)
675 .ok_or_else(|| invalid("receipt size overflow"))?;
676 }
677 _ => return Err(invalid("invalid receipt wire type")),
678 }
679 if pos > bytes.len() {
680 return Err(invalid("truncated receipt"));
681 }
682 }
683 Ok(())
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 fn certified_receipt(level: WriteDurability) -> WriteConcernReceipt {
690 let op = WriteOperationEvidence::new(
691 0,
692 "\u{001e}actordb.table.v1:4b56:78",
693 &[1],
694 crate::RecordVersion::new(7, 2),
695 "a".into(),
696 false,
697 None,
698 )
699 .unwrap();
700 WriteConcernReceipt::new("a".into(), 3, 2, 2, vec!["a".into(), "b".into()])
701 .unwrap()
702 .with_evidence(
703 WriteConcernEvidence::new(
704 op,
705 vec!["c".into(), "b".into(), "a".into()],
706 vec![
707 WriteAcknowledgement::new("a".into(), level),
708 WriteAcknowledgement::new("b".into(), level),
709 ],
710 )
711 .unwrap(),
712 )
713 .unwrap()
714 .with_requested_durability(WriteDurability::StableCommitted)
715 .unwrap()
716 }
717 #[test]
718 fn durability_policy_and_certified_receipts_roundtrip_without_lowering_promises() {
719 let options = WriteConcernOptions::builder()
720 .concern(WriteConcern::Majority)
721 .minimum_durability(WriteDurability::StableCommitted)
722 .build();
723 assert!(options.require_evidence());
724 assert_eq!(
725 WriteConcernOptions::decode(&options.encode().unwrap()).unwrap(),
726 options
727 );
728 let stable = certified_receipt(WriteDurability::StableCommitted);
729 assert!(stable.validate_for(&options, true).is_ok());
730 let outcome = WriteConcernOutcome::Satisfied(stable);
731 assert_eq!(
732 WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
733 outcome
734 );
735 let buffered = certified_receipt(WriteDurability::StoreCommitted);
736 assert!(buffered.validate_for(&options, true).is_err());
737 assert!(WriteConcernOutcome::decode(
738 &WriteConcernOutcome::Satisfied(buffered.clone()).encode()
739 )
740 .is_err());
741 assert!(buffered.validate_for(&options, false).is_ok());
742 let mut legacy = buffered;
743 legacy.evidence = None;
744 assert!(legacy.validate_for(&options, false).is_err());
745 assert!(WriteConcernOptions::decode(&[48, 3]).is_err());
746 assert!(WriteConcernOptions::decode(&[56, 2]).is_err());
747 }
748 #[test]
749 fn malformed_certificate_set_or_duplicate_evidence_is_rejected_before_certification() {
750 let receipt = certified_receipt(WriteDurability::StableCommitted);
751 let certificate = receipt.evidence().unwrap();
752 certificate
753 .operation()
754 .validate_application_write("KV", "x", &[1], false)
755 .unwrap();
756 assert!(certificate
757 .operation()
758 .validate_application_write("KV", "other", &[1], false)
759 .is_err());
760 for field in 0..3 {
761 let mut wire = certificate.encode();
762 match field {
763 0 => wire.operation.as_mut().unwrap().operation_digest[0] ^= 1,
764 1 => wire.replica_set_digest[0] ^= 1,
765 _ => wire.acknowledgements[0].replica_index = 100,
766 }
767 assert!(WriteConcernEvidence::decode(wire).is_err());
768 }
769 let mut duplicate = WriteConcernOutcome::Satisfied(receipt).encode();
770 duplicate.extend([82, 0]);
771 assert!(WriteConcernOutcome::decode(&duplicate).is_err());
772 assert!(evidence::preflight(&[18, 0].repeat(MAX_WRITE_CONCERN_REPLICAS + 1)).is_err());
773 }
774 #[test]
775 fn count_is_total_and_bounded_by_majority() {
776 assert_eq!(
777 WriteConcern::Count(3).required_acknowledgements(5).unwrap(),
778 3
779 );
780 assert_eq!(
781 WriteConcern::Majority.required_acknowledgements(4).unwrap(),
782 3
783 );
784 assert!(WriteConcern::Count(4).required_acknowledgements(5).is_err());
785 assert!(WriteConcern::Majority.required_acknowledgements(0).is_err());
786 assert_eq!(WriteConcern::Local.required_acknowledgements(0).unwrap(), 1);
787 }
788 #[test]
789 fn policy_roundtrip_rejects_unknown_and_duplicate_requirements() {
790 let options = WriteConcernOptions::builder()
791 .concern(WriteConcern::Count(2))
792 .build();
793 let bytes = options.encode().unwrap();
794 assert_eq!(WriteConcernOptions::decode(&bytes).unwrap(), options);
795 assert!(WriteConcernOptions::decode(&[bytes.clone(), bytes].concat()).is_err());
796 assert!(matches!(
797 WriteConcernOptions::decode(&[64, 1]),
798 Err(WriteConcernPolicyError::Unsupported(_))
799 ));
800 }
801 #[test]
802 fn strict_policy_roundtrips_and_uses_a_required_extension_field() {
803 let legacy = WriteConcernOptions::builder()
804 .concern(WriteConcern::Count(2))
805 .build();
806 assert!(!legacy.strict_acknowledgements());
807 let strict = legacy.clone().with_strict_acknowledgements(true);
808 let mut expected = legacy.encode().unwrap();
809 expected.extend([40, 1]);
812 assert_eq!(strict.encode().unwrap(), expected);
813 assert_eq!(WriteConcernOptions::decode(&expected).unwrap(), strict);
814 assert_eq!(strict.with_strict_acknowledgements(false), legacy);
815 assert!(WriteConcernOptions::decode(&[expected, vec![40, 1]].concat()).is_err());
816 assert!(WriteConcernOptions::decode(&[40, 2]).is_err());
817 }
818 #[test]
819 fn strict_receipts_reject_lowered_counts_even_for_timeout() {
820 let options = WriteConcernOptions::builder()
821 .concern(WriteConcern::Majority)
822 .strict_acknowledgements(true)
823 .build();
824 let lowered = WriteConcernReceipt::new("a".into(), 3, 2, 1, vec!["a".into()]).unwrap();
825 assert!(lowered.validate_for(&options, true).is_err());
826 assert!(lowered.validate_for(&options, false).is_err());
827 let preserved = WriteConcernReceipt::new("a".into(), 3, 2, 2, vec!["a".into()]).unwrap();
828 assert!(preserved.validate_for(&options, false).is_ok());
829 assert!(preserved.validate_for(&options, true).is_err());
830 }
831 #[test]
832 fn receipts_require_distinct_acknowledgements_and_receiver() {
833 assert!(
834 WriteConcernReceipt::new("a".into(), 3, 2, 2, vec!["a".into(), "a".into()]).is_err()
835 );
836 let receipt =
837 WriteConcernReceipt::new("a".into(), 3, 2, 2, vec!["a".into(), "b".into()]).unwrap();
838 let result = WriteConcernOutcome::Satisfied(receipt);
839 assert_eq!(
840 WriteConcernOutcome::decode(&result.encode()).unwrap(),
841 result
842 );
843 }
844 #[test]
845 fn region_policy_roundtrips_and_requires_canonical_boolean() {
846 let options = WriteConcernOptions::builder()
847 .require_other_region(true)
848 .build();
849 let encoded = options.encode().unwrap();
850 assert_eq!(WriteConcernOptions::decode(&encoded).unwrap(), options);
851 assert!(WriteConcernOptions::decode(&[encoded, vec![32, 1]].concat()).is_err());
852 assert!(WriteConcernOptions::decode(&[32, 2]).is_err());
853 }
854 #[test]
855 fn region_receipts_require_acknowledged_distinct_outside_nodes() {
856 let base =
857 WriteConcernReceipt::new("a".into(), 3, 1, 1, vec!["a".into(), "b".into()]).unwrap();
858 for nodes in [
859 vec!["a".into()],
860 vec!["outside".into()],
861 vec!["b".into(), "b".into()],
862 ] {
863 assert!(base
864 .clone()
865 .with_region_progress("west".into(), nodes)
866 .is_err());
867 }
868 let receipt = base
869 .with_region_progress("west".into(), vec!["b".into()])
870 .unwrap();
871 let options = WriteConcernOptions::default().with_require_other_region(true);
872 assert!(receipt.validate_for(&options, true).is_ok());
873 let outcome = WriteConcernOutcome::Satisfied(receipt);
874 assert_eq!(
875 WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
876 outcome
877 );
878 let timed_out = WriteConcernReceipt::new("a".into(), 3, 1, 1, vec!["a".into()])
879 .unwrap()
880 .with_region_progress("west".into(), Vec::new())
881 .unwrap();
882 assert!(timed_out.validate_for(&options, false).is_ok());
883 assert!(timed_out.validate_for(&options, true).is_err());
884 assert!(WriteConcernOutcome::decode(
885 &WriteConcernOutcome::Satisfied(timed_out.clone()).encode()
886 )
887 .is_err());
888 let outcome = WriteConcernOutcome::TimedOut(timed_out);
889 assert_eq!(
890 WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
891 outcome
892 );
893 }
894 #[test]
895 fn maximum_region_receipt_fits_and_preflight_bounds_region_fields() {
896 let ids: Vec<_> = (0..MAX_WRITE_CONCERN_REPLICAS)
897 .map(|i| format!("{i:04}{}", "x".repeat(251)))
898 .collect();
899 let receipt = WriteConcernReceipt::new(
900 ids[0].clone(),
901 MAX_WRITE_CONCERN_REPLICAS as u32,
902 1,
903 1,
904 ids.clone(),
905 )
906 .unwrap()
907 .with_region_progress("west".into(), ids[1..].to_vec())
908 .unwrap();
909 let outcome = WriteConcernOutcome::Satisfied(receipt);
910 assert_eq!(
911 WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
912 outcome
913 );
914 let excessive = [74, 1, b'n'].repeat(MAX_WRITE_CONCERN_REPLICAS + 1);
916 assert!(preflight_result(&excessive).is_err());
917 let mut oversized_region = vec![66, 128, 2];
918 oversized_region.extend([b'x'; 256]);
919 assert!(preflight_result(&oversized_region).is_err());
920 }
921}