Skip to main content

nodedb_cluster/rpc_codec/
execute.rs

1// SPDX-License-Identifier: BUSL-1.1
2
3//! ExecuteRequest / ExecuteResponse — cross-node physical-plan execution RPC.
4//!
5//! Discriminants 18 and 19 are permanently assigned to these variants.
6
7use nodedb_types::id::TxnId;
8
9use super::discriminants::*;
10use super::header::write_frame;
11use super::raft_rpc::RaftRpc;
12use crate::error::{ClusterError, Result};
13
14// ── Wire types ──────────────────────────────────────────────────────────────
15
16/// A single (collection, version) entry sent by the caller to let the receiver
17/// validate descriptor freshness before executing the plan.
18///
19/// Cross-version safety: new optional fields should be added as `Option<T>`.
20#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
21pub struct DescriptorVersionEntry {
22    pub collection: String,
23    pub version: u64,
24}
25
26/// Send an already-planned `PhysicalPlan` to a remote node for execution.
27#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
28pub struct ExecuteRequest {
29    /// zerompk-encoded PhysicalPlan (via nodedb::bridge::physical_plan::wire::encode).
30    pub plan_bytes: Vec<u8>,
31    /// Tenant ID authenticated on the originating node; trusted on the receiver.
32    pub tenant_id: u64,
33    /// Database scope authenticated on the originating node; trusted on the receiver.
34    /// `0` maps to `DatabaseId::DEFAULT` (the built-in `default` database).
35    pub database_id: u64,
36    /// Milliseconds remaining until the caller's deadline.
37    /// 0 means the deadline has already expired — receiver returns DeadlineExceeded.
38    pub deadline_remaining_ms: u64,
39    /// Distributed trace ID for observability (16-byte W3C-compatible TraceId).
40    pub trace_id: [u8; 16],
41    /// Caller's view of descriptor versions for every collection touched by the plan.
42    pub descriptor_versions: Vec<DescriptorVersionEntry>,
43    /// Transaction context for the plan, when this leg executes inside a session
44    /// transaction (e.g. a multi-node graph-MATCH leg). `None` for the common
45    /// non-transactional dispatch. Lets the receiver resolve the per-transaction
46    /// staging overlay for the id on the remote node.
47    pub txn_id: Option<TxnId>,
48}
49
50/// Response to an `ExecuteRequest`.
51#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
52pub struct ExecuteResponse {
53    pub success: bool,
54    /// Raw Data Plane response payloads, one per result set.
55    pub payloads: Vec<Vec<u8>>,
56    pub error: Option<TypedClusterError>,
57    /// Max read watermark LSN observed by the executing node's cores; 0 for
58    /// writes/errors. Mirrors [`ExecuteStreamChunk::watermark_lsn`]: raw `u64`
59    /// on the wire, converted to `Lsn` at the coordinator via `Lsn::new`.
60    pub watermark_lsn: u64,
61    /// Per-collection read-version LSN for the scanned collection (its
62    /// `coll_write_lsn` at read time, a WAL LSN); 0 for
63    /// writes/errors. The sound comparand for cross-shard OCC read validation,
64    /// distinct from the core-global `watermark_lsn`. Raw `u64` on the wire,
65    /// converted to `Lsn` at the coordinator via `Lsn::new`.
66    pub read_version_lsn: u64,
67}
68
69/// Typed error returned by the remote executor.
70#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
71pub enum TypedClusterError {
72    NotLeader {
73        group_id: u64,
74        leader_node_id: Option<u64>,
75        leader_addr: Option<String>,
76        term: u64,
77    },
78    DescriptorMismatch {
79        collection: String,
80        expected_version: u64,
81        actual_version: u64,
82    },
83    DeadlineExceeded {
84        elapsed_ms: u64,
85    },
86    /// Catch-all. `code` is a `nodedb_types::error::ErrorCode` as u32.
87    Internal {
88        code: u32,
89        message: String,
90    },
91}
92
93/// One streamed chunk of an `ExecuteStreamRequest` result.
94///
95/// Mirrors a `RowBatch` on the coordinator side: `payload` is a standalone
96/// msgpack array of row elements (the exact bytes the Data Plane produced for a
97/// single scan frame); `watermark_lsn` is that frame's read watermark. A
98/// streaming response is a sequence of these followed by exactly one
99/// [`ExecuteStreamEnd`].
100#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
101pub struct ExecuteStreamChunk {
102    pub payload: Vec<u8>,
103    pub watermark_lsn: u64,
104}
105
106/// Terminal frame of an `ExecuteStreamRequest` result.
107///
108/// `error: None` is a clean EOF (all chunks delivered). `error: Some(e)` is a
109/// terminal failure — any chunks already delivered are valid, but the result is
110/// incomplete and the consumer must surface the error.
111#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
112pub struct ExecuteStreamEnd {
113    pub error: Option<TypedClusterError>,
114}
115
116impl ExecuteResponse {
117    pub fn ok(payloads: Vec<Vec<u8>>, watermark_lsn: u64, read_version_lsn: u64) -> Self {
118        Self {
119            success: true,
120            payloads,
121            error: None,
122            watermark_lsn,
123            read_version_lsn,
124        }
125    }
126    pub fn err(error: TypedClusterError) -> Self {
127        Self {
128            success: false,
129            payloads: vec![],
130            error: Some(error),
131            watermark_lsn: 0,
132            read_version_lsn: 0,
133        }
134    }
135}
136
137// ── Codec ────────────────────────────────────────────────────────────────────
138
139macro_rules! to_bytes {
140    ($msg:expr) => {
141        rkyv::to_bytes::<rkyv::rancor::Error>($msg)
142            .map(|b| b.to_vec())
143            .map_err(|e| ClusterError::Codec {
144                detail: format!("rkyv serialize: {e}"),
145            })
146    };
147}
148
149macro_rules! from_bytes {
150    ($payload:expr, $T:ty, $name:expr) => {{
151        let mut aligned = rkyv::util::AlignedVec::<16>::with_capacity($payload.len());
152        aligned.extend_from_slice($payload);
153        rkyv::from_bytes::<$T, rkyv::rancor::Error>(&aligned).map_err(|e| ClusterError::Codec {
154            detail: format!("rkyv deserialize {}: {e}", $name),
155        })
156    }};
157}
158
159pub(super) fn encode_execute_req(msg: &ExecuteRequest, out: &mut Vec<u8>) -> Result<()> {
160    write_frame(RPC_EXECUTE_REQ, &to_bytes!(msg)?, out)
161}
162pub(super) fn encode_execute_resp(msg: &ExecuteResponse, out: &mut Vec<u8>) -> Result<()> {
163    write_frame(RPC_EXECUTE_RESP, &to_bytes!(msg)?, out)
164}
165
166pub(super) fn decode_execute_req(payload: &[u8]) -> Result<RaftRpc> {
167    Ok(RaftRpc::ExecuteRequest(from_bytes!(
168        payload,
169        ExecuteRequest,
170        "ExecuteRequest"
171    )?))
172}
173pub(super) fn decode_execute_resp(payload: &[u8]) -> Result<RaftRpc> {
174    Ok(RaftRpc::ExecuteResponse(from_bytes!(
175        payload,
176        ExecuteResponse,
177        "ExecuteResponse"
178    )?))
179}
180
181pub(super) fn encode_execute_stream_req(msg: &ExecuteRequest, out: &mut Vec<u8>) -> Result<()> {
182    write_frame(RPC_EXECUTE_STREAM_REQ, &to_bytes!(msg)?, out)
183}
184pub(super) fn encode_execute_stream_chunk(
185    msg: &ExecuteStreamChunk,
186    out: &mut Vec<u8>,
187) -> Result<()> {
188    write_frame(RPC_EXECUTE_STREAM_CHUNK, &to_bytes!(msg)?, out)
189}
190pub(super) fn encode_execute_stream_end(msg: &ExecuteStreamEnd, out: &mut Vec<u8>) -> Result<()> {
191    write_frame(RPC_EXECUTE_STREAM_END, &to_bytes!(msg)?, out)
192}
193
194pub(super) fn decode_execute_stream_req(payload: &[u8]) -> Result<RaftRpc> {
195    Ok(RaftRpc::ExecuteStreamRequest(from_bytes!(
196        payload,
197        ExecuteRequest,
198        "ExecuteStreamRequest"
199    )?))
200}
201pub(super) fn decode_execute_stream_chunk(payload: &[u8]) -> Result<RaftRpc> {
202    Ok(RaftRpc::ExecuteStreamChunk(from_bytes!(
203        payload,
204        ExecuteStreamChunk,
205        "ExecuteStreamChunk"
206    )?))
207}
208pub(super) fn decode_execute_stream_end(payload: &[u8]) -> Result<RaftRpc> {
209    Ok(RaftRpc::ExecuteStreamEnd(from_bytes!(
210        payload,
211        ExecuteStreamEnd,
212        "ExecuteStreamEnd"
213    )?))
214}
215
216/// Numeric code for `TypedClusterError::Internal` when plan bytes fail to decode.
217pub const PLAN_DECODE_FAILED: u32 = 0x_CE00_0001;
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    fn roundtrip_req(req: ExecuteRequest) -> ExecuteRequest {
224        let rpc = RaftRpc::ExecuteRequest(req);
225        let encoded = super::super::encode(&rpc).unwrap();
226        match super::super::decode(&encoded).unwrap() {
227            RaftRpc::ExecuteRequest(r) => r,
228            other => panic!("expected ExecuteRequest, got {other:?}"),
229        }
230    }
231
232    fn roundtrip_resp(resp: ExecuteResponse) -> ExecuteResponse {
233        let rpc = RaftRpc::ExecuteResponse(resp);
234        let encoded = super::super::encode(&rpc).unwrap();
235        match super::super::decode(&encoded).unwrap() {
236            RaftRpc::ExecuteResponse(r) => r,
237            other => panic!("expected ExecuteResponse, got {other:?}"),
238        }
239    }
240
241    #[test]
242    fn roundtrip_execute_request_basic() {
243        let req = ExecuteRequest {
244            plan_bytes: b"msgpack-plan-bytes".to_vec(),
245            tenant_id: 7,
246            database_id: 0,
247            deadline_remaining_ms: 5000,
248            trace_id: [
249                0xDE, 0xAD, 0xBE, 0xEF, 0x12, 0x34, 0x56, 0x78, 0xDE, 0xAD, 0xBE, 0xEF, 0x12, 0x34,
250                0x56, 0x78,
251            ],
252            descriptor_versions: vec![
253                DescriptorVersionEntry {
254                    collection: "orders".into(),
255                    version: 42,
256                },
257                DescriptorVersionEntry {
258                    collection: "users".into(),
259                    version: 1,
260                },
261            ],
262            txn_id: None,
263        };
264        let decoded = roundtrip_req(req.clone());
265        assert_eq!(decoded.plan_bytes, req.plan_bytes);
266        assert_eq!(decoded.tenant_id, 7);
267        assert_eq!(decoded.deadline_remaining_ms, 5000);
268        assert_eq!(
269            decoded.trace_id, req.trace_id,
270            "trace_id roundtrips correctly"
271        );
272        assert_eq!(decoded.descriptor_versions.len(), 2);
273        assert_eq!(decoded.descriptor_versions[0].collection, "orders");
274        assert_eq!(decoded.descriptor_versions[0].version, 42);
275    }
276
277    #[test]
278    fn roundtrip_execute_request_empty_descriptors() {
279        let req = ExecuteRequest {
280            plan_bytes: vec![0xAB, 0xCD],
281            tenant_id: 0,
282            database_id: 0,
283            deadline_remaining_ms: 1000,
284            trace_id: [0u8; 16],
285            descriptor_versions: vec![],
286            txn_id: None,
287        };
288        let decoded = roundtrip_req(req);
289        assert!(decoded.descriptor_versions.is_empty());
290    }
291
292    #[test]
293    fn roundtrip_execute_response_success() {
294        let resp = ExecuteResponse::ok(
295            vec![b"row1".to_vec(), b"row2".to_vec()],
296            0xCAFE_1234,
297            0xBEEF_5678,
298        );
299        let decoded = roundtrip_resp(resp);
300        assert!(decoded.success);
301        assert_eq!(decoded.payloads.len(), 2);
302        assert_eq!(decoded.payloads[0], b"row1");
303        assert!(decoded.error.is_none());
304        assert_eq!(
305            decoded.watermark_lsn, 0xCAFE_1234,
306            "read watermark roundtrips on the response body"
307        );
308        assert_eq!(
309            decoded.read_version_lsn, 0xBEEF_5678,
310            "per-collection read-version LSN roundtrips distinct from the watermark"
311        );
312    }
313
314    #[test]
315    fn roundtrip_execute_response_not_leader() {
316        let resp = ExecuteResponse::err(TypedClusterError::NotLeader {
317            group_id: 3,
318            leader_node_id: Some(1),
319            leader_addr: Some("10.0.0.1:9400".into()),
320            term: 7,
321        });
322        let decoded = roundtrip_resp(resp);
323        assert!(!decoded.success);
324        assert_eq!(
325            decoded.watermark_lsn, 0,
326            "error responses carry no watermark"
327        );
328        assert_eq!(
329            decoded.read_version_lsn, 0,
330            "error responses carry no read-version LSN"
331        );
332        match decoded.error {
333            Some(TypedClusterError::NotLeader {
334                group_id,
335                leader_node_id,
336                leader_addr,
337                term,
338            }) => {
339                assert_eq!(group_id, 3);
340                assert_eq!(leader_node_id, Some(1));
341                assert_eq!(leader_addr.as_deref(), Some("10.0.0.1:9400"));
342                assert_eq!(term, 7);
343            }
344            other => panic!("expected NotLeader, got {other:?}"),
345        }
346    }
347
348    #[test]
349    fn roundtrip_execute_response_descriptor_mismatch() {
350        let resp = ExecuteResponse::err(TypedClusterError::DescriptorMismatch {
351            collection: "orders".into(),
352            expected_version: 5,
353            actual_version: 6,
354        });
355        let decoded = roundtrip_resp(resp);
356        match decoded.error {
357            Some(TypedClusterError::DescriptorMismatch {
358                collection,
359                expected_version,
360                actual_version,
361            }) => {
362                assert_eq!(collection, "orders");
363                assert_eq!(expected_version, 5);
364                assert_eq!(actual_version, 6);
365            }
366            other => panic!("expected DescriptorMismatch, got {other:?}"),
367        }
368    }
369
370    #[test]
371    fn roundtrip_execute_response_deadline_exceeded() {
372        let resp = ExecuteResponse::err(TypedClusterError::DeadlineExceeded { elapsed_ms: 3000 });
373        let decoded = roundtrip_resp(resp);
374        match decoded.error {
375            Some(TypedClusterError::DeadlineExceeded { elapsed_ms }) => {
376                assert_eq!(elapsed_ms, 3000)
377            }
378            other => panic!("expected DeadlineExceeded, got {other:?}"),
379        }
380    }
381
382    #[test]
383    fn roundtrip_execute_response_internal_error() {
384        let resp = ExecuteResponse::err(TypedClusterError::Internal {
385            code: PLAN_DECODE_FAILED,
386            message: "failed to decode plan".into(),
387        });
388        let decoded = roundtrip_resp(resp);
389        match decoded.error {
390            Some(TypedClusterError::Internal { code, message }) => {
391                assert_eq!(code, PLAN_DECODE_FAILED);
392                assert!(message.contains("plan"));
393            }
394            other => panic!("expected Internal, got {other:?}"),
395        }
396    }
397
398    fn roundtrip_stream_chunk(chunk: ExecuteStreamChunk) -> ExecuteStreamChunk {
399        let rpc = RaftRpc::ExecuteStreamChunk(chunk);
400        let encoded = super::super::encode(&rpc).unwrap();
401        match super::super::decode(&encoded).unwrap() {
402            RaftRpc::ExecuteStreamChunk(c) => c,
403            other => panic!("expected ExecuteStreamChunk, got {other:?}"),
404        }
405    }
406
407    fn roundtrip_stream_end(end: ExecuteStreamEnd) -> ExecuteStreamEnd {
408        let rpc = RaftRpc::ExecuteStreamEnd(end);
409        let encoded = super::super::encode(&rpc).unwrap();
410        match super::super::decode(&encoded).unwrap() {
411            RaftRpc::ExecuteStreamEnd(e) => e,
412            other => panic!("expected ExecuteStreamEnd, got {other:?}"),
413        }
414    }
415
416    #[test]
417    fn roundtrip_execute_stream_request_reuses_execute_request_body() {
418        let req = ExecuteRequest {
419            plan_bytes: b"streaming-plan".to_vec(),
420            tenant_id: 11,
421            database_id: 2,
422            deadline_remaining_ms: 4242,
423            trace_id: [9u8; 16],
424            descriptor_versions: vec![DescriptorVersionEntry {
425                collection: "wide".into(),
426                version: 3,
427            }],
428            txn_id: None,
429        };
430        let rpc = RaftRpc::ExecuteStreamRequest(req.clone());
431        let encoded = super::super::encode(&rpc).unwrap();
432        match super::super::decode(&encoded).unwrap() {
433            RaftRpc::ExecuteStreamRequest(r) => {
434                assert_eq!(r.plan_bytes, req.plan_bytes);
435                assert_eq!(r.tenant_id, 11);
436                assert_eq!(r.database_id, 2);
437                assert_eq!(r.deadline_remaining_ms, 4242);
438                assert_eq!(r.trace_id, req.trace_id);
439                assert_eq!(r.descriptor_versions.len(), 1);
440                assert_eq!(r.descriptor_versions[0].collection, "wide");
441                assert_eq!(r.descriptor_versions[0].version, 3);
442            }
443            other => panic!("expected ExecuteStreamRequest, got {other:?}"),
444        }
445    }
446
447    #[test]
448    fn roundtrip_execute_stream_chunk_payload_and_lsn() {
449        let chunk = ExecuteStreamChunk {
450            payload: vec![0x91, 0x01, 0x02, 0x03],
451            watermark_lsn: 0xDEAD_BEEF,
452        };
453        let decoded = roundtrip_stream_chunk(chunk.clone());
454        assert_eq!(decoded.payload, chunk.payload);
455        assert_eq!(decoded.watermark_lsn, 0xDEAD_BEEF);
456    }
457
458    #[test]
459    fn roundtrip_execute_stream_end_clean_eof() {
460        let decoded = roundtrip_stream_end(ExecuteStreamEnd { error: None });
461        assert!(decoded.error.is_none());
462    }
463
464    #[test]
465    fn roundtrip_execute_stream_end_terminal_error() {
466        let decoded = roundtrip_stream_end(ExecuteStreamEnd {
467            error: Some(TypedClusterError::Internal {
468                code: PLAN_DECODE_FAILED,
469                message: "stream failed mid-flight".into(),
470            }),
471        });
472        match decoded.error {
473            Some(TypedClusterError::Internal { code, message }) => {
474                assert_eq!(code, PLAN_DECODE_FAILED);
475                assert!(message.contains("stream failed"));
476            }
477            other => panic!("expected Internal, got {other:?}"),
478        }
479    }
480
481    #[test]
482    fn roundtrip_execute_response_not_leader_no_hint() {
483        let resp = ExecuteResponse::err(TypedClusterError::NotLeader {
484            group_id: 0,
485            leader_node_id: None,
486            leader_addr: None,
487            term: 0,
488        });
489        let decoded = roundtrip_resp(resp);
490        match decoded.error {
491            Some(TypedClusterError::NotLeader {
492                leader_node_id,
493                leader_addr,
494                ..
495            }) => {
496                assert!(leader_node_id.is_none());
497                assert!(leader_addr.is_none());
498            }
499            other => panic!("expected NotLeader, got {other:?}"),
500        }
501    }
502}