Skip to main content

oxigdal_distributed/flight/
server.rs

1//! Arrow Flight server implementation for distributed data transfer.
2//!
3//! This module implements an Arrow Flight server that streams geospatial data
4//! between nodes using zero-copy transfers.
5
6use crate::error::{DistributedError, Result};
7use arrow::record_batch::RecordBatch;
8use arrow_flight::{
9    Action, ActionType, Criteria, Empty, FlightData, FlightDescriptor, FlightEndpoint, FlightInfo,
10    HandshakeRequest, HandshakeResponse, PutResult, SchemaAsIpc, SchemaResult, Ticket,
11    flight_service_server::{FlightService, FlightServiceServer},
12};
13use arrow_ipc::writer::IpcWriteOptions;
14use bytes::Bytes;
15use futures::{Stream, StreamExt, stream};
16use std::collections::HashMap;
17use std::pin::Pin;
18use std::sync::{Arc, RwLock};
19use tonic::{Request, Response, Streaming};
20use tracing::{debug, info};
21
22/// Flight server for serving geospatial data.
23pub struct FlightServer {
24    /// Stored data partitions (ticket -> RecordBatch).
25    data_store: Arc<RwLock<HashMap<String, Arc<RecordBatch>>>>,
26    /// Authentication tokens.
27    auth_tokens: Arc<RwLock<HashMap<String, String>>>,
28    /// Enable authentication.
29    enable_auth: bool,
30}
31
32impl FlightServer {
33    /// Create a new Flight server.
34    pub fn new() -> Self {
35        Self {
36            data_store: Arc::new(RwLock::new(HashMap::new())),
37            auth_tokens: Arc::new(RwLock::new(HashMap::new())),
38            enable_auth: false,
39        }
40    }
41
42    /// Enable authentication.
43    pub fn with_auth(mut self) -> Self {
44        self.enable_auth = true;
45        self
46    }
47
48    /// Store data with a ticket.
49    pub fn store_data(&self, ticket: String, data: Arc<RecordBatch>) -> Result<()> {
50        let mut store = self
51            .data_store
52            .write()
53            .map_err(|_| DistributedError::flight_rpc("Failed to acquire data store lock"))?;
54
55        store.insert(ticket, data);
56        Ok(())
57    }
58
59    /// Retrieve data by ticket.
60    pub fn get_data(&self, ticket: &str) -> Result<Option<Arc<RecordBatch>>> {
61        let store = self
62            .data_store
63            .read()
64            .map_err(|_| DistributedError::flight_rpc("Failed to acquire data store lock"))?;
65
66        Ok(store.get(ticket).cloned())
67    }
68
69    /// Remove data by ticket.
70    pub fn remove_data(&self, ticket: &str) -> Result<Option<Arc<RecordBatch>>> {
71        let mut store = self
72            .data_store
73            .write()
74            .map_err(|_| DistributedError::flight_rpc("Failed to acquire data store lock"))?;
75
76        Ok(store.remove(ticket))
77    }
78
79    /// List all available tickets.
80    pub fn list_tickets(&self) -> Result<Vec<String>> {
81        let store = self
82            .data_store
83            .read()
84            .map_err(|_| DistributedError::flight_rpc("Failed to acquire data store lock"))?;
85
86        Ok(store.keys().cloned().collect())
87    }
88
89    /// Add authentication token.
90    pub fn add_auth_token(&self, token: String, user: String) -> Result<()> {
91        let mut tokens = self
92            .auth_tokens
93            .write()
94            .map_err(|_| DistributedError::authentication("Failed to acquire auth tokens lock"))?;
95
96        tokens.insert(token, user);
97        Ok(())
98    }
99
100    /// Convert to tonic service.
101    pub fn into_service(self) -> FlightServiceServer<Self> {
102        FlightServiceServer::new(self)
103    }
104}
105
106impl Default for FlightServer {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112#[tonic::async_trait]
113impl FlightService for FlightServer {
114    type HandshakeStream =
115        Pin<Box<dyn Stream<Item = std::result::Result<HandshakeResponse, tonic::Status>> + Send>>;
116    type ListFlightsStream =
117        Pin<Box<dyn Stream<Item = std::result::Result<FlightInfo, tonic::Status>> + Send>>;
118    type DoGetStream =
119        Pin<Box<dyn Stream<Item = std::result::Result<FlightData, tonic::Status>> + Send>>;
120    type DoPutStream =
121        Pin<Box<dyn Stream<Item = std::result::Result<PutResult, tonic::Status>> + Send>>;
122    type DoActionStream = Pin<
123        Box<dyn Stream<Item = std::result::Result<arrow_flight::Result, tonic::Status>> + Send>,
124    >;
125    type ListActionsStream =
126        Pin<Box<dyn Stream<Item = std::result::Result<ActionType, tonic::Status>> + Send>>;
127    type DoExchangeStream =
128        Pin<Box<dyn Stream<Item = std::result::Result<FlightData, tonic::Status>> + Send>>;
129
130    async fn handshake(
131        &self,
132        _request: Request<Streaming<HandshakeRequest>>,
133    ) -> std::result::Result<Response<Self::HandshakeStream>, tonic::Status> {
134        debug!("Handshake request received");
135
136        // Simple handshake - just acknowledge
137        let response = HandshakeResponse {
138            protocol_version: 0,
139            payload: Bytes::new(),
140        };
141
142        let stream = stream::once(async { Ok(response) });
143        Ok(Response::new(Box::pin(stream)))
144    }
145
146    async fn list_flights(
147        &self,
148        _request: Request<Criteria>,
149    ) -> std::result::Result<Response<Self::ListFlightsStream>, tonic::Status> {
150        debug!("List flights request received");
151
152        // Return empty stream - we don't support flight listing yet
153        let stream = stream::empty();
154        Ok(Response::new(Box::pin(stream)))
155    }
156
157    async fn get_flight_info(
158        &self,
159        request: Request<FlightDescriptor>,
160    ) -> std::result::Result<Response<FlightInfo>, tonic::Status> {
161        let descriptor = request.into_inner();
162        debug!("Get flight info request: {:?}", descriptor);
163
164        // Resolve ticket key from descriptor: prefer first path segment, fall back to cmd bytes.
165        let ticket_key = if !descriptor.path.is_empty() {
166            descriptor.path[0].clone()
167        } else if !descriptor.cmd.is_empty() {
168            String::from_utf8(descriptor.cmd.to_vec())
169                .map_err(|e| tonic::Status::invalid_argument(format!("Invalid cmd: {}", e)))?
170        } else {
171            return Err(tonic::Status::invalid_argument(
172                "FlightDescriptor must have a path or cmd",
173            ));
174        };
175
176        let data = self
177            .get_data(&ticket_key)
178            .map_err(|e| tonic::Status::internal(e.to_string()))?
179            .ok_or_else(|| tonic::Status::not_found(format!("Flight not found: {}", ticket_key)))?;
180
181        let schema = data.schema();
182        let ipc_opts = IpcWriteOptions::default();
183        let schema_bytes: arrow_flight::IpcMessage = SchemaAsIpc::new(schema.as_ref(), &ipc_opts)
184            .try_into()
185            .map_err(|e: arrow_schema::ArrowError| {
186                tonic::Status::internal(format!("Schema encode error: {}", e))
187            })?;
188
189        let endpoint = FlightEndpoint {
190            ticket: Some(Ticket {
191                ticket: Bytes::from(ticket_key),
192            }),
193            location: vec![],
194            expiration_time: None,
195            app_metadata: Bytes::new(),
196        };
197
198        let flight_info = FlightInfo {
199            schema: schema_bytes.0,
200            flight_descriptor: Some(descriptor),
201            endpoint: vec![endpoint],
202            total_records: data.num_rows() as i64,
203            total_bytes: -1,
204            ordered: false,
205            app_metadata: Bytes::new(),
206        };
207
208        Ok(Response::new(flight_info))
209    }
210
211    async fn get_schema(
212        &self,
213        request: Request<FlightDescriptor>,
214    ) -> std::result::Result<Response<SchemaResult>, tonic::Status> {
215        let descriptor = request.into_inner();
216        debug!("Get schema request received");
217
218        let ticket_key = if !descriptor.path.is_empty() {
219            descriptor.path[0].clone()
220        } else if !descriptor.cmd.is_empty() {
221            String::from_utf8(descriptor.cmd.to_vec())
222                .map_err(|e| tonic::Status::invalid_argument(format!("Invalid cmd: {}", e)))?
223        } else {
224            return Err(tonic::Status::invalid_argument(
225                "FlightDescriptor must have a path or cmd",
226            ));
227        };
228
229        let data = self
230            .get_data(&ticket_key)
231            .map_err(|e| tonic::Status::internal(e.to_string()))?
232            .ok_or_else(|| tonic::Status::not_found(format!("Flight not found: {}", ticket_key)))?;
233
234        let schema = data.schema();
235        let ipc_opts = IpcWriteOptions::default();
236        let schema_result: SchemaResult = SchemaAsIpc::new(schema.as_ref(), &ipc_opts)
237            .try_into()
238            .map_err(|e: arrow_schema::ArrowError| {
239            tonic::Status::internal(format!("Schema encode error: {}", e))
240        })?;
241
242        Ok(Response::new(schema_result))
243    }
244
245    async fn do_get(
246        &self,
247        request: Request<Ticket>,
248    ) -> std::result::Result<Response<Self::DoGetStream>, tonic::Status> {
249        let ticket = request.into_inner();
250        let ticket_str = String::from_utf8(ticket.ticket.to_vec())
251            .map_err(|e| tonic::Status::invalid_argument(format!("Invalid ticket: {}", e)))?;
252
253        info!("DoGet request for ticket: {}", ticket_str);
254
255        // Retrieve data
256        let data = self
257            .get_data(&ticket_str)
258            .map_err(|e| tonic::Status::internal(e.to_string()))?
259            .ok_or_else(|| tonic::Status::not_found(format!("Ticket not found: {}", ticket_str)))?;
260
261        // Convert RecordBatch to FlightData stream
262        let flight_data_vec = arrow_flight::utils::batches_to_flight_data(
263            data.schema().as_ref(),
264            vec![(*data).clone()],
265        )
266        .map_err(|e| tonic::Status::internal(format!("Failed to encode batches: {}", e)))?
267        .into_iter()
268        .map(Ok)
269        .collect::<Vec<_>>();
270
271        let stream = stream::iter(flight_data_vec);
272        Ok(Response::new(Box::pin(stream)))
273    }
274
275    async fn do_put(
276        &self,
277        request: Request<Streaming<FlightData>>,
278    ) -> std::result::Result<Response<Self::DoPutStream>, tonic::Status> {
279        debug!("DoPut request received");
280
281        let mut stream = request.into_inner();
282        let mut flight_data_vec = Vec::new();
283
284        // Collect all FlightData messages
285        while let Some(data_result) = stream.next().await {
286            flight_data_vec.push(data_result?);
287        }
288
289        // Convert FlightData to RecordBatches
290        let batches = arrow_flight::utils::flight_data_to_batches(&flight_data_vec)
291            .map_err(|e| tonic::Status::internal(format!("Failed to decode batches: {}", e)))?;
292
293        info!("DoPut received {} batches", batches.len());
294
295        // Store batches (using a generated ticket)
296        for (i, batch) in batches.into_iter().enumerate() {
297            let ticket = format!("uploaded_{}", i);
298            self.store_data(ticket, Arc::new(batch))
299                .map_err(|e| tonic::Status::internal(e.to_string()))?;
300        }
301
302        // Return success
303        let result = PutResult {
304            app_metadata: Bytes::new(),
305        };
306
307        let stream = stream::once(async { Ok(result) });
308        Ok(Response::new(Box::pin(stream)))
309    }
310
311    async fn do_action(
312        &self,
313        request: Request<Action>,
314    ) -> std::result::Result<Response<Self::DoActionStream>, tonic::Status> {
315        let action = request.into_inner();
316        info!("DoAction request: {}", action.r#type);
317
318        match action.r#type.as_str() {
319            "list_tickets" => {
320                let tickets = self
321                    .list_tickets()
322                    .map_err(|e| tonic::Status::internal(e.to_string()))?;
323
324                let result = arrow_flight::Result {
325                    body: serde_json::to_vec(&tickets)
326                        .map_err(|e| {
327                            tonic::Status::internal(format!("Serialization error: {}", e))
328                        })?
329                        .into(),
330                };
331
332                let stream = stream::once(async { Ok(result) });
333                Ok(Response::new(Box::pin(stream)))
334            }
335            "remove_ticket" => {
336                let ticket = String::from_utf8(action.body.to_vec()).map_err(|e| {
337                    tonic::Status::invalid_argument(format!("Invalid ticket: {}", e))
338                })?;
339
340                self.remove_data(&ticket)
341                    .map_err(|e| tonic::Status::internal(e.to_string()))?;
342
343                let result = arrow_flight::Result {
344                    body: Bytes::from("removed"),
345                };
346
347                let stream = stream::once(async { Ok(result) });
348                Ok(Response::new(Box::pin(stream)))
349            }
350            "list_actions" => {
351                // Reflection action: return JSON array of supported action descriptors.
352                let actions = vec![
353                    serde_json::json!({"type": "list_tickets", "description": "List all available tickets"}),
354                    serde_json::json!({"type": "remove_ticket", "description": "Remove a ticket from the server"}),
355                    serde_json::json!({"type": "list_actions", "description": "List all supported actions (reflection)"}),
356                    serde_json::json!({"type": "ping", "description": "Health check — returns 'pong'"}),
357                ];
358
359                let result = arrow_flight::Result {
360                    body: serde_json::to_vec(&actions)
361                        .map_err(|e| {
362                            tonic::Status::internal(format!("Serialization error: {}", e))
363                        })?
364                        .into(),
365                };
366
367                let stream = stream::once(async { Ok(result) });
368                Ok(Response::new(Box::pin(stream)))
369            }
370            "ping" => {
371                let result = arrow_flight::Result {
372                    body: Bytes::from_static(b"pong"),
373                };
374                let stream = stream::once(async { Ok(result) });
375                Ok(Response::new(Box::pin(stream)))
376            }
377            _ => Err(tonic::Status::unimplemented(format!(
378                "Action not implemented: {}",
379                action.r#type
380            ))),
381        }
382    }
383
384    async fn list_actions(
385        &self,
386        _request: Request<Empty>,
387    ) -> std::result::Result<Response<Self::ListActionsStream>, tonic::Status> {
388        debug!("List actions request received");
389
390        let actions = vec![
391            ActionType {
392                r#type: "list_tickets".to_string(),
393                description: "List all available tickets".to_string(),
394            },
395            ActionType {
396                r#type: "remove_ticket".to_string(),
397                description: "Remove a ticket from the server".to_string(),
398            },
399        ];
400
401        let stream = stream::iter(actions.into_iter().map(Ok));
402        Ok(Response::new(Box::pin(stream)))
403    }
404
405    async fn do_exchange(
406        &self,
407        request: Request<Streaming<FlightData>>,
408    ) -> std::result::Result<Response<Self::DoExchangeStream>, tonic::Status> {
409        debug!("DoExchange request received — echo/passthrough mode");
410
411        let mut incoming = request.into_inner();
412        let mut echo_items: Vec<std::result::Result<FlightData, tonic::Status>> = Vec::new();
413
414        while let Some(item) = incoming.next().await {
415            match item {
416                Ok(flight_data) => {
417                    echo_items.push(Ok(flight_data));
418                }
419                Err(status) => {
420                    // Propagate the first error as the terminal item.
421                    echo_items.push(Err(status));
422                    break;
423                }
424            }
425        }
426
427        info!("DoExchange echoing {} items", echo_items.len());
428        let stream = stream::iter(echo_items);
429        Ok(Response::new(Box::pin(stream)))
430    }
431
432    async fn poll_flight_info(
433        &self,
434        request: Request<FlightDescriptor>,
435    ) -> std::result::Result<Response<arrow_flight::PollInfo>, tonic::Status> {
436        let descriptor = request.into_inner();
437        debug!("Poll flight info request received");
438
439        // Resolve the ticket key from the descriptor (same logic as get_flight_info).
440        let ticket_key = if !descriptor.path.is_empty() {
441            descriptor.path[0].clone()
442        } else if !descriptor.cmd.is_empty() {
443            String::from_utf8(descriptor.cmd.to_vec())
444                .map_err(|e| tonic::Status::invalid_argument(format!("Invalid cmd: {}", e)))?
445        } else {
446            return Err(tonic::Status::invalid_argument(
447                "FlightDescriptor must have a path or cmd",
448            ));
449        };
450
451        let data_opt = self
452            .get_data(&ticket_key)
453            .map_err(|e| tonic::Status::internal(e.to_string()))?;
454
455        // If data is ready, return complete FlightInfo (no pending descriptor, progress = 1.0).
456        // If data is still pending, echo the descriptor back (client should retry), progress = None.
457        if let Some(data) = data_opt {
458            let schema = data.schema();
459            let ipc_opts = IpcWriteOptions::default();
460            let schema_bytes: arrow_flight::IpcMessage =
461                SchemaAsIpc::new(schema.as_ref(), &ipc_opts)
462                    .try_into()
463                    .map_err(|e: arrow_schema::ArrowError| {
464                        tonic::Status::internal(format!("Schema encode error: {}", e))
465                    })?;
466
467            let endpoint = FlightEndpoint {
468                ticket: Some(Ticket {
469                    ticket: Bytes::from(ticket_key),
470                }),
471                location: vec![],
472                expiration_time: None,
473                app_metadata: Bytes::new(),
474            };
475
476            let flight_info = FlightInfo {
477                schema: schema_bytes.0,
478                flight_descriptor: Some(descriptor),
479                endpoint: vec![endpoint],
480                total_records: data.num_rows() as i64,
481                total_bytes: -1,
482                ordered: false,
483                app_metadata: Bytes::new(),
484            };
485
486            Ok(Response::new(arrow_flight::PollInfo {
487                info: Some(flight_info),
488                // flight_descriptor is None -> indicates the query is complete.
489                flight_descriptor: None,
490                progress: Some(1.0),
491                expiration_time: None,
492            }))
493        } else {
494            // Data not yet ready; client should poll again using the same descriptor.
495            Ok(Response::new(arrow_flight::PollInfo {
496                info: None,
497                flight_descriptor: Some(descriptor),
498                progress: None,
499                expiration_time: None,
500            }))
501        }
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508    use arrow::array::Int32Array;
509    use arrow::datatypes::{DataType, Field, Schema};
510
511    fn create_test_batch() -> std::result::Result<Arc<RecordBatch>, Box<dyn std::error::Error>> {
512        let schema = Arc::new(Schema::new(vec![Field::new(
513            "value",
514            DataType::Int32,
515            false,
516        )]));
517
518        let array = Int32Array::from(vec![1, 2, 3, 4, 5]);
519
520        Ok(Arc::new(RecordBatch::try_new(
521            schema,
522            vec![Arc::new(array)],
523        )?))
524    }
525
526    #[test]
527    fn test_server_creation() {
528        let server = FlightServer::new();
529        assert!(!server.enable_auth);
530    }
531
532    #[test]
533    fn test_store_and_retrieve_data() -> std::result::Result<(), Box<dyn std::error::Error>> {
534        let server = FlightServer::new();
535        let batch = create_test_batch()?;
536
537        server.store_data("test_ticket".to_string(), batch.clone())?;
538
539        let retrieved = server
540            .get_data("test_ticket")?
541            .ok_or_else(|| Box::<dyn std::error::Error>::from("should exist"))?;
542
543        assert_eq!(retrieved.num_rows(), batch.num_rows());
544        Ok(())
545    }
546
547    #[test]
548    fn test_remove_data() -> std::result::Result<(), Box<dyn std::error::Error>> {
549        let server = FlightServer::new();
550        let batch = create_test_batch()?;
551
552        server.store_data("test_ticket".to_string(), batch)?;
553
554        let removed = server
555            .remove_data("test_ticket")?
556            .ok_or_else(|| Box::<dyn std::error::Error>::from("should exist"))?;
557
558        assert_eq!(removed.num_rows(), 5);
559
560        let retrieved = server.get_data("test_ticket")?;
561        assert!(retrieved.is_none());
562        Ok(())
563    }
564
565    #[test]
566    fn test_list_tickets() -> std::result::Result<(), Box<dyn std::error::Error>> {
567        let server = FlightServer::new();
568
569        server.store_data("ticket1".to_string(), create_test_batch()?)?;
570        server.store_data("ticket2".to_string(), create_test_batch()?)?;
571
572        let tickets = server.list_tickets()?;
573        assert_eq!(tickets.len(), 2);
574        assert!(tickets.contains(&"ticket1".to_string()));
575        assert!(tickets.contains(&"ticket2".to_string()));
576        Ok(())
577    }
578
579    #[test]
580    fn test_authentication() -> std::result::Result<(), Box<dyn std::error::Error>> {
581        let server = FlightServer::new().with_auth();
582        assert!(server.enable_auth);
583
584        server.add_auth_token("token123".to_string(), "user1".to_string())?;
585
586        // Verify token exists via auth_tokens (verify_token method not exposed)
587        assert!(
588            server
589                .auth_tokens
590                .read()
591                .map_err(|e| Box::<dyn std::error::Error>::from(format!("lock poisoned: {}", e)))?
592                .contains_key("token123")
593        );
594        assert!(
595            !server
596                .auth_tokens
597                .read()
598                .map_err(|e| Box::<dyn std::error::Error>::from(format!("lock poisoned: {}", e)))?
599                .contains_key("invalid")
600        );
601        Ok(())
602    }
603}