1use std::collections::HashSet;
4use std::fmt;
5use std::time::{Duration, Instant};
6
7use prost::Message;
8
9use crate::meta;
10
11pub const MAX_WRITE_CONCERN_REPLICAS: usize = 4096;
12pub const MAX_WRITE_CONCERN_POLICY_BYTES: usize = 1024;
13const MAX_RESULT_BYTES: usize = 2 * MAX_WRITE_CONCERN_REPLICAS * 258 + 8192;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17#[non_exhaustive]
18pub enum WriteConcern {
19 #[default]
20 Local,
21 Count(u32),
22 Majority,
23}
24
25impl WriteConcern {
26 pub fn required_acknowledgements(
27 self,
28 replica_count: usize,
29 ) -> Result<u32, WriteConcernPolicyError> {
30 let majority = replica_count / 2 + 1;
31 match self {
32 Self::Local | Self::Count(1) => Ok(1),
33 Self::Count(0) => Err(invalid("acknowledgement count must be positive")),
34 Self::Count(count) if replica_count != 0 && count as usize <= majority => Ok(count),
35 Self::Count(_) if replica_count == 0 => Err(invalid(
36 "nonlocal concern requires explicit replica placement",
37 )),
38 Self::Count(_) => Err(invalid("acknowledgement count exceeds majority")),
39 Self::Majority if replica_count == 0 => Err(invalid(
40 "majority concern requires explicit replica placement",
41 )),
42 Self::Majority => u32::try_from(majority)
43 .map_err(|_| invalid("replica count exceeds supported range")),
44 }
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, bon::Builder)]
51pub struct WriteConcernOptions {
52 #[builder(default)]
53 concern: WriteConcern,
54 #[builder(default = Duration::from_secs(5))]
55 timeout: Duration,
56 #[builder(default)]
59 require_other_region: bool,
60}
61
62impl Default for WriteConcernOptions {
63 fn default() -> Self {
64 Self::builder().build()
65 }
66}
67
68impl WriteConcernOptions {
69 pub fn concern(&self) -> WriteConcern {
70 self.concern
71 }
72 pub fn timeout(&self) -> Duration {
73 self.timeout
74 }
75 pub fn require_other_region(&self) -> bool {
77 self.require_other_region
78 }
79 pub fn with_require_other_region(mut self, required: bool) -> Self {
81 self.require_other_region = required;
82 self
83 }
84 pub fn validate(&self) -> Result<(), WriteConcernPolicyError> {
85 let ms = self.timeout.as_millis();
86 if ms == 0 || ms > u64::MAX as u128 || Instant::now().checked_add(self.timeout).is_none() {
87 return Err(invalid(
88 "write concern timeout must be representable and at least one millisecond",
89 ));
90 }
91 if self.concern == WriteConcern::Count(0) {
92 return Err(invalid("acknowledgement count must be positive"));
93 }
94 Ok(())
95 }
96
97 pub fn encode(&self) -> Result<Vec<u8>, WriteConcernPolicyError> {
98 self.validate()?;
99 let (level, acknowledgements) = match self.concern {
100 WriteConcern::Local => (meta::WriteConcernLevel::WriteConcernLocal, 0),
101 WriteConcern::Count(n) => (meta::WriteConcernLevel::WriteConcernCount, n),
102 WriteConcern::Majority => (meta::WriteConcernLevel::WriteConcernMajority, 0),
103 };
104 Ok(meta::WriteConcernPolicy {
105 level: level as i32,
106 acknowledgements,
107 timeout_ms: self.timeout.as_millis() as u64,
108 require_other_region: self.require_other_region,
109 }
110 .encode_to_vec())
111 }
112
113 pub fn decode(bytes: &[u8]) -> Result<Self, WriteConcernPolicyError> {
116 preflight_policy(bytes)?;
117 let policy = meta::WriteConcernPolicy::decode(bytes).map_err(|e| invalid(e.to_string()))?;
118 let concern = match meta::WriteConcernLevel::try_from(policy.level) {
119 Ok(meta::WriteConcernLevel::WriteConcernLocal) if policy.acknowledgements == 0 => {
120 WriteConcern::Local
121 }
122 Ok(meta::WriteConcernLevel::WriteConcernCount) => {
123 WriteConcern::Count(policy.acknowledgements)
124 }
125 Ok(meta::WriteConcernLevel::WriteConcernMajority) if policy.acknowledgements == 0 => {
126 WriteConcern::Majority
127 }
128 _ => return Err(invalid("invalid acknowledgement policy")),
129 };
130 let options = Self {
131 concern,
132 timeout: Duration::from_millis(policy.timeout_ms),
133 require_other_region: policy.require_other_region,
134 };
135 options.validate()?;
136 Ok(options)
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141#[non_exhaustive]
142pub enum WriteConcernPolicyError {
143 Invalid(String),
144 Unsupported(String),
145}
146
147impl fmt::Display for WriteConcernPolicyError {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 match self {
150 Self::Invalid(s) | Self::Unsupported(s) => f.write_str(s),
151 }
152 }
153}
154impl std::error::Error for WriteConcernPolicyError {}
155fn invalid(s: impl Into<String>) -> WriteConcernPolicyError {
156 WriteConcernPolicyError::Invalid(s.into())
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct WriteConcernReceipt {
163 receiving_node: String,
164 replica_count: u32,
165 requested: u32,
166 required: u32,
167 acknowledged_nodes: Vec<String>,
168 receiving_region: Option<String>,
169 outside_region_acknowledged_nodes: Vec<String>,
170}
171
172impl WriteConcernReceipt {
173 pub fn new(
174 receiving_node: String,
175 replica_count: u32,
176 requested: u32,
177 required: u32,
178 acknowledged_nodes: Vec<String>,
179 ) -> Result<Self, WriteConcernPolicyError> {
180 let receipt = Self {
181 receiving_node,
182 replica_count,
183 requested,
184 required,
185 acknowledged_nodes,
186 receiving_region: None,
187 outside_region_acknowledged_nodes: Vec::new(),
188 };
189 receipt.validate()?;
190 Ok(receipt)
191 }
192 pub fn receiving_node(&self) -> &str {
193 &self.receiving_node
194 }
195 pub fn replica_count(&self) -> Option<u32> {
196 (self.replica_count != 0).then_some(self.replica_count)
197 }
198 pub fn requested_acknowledgements(&self) -> u32 {
199 self.requested
200 }
201 pub fn required_acknowledgements(&self) -> u32 {
202 self.required
203 }
204 pub fn acknowledged_nodes(&self) -> &[String] {
205 &self.acknowledged_nodes
206 }
207 pub fn was_overridden(&self) -> bool {
208 self.required < self.requested
209 }
210
211 pub fn with_region_progress(
213 mut self,
214 receiving_region: String,
215 outside_region_acknowledged_nodes: Vec<String>,
216 ) -> Result<Self, WriteConcernPolicyError> {
217 self.receiving_region = Some(receiving_region);
218 self.outside_region_acknowledged_nodes = outside_region_acknowledged_nodes;
219 self.validate()?;
220 Ok(self)
221 }
222 pub fn receiving_region(&self) -> Option<&str> {
224 self.receiving_region.as_deref()
225 }
226 pub fn outside_region_acknowledged_nodes(&self) -> &[String] {
229 &self.outside_region_acknowledged_nodes
230 }
231 pub fn other_region_satisfied(&self) -> bool {
233 self.receiving_region.is_some() && !self.outside_region_acknowledged_nodes.is_empty()
234 }
235 pub fn validate_for(
238 &self,
239 options: &WriteConcernOptions,
240 satisfied: bool,
241 ) -> Result<(), WriteConcernPolicyError> {
242 self.validate()?;
243 let expected = options
244 .concern()
245 .required_acknowledgements(self.replica_count as usize)?;
246 if self.requested != expected {
247 return Err(invalid(
248 "write concern reply changed the requested threshold",
249 ));
250 }
251 if options.require_other_region()
252 && (self.receiving_region.is_none() || satisfied && !self.other_region_satisfied())
253 {
254 return Err(invalid(
255 "write concern reply lacks required region progress",
256 ));
257 }
258 if satisfied && self.acknowledged_nodes.len() < self.required as usize {
259 return Err(invalid("receipt lacks required acknowledgements"));
260 }
261 Ok(())
262 }
263
264 fn validate(&self) -> Result<(), WriteConcernPolicyError> {
265 let valid_id = |id: &str| !id.is_empty() && id.len() <= 255;
266 let acknowledged: HashSet<_> = self.acknowledged_nodes.iter().collect();
267 let outside: HashSet<_> = self.outside_region_acknowledged_nodes.iter().collect();
268 if !valid_id(&self.receiving_node)
269 || self.requested == 0
270 || self.required == 0
271 || self.required > self.requested
272 || self.replica_count as usize > MAX_WRITE_CONCERN_REPLICAS
273 || (self.replica_count == 0 && self.requested != 1)
274 || (self.replica_count > 0 && self.requested > self.replica_count / 2 + 1)
275 || self.acknowledged_nodes.len() > MAX_WRITE_CONCERN_REPLICAS
276 || (self.replica_count == 0 && self.acknowledged_nodes.len() != 1)
277 || (self.replica_count > 0
278 && self.acknowledged_nodes.len() > self.replica_count as usize)
279 || self.acknowledged_nodes.iter().any(|id| !valid_id(id))
280 || acknowledged.len() != self.acknowledged_nodes.len()
281 || !self
282 .acknowledged_nodes
283 .iter()
284 .any(|id| id == &self.receiving_node)
285 || self
286 .receiving_region
287 .as_ref()
288 .is_some_and(|region| !valid_id(region))
289 || self.receiving_region.is_some() && self.replica_count == 0
290 || self.receiving_region.is_none() && !self.outside_region_acknowledged_nodes.is_empty()
291 || self.outside_region_acknowledged_nodes.len() > self.acknowledged_nodes.len()
292 || outside.len() != self.outside_region_acknowledged_nodes.len()
293 || self
294 .outside_region_acknowledged_nodes
295 .iter()
296 .any(|id| id == &self.receiving_node || !acknowledged.contains(id))
297 {
298 return Err(invalid("invalid write concern receipt"));
299 }
300 Ok(())
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq)]
305#[non_exhaustive]
306pub enum WriteConcernOutcome {
307 Satisfied(WriteConcernReceipt),
308 TimedOut(WriteConcernReceipt),
309 InvalidPolicy(String),
310 Unsupported(String),
311 Backpressure,
312}
313
314impl WriteConcernOutcome {
315 pub fn encode(&self) -> Vec<u8> {
316 let mut result = meta::WriteConcernResult::default();
317 let (status, receipt) = match self {
318 Self::Satisfied(receipt) => (
319 meta::WriteConcernStatus::WriteConcernSatisfied,
320 Some(receipt),
321 ),
322 Self::TimedOut(receipt) => (
323 meta::WriteConcernStatus::WriteConcernTimedOut,
324 Some(receipt),
325 ),
326 Self::InvalidPolicy(error) => {
327 result.error = error.clone();
328 (meta::WriteConcernStatus::WriteConcernInvalid, None)
329 }
330 Self::Unsupported(error) => {
331 result.error = error.clone();
332 (meta::WriteConcernStatus::WriteConcernUnsupported, None)
333 }
334 Self::Backpressure => (meta::WriteConcernStatus::WriteConcernBackpressure, None),
335 };
336 result.status = status as i32;
337 if let Some(receipt) = receipt {
338 result.receiving_node = receipt.receiving_node.clone();
339 result.replica_count = receipt.replica_count;
340 result.requested_acknowledgements = receipt.requested;
341 result.required_acknowledgements = receipt.required;
342 result.acknowledged_nodes = receipt.acknowledged_nodes.clone();
343 result.receiving_region = receipt.receiving_region.clone();
344 result.outside_region_acknowledged_nodes =
345 receipt.outside_region_acknowledged_nodes.clone();
346 }
347 result.encode_to_vec()
348 }
349 pub fn decode(bytes: &[u8]) -> Result<Self, WriteConcernPolicyError> {
350 preflight_result(bytes)?;
351 let result = meta::WriteConcernResult::decode(bytes).map_err(|e| invalid(e.to_string()))?;
352 match meta::WriteConcernStatus::try_from(result.status) {
353 Ok(
354 meta::WriteConcernStatus::WriteConcernSatisfied
355 | meta::WriteConcernStatus::WriteConcernTimedOut,
356 ) => {
357 let mut receipt = WriteConcernReceipt::new(
358 result.receiving_node,
359 result.replica_count,
360 result.requested_acknowledgements,
361 result.required_acknowledgements,
362 result.acknowledged_nodes,
363 )?;
364 if let Some(region) = result.receiving_region {
365 receipt = receipt
366 .with_region_progress(region, result.outside_region_acknowledged_nodes)?;
367 } else if !result.outside_region_acknowledged_nodes.is_empty() {
368 return Err(invalid("region progress lacks receiving region"));
369 }
370 if result.status == meta::WriteConcernStatus::WriteConcernSatisfied as i32 {
371 if receipt.acknowledged_nodes.len() < receipt.required as usize {
372 return Err(invalid("receipt lacks required acknowledgements"));
373 }
374 if receipt.receiving_region.is_some() && !receipt.other_region_satisfied() {
375 return Err(invalid("receipt lacks required region acknowledgement"));
376 }
377 Ok(Self::Satisfied(receipt))
378 } else {
379 Ok(Self::TimedOut(receipt))
380 }
381 }
382 Ok(meta::WriteConcernStatus::WriteConcernInvalid) => {
383 Ok(Self::InvalidPolicy(result.error))
384 }
385 Ok(meta::WriteConcernStatus::WriteConcernUnsupported) => {
386 Ok(Self::Unsupported(result.error))
387 }
388 Ok(meta::WriteConcernStatus::WriteConcernBackpressure) => Ok(Self::Backpressure),
389 _ => Err(invalid("unknown write concern outcome")),
390 }
391 }
392}
393
394fn varint(bytes: &[u8], pos: &mut usize) -> Result<u64, WriteConcernPolicyError> {
395 let mut value = 0;
396 for shift in (0..70).step_by(7) {
397 let byte = *bytes.get(*pos).ok_or_else(|| invalid("truncated policy"))?;
398 *pos += 1;
399 if shift == 63 && byte > 1 {
400 return Err(invalid("overflowing policy varint"));
401 }
402 value |= u64::from(byte & 127) << shift;
403 if byte & 128 == 0 {
404 return Ok(value);
405 }
406 }
407 Err(invalid("overflowing policy varint"))
408}
409
410pub fn preflight_policy(bytes: &[u8]) -> Result<(), WriteConcernPolicyError> {
411 if bytes.len() > MAX_WRITE_CONCERN_POLICY_BYTES {
412 return Err(invalid("write concern policy exceeds byte limit"));
413 }
414 let mut pos = 0;
415 let mut seen = [false; 5];
416 while pos < bytes.len() {
417 let tag = varint(bytes, &mut pos)?;
418 let field = tag >> 3;
419 if !(1..=4).contains(&field) {
420 return Err(WriteConcernPolicyError::Unsupported(
421 "unsupported write concern policy field".into(),
422 ));
423 }
424 if tag & 7 != 0 || seen[field as usize] {
425 return Err(invalid("invalid or duplicate policy field"));
426 }
427 seen[field as usize] = true;
428 let value = varint(bytes, &mut pos)?;
429 if field == 4 && value > 1 {
430 return Err(invalid("invalid region requirement boolean"));
431 }
432 }
433 Ok(())
434}
435
436pub fn preflight_result(bytes: &[u8]) -> Result<(), WriteConcernPolicyError> {
437 if bytes.len() > MAX_RESULT_BYTES {
438 return Err(invalid("write concern result exceeds byte limit"));
439 }
440 let mut pos = 0;
441 let mut nodes = 0usize;
442 let mut outside_nodes = 0usize;
443 while pos < bytes.len() {
444 let tag = varint(bytes, &mut pos)?;
445 let field = tag >> 3;
446 if field == 0 {
447 return Err(invalid("invalid receipt field"));
448 }
449 match tag & 7 {
450 0 => {
451 varint(bytes, &mut pos)?;
452 }
453 1 => {
454 pos = pos
455 .checked_add(8)
456 .ok_or_else(|| invalid("receipt size overflow"))?;
457 }
458 5 => {
459 pos = pos
460 .checked_add(4)
461 .ok_or_else(|| invalid("receipt size overflow"))?;
462 }
463 2 => {
464 let len = usize::try_from(varint(bytes, &mut pos)?)
465 .map_err(|_| invalid("receipt size overflow"))?;
466 if field == 6 {
467 nodes += 1;
468 if nodes > MAX_WRITE_CONCERN_REPLICAS || len > 255 {
469 return Err(invalid("receipt acknowledgement limit"));
470 }
471 }
472 if field == 9 {
473 outside_nodes += 1;
474 if outside_nodes > MAX_WRITE_CONCERN_REPLICAS || len > 255 {
475 return Err(invalid("receipt region acknowledgement limit"));
476 }
477 }
478 if (field == 2 || field == 8) && len > 255 || field == 7 && len > 4096 {
479 return Err(invalid("receipt string limit"));
480 }
481 pos = pos
482 .checked_add(len)
483 .ok_or_else(|| invalid("receipt size overflow"))?;
484 }
485 _ => return Err(invalid("invalid receipt wire type")),
486 }
487 if pos > bytes.len() {
488 return Err(invalid("truncated receipt"));
489 }
490 }
491 Ok(())
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 #[test]
498 fn count_is_total_and_bounded_by_majority() {
499 assert_eq!(
500 WriteConcern::Count(3).required_acknowledgements(5).unwrap(),
501 3
502 );
503 assert_eq!(
504 WriteConcern::Majority.required_acknowledgements(4).unwrap(),
505 3
506 );
507 assert!(WriteConcern::Count(4).required_acknowledgements(5).is_err());
508 assert!(WriteConcern::Majority.required_acknowledgements(0).is_err());
509 assert_eq!(WriteConcern::Local.required_acknowledgements(0).unwrap(), 1);
510 }
511 #[test]
512 fn policy_roundtrip_rejects_unknown_and_duplicate_requirements() {
513 let options = WriteConcernOptions::builder()
514 .concern(WriteConcern::Count(2))
515 .build();
516 let bytes = options.encode().unwrap();
517 assert_eq!(WriteConcernOptions::decode(&bytes).unwrap(), options);
518 assert!(WriteConcernOptions::decode(&[bytes.clone(), bytes].concat()).is_err());
519 assert!(matches!(
520 WriteConcernOptions::decode(&[40, 1]),
521 Err(WriteConcernPolicyError::Unsupported(_))
522 ));
523 }
524 #[test]
525 fn receipts_require_distinct_acknowledgements_and_receiver() {
526 assert!(
527 WriteConcernReceipt::new("a".into(), 3, 2, 2, vec!["a".into(), "a".into()]).is_err()
528 );
529 let receipt =
530 WriteConcernReceipt::new("a".into(), 3, 2, 2, vec!["a".into(), "b".into()]).unwrap();
531 let result = WriteConcernOutcome::Satisfied(receipt);
532 assert_eq!(
533 WriteConcernOutcome::decode(&result.encode()).unwrap(),
534 result
535 );
536 }
537 #[test]
538 fn region_policy_roundtrips_and_requires_canonical_boolean() {
539 let options = WriteConcernOptions::builder()
540 .require_other_region(true)
541 .build();
542 let encoded = options.encode().unwrap();
543 assert_eq!(WriteConcernOptions::decode(&encoded).unwrap(), options);
544 assert!(WriteConcernOptions::decode(&[encoded, vec![32, 1]].concat()).is_err());
545 assert!(WriteConcernOptions::decode(&[32, 2]).is_err());
546 }
547 #[test]
548 fn region_receipts_require_acknowledged_distinct_outside_nodes() {
549 let base =
550 WriteConcernReceipt::new("a".into(), 3, 1, 1, vec!["a".into(), "b".into()]).unwrap();
551 for nodes in [
552 vec!["a".into()],
553 vec!["outside".into()],
554 vec!["b".into(), "b".into()],
555 ] {
556 assert!(base
557 .clone()
558 .with_region_progress("west".into(), nodes)
559 .is_err());
560 }
561 let receipt = base
562 .with_region_progress("west".into(), vec!["b".into()])
563 .unwrap();
564 let options = WriteConcernOptions::default().with_require_other_region(true);
565 assert!(receipt.validate_for(&options, true).is_ok());
566 let outcome = WriteConcernOutcome::Satisfied(receipt);
567 assert_eq!(
568 WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
569 outcome
570 );
571 let timed_out = WriteConcernReceipt::new("a".into(), 3, 1, 1, vec!["a".into()])
572 .unwrap()
573 .with_region_progress("west".into(), Vec::new())
574 .unwrap();
575 assert!(timed_out.validate_for(&options, false).is_ok());
576 assert!(timed_out.validate_for(&options, true).is_err());
577 assert!(WriteConcernOutcome::decode(
578 &WriteConcernOutcome::Satisfied(timed_out.clone()).encode()
579 )
580 .is_err());
581 let outcome = WriteConcernOutcome::TimedOut(timed_out);
582 assert_eq!(
583 WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
584 outcome
585 );
586 }
587 #[test]
588 fn maximum_region_receipt_fits_and_preflight_bounds_region_fields() {
589 let ids: Vec<_> = (0..MAX_WRITE_CONCERN_REPLICAS)
590 .map(|i| format!("{i:04}{}", "x".repeat(251)))
591 .collect();
592 let receipt = WriteConcernReceipt::new(
593 ids[0].clone(),
594 MAX_WRITE_CONCERN_REPLICAS as u32,
595 1,
596 1,
597 ids.clone(),
598 )
599 .unwrap()
600 .with_region_progress("west".into(), ids[1..].to_vec())
601 .unwrap();
602 let outcome = WriteConcernOutcome::Satisfied(receipt);
603 assert_eq!(
604 WriteConcernOutcome::decode(&outcome.encode()).unwrap(),
605 outcome
606 );
607 let excessive = [74, 1, b'n'].repeat(MAX_WRITE_CONCERN_REPLICAS + 1);
609 assert!(preflight_result(&excessive).is_err());
610 let mut oversized_region = vec![66, 128, 2];
611 oversized_region.extend([b'x'; 256]);
612 assert!(preflight_result(&oversized_region).is_err());
613 }
614}