1use std::sync::Arc;
17
18use futures::StreamExt;
19
20use crate::circuit_breaker::CircuitBreaker;
21use crate::error::{ClusterError, Result};
22use crate::wire::{VShardEnvelope, VShardMessageType};
23
24async fn call_with_wrong_owner_retry(
34 dispatch: &Arc<dyn ShardRpcDispatch>,
35 env: VShardEnvelope,
36 timeout_ms: u64,
37) -> std::result::Result<VShardEnvelope, ClusterError> {
38 let result = tokio::time::timeout(
39 std::time::Duration::from_millis(timeout_ms),
40 dispatch.call(env.clone(), timeout_ms),
41 )
42 .await;
43
44 match result {
45 Ok(Ok(resp)) => return Ok(resp),
46 Ok(Err(ClusterError::WrongOwner { .. })) => {
47 }
49 Ok(Err(e)) => return Err(e),
50 Err(_elapsed) => {
51 return Err(ClusterError::Transport {
52 detail: format!(
53 "array shard {}: RPC timed out after {timeout_ms}ms",
54 env.vshard_id
55 ),
56 });
57 }
58 }
59
60 let result = tokio::time::timeout(
62 std::time::Duration::from_millis(timeout_ms),
63 dispatch.call(env.clone(), timeout_ms),
64 )
65 .await;
66
67 match result {
68 Ok(Ok(resp)) => Ok(resp),
69 Ok(Err(e)) => Err(e),
70 Err(_elapsed) => Err(ClusterError::Transport {
71 detail: format!(
72 "array shard {}: RPC timed out after {timeout_ms}ms (retry)",
73 env.vshard_id
74 ),
75 }),
76 }
77}
78
79use super::rpc::ShardRpcDispatch;
80
81fn counts_against_breaker(err: &ClusterError) -> bool {
92 !matches!(err, ClusterError::WrongOwner { .. })
93}
94
95pub struct FanOutParams {
97 pub shard_ids: Vec<u32>,
99 pub timeout_ms: u64,
101 pub source_node: u64,
103}
104
105pub struct FanOutPartitionedParams {
108 pub timeout_ms: u64,
110 pub source_node: u64,
112}
113
114pub async fn fan_out(
124 params: &FanOutParams,
125 opcode: u32,
126 req_bytes: &[u8],
127 dispatch: &Arc<dyn ShardRpcDispatch>,
128 circuit_breaker: &CircuitBreaker,
129) -> Result<Vec<(u32, Vec<u8>)>> {
130 if params.shard_ids.is_empty() {
131 return Ok(Vec::new());
132 }
133
134 let mut futs = futures::stream::FuturesUnordered::new();
137
138 for &shard_id in ¶ms.shard_ids {
139 circuit_breaker.check(shard_id as u64)?;
141
142 let env = VShardEnvelope::new(
143 msg_type_from_opcode(opcode)?,
144 params.source_node,
145 0, shard_id,
147 req_bytes.to_vec(),
148 );
149 let timeout_ms = params.timeout_ms;
150 let dispatch = Arc::clone(dispatch);
151 let cb_shard = shard_id;
152
153 futs.push(async move {
154 match call_with_wrong_owner_retry(&dispatch, env, timeout_ms).await {
155 Ok(resp) => Ok((cb_shard, resp.payload)),
156 Err(e) => Err((cb_shard, e)),
157 }
158 });
159 }
160
161 let mut results = Vec::with_capacity(params.shard_ids.len());
162 while let Some(outcome) = futs.next().await {
163 match outcome {
164 Ok((shard_id, payload)) => {
165 circuit_breaker.record_success(shard_id as u64);
166 results.push((shard_id, payload));
167 }
168 Err((shard_id, e)) => {
169 if counts_against_breaker(&e) {
170 circuit_breaker.record_failure(shard_id as u64);
171 }
172 return Err(e);
173 }
174 }
175 }
176
177 Ok(results)
178}
179
180pub async fn fan_out_partitioned(
185 params: &FanOutPartitionedParams,
186 opcode: u32,
187 per_shard: &[(u32, Vec<u8>)],
188 dispatch: &Arc<dyn ShardRpcDispatch>,
189 circuit_breaker: &CircuitBreaker,
190) -> Result<Vec<(u32, Vec<u8>)>> {
191 if per_shard.is_empty() {
192 return Ok(Vec::new());
193 }
194
195 let mut futs = futures::stream::FuturesUnordered::new();
196
197 for (shard_id, payload) in per_shard {
198 circuit_breaker.check(*shard_id as u64)?;
199
200 let env = VShardEnvelope::new(
201 msg_type_from_opcode(opcode)?,
202 params.source_node,
203 0,
204 *shard_id,
205 payload.clone(),
206 );
207 let timeout_ms = params.timeout_ms;
208 let dispatch = Arc::clone(dispatch);
209 let cb_shard = *shard_id;
210
211 futs.push(async move {
212 match call_with_wrong_owner_retry(&dispatch, env, timeout_ms).await {
213 Ok(resp) => Ok((cb_shard, resp.payload)),
214 Err(e) => Err((cb_shard, e)),
215 }
216 });
217 }
218
219 let mut results = Vec::with_capacity(per_shard.len());
220 while let Some(outcome) = futs.next().await {
221 match outcome {
222 Ok((shard_id, payload)) => {
223 circuit_breaker.record_success(shard_id as u64);
224 results.push((shard_id, payload));
225 }
226 Err((shard_id, e)) => {
227 if counts_against_breaker(&e) {
228 circuit_breaker.record_failure(shard_id as u64);
229 }
230 return Err(e);
231 }
232 }
233 }
234
235 Ok(results)
236}
237
238fn msg_type_from_opcode(opcode: u32) -> Result<VShardMessageType> {
243 match opcode {
244 80 => Ok(VShardMessageType::ArrayShardSliceReq),
245 81 => Ok(VShardMessageType::ArrayShardSliceResp),
246 82 => Ok(VShardMessageType::ArrayShardAggReq),
247 83 => Ok(VShardMessageType::ArrayShardAggResp),
248 84 => Ok(VShardMessageType::ArrayShardPutReq),
249 85 => Ok(VShardMessageType::ArrayShardPutResp),
250 86 => Ok(VShardMessageType::ArrayShardDeleteReq),
251 87 => Ok(VShardMessageType::ArrayShardDeleteResp),
252 88 => Ok(VShardMessageType::ArrayShardSurrogateBitmapReq),
253 89 => Ok(VShardMessageType::ArrayShardSurrogateBitmapResp),
254 other => Err(ClusterError::Codec {
255 detail: format!("msg_type_from_opcode: unknown array opcode {other}"),
256 }),
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use std::sync::Arc;
263
264 use async_trait::async_trait;
265
266 use crate::circuit_breaker::{CircuitBreaker, CircuitBreakerConfig};
267 use crate::error::{ClusterError, Result};
268 use crate::wire::VShardEnvelope;
269
270 use super::super::rpc::ShardRpcDispatch;
271 use super::{FanOutParams, FanOutPartitionedParams, fan_out, fan_out_partitioned};
272
273 struct EchoDispatch;
276
277 #[async_trait]
278 impl ShardRpcDispatch for EchoDispatch {
279 async fn call(&self, req: VShardEnvelope, _timeout_ms: u64) -> Result<VShardEnvelope> {
280 let resp_opcode = req.msg_type as u32 + 1;
282 let resp_type = super::msg_type_from_opcode(resp_opcode)?;
283 Ok(VShardEnvelope::new(
284 resp_type,
285 req.target_node,
286 req.source_node,
287 req.vshard_id,
288 req.payload,
289 ))
290 }
291 }
292
293 struct FailDispatch;
295
296 #[async_trait]
297 impl ShardRpcDispatch for FailDispatch {
298 async fn call(&self, _req: VShardEnvelope, _timeout_ms: u64) -> Result<VShardEnvelope> {
299 Err(ClusterError::Transport {
300 detail: "injected failure".into(),
301 })
302 }
303 }
304
305 fn cb() -> CircuitBreaker {
306 CircuitBreaker::new(CircuitBreakerConfig::default())
307 }
308
309 #[tokio::test]
310 async fn fan_out_broadcasts_to_all_shards() {
311 let dispatch: Arc<dyn ShardRpcDispatch> = Arc::new(EchoDispatch);
312 let params = FanOutParams {
313 shard_ids: vec![0, 1, 2],
314 timeout_ms: 1000,
315 source_node: 42,
316 };
317 let req_bytes = b"test-payload";
318 let results = fan_out(
319 ¶ms,
320 super::super::opcodes::ARRAY_SHARD_SLICE_REQ,
321 req_bytes,
322 &dispatch,
323 &cb(),
324 )
325 .await
326 .expect("fan_out should succeed");
327
328 assert_eq!(results.len(), 3);
329 for (_, payload) in &results {
331 assert_eq!(payload.as_slice(), req_bytes);
332 }
333 let mut ids: Vec<u32> = results.iter().map(|(id, _)| *id).collect();
335 ids.sort_unstable();
336 assert_eq!(ids, vec![0, 1, 2]);
337 }
338
339 #[tokio::test]
340 async fn fan_out_empty_shards_returns_empty() {
341 let dispatch: Arc<dyn ShardRpcDispatch> = Arc::new(EchoDispatch);
342 let params = FanOutParams {
343 shard_ids: vec![],
344 timeout_ms: 1000,
345 source_node: 1,
346 };
347 let results = fan_out(
348 ¶ms,
349 super::super::opcodes::ARRAY_SHARD_SLICE_REQ,
350 b"",
351 &dispatch,
352 &cb(),
353 )
354 .await
355 .expect("empty fan_out should succeed");
356 assert!(results.is_empty());
357 }
358
359 #[tokio::test]
360 async fn fan_out_propagates_shard_error() {
361 let dispatch: Arc<dyn ShardRpcDispatch> = Arc::new(FailDispatch);
362 let params = FanOutParams {
363 shard_ids: vec![0, 1],
364 timeout_ms: 1000,
365 source_node: 1,
366 };
367 let err = fan_out(
368 ¶ms,
369 super::super::opcodes::ARRAY_SHARD_SLICE_REQ,
370 b"",
371 &dispatch,
372 &cb(),
373 )
374 .await
375 .expect_err("fan_out should propagate shard failure");
376 assert!(
377 matches!(err, ClusterError::Transport { .. }),
378 "expected Transport error, got {err:?}"
379 );
380 }
381
382 #[tokio::test]
383 async fn fan_out_partitioned_dispatches_distinct_payloads() {
384 let dispatch: Arc<dyn ShardRpcDispatch> = Arc::new(EchoDispatch);
385 let params = FanOutPartitionedParams {
386 timeout_ms: 1000,
387 source_node: 1,
388 };
389 let per_shard = vec![
390 (0u32, b"shard0-data".to_vec()),
391 (1u32, b"shard1-data".to_vec()),
392 ];
393 let results = fan_out_partitioned(
394 ¶ms,
395 super::super::opcodes::ARRAY_SHARD_PUT_REQ,
396 &per_shard,
397 &dispatch,
398 &cb(),
399 )
400 .await
401 .expect("fan_out_partitioned should succeed");
402
403 assert_eq!(results.len(), 2);
404 let mut sorted = results.clone();
405 sorted.sort_unstable_by_key(|(id, _)| *id);
406 assert_eq!(sorted[0].1, b"shard0-data");
407 assert_eq!(sorted[1].1, b"shard1-data");
408 }
409
410 #[tokio::test]
411 async fn circuit_breaker_open_blocks_fan_out() {
412 use crate::circuit_breaker::CircuitBreakerConfig;
413 use std::time::Duration;
414
415 let dispatch: Arc<dyn ShardRpcDispatch> = Arc::new(EchoDispatch);
416 let cb = CircuitBreaker::new(CircuitBreakerConfig {
417 failure_threshold: 1,
418 cooldown: Duration::from_secs(60),
419 });
420 cb.record_failure(0);
422
423 let params = FanOutParams {
424 shard_ids: vec![0],
425 timeout_ms: 1000,
426 source_node: 1,
427 };
428 let err = fan_out(
429 ¶ms,
430 super::super::opcodes::ARRAY_SHARD_SLICE_REQ,
431 b"",
432 &dispatch,
433 &cb,
434 )
435 .await
436 .expect_err("open circuit should block fan_out");
437 assert!(
438 matches!(err, ClusterError::CircuitOpen { .. }),
439 "expected CircuitOpen, got {err:?}"
440 );
441 }
442
443 use std::sync::atomic::{AtomicU32, Ordering};
446
447 struct WrongOwnerThenEchoDispatch {
450 call_count: Arc<AtomicU32>,
451 fail_count: u32,
452 }
453
454 #[async_trait]
455 impl ShardRpcDispatch for WrongOwnerThenEchoDispatch {
456 async fn call(&self, req: VShardEnvelope, _timeout_ms: u64) -> Result<VShardEnvelope> {
457 let n = self.call_count.fetch_add(1, Ordering::SeqCst);
458 if n < self.fail_count {
459 return Err(ClusterError::WrongOwner {
460 vshard_id: req.vshard_id,
461 expected_owner_node: None,
462 });
463 }
464 let resp_opcode = req.msg_type as u32 + 1;
465 let resp_type = super::msg_type_from_opcode(resp_opcode)?;
466 Ok(VShardEnvelope::new(
467 resp_type,
468 req.target_node,
469 req.source_node,
470 req.vshard_id,
471 req.payload,
472 ))
473 }
474 }
475
476 #[tokio::test]
477 async fn wrong_owner_triggers_retry_once() {
478 let call_count = Arc::new(AtomicU32::new(0));
480 let dispatch: Arc<dyn ShardRpcDispatch> = Arc::new(WrongOwnerThenEchoDispatch {
481 call_count: call_count.clone(),
482 fail_count: 1,
483 });
484 let params = FanOutParams {
485 shard_ids: vec![0],
486 timeout_ms: 1000,
487 source_node: 1,
488 };
489 let result = fan_out(
490 ¶ms,
491 super::super::opcodes::ARRAY_SHARD_SLICE_REQ,
492 b"payload",
493 &dispatch,
494 &cb(),
495 )
496 .await
497 .expect("fan_out should succeed after one WrongOwner retry");
498
499 assert_eq!(result.len(), 1);
500 assert_eq!(
501 call_count.load(Ordering::SeqCst),
502 2,
503 "must have called dispatch twice"
504 );
505 }
506
507 #[tokio::test]
508 async fn wrong_owner_twice_propagates() {
509 let call_count = Arc::new(AtomicU32::new(0));
511 let dispatch: Arc<dyn ShardRpcDispatch> = Arc::new(WrongOwnerThenEchoDispatch {
512 call_count: call_count.clone(),
513 fail_count: 2,
514 });
515 let params = FanOutParams {
516 shard_ids: vec![0],
517 timeout_ms: 1000,
518 source_node: 1,
519 };
520 let err = fan_out(
521 ¶ms,
522 super::super::opcodes::ARRAY_SHARD_SLICE_REQ,
523 b"payload",
524 &dispatch,
525 &cb(),
526 )
527 .await
528 .expect_err("fan_out should propagate WrongOwner when both attempts fail");
529
530 assert!(
531 matches!(err, ClusterError::WrongOwner { .. }),
532 "expected WrongOwner, got {err:?}"
533 );
534 assert_eq!(
535 call_count.load(Ordering::SeqCst),
536 2,
537 "must have called dispatch twice"
538 );
539 }
540
541 #[tokio::test]
542 async fn wrong_owner_does_not_trip_breaker() {
543 use crate::circuit_breaker::CircuitState;
544
545 let call_count = Arc::new(AtomicU32::new(0));
548 let dispatch: Arc<dyn ShardRpcDispatch> = Arc::new(WrongOwnerThenEchoDispatch {
549 call_count: call_count.clone(),
550 fail_count: 5, });
552 let cb = cb();
553 let params = FanOutParams {
554 shard_ids: vec![0],
555 timeout_ms: 1000,
556 source_node: 1,
557 };
558 let err = fan_out(
559 ¶ms,
560 super::super::opcodes::ARRAY_SHARD_SLICE_REQ,
561 b"payload",
562 &dispatch,
563 &cb,
564 )
565 .await
566 .expect_err("fan_out should propagate WrongOwner");
567
568 assert!(
569 matches!(err, ClusterError::WrongOwner { .. }),
570 "expected WrongOwner, got {err:?}"
571 );
572 assert_eq!(
575 cb.failure_count(0),
576 0,
577 "WrongOwner must not increment the breaker failure count"
578 );
579 assert_eq!(
580 cb.state(0),
581 CircuitState::Closed,
582 "WrongOwner must leave the breaker Closed"
583 );
584 }
585}