oxigeo_distributed/flight/
wire.rs1use crate::error::{DistributedError, Result};
22use crate::task::Task;
23use arrow::record_batch::RecordBatch;
24use arrow_ipc::reader::StreamReader;
25use arrow_ipc::writer::StreamWriter;
26use bytes::Bytes;
27use serde::{Deserialize, Serialize};
28
29pub const EXECUTE_TASK_ACTION: &str = "execute_task";
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ExecuteTaskRequest {
35 pub task: Task,
37 pub has_input: bool,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ExecuteTaskResponse {
44 pub success: bool,
46 pub error: Option<String>,
48 pub execution_time_ms: u64,
50 pub num_rows: usize,
52 pub has_output: bool,
54}
55
56pub fn encode_batch_ipc(batch: &RecordBatch) -> Result<Vec<u8>> {
58 let mut buffer: Vec<u8> = Vec::new();
59 {
60 let mut writer = StreamWriter::try_new(&mut buffer, batch.schema().as_ref())
61 .map_err(|e| DistributedError::arrow(format!("IPC writer init failed: {e}")))?;
62 writer
63 .write(batch)
64 .map_err(|e| DistributedError::arrow(format!("IPC write failed: {e}")))?;
65 writer
66 .finish()
67 .map_err(|e| DistributedError::arrow(format!("IPC finish failed: {e}")))?;
68 }
69 Ok(buffer)
70}
71
72pub fn decode_batch_ipc(bytes: &[u8]) -> Result<RecordBatch> {
74 let reader = StreamReader::try_new(std::io::Cursor::new(bytes), None)
75 .map_err(|e| DistributedError::arrow(format!("IPC reader init failed: {e}")))?;
76 let mut batch = None;
77 for item in reader {
78 let b = item.map_err(|e| DistributedError::arrow(format!("IPC read failed: {e}")))?;
79 if batch.is_none() {
81 batch = Some(b);
82 }
83 }
84 batch.ok_or_else(|| DistributedError::arrow("IPC stream carried no record batch"))
85}
86
87fn frame(header_json: Vec<u8>, batch: Option<&RecordBatch>) -> Result<Bytes> {
89 let header_len = u32::try_from(header_json.len())
90 .map_err(|_| DistributedError::flight_rpc("execute_task header too large"))?;
91
92 let mut out = Vec::with_capacity(4 + header_json.len());
93 out.extend_from_slice(&header_len.to_le_bytes());
94 out.extend_from_slice(&header_json);
95 if let Some(batch) = batch {
96 out.extend_from_slice(&encode_batch_ipc(batch)?);
97 }
98 Ok(Bytes::from(out))
99}
100
101fn unframe(bytes: &[u8]) -> Result<(&[u8], &[u8])> {
103 if bytes.len() < 4 {
104 return Err(DistributedError::flight_rpc(
105 "execute_task frame shorter than its length prefix",
106 ));
107 }
108 let header_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
109 let header_end = 4usize
110 .checked_add(header_len)
111 .filter(|&end| end <= bytes.len())
112 .ok_or_else(|| DistributedError::flight_rpc("execute_task header length out of range"))?;
113 Ok((&bytes[4..header_end], &bytes[header_end..]))
114}
115
116pub fn encode_execute_request(task: &Task, input: Option<&RecordBatch>) -> Result<Bytes> {
119 let header = ExecuteTaskRequest {
120 task: task.clone(),
121 has_input: input.is_some(),
122 };
123 let header_json = serde_json::to_vec(&header)
124 .map_err(|e| DistributedError::task_serialization(format!("encode request: {e}")))?;
125 frame(header_json, input)
126}
127
128pub fn decode_execute_request(bytes: &[u8]) -> Result<(Task, Option<RecordBatch>)> {
130 let (header_bytes, tail) = unframe(bytes)?;
131 let header: ExecuteTaskRequest = serde_json::from_slice(header_bytes)
132 .map_err(|e| DistributedError::task_serialization(format!("decode request: {e}")))?;
133 let input = if header.has_input {
134 Some(decode_batch_ipc(tail)?)
135 } else {
136 None
137 };
138 Ok((header.task, input))
139}
140
141pub fn encode_execute_response(
144 response: &ExecuteTaskResponse,
145 output: Option<&RecordBatch>,
146) -> Result<Bytes> {
147 let header_json = serde_json::to_vec(response)
148 .map_err(|e| DistributedError::task_serialization(format!("encode response: {e}")))?;
149 frame(header_json, output)
150}
151
152pub fn decode_execute_response(bytes: &[u8]) -> Result<(ExecuteTaskResponse, Option<RecordBatch>)> {
155 let (header_bytes, tail) = unframe(bytes)?;
156 let header: ExecuteTaskResponse = serde_json::from_slice(header_bytes)
157 .map_err(|e| DistributedError::task_serialization(format!("decode response: {e}")))?;
158 let output = if header.has_output {
159 Some(decode_batch_ipc(tail)?)
160 } else {
161 None
162 };
163 Ok((header, output))
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use crate::task::{PartitionId, TaskId, TaskOperation};
170 use arrow::array::Int32Array;
171 use arrow::datatypes::{DataType, Field, Schema};
172 use std::sync::Arc;
173
174 fn sample_batch() -> RecordBatch {
175 let schema = Arc::new(Schema::new(vec![Field::new(
176 "value",
177 DataType::Int32,
178 false,
179 )]));
180 RecordBatch::try_new(
181 schema,
182 vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5]))],
183 )
184 .expect("batch")
185 }
186
187 fn sample_task() -> Task {
188 Task::new(
189 TaskId(7),
190 PartitionId(2),
191 TaskOperation::Filter {
192 expression: "value > 2".to_string(),
193 },
194 )
195 }
196
197 #[test]
198 fn batch_ipc_round_trips() {
199 let batch = sample_batch();
200 let bytes = encode_batch_ipc(&batch).expect("encode");
201 let restored = decode_batch_ipc(&bytes).expect("decode");
202 assert_eq!(restored.num_rows(), 5);
203 assert_eq!(restored.num_columns(), 1);
204 }
205
206 #[test]
207 fn request_round_trips_with_input() {
208 let task = sample_task();
209 let batch = sample_batch();
210 let body = encode_execute_request(&task, Some(&batch)).expect("encode");
211 let (decoded_task, input) = decode_execute_request(&body).expect("decode");
212 assert_eq!(decoded_task.id, TaskId(7));
213 let input = input.expect("input present");
214 assert_eq!(input.num_rows(), 5);
215 }
216
217 #[test]
218 fn request_round_trips_without_input() {
219 let task = sample_task();
220 let body = encode_execute_request(&task, None).expect("encode");
221 let (decoded_task, input) = decode_execute_request(&body).expect("decode");
222 assert_eq!(decoded_task.partition_id, PartitionId(2));
223 assert!(input.is_none());
224 }
225
226 #[test]
227 fn response_round_trips_with_output() {
228 let batch = sample_batch();
229 let resp = ExecuteTaskResponse {
230 success: true,
231 error: None,
232 execution_time_ms: 42,
233 num_rows: 5,
234 has_output: true,
235 };
236 let body = encode_execute_response(&resp, Some(&batch)).expect("encode");
237 let (decoded, output) = decode_execute_response(&body).expect("decode");
238 assert!(decoded.success);
239 assert_eq!(decoded.execution_time_ms, 42);
240 assert_eq!(output.expect("output").num_rows(), 5);
241 }
242
243 #[test]
244 fn unframe_rejects_truncated_frame() {
245 assert!(unframe(&[0, 1]).is_err());
246 assert!(unframe(&[255, 255, 255, 255, 0]).is_err());
248 }
249}