1use std::sync::Arc;
40
41use dashmap::DashMap;
42
43use super::replication::ChannelId;
44use super::replication_runtime::{Inbound, ReplicationInboundRouter, ReplicationRuntimeHandle};
45
46#[derive(Default)]
51pub struct RedexReplicationRouter {
52 runtimes: DashMap<ChannelId, Arc<ReplicationRuntimeHandle>>,
53}
54
55impl RedexReplicationRouter {
56 pub fn new() -> Self {
58 Self {
59 runtimes: DashMap::new(),
60 }
61 }
62
63 pub fn register(
69 &self,
70 channel_id: ChannelId,
71 handle: Arc<ReplicationRuntimeHandle>,
72 ) -> Option<Arc<ReplicationRuntimeHandle>> {
73 self.runtimes.insert(channel_id, handle)
74 }
75
76 pub fn get(&self, channel_id: &ChannelId) -> Option<Arc<ReplicationRuntimeHandle>> {
80 self.runtimes.get(channel_id).map(|e| e.value().clone())
81 }
82
83 pub fn unregister(&self, channel_id: &ChannelId) -> Option<Arc<ReplicationRuntimeHandle>> {
87 self.runtimes.remove(channel_id).map(|(_, v)| v)
88 }
89
90 pub fn len(&self) -> usize {
92 self.runtimes.len()
93 }
94
95 pub fn is_empty(&self) -> bool {
97 self.runtimes.is_empty()
98 }
99
100 pub fn snapshot_handles(&self) -> Vec<(ChannelId, Arc<ReplicationRuntimeHandle>)> {
105 self.runtimes
106 .iter()
107 .map(|e| (*e.key(), e.value().clone()))
108 .collect()
109 }
110}
111
112impl ReplicationInboundRouter for RedexReplicationRouter {
113 fn try_route(&self, channel_id: ChannelId, inbound: Inbound) -> Result<(), Inbound> {
114 match self.runtimes.get(&channel_id) {
115 Some(handle) => handle.value().try_dispatch(inbound),
116 None => Err(inbound),
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::adapter::net::channel::ChannelName;
125 use crate::adapter::net::redex::replication::{ReplicaRole, SyncHeartbeat};
126
127 fn cid_for(name: &str) -> ChannelId {
128 let cn = ChannelName::new(name).unwrap();
129 ChannelId::from_name(&cn)
130 }
131
132 fn dummy_inbound(channel_id: ChannelId) -> Inbound {
133 Inbound::Heartbeat {
134 from: 0xAA,
135 msg: SyncHeartbeat {
136 channel_id,
137 tail_seq: 0,
138 role: ReplicaRole::Replica,
139 wall_clock_ms: 0,
140 },
141 }
142 }
143
144 #[test]
145 fn unknown_channel_returns_inbound_back() {
146 let router = RedexReplicationRouter::new();
147 let cid = cid_for("test/unknown");
148 let event = dummy_inbound(cid);
149 let result = router.try_route(cid, event);
150 assert!(result.is_err(), "unknown channel must reject");
151 }
152
153 #[test]
154 fn empty_router_reports_empty() {
155 let router = RedexReplicationRouter::new();
156 assert!(router.is_empty());
157 assert_eq!(router.len(), 0);
158 assert!(router.get(&cid_for("nothing")).is_none());
159 }
160
161 #[test]
162 fn unregister_returns_handle_and_drops_registration() {
163 let rt = tokio::runtime::Runtime::new().unwrap();
166 let _guard = rt.enter();
167 let cid = cid_for("test/unregister");
168 let handle = build_dummy_handle();
169 let router = RedexReplicationRouter::new();
170 router.register(cid, handle.clone());
171 assert_eq!(router.len(), 1);
172 let removed = router.unregister(&cid);
173 assert!(removed.is_some(), "unregister must return the handle");
174 assert!(router.is_empty());
175 let result = router.try_route(cid, dummy_inbound(cid));
178 assert!(result.is_err());
179 rt.block_on(handle.cancel());
181 }
182
183 #[test]
184 fn register_replaces_returns_previous_handle() {
185 let rt = tokio::runtime::Runtime::new().unwrap();
186 let _guard = rt.enter();
187 let cid = cid_for("test/replace");
188 let first = build_dummy_handle();
189 let second = build_dummy_handle();
190 let router = RedexReplicationRouter::new();
191 assert!(router.register(cid, first.clone()).is_none());
192 let previous = router.register(cid, second.clone());
193 assert!(
194 previous.is_some(),
195 "second register must return the prior handle"
196 );
197 assert_eq!(router.len(), 1, "still one channel — second replaced first");
198 rt.block_on(first.cancel());
199 rt.block_on(second.cancel());
200 }
201
202 #[test]
203 fn try_route_to_registered_channel_dispatches() {
204 let rt = tokio::runtime::Runtime::new().unwrap();
205 let _guard = rt.enter();
206 let cid = cid_for("test/runtime");
207 let handle = build_dummy_handle();
208 let router = RedexReplicationRouter::new();
209 router.register(cid, handle.clone());
210 let result = router.try_route(cid, dummy_inbound(cid));
212 assert!(result.is_ok(), "registered channel must route");
213 rt.block_on(handle.cancel());
214 }
215
216 fn build_dummy_handle() -> Arc<ReplicationRuntimeHandle> {
220 use super::super::file::RedexFile;
221 use super::super::manager::Redex;
222 use super::super::replication_budget::BandwidthBudget;
223 use super::super::replication_config::ReplicationConfig;
224 use super::super::replication_coordinator::{
225 ChainTagSink, ChannelIdentity, ReplicationCoordinator,
226 };
227 use super::super::replication_metrics::ReplicationMetricsRegistry;
228 use super::super::replication_runtime::{
229 spawn_replication_runtime, ReplicationDispatcher, RuntimeInputs,
230 };
231 use crate::adapter::net::behavior::placement::NodeId;
232 use crate::adapter::net::channel::ChannelName;
233 use crate::adapter::net::redex::config::RedexFileConfig;
234 use crate::error::AdapterError;
235 use parking_lot::Mutex;
236 use std::time::{Duration, Instant};
237
238 struct NoopSink;
239 #[async_trait::async_trait]
240 impl ChainTagSink for NoopSink {
241 async fn announce_chain(
242 &self,
243 _origin_hash: u64,
244 _tip_seq: u64,
245 ) -> Result<(), AdapterError> {
246 Ok(())
247 }
248 async fn withdraw_chain(&self, _origin_hash: u64) -> Result<(), AdapterError> {
249 Ok(())
250 }
251 }
252 struct NoopDispatcher;
253 #[async_trait::async_trait]
254 impl ReplicationDispatcher for NoopDispatcher {
255 async fn send_heartbeat(
256 &self,
257 _target: NodeId,
258 _msg: SyncHeartbeat,
259 ) -> Result<(), AdapterError> {
260 Ok(())
261 }
262 async fn send_sync_request(
263 &self,
264 _target: NodeId,
265 _msg: super::super::replication::SyncRequest,
266 ) -> Result<(), AdapterError> {
267 Ok(())
268 }
269 async fn send_sync_response(
270 &self,
271 _target: NodeId,
272 _msg: super::super::replication::SyncResponse,
273 ) -> Result<(), AdapterError> {
274 Ok(())
275 }
276 async fn send_sync_nack(
277 &self,
278 _target: NodeId,
279 _msg: super::super::replication::SyncNack,
280 ) -> Result<(), AdapterError> {
281 Ok(())
282 }
283 }
284
285 let cn = ChannelName::new("test/runtime").unwrap();
286 let redex = Redex::new();
287 let file: RedexFile = redex.open_file(&cn, RedexFileConfig::default()).unwrap();
288 let registry = ReplicationMetricsRegistry::new();
289 let coordinator = Arc::new(ReplicationCoordinator::new(
290 ChannelIdentity {
291 channel_name: "test/runtime".to_string(),
292 origin_hash: 0xCAFE_BABE,
293 },
294 ReplicationConfig::new(),
295 Arc::new(NoopSink) as Arc<dyn ChainTagSink>,
296 ®istry,
297 ));
298 let inputs = RuntimeInputs {
299 channel: ChannelIdentity {
300 channel_name: "test/runtime".to_string(),
301 origin_hash: 0xCAFE_BABE,
302 },
303 channel_id: cid_for("test/runtime"),
304 self_node_id: 0x10,
305 replica_set: vec![0x10, 0x20],
306 heartbeat_ms: 60_000, wall_clock_provider: Arc::new(|| 0),
308 tail_provider: Arc::new(|| 0),
309 rtt_lookup: Arc::new(|_| Some(Duration::from_millis(5))),
310 file,
311 default_bandwidth_class: Default::default(),
312 background_fraction: 0.3,
313 };
314 let budget = Arc::new(Mutex::new(BandwidthBudget::new(
315 0.5,
316 1_000_000,
317 Instant::now(),
318 )));
319 Arc::new(spawn_replication_runtime(
320 inputs,
321 coordinator,
322 Arc::new(NoopDispatcher),
323 budget,
324 ))
325 }
326}