Skip to main content

nodedb_cluster/rpc_codec/
calvin_submit.rs

1// SPDX-License-Identifier: BUSL-1.1
2
3//! Routed Calvin-submit one-shot RPC (Cv1).
4//!
5//! A coordinator that wants to run a cross-shard Calvin transaction must submit
6//! the `TxClass` on the SEQUENCER-GROUP LEADER: only the leader's sequencer
7//! service assigns transactions, and only the leader's local
8//! `CalvinCompletionRegistry` receives BOTH the `note_assigned` (from the
9//! service) and the replicated `note_completion_ack` (applied on every member).
10//! A non-leader coordinator therefore cannot submit-and-await locally — its
11//! inbox is drained and discarded, and its registry never sees the assignment.
12//!
13//! When this coordinator is NOT the sequencer leader it sends a
14//! [`SubmitCalvinTxnRequest`] (carrying the msgpack-encoded `TxClass`) to the
15//! leader; the leader runs the submit-and-await locally and replies with exactly
16//! one [`SubmitCalvinTxnResponse`]. One-shot request/response; no streaming.
17//!
18//! Discriminants 36/37 are permanently assigned to these variants.
19
20use super::discriminants::*;
21use super::execute::TypedClusterError;
22use super::header::write_frame;
23use super::raft_rpc::RaftRpc;
24use crate::error::{ClusterError, Result};
25
26// ── Wire types ──────────────────────────────────────────────────────────────
27
28/// Coordinator → sequencer-leader routed Calvin-submit request (Cv1).
29///
30/// Carries the `TxClass` as opaque msgpack bytes (`tx_class_bytes`); the leader
31/// decodes it, submits it to its local sequencer inbox, and awaits assignment +
32/// completion before replying. The `deadline_remaining_ms` / `trace_id` fields
33/// mirror [`AssignSurrogateRequest`](super::surrogate::AssignSurrogateRequest)
34/// so the leader-side handler shares the same deadline / tracing prologue as the
35/// other one-shot RPCs; the leader bounds its submit-and-await by
36/// `deadline_remaining_ms`.
37///
38/// Cross-version safety: new optional fields should be added as `Option<T>`.
39#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
40pub struct SubmitCalvinTxnRequest {
41    /// The `TxClass` to submit, encoded with `zerompk::to_msgpack_vec`.
42    pub tx_class_bytes: Vec<u8>,
43    /// Deadline budget remaining for the submit-and-await on the leader (ms).
44    pub deadline_remaining_ms: u64,
45    pub trace_id: [u8; 16],
46}
47
48/// Terminal reply to a [`SubmitCalvinTxnRequest`].
49///
50/// `error: None` means the leader submitted the transaction and observed its
51/// completion ack (the cross-shard write committed). `error: Some(e)` means the
52/// leader-side submit-and-await failed (sequencer rejected the txn, assignment /
53/// completion timed out, a channel closed, the `TxClass` failed to decode, or no
54/// Calvin-submit hook is configured).
55///
56/// `payload_bytes` carries the applied transaction's RETURNING rows (the
57/// Data-Plane response payload) back to a remote coordinator when the submitted
58/// write had a RETURNING clause; it is `None` for plain writes with no rows to
59/// surface. These are a QUERY RESULT, not replicated state — they ride this
60/// non-Raft RPC response, never the sequencer Raft log.
61#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
62pub struct SubmitCalvinTxnResponse {
63    pub error: Option<TypedClusterError>,
64    pub payload_bytes: Option<Vec<u8>>,
65}
66
67/// Coordinator → sequencer-leader routed Calvin-INBOX request (Cv1).
68///
69/// The OLLP dependent sibling of [`SubmitCalvinTxnRequest`]: it submits a
70/// dependent `TxClass` to the leader's sequencer inbox and asks the leader to
71/// reply with the ASSIGNMENT (`inbox_seq` / `epoch` / `position` /
72/// `participants`) AS SOON AS it is sequenced — it does NOT await completion.
73/// Carries the same `tx_class_bytes` / `deadline_remaining_ms` / `trace_id`
74/// prologue as [`SubmitCalvinTxnRequest`] so the leader-side handler shares the
75/// same deadline / tracing shape; the leader bounds its submit-and-assign wait
76/// by `deadline_remaining_ms`.
77///
78/// Cross-version safety: new optional fields should be added as `Option<T>`.
79#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
80pub struct SubmitCalvinInboxRequest {
81    /// The `TxClass` to submit, encoded with `zerompk::to_msgpack_vec`.
82    pub tx_class_bytes: Vec<u8>,
83    /// Deadline budget remaining for the submit-and-assign on the leader (ms).
84    pub deadline_remaining_ms: u64,
85    pub trace_id: [u8; 16],
86}
87
88/// Terminal reply to a [`SubmitCalvinInboxRequest`].
89///
90/// `error: None` means the leader submitted the transaction and observed its
91/// ASSIGNMENT (the numeric fields carry the sequencer-assigned
92/// `inbox_seq` / `epoch` / `position` / `participants`); the leader did NOT
93/// await completion. `error: Some(e)` means the leader-side submit-and-assign
94/// failed (sequencer rejected the txn, assignment timed out, a channel closed,
95/// the `TxClass` failed to decode, or no Calvin-inbox hook is configured); the
96/// numeric fields are 0 in that case.
97#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
98pub struct SubmitCalvinInboxResponse {
99    pub inbox_seq: u64,
100    pub epoch: u64,
101    pub position: u32,
102    pub participants: u64,
103    pub error: Option<TypedClusterError>,
104}
105
106// ── Codec ────────────────────────────────────────────────────────────────────
107
108macro_rules! to_bytes {
109    ($msg:expr) => {
110        rkyv::to_bytes::<rkyv::rancor::Error>($msg)
111            .map(|b| b.to_vec())
112            .map_err(|e| ClusterError::Codec {
113                detail: format!("rkyv serialize: {e}"),
114            })
115    };
116}
117
118macro_rules! from_bytes {
119    ($payload:expr, $T:ty, $name:expr) => {{
120        let mut aligned = rkyv::util::AlignedVec::<16>::with_capacity($payload.len());
121        aligned.extend_from_slice($payload);
122        rkyv::from_bytes::<$T, rkyv::rancor::Error>(&aligned).map_err(|e| ClusterError::Codec {
123            detail: format!("rkyv deserialize {}: {e}", $name),
124        })
125    }};
126}
127
128pub(super) fn encode_submit_calvin_txn_req(
129    msg: &SubmitCalvinTxnRequest,
130    out: &mut Vec<u8>,
131) -> Result<()> {
132    write_frame(RPC_SUBMIT_CALVIN_TXN_REQ, &to_bytes!(msg)?, out)
133}
134pub(super) fn encode_submit_calvin_txn_resp(
135    msg: &SubmitCalvinTxnResponse,
136    out: &mut Vec<u8>,
137) -> Result<()> {
138    write_frame(RPC_SUBMIT_CALVIN_TXN_RESP, &to_bytes!(msg)?, out)
139}
140
141pub(super) fn decode_submit_calvin_txn_req(payload: &[u8]) -> Result<RaftRpc> {
142    Ok(RaftRpc::SubmitCalvinTxnRequest(from_bytes!(
143        payload,
144        SubmitCalvinTxnRequest,
145        "SubmitCalvinTxnRequest"
146    )?))
147}
148pub(super) fn decode_submit_calvin_txn_resp(payload: &[u8]) -> Result<RaftRpc> {
149    Ok(RaftRpc::SubmitCalvinTxnResponse(from_bytes!(
150        payload,
151        SubmitCalvinTxnResponse,
152        "SubmitCalvinTxnResponse"
153    )?))
154}
155
156pub(super) fn encode_submit_calvin_inbox_req(
157    msg: &SubmitCalvinInboxRequest,
158    out: &mut Vec<u8>,
159) -> Result<()> {
160    write_frame(RPC_SUBMIT_CALVIN_INBOX_REQ, &to_bytes!(msg)?, out)
161}
162pub(super) fn encode_submit_calvin_inbox_resp(
163    msg: &SubmitCalvinInboxResponse,
164    out: &mut Vec<u8>,
165) -> Result<()> {
166    write_frame(RPC_SUBMIT_CALVIN_INBOX_RESP, &to_bytes!(msg)?, out)
167}
168
169pub(super) fn decode_submit_calvin_inbox_req(payload: &[u8]) -> Result<RaftRpc> {
170    Ok(RaftRpc::SubmitCalvinInboxRequest(from_bytes!(
171        payload,
172        SubmitCalvinInboxRequest,
173        "SubmitCalvinInboxRequest"
174    )?))
175}
176pub(super) fn decode_submit_calvin_inbox_resp(payload: &[u8]) -> Result<RaftRpc> {
177    Ok(RaftRpc::SubmitCalvinInboxResponse(from_bytes!(
178        payload,
179        SubmitCalvinInboxResponse,
180        "SubmitCalvinInboxResponse"
181    )?))
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    fn roundtrip_req(req: SubmitCalvinTxnRequest) -> SubmitCalvinTxnRequest {
189        let rpc = RaftRpc::SubmitCalvinTxnRequest(req);
190        let encoded = super::super::encode(&rpc).unwrap();
191        match super::super::decode(&encoded).unwrap() {
192            RaftRpc::SubmitCalvinTxnRequest(r) => r,
193            other => panic!("expected SubmitCalvinTxnRequest, got {other:?}"),
194        }
195    }
196
197    fn roundtrip_resp(resp: SubmitCalvinTxnResponse) -> SubmitCalvinTxnResponse {
198        let rpc = RaftRpc::SubmitCalvinTxnResponse(resp);
199        let encoded = super::super::encode(&rpc).unwrap();
200        match super::super::decode(&encoded).unwrap() {
201            RaftRpc::SubmitCalvinTxnResponse(r) => r,
202            other => panic!("expected SubmitCalvinTxnResponse, got {other:?}"),
203        }
204    }
205
206    #[test]
207    fn roundtrip_submit_calvin_txn_request() {
208        let req = SubmitCalvinTxnRequest {
209            tx_class_bytes: vec![0x01, 0x02, 0x03, 0x04],
210            deadline_remaining_ms: 30_000,
211            trace_id: [7u8; 16],
212        };
213        let decoded = roundtrip_req(req);
214        assert_eq!(decoded.tx_class_bytes, vec![0x01, 0x02, 0x03, 0x04]);
215        assert_eq!(decoded.deadline_remaining_ms, 30_000);
216        assert_eq!(decoded.trace_id, [7u8; 16]);
217    }
218
219    #[test]
220    fn roundtrip_submit_calvin_txn_request_empty() {
221        let req = SubmitCalvinTxnRequest {
222            tx_class_bytes: vec![],
223            deadline_remaining_ms: 0,
224            trace_id: [0u8; 16],
225        };
226        let decoded = roundtrip_req(req);
227        assert!(decoded.tx_class_bytes.is_empty());
228        assert_eq!(decoded.deadline_remaining_ms, 0);
229    }
230
231    #[test]
232    fn roundtrip_submit_calvin_txn_response_ok() {
233        let decoded = roundtrip_resp(SubmitCalvinTxnResponse {
234            error: None,
235            payload_bytes: Some(vec![0xAA, 0xBB]),
236        });
237        assert!(decoded.error.is_none());
238        assert_eq!(decoded.payload_bytes, Some(vec![0xAA, 0xBB]));
239    }
240
241    #[test]
242    fn roundtrip_submit_calvin_txn_response_error() {
243        let decoded = roundtrip_resp(SubmitCalvinTxnResponse {
244            error: Some(TypedClusterError::Internal {
245                code: 0,
246                message: "calvin-submit not configured".into(),
247            }),
248            payload_bytes: None,
249        });
250        match decoded.error {
251            Some(TypedClusterError::Internal { code, message }) => {
252                assert_eq!(code, 0);
253                assert!(message.contains("calvin-submit"));
254            }
255            other => panic!("expected Internal, got {other:?}"),
256        }
257    }
258
259    fn roundtrip_inbox_req(req: SubmitCalvinInboxRequest) -> SubmitCalvinInboxRequest {
260        let rpc = RaftRpc::SubmitCalvinInboxRequest(req);
261        let encoded = super::super::encode(&rpc).unwrap();
262        match super::super::decode(&encoded).unwrap() {
263            RaftRpc::SubmitCalvinInboxRequest(r) => r,
264            other => panic!("expected SubmitCalvinInboxRequest, got {other:?}"),
265        }
266    }
267
268    fn roundtrip_inbox_resp(resp: SubmitCalvinInboxResponse) -> SubmitCalvinInboxResponse {
269        let rpc = RaftRpc::SubmitCalvinInboxResponse(resp);
270        let encoded = super::super::encode(&rpc).unwrap();
271        match super::super::decode(&encoded).unwrap() {
272            RaftRpc::SubmitCalvinInboxResponse(r) => r,
273            other => panic!("expected SubmitCalvinInboxResponse, got {other:?}"),
274        }
275    }
276
277    #[test]
278    fn roundtrip_submit_calvin_inbox_request() {
279        let req = SubmitCalvinInboxRequest {
280            tx_class_bytes: vec![0x0a, 0x0b, 0x0c],
281            deadline_remaining_ms: 15_000,
282            trace_id: [3u8; 16],
283        };
284        let decoded = roundtrip_inbox_req(req);
285        assert_eq!(decoded.tx_class_bytes, vec![0x0a, 0x0b, 0x0c]);
286        assert_eq!(decoded.deadline_remaining_ms, 15_000);
287        assert_eq!(decoded.trace_id, [3u8; 16]);
288    }
289
290    #[test]
291    fn roundtrip_submit_calvin_inbox_response_ok() {
292        let decoded = roundtrip_inbox_resp(SubmitCalvinInboxResponse {
293            inbox_seq: 42,
294            epoch: 7,
295            position: 3,
296            participants: 5,
297            error: None,
298        });
299        assert_eq!(decoded.inbox_seq, 42);
300        assert_eq!(decoded.epoch, 7);
301        assert_eq!(decoded.position, 3);
302        assert_eq!(decoded.participants, 5);
303        assert!(decoded.error.is_none());
304    }
305
306    #[test]
307    fn roundtrip_submit_calvin_inbox_response_error() {
308        let decoded = roundtrip_inbox_resp(SubmitCalvinInboxResponse {
309            inbox_seq: 0,
310            epoch: 0,
311            position: 0,
312            participants: 0,
313            error: Some(TypedClusterError::Internal {
314                code: 0,
315                message: "calvin-inbox not configured".into(),
316            }),
317        });
318        assert_eq!(decoded.inbox_seq, 0);
319        assert_eq!(decoded.epoch, 0);
320        assert_eq!(decoded.position, 0);
321        assert_eq!(decoded.participants, 0);
322        match decoded.error {
323            Some(TypedClusterError::Internal { code, message }) => {
324                assert_eq!(code, 0);
325                assert!(message.contains("calvin-inbox"));
326            }
327            other => panic!("expected Internal, got {other:?}"),
328        }
329    }
330}