use std::collections::HashMap;
use std::pin::Pin;
use std::sync::Arc;
use async_trait::async_trait;
use futures::{Stream, StreamExt};
use openraft::RaftTypeConfig;
use tsoracle_consensus::{ConsensusDriver, ConsensusError, LeaderState};
use tsoracle_core::{Epoch, TsoPeer};
use tsoracle_openraft_toolkit::LeadershipState;
use tsoracle_openraft_toolkit::leadership_events_from_metrics;
use crate::host::OpenraftHighWaterHost;
pub struct OpenraftDriver<H: OpenraftHighWaterHost> {
host: Arc<H>,
peers: Arc<HashMap<<H::Config as RaftTypeConfig>::NodeId, String>>,
}
impl<H: OpenraftHighWaterHost> OpenraftDriver<H> {
pub fn new(host: H) -> Arc<Self> {
Arc::new(Self {
host: Arc::new(host),
peers: Arc::new(HashMap::new()),
})
}
pub fn with_peers(host: H, peers: impl IntoIterator<Item = TsoPeer>) -> Arc<Self>
where
<H::Config as RaftTypeConfig>::NodeId: From<u64>,
{
Arc::new(Self {
host: Arc::new(host),
peers: Arc::new(endpoint_map_from_peers::<H::Config>(peers)),
})
}
pub fn from_arc(host: Arc<H>) -> Arc<Self> {
Arc::new(Self {
host,
peers: Arc::new(HashMap::new()),
})
}
pub fn from_arc_with_peers(host: Arc<H>, peers: impl IntoIterator<Item = TsoPeer>) -> Arc<Self>
where
<H::Config as RaftTypeConfig>::NodeId: From<u64>,
{
Arc::new(Self {
host,
peers: Arc::new(endpoint_map_from_peers::<H::Config>(peers)),
})
}
}
#[async_trait]
impl<H: OpenraftHighWaterHost> ConsensusDriver for OpenraftDriver<H> {
fn leadership_events(&self) -> Pin<Box<dyn Stream<Item = LeaderState> + Send>> {
let host = Arc::clone(&self.host);
let peers = Arc::clone(&self.peers);
Box::pin(owned_leadership_stream::<H>(host, peers))
}
async fn load_high_water(&self) -> Result<u64, ConsensusError> {
self.host.current_high_water().await
}
async fn persist_high_water(
&self,
at_least: u64,
_epoch: Epoch,
) -> Result<u64, ConsensusError> {
tsoracle_consensus::reject_out_of_range_advance(at_least)?;
self.host.submit_advance(at_least).await
}
}
fn owned_leadership_stream<H: OpenraftHighWaterHost>(
host: Arc<H>,
peers: Arc<HashMap<<H::Config as RaftTypeConfig>::NodeId, String>>,
) -> impl Stream<Item = LeaderState> + Send + 'static {
let rx = host.metrics();
let inner: Pin<Box<dyn Stream<Item = LeaderState> + Send>> = Box::pin(
leadership_events_from_metrics::<H::Config>(rx)
.map(move |state| map_leader_state::<H::Config>(state, peers.as_ref())),
);
KeepAlive { _host: host, inner }
}
struct KeepAlive<H: OpenraftHighWaterHost> {
_host: Arc<H>,
inner: Pin<Box<dyn Stream<Item = LeaderState> + Send>>,
}
impl<H: OpenraftHighWaterHost> Stream for KeepAlive<H> {
type Item = LeaderState;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.inner.as_mut().poll_next(cx)
}
}
fn endpoint_map_from_peers<C: RaftTypeConfig>(
peers: impl IntoIterator<Item = TsoPeer>,
) -> HashMap<C::NodeId, String>
where
C::NodeId: From<u64>,
{
peers
.into_iter()
.map(|peer| (C::NodeId::from(peer.node_id), peer.endpoint))
.collect()
}
fn map_leader_state<C: RaftTypeConfig>(
s: LeadershipState<C>,
peers: &HashMap<C::NodeId, String>,
) -> LeaderState {
match s {
LeadershipState::Leader { term } => LeaderState::Leader {
epoch: Epoch(u128::from(term)),
},
LeadershipState::Follower { term, leader } => LeaderState::Follower {
leader_endpoint: leader.and_then(|(id, _node)| peers.get(&id).cloned()),
leader_epoch: Some(Epoch(u128::from(term))),
},
LeadershipState::Candidate { .. }
| LeadershipState::Learner
| LeadershipState::Shutdown => LeaderState::Unknown,
}
}
#[cfg(test)]
mod tests {
use super::{endpoint_map_from_peers, map_leader_state};
use crate::type_config::TypeConfig;
use std::collections::HashMap;
use tsoracle_consensus::LeaderState;
use tsoracle_core::{Epoch, TsoPeer};
use tsoracle_openraft_toolkit::LeadershipState;
#[test]
fn endpoint_map_from_peers_keys_each_endpoint_by_node_id() {
let map = endpoint_map_from_peers::<TypeConfig>([
TsoPeer {
node_id: 2,
endpoint: "http://node-2:50051".to_string(),
},
TsoPeer {
node_id: 3,
endpoint: "http://node-3:50051".to_string(),
},
]);
assert_eq!(map.len(), 2);
assert_eq!(map.get(&2), Some(&"http://node-2:50051".to_string()));
assert_eq!(map.get(&3), Some(&"http://node-3:50051".to_string()));
}
#[test]
fn endpoint_map_from_peers_then_resolves_follower_endpoint() {
let peers = endpoint_map_from_peers::<TypeConfig>([TsoPeer {
node_id: 2,
endpoint: "http://node-2:50051".to_string(),
}]);
let state = map_leader_state::<TypeConfig>(
LeadershipState::Follower {
term: 4,
leader: Some((
2u64,
crate::type_config::OpenraftPeer {
addr: "raft-addr".into(),
},
)),
},
&peers,
);
assert_eq!(
state,
LeaderState::Follower {
leader_endpoint: Some("http://node-2:50051".into()),
leader_epoch: Some(Epoch(4)),
}
);
}
#[test]
fn leader_maps_to_leader_with_epoch() {
let peers = HashMap::new();
let s = map_leader_state::<TypeConfig>(LeadershipState::Leader { term: 7 }, &peers);
assert_eq!(s, LeaderState::Leader { epoch: Epoch(7) });
}
#[test]
fn follower_with_no_leader_maps_to_follower_with_epoch() {
let peers = HashMap::new();
let s = map_leader_state::<TypeConfig>(
LeadershipState::Follower {
term: 3,
leader: None,
},
&peers,
);
assert_eq!(
s,
LeaderState::Follower {
leader_endpoint: None,
leader_epoch: Some(Epoch(3))
}
);
}
#[test]
fn follower_with_known_leader_resolves_endpoint_and_epoch() {
let mut peers = HashMap::new();
peers.insert(2u64, "http://node-2:50051".to_string());
let s = map_leader_state::<TypeConfig>(
LeadershipState::Follower {
term: 4,
leader: Some((
2u64,
crate::type_config::OpenraftPeer {
addr: "raft-addr".into(),
},
)),
},
&peers,
);
assert_eq!(
s,
LeaderState::Follower {
leader_endpoint: Some("http://node-2:50051".into()),
leader_epoch: Some(Epoch(4)),
}
);
}
#[test]
fn follower_with_leader_absent_from_peer_map_has_no_endpoint() {
let peers = HashMap::new();
let s = map_leader_state::<TypeConfig>(
LeadershipState::Follower {
term: 4,
leader: Some((
9u64,
crate::type_config::OpenraftPeer {
addr: "raft-addr".into(),
},
)),
},
&peers,
);
assert_eq!(
s,
LeaderState::Follower {
leader_endpoint: None,
leader_epoch: Some(Epoch(4))
}
);
}
#[test]
fn follower_with_leader_absent_from_nonempty_peer_map_has_no_endpoint() {
let mut peers = HashMap::new();
peers.insert(1u64, "http://node-1:50051".to_string());
peers.insert(3u64, "http://node-3:50051".to_string());
let s = map_leader_state::<TypeConfig>(
LeadershipState::Follower {
term: 5,
leader: Some((
2u64,
crate::type_config::OpenraftPeer {
addr: "raft-addr".into(),
},
)),
},
&peers,
);
assert_eq!(
s,
LeaderState::Follower {
leader_endpoint: None,
leader_epoch: Some(Epoch(5))
}
);
}
#[test]
fn candidate_maps_to_unknown() {
let peers = HashMap::new();
let s = map_leader_state::<TypeConfig>(LeadershipState::Candidate { term: 5 }, &peers);
assert_eq!(s, LeaderState::Unknown);
}
#[test]
fn learner_maps_to_unknown() {
let peers = HashMap::new();
let s = map_leader_state::<TypeConfig>(LeadershipState::Learner, &peers);
assert_eq!(s, LeaderState::Unknown);
}
#[test]
fn shutdown_maps_to_unknown() {
let peers = HashMap::new();
let s = map_leader_state::<TypeConfig>(LeadershipState::Shutdown, &peers);
assert_eq!(s, LeaderState::Unknown);
}
struct EchoHost {
rx: openraft::type_config::alias::WatchReceiverOf<
TypeConfig,
openraft::RaftMetrics<TypeConfig>,
>,
}
#[async_trait::async_trait]
impl crate::host::OpenraftHighWaterHost for EchoHost {
type Config = TypeConfig;
fn metrics(
&self,
) -> openraft::type_config::alias::WatchReceiverOf<
Self::Config,
openraft::RaftMetrics<Self::Config>,
> {
self.rx.clone()
}
async fn current_high_water(&self) -> Result<u64, tsoracle_consensus::ConsensusError> {
Ok(0)
}
async fn submit_advance(
&self,
at_least: u64,
) -> Result<u64, tsoracle_consensus::ConsensusError> {
Ok(at_least)
}
}
fn echo_driver() -> std::sync::Arc<super::OpenraftDriver<EchoHost>> {
use openraft::type_config::TypeConfigExt;
let metrics = openraft::RaftMetrics::<TypeConfig>::new_initial(1u64);
let (_tx, rx) = <TypeConfig as TypeConfigExt>::watch_channel(metrics);
super::OpenraftDriver::new(EchoHost { rx })
}
#[tokio::test]
async fn persist_high_water_rejects_out_of_range_before_submit() {
use tsoracle_consensus::ConsensusDriver;
use tsoracle_core::PHYSICAL_MS_MAX;
let driver = echo_driver();
let err = driver
.persist_high_water(PHYSICAL_MS_MAX + 1, Epoch::ZERO)
.await
.expect_err("an out-of-range advance must be rejected, not persisted");
assert!(
matches!(err, tsoracle_consensus::ConsensusError::PermanentDriver(_)),
"out-of-range advance must classify as PermanentDriver, got {err:?}"
);
assert_eq!(
driver
.persist_high_water(PHYSICAL_MS_MAX, Epoch::ZERO)
.await
.expect("the maximum in-range value must persist"),
PHYSICAL_MS_MAX
);
}
}