use std::collections::BTreeSet;
use crate::db::DistributedDatabaseConfig;
use crate::sync::topology::SyncNodeId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WriteMembership {
pub total_nodes: usize,
pub send_targets: Vec<SyncNodeId>,
}
#[must_use]
pub fn resolve_membership(
config: &DistributedDatabaseConfig,
reachable: &BTreeSet<SyncNodeId>,
) -> WriteMembership {
let total_nodes = config.nodes.len();
let mut emitted = BTreeSet::new();
let mut send_targets = Vec::new();
for node in &config.nodes {
if node == &config.local_node {
continue;
}
if reachable.contains(node) && emitted.insert(node.clone()) {
send_targets.push(node.clone());
}
}
WriteMembership {
total_nodes,
send_targets,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::consistency::{ConsistencyError, StrongConsistency, wait_for_quorum};
use std::time::Duration;
fn config(local: &str, nodes: &[&str]) -> DistributedDatabaseConfig {
DistributedDatabaseConfig {
local_node: SyncNodeId::from(local),
nodes: nodes.iter().map(|name| SyncNodeId::from(*name)).collect(),
topology: None,
sync_interval: 1,
}
}
fn reachable(names: &[&str]) -> BTreeSet<SyncNodeId> {
names.iter().map(|name| SyncNodeId::from(*name)).collect()
}
#[test]
fn total_nodes_is_full_membership_not_reachable_subset() {
let config = config("a", &["a", "b", "c"]);
let membership = resolve_membership(&config, &reachable(&["a"]));
assert_eq!(membership.total_nodes, 3, "denominator is full membership");
assert!(
membership.send_targets.is_empty(),
"no reachable peers to propose to"
);
}
#[test]
fn send_targets_are_reachable_peers_excluding_local() {
let config = config("a", &["a", "b", "c"]);
let membership = resolve_membership(&config, &reachable(&["a", "b", "c"]));
assert_eq!(membership.total_nodes, 3);
assert_eq!(
membership.send_targets,
vec![SyncNodeId::from("b"), SyncNodeId::from("c")],
"local node is never a send target; peers in config order"
);
}
#[test]
fn unknown_reachable_names_cannot_inflate_send_targets() {
let config = config("a", &["a", "b"]);
let membership = resolve_membership(&config, &reachable(&["b", "z"]));
assert_eq!(membership.total_nodes, 2);
assert_eq!(membership.send_targets, vec![SyncNodeId::from("b")]);
}
#[test]
fn minority_denominator_fences_against_self_quorum_q3() {
let config = config("c", &["a", "b", "c"]);
let membership = resolve_membership(&config, &reachable(&["c"]));
assert_eq!(membership.total_nodes, 3);
let strong = StrongConsistency::new(membership.total_nodes, Duration::from_millis(5));
let outcome = wait_for_quorum::<SyncNodeId, _>(strong, std::iter::empty());
assert!(
matches!(
outcome,
Err(ConsistencyError::QuorumTimeout { .. }
| ConsistencyError::QuorumUnavailable { .. })
),
"minority must be fenced, got {outcome:?}"
);
}
}