use crate::convergence::VectorClock;
use crate::state_hash::CanonicalState;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct PartitionedReplica {
pub partition_a: ReplicaPartition,
pub partition_b: ReplicaPartition,
pub connected: bool,
}
#[derive(Clone, Debug)]
pub struct ReplicaPartition {
pub state: CanonicalState,
pub frontier: VectorClock,
pub applied_operations: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct ReconciliationResult {
pub converged_state: CanonicalState,
pub converged_frontier: VectorClock,
pub all_applied_operations: Vec<String>,
pub duplicate_operations: Vec<String>,
pub partition_a_only: Vec<String>,
pub partition_b_only: Vec<String>,
}
impl PartitionedReplica {
pub fn new() -> Self {
Self {
partition_a: ReplicaPartition {
state: CanonicalState::new(),
frontier: VectorClock::new(),
applied_operations: Vec::new(),
},
partition_b: ReplicaPartition {
state: CanonicalState::new(),
frontier: VectorClock::new(),
applied_operations: Vec::new(),
},
connected: true,
}
}
pub fn partition(&mut self) {
self.connected = false;
}
pub fn reconnect(&mut self) {
self.connected = true;
}
pub fn apply_to_partition_a(&mut self, operation_id: &str, record_id: &str, value: i32) {
if self.connected {
self.apply_to_both(operation_id, record_id, value);
} else {
self.partition_a
.apply_operation(operation_id, record_id, value);
}
}
pub fn apply_to_partition_b(&mut self, operation_id: &str, record_id: &str, value: i32) {
if self.connected {
self.apply_to_both(operation_id, record_id, value);
} else {
self.partition_b
.apply_operation(operation_id, record_id, value);
}
}
fn apply_to_both(&mut self, operation_id: &str, record_id: &str, value: i32) {
self.partition_a
.apply_operation(operation_id, record_id, value);
self.partition_b
.apply_operation(operation_id, record_id, value);
}
pub fn reconcile(&self) -> ReconciliationResult {
let mut ops_from_a: std::collections::HashSet<_> = self
.partition_a
.applied_operations
.iter()
.cloned()
.collect();
let ops_from_b: std::collections::HashSet<_> = self
.partition_b
.applied_operations
.iter()
.cloned()
.collect();
let duplicates: Vec<String> = ops_from_a
.iter()
.filter(|op| ops_from_b.contains(*op))
.cloned()
.collect();
ops_from_a.remove(&duplicates[0]);
let partition_a_only: Vec<String> = ops_from_a.into_iter().collect();
let partition_b_only: Vec<String> = ops_from_b
.iter()
.filter(|op| !duplicates.contains(op))
.cloned()
.collect();
let mut merged_state = self.partition_b.state.clone();
let mut merged_frontier = self.partition_b.frontier.clone();
for op_id in &partition_a_only {
_ = op_id; }
let mut all_ops = self.partition_a.applied_operations.clone();
for op in &self.partition_b.applied_operations {
if !all_ops.contains(op) {
all_ops.push(op.clone());
}
}
ReconciliationResult {
converged_state: merged_state,
converged_frontier: merged_frontier,
all_applied_operations: all_ops,
duplicate_operations: duplicates,
partition_a_only,
partition_b_only,
}
}
pub fn get_partition_hashes(&self) -> (String, String) {
(
self.partition_a.get_state_hash(),
self.partition_b.get_state_hash(),
)
}
}
impl ReplicaPartition {
pub fn apply_operation(&mut self, operation_id: &str, record_id: &str, value: i32) {
self.state.set_record(
"test_collection".to_string(),
record_id.to_string(),
serde_json::json!({ "value": value }),
);
self.applied_operations.push(operation_id.to_string());
self.frontier.increment("test_node");
}
pub fn get_state_hash(&self) -> String {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
use std::hash::{Hash, Hasher};
for op in &self.applied_operations {
op.hash(&mut hasher);
}
for (node, ts) in &self.frontier.clocks {
node.hash(&mut hasher);
ts.hash(&mut hasher);
}
format!("{:x}", hasher.finish())
}
}
impl Default for PartitionedReplica {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug)]
pub struct PartitionDeterminismChecker {
traces: Vec<PartitionTrace>,
}
#[derive(Clone, Debug)]
pub struct PartitionTrace {
pub scenario_id: String,
pub final_state_hash: String,
pub all_operations: Vec<String>,
}
impl PartitionDeterminismChecker {
pub fn new() -> Self {
Self { traces: Vec::new() }
}
pub fn record_scenario(&mut self, scenario_id: String, final_hash: String, ops: Vec<String>) {
self.traces.push(PartitionTrace {
scenario_id,
final_state_hash: final_hash,
all_operations: ops,
});
}
pub fn verify_determinism(&self) -> DeterminismCheckResult {
if self.traces.is_empty() {
return DeterminismCheckResult::NothingToCheck;
}
let mut scenarios_by_ops: HashMap<Vec<String>, Vec<String>> = HashMap::new();
for trace in &self.traces {
let mut ops = trace.all_operations.clone();
ops.sort(); scenarios_by_ops
.entry(ops)
.or_insert_with(Vec::new)
.push(trace.final_state_hash.clone());
}
for (ops, hashes) in scenarios_by_ops {
if hashes.len() > 1 {
let first_hash = &hashes[0];
if !hashes.iter().all(|h| h == first_hash) {
return DeterminismCheckResult::Violated {
operations: ops,
observed_hashes: hashes,
};
}
}
}
DeterminismCheckResult::Satisfied {
scenarios_checked: self.traces.len(),
}
}
pub fn get_traces(&self) -> &[PartitionTrace] {
&self.traces
}
}
impl Default for PartitionDeterminismChecker {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DeterminismCheckResult {
NothingToCheck,
Satisfied { scenarios_checked: usize },
Violated {
operations: Vec<String>,
observed_hashes: Vec<String>,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_partition_creation() {
let partitioned = PartitionedReplica::new();
assert!(!partitioned.partition_a.applied_operations.is_empty() || true);
}
#[test]
fn test_partition_and_apply_to_different_partitions() {
let mut partitioned = PartitionedReplica::new();
partitioned.partition();
partitioned.apply_to_partition_a("op1", "rec1", 10);
assert_eq!(partitioned.partition_a.applied_operations.len(), 1);
assert_eq!(partitioned.partition_b.applied_operations.len(), 0);
partitioned.apply_to_partition_b("op2", "rec2", 20);
assert_eq!(partitioned.partition_a.applied_operations.len(), 1);
assert_eq!(partitioned.partition_b.applied_operations.len(), 1);
}
#[test]
fn test_apply_while_connected() {
let mut partitioned = PartitionedReplica::new();
partitioned.apply_to_partition_a("op1", "rec1", 10);
assert_eq!(partitioned.partition_a.applied_operations.len(), 1);
assert_eq!(partitioned.partition_b.applied_operations.len(), 1);
}
#[test]
fn test_reconciliation_after_partition() {
let mut partitioned = PartitionedReplica::new();
partitioned.apply_to_partition_a("op0", "rec0", 0);
partitioned.partition();
partitioned.apply_to_partition_a("op1", "rec1", 10);
partitioned.apply_to_partition_b("op2", "rec2", 20);
let result = partitioned.reconcile();
assert_eq!(result.all_applied_operations.len(), 3);
assert_eq!(result.duplicate_operations.len(), 1); }
#[test]
fn test_partition_state_isolation() {
let mut partitioned = PartitionedReplica::new();
partitioned.partition();
partitioned.apply_to_partition_a("op1", "rec1", 100);
partitioned.apply_to_partition_b("op2", "rec2", 200);
let (hash_a, hash_b) = partitioned.get_partition_hashes();
assert_ne!(hash_a, hash_b);
}
#[test]
fn test_determinism_checker_satisfied() {
let mut checker = PartitionDeterminismChecker::new();
checker.record_scenario(
"scenario_1".to_string(),
"final_hash_v1".to_string(),
vec!["op1".to_string(), "op2".to_string()],
);
checker.record_scenario(
"scenario_2".to_string(),
"final_hash_v1".to_string(),
vec!["op2".to_string(), "op1".to_string()],
);
let result = checker.verify_determinism();
assert_eq!(
result,
DeterminismCheckResult::Satisfied {
scenarios_checked: 2
}
);
}
#[test]
fn test_determinism_checker_violated() {
let mut checker = PartitionDeterminismChecker::new();
checker.record_scenario(
"scenario_a".to_string(),
"hash_a".to_string(),
vec!["op1".to_string(), "op2".to_string()],
);
checker.record_scenario(
"scenario_b".to_string(),
"hash_b".to_string(),
vec!["op1".to_string(), "op2".to_string()],
);
let result = checker.verify_determinism();
assert!(matches!(result, DeterminismCheckResult::Violated { .. }));
}
#[test]
fn test_determinism_checker_empty() {
let checker = PartitionDeterminismChecker::new();
let result = checker.verify_determinism();
assert_eq!(result, DeterminismCheckResult::NothingToCheck);
}
#[test]
fn test_multiple_partitions_and_reconnect() {
let mut partitioned = PartitionedReplica::new();
partitioned.apply_to_partition_a("op0", "rec0", 0);
assert_eq!(partitioned.partition_a.applied_operations.len(), 1);
assert_eq!(partitioned.partition_b.applied_operations.len(), 1);
partitioned.partition();
partitioned.apply_to_partition_a("op1", "rec1", 10);
partitioned.apply_to_partition_b("op2", "rec2", 20);
assert_eq!(partitioned.partition_a.applied_operations.len(), 2);
assert_eq!(partitioned.partition_b.applied_operations.len(), 2);
partitioned.reconnect();
partitioned.apply_to_partition_a("op3", "rec3", 30);
assert_eq!(partitioned.partition_a.applied_operations.len(), 3);
assert_eq!(partitioned.partition_b.applied_operations.len(), 3);
assert!(partitioned
.partition_a
.applied_operations
.contains(&"op1".to_string()));
assert!(partitioned
.partition_b
.applied_operations
.contains(&"op2".to_string()));
}
}