Skip to main content

actordb_protocol/
write_concern.rs

1//! Acknowledgement thresholds do not establish a global mutation order.
2
3use 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/// Total acknowledgements, including the receiving node.
16#[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/// Per-write wait policy. Timeout starts when the receiving node applies the
49/// update. Transport and actor admission have their own deadlines.
50#[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    /// Additionally require a processed acknowledgement outside the receiver's
57    /// configured region, including for local and count-one policies.
58    #[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    /// Whether success additionally requires a processed outside-region replica.
76    pub fn require_other_region(&self) -> bool {
77        self.require_other_region
78    }
79    /// Add or clear the independent region acknowledgement requirement.
80    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    /// Unknown policy fields are rejected, so an older implementation cannot
114    /// silently ignore a new required acknowledgement constraint.
115    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/// Actual acknowledgement progress. The receiving node always counts as one.
160/// An unknown replica count is represented by `None` for local-only writes.
161#[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    /// Attach progress against the receiving node's frozen region requirement.
212    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    /// Frozen receiving-region label, present when a region requirement was used.
223    pub fn receiving_region(&self) -> Option<&str> {
224        self.receiving_region.as_deref()
225    }
226    /// Eligible acknowledged identities classified outside the receiving region
227    /// by the receiving node's frozen configuration.
228    pub fn outside_region_acknowledged_nodes(&self) -> &[String] {
229        &self.outside_region_acknowledged_nodes
230    }
231    /// Whether an eligible outside-region processing acknowledgement was observed.
232    pub fn other_region_satisfied(&self) -> bool {
233        self.receiving_region.is_some() && !self.outside_region_acknowledged_nodes.is_empty()
234    }
235    /// Validate receipt correlation with the requested policy. A timed-out
236    /// receipt may lack the required progress but must preserve policy metadata.
237    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        // Field 9, 4097 one-byte strings.
608        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}