1use std::sync::Arc;
17
18use crate::error::{ClusterError, Result};
19
20use super::local_executor::ArrayLocalExecutor;
21use super::opcodes::{
22 ARRAY_SHARD_AGG_REQ, ARRAY_SHARD_DELETE_REQ, ARRAY_SHARD_PUT_REQ, ARRAY_SHARD_SLICE_REQ,
23 ARRAY_SHARD_SURROGATE_BITMAP_REQ,
24};
25use super::routing::{array_vshard_for_tile, array_vshards_for_slice};
26use super::wire::{
27 ArrayShardAggReq, ArrayShardAggResp, ArrayShardDeleteReq, ArrayShardDeleteResp,
28 ArrayShardPutReq, ArrayShardPutResp, ArrayShardSliceReq, ArrayShardSliceResp,
29 ArrayShardSurrogateBitmapReq, ArrayShardSurrogateBitmapResp,
30};
31
32pub async fn handle_array_shard_rpc(
42 opcode: u32,
43 local_vshard_id: u32,
44 payload: &[u8],
45 executor: &Arc<dyn ArrayLocalExecutor>,
46) -> Result<Vec<u8>> {
47 match opcode {
48 ARRAY_SHARD_SLICE_REQ => handle_slice(local_vshard_id, payload, executor).await,
49 ARRAY_SHARD_AGG_REQ => handle_agg(local_vshard_id, payload, executor).await,
50 ARRAY_SHARD_PUT_REQ => handle_put(local_vshard_id, payload, executor).await,
51 ARRAY_SHARD_DELETE_REQ => handle_delete(local_vshard_id, payload, executor).await,
52 ARRAY_SHARD_SURROGATE_BITMAP_REQ => {
53 handle_surrogate_bitmap(local_vshard_id, payload, executor).await
54 }
55 other => Err(ClusterError::Codec {
56 detail: format!("handle_array_shard_rpc: unknown opcode {other}"),
57 }),
58 }
59}
60
61async fn handle_slice(
62 local_vshard_id: u32,
63 payload: &[u8],
64 executor: &Arc<dyn ArrayLocalExecutor>,
65) -> Result<Vec<u8>> {
66 let req: ArrayShardSliceReq =
67 zerompk::from_msgpack(payload).map_err(|e| ClusterError::Codec {
68 detail: format!("ArrayShardSliceReq decode: {e}"),
69 })?;
70
71 validate_slice_routing(&req, local_vshard_id)?;
72
73 let exec = executor.exec_slice(&req).await?;
74
75 let truncated = req.limit > 0 && exec.rows.len() >= req.limit as usize;
76 let resp = ArrayShardSliceResp {
77 shard_id: local_vshard_id,
78 rows_msgpack: exec.rows,
79 truncated,
80 truncated_before_horizon: exec.truncated_before_horizon,
81 };
82 serialise(resp)
83}
84
85async fn handle_agg(
86 local_vshard_id: u32,
87 payload: &[u8],
88 executor: &Arc<dyn ArrayLocalExecutor>,
89) -> Result<Vec<u8>> {
90 let req: ArrayShardAggReq =
91 zerompk::from_msgpack(payload).map_err(|e| ClusterError::Codec {
92 detail: format!("ArrayShardAggReq decode: {e}"),
93 })?;
94
95 let exec = executor.exec_agg(&req).await?;
96
97 let resp = ArrayShardAggResp {
98 shard_id: local_vshard_id,
99 partials: exec.partials,
100 truncated_before_horizon: exec.truncated_before_horizon,
101 };
102 serialise(resp)
103}
104
105async fn handle_put(
106 local_vshard_id: u32,
107 payload: &[u8],
108 executor: &Arc<dyn ArrayLocalExecutor>,
109) -> Result<Vec<u8>> {
110 let req: ArrayShardPutReq =
111 zerompk::from_msgpack(payload).map_err(|e| ClusterError::Codec {
112 detail: format!("ArrayShardPutReq decode: {e}"),
113 })?;
114
115 validate_put_routing(&req, local_vshard_id)?;
121
122 let applied_lsn = executor.exec_put(&req).await?;
123 let resp = ArrayShardPutResp {
124 shard_id: local_vshard_id,
125 applied_lsn,
126 };
127 serialise(resp)
128}
129
130async fn handle_delete(
131 local_vshard_id: u32,
132 payload: &[u8],
133 executor: &Arc<dyn ArrayLocalExecutor>,
134) -> Result<Vec<u8>> {
135 let req: ArrayShardDeleteReq =
136 zerompk::from_msgpack(payload).map_err(|e| ClusterError::Codec {
137 detail: format!("ArrayShardDeleteReq decode: {e}"),
138 })?;
139
140 validate_delete_routing(&req, local_vshard_id)?;
141
142 let applied_lsn = executor.exec_delete(&req).await?;
143 let resp = ArrayShardDeleteResp {
144 shard_id: local_vshard_id,
145 applied_lsn,
146 };
147 serialise(resp)
148}
149
150async fn handle_surrogate_bitmap(
151 local_vshard_id: u32,
152 payload: &[u8],
153 executor: &Arc<dyn ArrayLocalExecutor>,
154) -> Result<Vec<u8>> {
155 let req: ArrayShardSurrogateBitmapReq =
156 zerompk::from_msgpack(payload).map_err(|e| ClusterError::Codec {
157 detail: format!("ArrayShardSurrogateBitmapReq decode: {e}"),
158 })?;
159
160 let bitmap_msgpack = executor
161 .exec_surrogate_bitmap_scan(&req.array_id_msgpack, &req.slice_msgpack)
162 .await?;
163
164 let resp = ArrayShardSurrogateBitmapResp {
165 shard_id: local_vshard_id,
166 bitmap_msgpack,
167 };
168 serialise(resp)
169}
170
171fn validate_put_routing(req: &ArrayShardPutReq, local_vshard_id: u32) -> Result<()> {
178 if req.prefix_bits == 0 {
179 return Ok(());
180 }
181 let expected = array_vshard_for_tile(req.representative_hilbert_prefix, req.prefix_bits)?;
182 if expected != local_vshard_id {
183 return Err(ClusterError::WrongOwner {
184 vshard_id: local_vshard_id,
185 expected_owner_node: None,
186 });
187 }
188 Ok(())
189}
190
191fn validate_slice_routing(req: &ArrayShardSliceReq, local_vshard_id: u32) -> Result<()> {
198 if req.prefix_bits == 0 || req.slice_hilbert_ranges.is_empty() {
199 return Ok(());
200 }
201 let covered = array_vshards_for_slice(
204 &req.slice_hilbert_ranges,
205 req.prefix_bits,
206 crate::routing::VSHARD_COUNT,
207 )?;
208 if covered.binary_search(&local_vshard_id).is_err() {
209 return Err(ClusterError::WrongOwner {
210 vshard_id: local_vshard_id,
211 expected_owner_node: None,
212 });
213 }
214 Ok(())
215}
216
217fn validate_delete_routing(req: &ArrayShardDeleteReq, local_vshard_id: u32) -> Result<()> {
222 if req.prefix_bits == 0 {
223 return Ok(());
224 }
225 let expected = array_vshard_for_tile(req.representative_hilbert_prefix, req.prefix_bits)?;
226 if expected != local_vshard_id {
227 return Err(ClusterError::WrongOwner {
228 vshard_id: local_vshard_id,
229 expected_owner_node: None,
230 });
231 }
232 Ok(())
233}
234
235fn serialise<T: zerompk::ToMessagePack>(val: T) -> Result<Vec<u8>> {
236 zerompk::to_msgpack_vec(&val).map_err(|e| ClusterError::Codec {
237 detail: format!("array response serialise: {e}"),
238 })
239}
240
241#[cfg(test)]
242mod tests {
243 use std::sync::Arc;
244
245 use async_trait::async_trait;
246
247 use crate::distributed_array::merge::ArrayAggPartial;
248 use crate::distributed_array::wire::{ArrayShardAggReq, ArrayShardDeleteReq, ArrayShardPutReq};
249 use crate::error::Result;
250
251 use super::super::local_executor::{ArrayAggExec, ArrayLocalExecutor, ArraySliceExec};
252 use super::super::opcodes::{
253 ARRAY_SHARD_AGG_REQ, ARRAY_SHARD_DELETE_REQ, ARRAY_SHARD_PUT_REQ, ARRAY_SHARD_SLICE_REQ,
254 ARRAY_SHARD_SURROGATE_BITMAP_REQ,
255 };
256 use super::super::wire::{
257 ArrayShardAggResp, ArrayShardDeleteResp, ArrayShardPutResp, ArrayShardSliceResp,
258 ArrayShardSurrogateBitmapResp,
259 };
260 use super::handle_array_shard_rpc;
261
262 struct StubExecutor {
266 rows: Vec<Vec<u8>>,
267 bitmap: Vec<u8>,
268 partials: Vec<ArrayAggPartial>,
269 truncated_before_horizon: bool,
270 }
271
272 #[async_trait]
273 impl ArrayLocalExecutor for StubExecutor {
274 async fn exec_slice(
275 &self,
276 _req: &super::super::wire::ArrayShardSliceReq,
277 ) -> Result<ArraySliceExec> {
278 Ok(ArraySliceExec {
279 rows: self.rows.clone(),
280 truncated_before_horizon: self.truncated_before_horizon,
281 })
282 }
283
284 async fn exec_surrogate_bitmap_scan(
285 &self,
286 _array_id_msgpack: &[u8],
287 _slice_msgpack: &[u8],
288 ) -> Result<Vec<u8>> {
289 Ok(self.bitmap.clone())
290 }
291
292 async fn exec_agg(&self, _req: &ArrayShardAggReq) -> Result<ArrayAggExec> {
293 Ok(ArrayAggExec {
294 partials: self.partials.clone(),
295 truncated_before_horizon: self.truncated_before_horizon,
296 })
297 }
298
299 async fn exec_put(&self, req: &ArrayShardPutReq) -> Result<u64> {
300 Ok(req.wal_lsn)
301 }
302
303 async fn exec_delete(&self, req: &ArrayShardDeleteReq) -> Result<u64> {
304 Ok(req.wal_lsn)
305 }
306 }
307
308 fn make_slice_req_bytes() -> Vec<u8> {
309 let req = super::super::wire::ArrayShardSliceReq {
310 array_id_msgpack: vec![],
311 slice_msgpack: vec![],
312 attr_projection: vec![],
313 limit: 10,
314 cell_filter_msgpack: vec![],
315 prefix_bits: 0,
317 slice_hilbert_ranges: vec![],
318 shard_hilbert_range: None,
319 system_time: nodedb_types::SystemTimeScope::Current,
320 valid_at_ms: None,
321 };
322 zerompk::to_msgpack_vec(&req).unwrap()
323 }
324
325 fn make_agg_req_bytes() -> Vec<u8> {
326 let reducer_bytes = vec![0x00u8]; let req = super::super::wire::ArrayShardAggReq {
332 array_id_msgpack: vec![],
333 attr_idx: 0,
334 reducer_msgpack: reducer_bytes,
335 group_by_dim: -1,
336 cell_filter_msgpack: vec![],
337 shard_hilbert_range: None,
338 system_as_of: None,
339 valid_at_ms: None,
340 };
341 zerompk::to_msgpack_vec(&req).unwrap()
342 }
343
344 fn make_bitmap_req_bytes() -> Vec<u8> {
345 let req = super::super::wire::ArrayShardSurrogateBitmapReq {
346 array_id_msgpack: vec![],
347 slice_msgpack: vec![],
348 };
349 zerompk::to_msgpack_vec(&req).unwrap()
350 }
351
352 #[tokio::test]
353 async fn handle_slice_returns_executor_rows() {
354 let row = b"row-data".to_vec();
355 let executor: Arc<dyn ArrayLocalExecutor> = Arc::new(StubExecutor {
356 rows: vec![row.clone(), row.clone()],
357 bitmap: vec![],
358 partials: vec![],
359 truncated_before_horizon: false,
360 });
361 let payload = make_slice_req_bytes();
362 let resp_bytes = handle_array_shard_rpc(ARRAY_SHARD_SLICE_REQ, 0, &payload, &executor)
363 .await
364 .expect("slice handler should succeed");
365
366 let resp: ArrayShardSliceResp =
367 zerompk::from_msgpack(&resp_bytes).expect("response should deserialise");
368 assert_eq!(resp.rows_msgpack.len(), 2);
369 assert_eq!(resp.rows_msgpack[0], row);
370 }
371
372 #[tokio::test]
373 async fn handle_slice_propagates_truncated_before_horizon() {
374 let executor: Arc<dyn ArrayLocalExecutor> = Arc::new(StubExecutor {
379 rows: vec![],
380 bitmap: vec![],
381 partials: vec![],
382 truncated_before_horizon: true,
383 });
384 let payload = make_slice_req_bytes();
385 let resp_bytes = handle_array_shard_rpc(ARRAY_SHARD_SLICE_REQ, 0, &payload, &executor)
386 .await
387 .expect("slice handler should succeed");
388 let resp: ArrayShardSliceResp =
389 zerompk::from_msgpack(&resp_bytes).expect("response should deserialise");
390 assert!(
391 resp.truncated_before_horizon,
392 "handler must forward the executor's below-horizon flag"
393 );
394 }
395
396 #[tokio::test]
397 async fn handle_agg_propagates_truncated_before_horizon() {
398 let executor: Arc<dyn ArrayLocalExecutor> = Arc::new(StubExecutor {
399 rows: vec![],
400 bitmap: vec![],
401 partials: vec![],
402 truncated_before_horizon: true,
403 });
404 let payload = make_agg_req_bytes();
405 let resp_bytes = handle_array_shard_rpc(ARRAY_SHARD_AGG_REQ, 0, &payload, &executor)
406 .await
407 .expect("agg handler should succeed");
408 let resp: ArrayShardAggResp =
409 zerompk::from_msgpack(&resp_bytes).expect("response should deserialise");
410 assert!(
411 resp.truncated_before_horizon,
412 "handler must forward the executor's below-horizon flag"
413 );
414 }
415
416 #[tokio::test]
417 async fn handle_surrogate_bitmap_returns_executor_bitmap() {
418 let bitmap = vec![0xDE, 0xAD, 0xBE, 0xEF];
419 let executor: Arc<dyn ArrayLocalExecutor> = Arc::new(StubExecutor {
420 rows: vec![],
421 bitmap: bitmap.clone(),
422 partials: vec![],
423 truncated_before_horizon: false,
424 });
425 let payload = make_bitmap_req_bytes();
426 let resp_bytes =
427 handle_array_shard_rpc(ARRAY_SHARD_SURROGATE_BITMAP_REQ, 1, &payload, &executor)
428 .await
429 .expect("bitmap handler should succeed");
430
431 let resp: ArrayShardSurrogateBitmapResp =
432 zerompk::from_msgpack(&resp_bytes).expect("response should deserialise");
433 assert_eq!(resp.shard_id, 1);
434 assert_eq!(resp.bitmap_msgpack, bitmap);
435 }
436
437 #[tokio::test]
438 async fn handle_agg_returns_executor_partials() {
439 let partial = ArrayAggPartial::from_single(0, 42.0);
440 let executor: Arc<dyn ArrayLocalExecutor> = Arc::new(StubExecutor {
441 rows: vec![],
442 bitmap: vec![],
443 partials: vec![partial.clone()],
444 truncated_before_horizon: false,
445 });
446 let payload = make_agg_req_bytes();
447 let resp_bytes = handle_array_shard_rpc(ARRAY_SHARD_AGG_REQ, 3, &payload, &executor)
448 .await
449 .expect("agg handler should succeed");
450
451 let resp: ArrayShardAggResp =
452 zerompk::from_msgpack(&resp_bytes).expect("response should deserialise");
453 assert_eq!(resp.shard_id, 3);
454 assert_eq!(resp.partials.len(), 1);
455 assert_eq!(resp.partials[0].count, 1);
456 assert!((resp.partials[0].sum - 42.0).abs() < f64::EPSILON);
457 }
458
459 fn make_put_req_bytes(vshard_id: u32, prefix_bits: u8) -> Vec<u8> {
460 let req = super::super::wire::ArrayShardPutReq {
465 array_id_msgpack: vec![],
466 cells_msgpack: vec![],
467 wal_lsn: 77,
468 representative_hilbert_prefix: if prefix_bits == 0 {
469 0
470 } else {
471 0u64
473 },
474 prefix_bits,
475 };
476 let _ = vshard_id; zerompk::to_msgpack_vec(&req).unwrap()
480 }
481
482 fn make_delete_req_bytes() -> Vec<u8> {
483 let req = super::super::wire::ArrayShardDeleteReq {
484 array_id_msgpack: vec![],
485 coords_msgpack: vec![],
486 wal_lsn: 88,
487 representative_hilbert_prefix: 0,
489 prefix_bits: 0,
490 };
491 zerompk::to_msgpack_vec(&req).unwrap()
492 }
493
494 #[tokio::test]
495 async fn handle_put_delegates_to_executor_and_echoes_lsn() {
496 let executor: Arc<dyn ArrayLocalExecutor> = Arc::new(StubExecutor {
497 rows: vec![],
498 bitmap: vec![],
499 partials: vec![],
500 truncated_before_horizon: false,
501 });
502 let payload = make_put_req_bytes(0, 0);
504 let resp_bytes = handle_array_shard_rpc(ARRAY_SHARD_PUT_REQ, 0, &payload, &executor)
505 .await
506 .expect("put handler should succeed");
507
508 let resp: ArrayShardPutResp =
509 zerompk::from_msgpack(&resp_bytes).expect("response should deserialise");
510 assert_eq!(resp.shard_id, 0);
511 assert_eq!(resp.applied_lsn, 77);
512 }
513
514 #[tokio::test]
515 async fn handle_delete_delegates_to_executor_and_echoes_lsn() {
516 let executor: Arc<dyn ArrayLocalExecutor> = Arc::new(StubExecutor {
517 rows: vec![],
518 bitmap: vec![],
519 partials: vec![],
520 truncated_before_horizon: false,
521 });
522 let payload = make_delete_req_bytes();
523 let resp_bytes = handle_array_shard_rpc(ARRAY_SHARD_DELETE_REQ, 2, &payload, &executor)
524 .await
525 .expect("delete handler should succeed");
526
527 let resp: ArrayShardDeleteResp =
528 zerompk::from_msgpack(&resp_bytes).expect("response should deserialise");
529 assert_eq!(resp.shard_id, 2);
530 assert_eq!(resp.applied_lsn, 88);
531 }
532
533 #[tokio::test]
534 async fn handle_put_rejects_misrouted_request() {
535 let executor: Arc<dyn ArrayLocalExecutor> = Arc::new(StubExecutor {
536 rows: vec![],
537 bitmap: vec![],
538 partials: vec![],
539 truncated_before_horizon: false,
540 });
541 let req = super::super::wire::ArrayShardPutReq {
545 array_id_msgpack: vec![],
546 cells_msgpack: vec![],
547 wal_lsn: 1,
548 representative_hilbert_prefix: 0,
549 prefix_bits: 10,
550 };
551 let payload = zerompk::to_msgpack_vec(&req).unwrap();
552 let err = handle_array_shard_rpc(ARRAY_SHARD_PUT_REQ, 7, &payload, &executor)
553 .await
554 .expect_err("misrouted put should fail");
555 assert!(
556 matches!(
557 err,
558 crate::error::ClusterError::WrongOwner { vshard_id: 7, .. }
559 ),
560 "expected WrongOwner for vshard 7, got {err:?}"
561 );
562 }
563
564 #[test]
565 fn validate_slice_routing_rejects_disjoint_range() {
566 let req = super::super::wire::ArrayShardSliceReq {
571 array_id_msgpack: vec![],
572 slice_msgpack: vec![],
573 attr_projection: vec![],
574 limit: 10,
575 cell_filter_msgpack: vec![],
576 prefix_bits: 10,
577 slice_hilbert_ranges: vec![(0x0040_0000_0000_0000, 0x0040_0000_0000_0000)],
578 shard_hilbert_range: None,
579 system_time: nodedb_types::SystemTimeScope::Current,
580 valid_at_ms: None,
581 };
582 let err = super::validate_slice_routing(&req, 5)
583 .expect_err("disjoint Hilbert range should reject");
584 assert!(
585 matches!(
586 err,
587 crate::error::ClusterError::WrongOwner { vshard_id: 5, .. }
588 ),
589 "expected WrongOwner for vshard 5, got {err:?}"
590 );
591 }
592
593 #[test]
594 fn validate_slice_routing_accepts_overlapping_range() {
595 let req = super::super::wire::ArrayShardSliceReq {
597 array_id_msgpack: vec![],
598 slice_msgpack: vec![],
599 attr_projection: vec![],
600 limit: 10,
601 cell_filter_msgpack: vec![],
602 prefix_bits: 10,
603 slice_hilbert_ranges: vec![(0x0040_0000_0000_0000, 0x0040_0000_0000_0000)],
604 shard_hilbert_range: None,
605 system_time: nodedb_types::SystemTimeScope::Current,
606 valid_at_ms: None,
607 };
608 super::validate_slice_routing(&req, 1).expect("overlapping range should accept");
609 }
610
611 #[tokio::test]
612 async fn handle_unknown_opcode_returns_codec_error() {
613 let executor: Arc<dyn ArrayLocalExecutor> = Arc::new(StubExecutor {
614 rows: vec![],
615 bitmap: vec![],
616 partials: vec![],
617 truncated_before_horizon: false,
618 });
619 let err = handle_array_shard_rpc(0xFF, 0, &[], &executor)
620 .await
621 .expect_err("unknown opcode should fail");
622 assert!(
623 matches!(err, crate::error::ClusterError::Codec { .. }),
624 "{err:?}"
625 );
626 }
627}