use std::collections::HashSet;
use std::hash::Hash;
use parking_lot::RwLock;
pub struct PartitionController<NodeId> {
isolated: RwLock<HashSet<NodeId>>,
cut_edges: RwLock<HashSet<(NodeId, NodeId)>>,
}
impl<NodeId> PartitionController<NodeId>
where
NodeId: Eq + Hash + Copy,
{
pub fn new() -> Self {
Self {
isolated: RwLock::new(HashSet::new()),
cut_edges: RwLock::new(HashSet::new()),
}
}
pub fn isolate(&self, node: NodeId) {
self.isolated.write().insert(node);
}
pub fn heal(&self, node: NodeId) {
self.isolated.write().remove(&node);
}
pub fn cut_edge(&self, from: NodeId, to: NodeId) {
self.cut_edges.write().insert((from, to));
}
pub fn restore_edge(&self, from: NodeId, to: NodeId) {
self.cut_edges.write().remove(&(from, to));
}
pub fn is_reachable(&self, from: NodeId, to: NodeId) -> bool {
let isolated = self.isolated.read();
if isolated.contains(&from) || isolated.contains(&to) {
return false;
}
!self.cut_edges.read().contains(&(from, to))
}
}
impl<NodeId> Default for PartitionController<NodeId>
where
NodeId: Eq + Hash + Copy,
{
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_fully_reachable() {
let p = PartitionController::<u64>::new();
assert!(p.is_reachable(1, 2));
assert!(p.is_reachable(2, 1));
assert!(p.is_reachable(1, 1));
}
#[test]
fn isolate_blocks_both_directions() {
let p = PartitionController::<u64>::new();
p.isolate(1);
assert!(!p.is_reachable(1, 2));
assert!(!p.is_reachable(2, 1));
assert!(p.is_reachable(2, 3));
}
#[test]
fn heal_restores_node_level_reachability() {
let p = PartitionController::<u64>::new();
p.isolate(1);
p.heal(1);
assert!(p.is_reachable(1, 2));
assert!(p.is_reachable(2, 1));
}
#[test]
fn cut_edge_blocks_only_one_direction() {
let p = PartitionController::<u64>::new();
p.cut_edge(1, 2);
assert!(!p.is_reachable(1, 2));
assert!(p.is_reachable(2, 1));
}
#[test]
fn restore_edge_undoes_cut() {
let p = PartitionController::<u64>::new();
p.cut_edge(1, 2);
p.restore_edge(1, 2);
assert!(p.is_reachable(1, 2));
}
#[test]
fn heal_does_not_restore_individually_cut_edges() {
let p = PartitionController::<u64>::new();
p.cut_edge(1, 2);
p.isolate(1);
p.heal(1);
assert!(!p.is_reachable(1, 2));
assert!(p.is_reachable(2, 1));
}
#[test]
fn default_matches_new() {
let p = PartitionController::<u64>::default();
assert!(p.is_reachable(1, 2));
assert!(p.is_reachable(2, 1));
}
#[test]
fn lock_does_not_poison_when_a_panic_occurs_inside_a_critical_section() {
use std::hash::{Hash, Hasher};
use std::panic::{AssertUnwindSafe, catch_unwind};
#[derive(Clone, Copy, Eq)]
struct Node(u64);
impl PartialEq for Node {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Hash for Node {
fn hash<H: Hasher>(&self, state: &mut H) {
assert_ne!(self.0, 0xDEAD, "simulated downstream panic inside hash()");
self.0.hash(state);
}
}
let p = PartitionController::<Node>::new();
let outcome = catch_unwind(AssertUnwindSafe(|| p.isolate(Node(0xDEAD))));
assert!(
outcome.is_err(),
"expected the simulated panic to propagate"
);
p.isolate(Node(7));
assert!(!p.is_reachable(Node(7), Node(1)));
assert!(p.is_reachable(Node(1), Node(2)));
}
}