use std::collections::BTreeSet;
pub const TWO_PHASE_MAX_GROUPS: usize = 3;
pub const TWO_PHASE_MAX_STEPS: usize = 16;
pub const TWO_PHASE_MAX_PAYLOAD: usize = 64 * 1024;
pub const TWO_PHASE_DEFAULT_PREPARE_TIMEOUT_MS: u64 = 300_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TwoPhaseStep {
pub key: Vec<u8>,
pub command: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TwoPhasePlan {
pub tx_id: Vec<u8>,
pub steps: Vec<TwoPhaseStep>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TwoPhasePlanError {
EmptyTxId,
EmptyPlan,
TooManySteps,
PayloadTooLarge {
step: usize,
},
UnroutableKey {
step: usize,
},
TooManyGroups {
groups: usize,
},
}
impl std::fmt::Display for TwoPhasePlanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyTxId => f.write_str("transaction id must not be empty"),
Self::EmptyPlan => f.write_str("plan must contain at least one step"),
Self::TooManySteps => write!(f, "plan exceeds {TWO_PHASE_MAX_STEPS} steps"),
Self::PayloadTooLarge { step } => {
write!(
f,
"step {step} payload exceeds {TWO_PHASE_MAX_PAYLOAD} bytes"
)
}
Self::UnroutableKey { step } => write!(f, "step {step} key is not routable"),
Self::TooManyGroups { groups } => write!(
f,
"plan spans {groups} groups; maximum is {TWO_PHASE_MAX_GROUPS}"
),
}
}
}
impl std::error::Error for TwoPhasePlanError {}
pub fn validate_two_phase_plan(
plan: &TwoPhasePlan,
group_for_key: impl Fn(&[u8]) -> Option<u32>,
) -> Result<(), TwoPhasePlanError> {
if plan.tx_id.is_empty() {
return Err(TwoPhasePlanError::EmptyTxId);
}
if plan.steps.is_empty() {
return Err(TwoPhasePlanError::EmptyPlan);
}
if plan.steps.len() > TWO_PHASE_MAX_STEPS {
return Err(TwoPhasePlanError::TooManySteps);
}
let mut groups = BTreeSet::new();
for (step, item) in plan.steps.iter().enumerate() {
if item.command.len() > TWO_PHASE_MAX_PAYLOAD {
return Err(TwoPhasePlanError::PayloadTooLarge { step });
}
let Some(group) = group_for_key(&item.key) else {
return Err(TwoPhasePlanError::UnroutableKey { step });
};
groups.insert(group);
}
if groups.len() > TWO_PHASE_MAX_GROUPS {
return Err(TwoPhasePlanError::TooManyGroups {
groups: groups.len(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_two_group_plan() {
let plan = TwoPhasePlan {
tx_id: b"tx".to_vec(),
steps: vec![
TwoPhaseStep {
key: b"a".to_vec(),
command: vec![1],
},
TwoPhaseStep {
key: b"b".to_vec(),
command: vec![2],
},
],
};
validate_two_phase_plan(&plan, |key| Some(u32::from(key != b"a"))).expect("valid");
}
#[test]
fn rejects_four_groups() {
let plan = TwoPhasePlan {
tx_id: b"tx".to_vec(),
steps: (0..4)
.map(|i| TwoPhaseStep {
key: vec![u8::try_from(i).expect("test key fits u8")],
command: vec![1],
})
.collect(),
};
assert!(matches!(
validate_two_phase_plan(&plan, |key| Some(u32::from(key[0]))),
Err(TwoPhasePlanError::TooManyGroups { groups: 4 })
));
}
}