Skip to main content

oxigeo_distributed/flight/
client.rs

1//! Arrow Flight client implementation for distributed data transfer.
2//!
3//! This module implements an Arrow Flight client for fetching and sending
4//! geospatial data using zero-copy transfers.
5
6use crate::error::{DistributedError, Result};
7use crate::flight::wire::{self, ExecuteTaskResponse};
8use crate::task::Task;
9use arrow::record_batch::RecordBatch;
10use arrow_flight::{Action, HandshakeRequest, Ticket, flight_service_client::FlightServiceClient};
11use bytes::Bytes;
12use futures::StreamExt;
13use std::time::Duration;
14use tonic::transport::{Channel, Endpoint};
15use tracing::{debug, info, warn};
16
17/// Flight client for fetching geospatial data.
18pub struct FlightClient {
19    /// gRPC client.
20    client: FlightServiceClient<Channel>,
21    /// Server address.
22    address: String,
23}
24
25impl FlightClient {
26    /// Create a new Flight client.
27    pub async fn new(address: String) -> Result<Self> {
28        info!("Connecting to Flight server at {}", address);
29
30        let endpoint = Endpoint::from_shared(address.clone())
31            .map_err(|e| DistributedError::worker_connection(format!("Invalid endpoint: {}", e)))?
32            .connect_timeout(Duration::from_secs(10))
33            .timeout(Duration::from_secs(60))
34            .tcp_keepalive(Some(Duration::from_secs(30)))
35            .http2_keep_alive_interval(Duration::from_secs(30))
36            .keep_alive_timeout(Duration::from_secs(10));
37
38        let channel = endpoint.connect().await.map_err(|e| {
39            DistributedError::worker_connection(format!("Connection failed: {}", e))
40        })?;
41
42        let client = FlightServiceClient::new(channel);
43
44        Ok(Self { client, address })
45    }
46
47    /// Perform handshake with the server.
48    pub async fn handshake(&mut self) -> Result<()> {
49        debug!("Performing handshake with {}", self.address);
50
51        let request = tonic::Request::new(futures::stream::once(async {
52            HandshakeRequest {
53                protocol_version: 0,
54                payload: Bytes::new(),
55            }
56        }));
57
58        let mut response_stream = self
59            .client
60            .handshake(request)
61            .await
62            .map_err(|e| DistributedError::flight_rpc(format!("Handshake failed: {}", e)))?
63            .into_inner();
64
65        // Read handshake response
66        while let Some(response) = response_stream.next().await {
67            let _handshake_response = response
68                .map_err(|e| DistributedError::flight_rpc(format!("Handshake error: {}", e)))?;
69            debug!("Handshake successful");
70        }
71
72        Ok(())
73    }
74
75    /// Fetch data from the server using a ticket.
76    pub async fn get_data(&mut self, ticket: String) -> Result<Vec<RecordBatch>> {
77        info!("Fetching data for ticket: {}", ticket);
78
79        let ticket = Ticket {
80            ticket: Bytes::from(ticket),
81        };
82
83        let request = tonic::Request::new(ticket);
84
85        let mut stream = self
86            .client
87            .do_get(request)
88            .await
89            .map_err(|e| DistributedError::flight_rpc(format!("DoGet failed: {}", e)))?
90            .into_inner();
91
92        let mut flight_data_vec = Vec::new();
93
94        while let Some(data_result) = stream.next().await {
95            flight_data_vec.push(
96                data_result
97                    .map_err(|e| DistributedError::flight_rpc(format!("Stream error: {}", e)))?,
98            );
99        }
100
101        // Convert FlightData to RecordBatches
102        let batches = arrow_flight::utils::flight_data_to_batches(&flight_data_vec)
103            .map_err(|e| DistributedError::arrow(format!("Failed to decode batches: {}", e)))?;
104
105        info!("Received {} batches", batches.len());
106        Ok(batches)
107    }
108
109    /// Send data to the server.
110    pub async fn put_data(&mut self, batches: Vec<RecordBatch>) -> Result<()> {
111        info!("Sending {} batches to server", batches.len());
112
113        if batches.is_empty() {
114            return Err(DistributedError::flight_rpc("No batches to send"));
115        }
116
117        // Convert batches to FlightData
118        let flight_data_vec =
119            arrow_flight::utils::batches_to_flight_data(batches[0].schema().as_ref(), batches)
120                .map_err(|e| DistributedError::arrow(format!("Failed to encode batches: {}", e)))?;
121
122        let request = tonic::Request::new(futures::stream::iter(flight_data_vec));
123
124        let mut response_stream = self
125            .client
126            .do_put(request)
127            .await
128            .map_err(|e| DistributedError::flight_rpc(format!("DoPut failed: {}", e)))?
129            .into_inner();
130
131        // Read put results
132        while let Some(result) = response_stream.next().await {
133            let _put_result =
134                result.map_err(|e| DistributedError::flight_rpc(format!("Put error: {}", e)))?;
135        }
136
137        info!("Data sent successfully");
138        Ok(())
139    }
140
141    /// Execute an action on the server.
142    pub async fn do_action(&mut self, action_type: String, body: Bytes) -> Result<Vec<Bytes>> {
143        debug!("Executing action: {}", action_type);
144
145        let action = Action {
146            r#type: action_type.clone(),
147            body,
148        };
149
150        let request = tonic::Request::new(action);
151
152        let mut stream = self
153            .client
154            .do_action(request)
155            .await
156            .map_err(|e| DistributedError::flight_rpc(format!("DoAction failed: {}", e)))?
157            .into_inner();
158
159        let mut results = Vec::new();
160
161        while let Some(result) = stream.next().await {
162            let action_result =
163                result.map_err(|e| DistributedError::flight_rpc(format!("Action error: {}", e)))?;
164            results.push(action_result.body);
165        }
166
167        debug!(
168            "Action {} completed with {} results",
169            action_type,
170            results.len()
171        );
172        Ok(results)
173    }
174
175    /// Dispatch a task (with its input partition) to the remote worker's Flight
176    /// server and await the executed result.
177    ///
178    /// This is the client half of the coordinator → Flight → worker path: it
179    /// ships the serialized `Task` plus its `input` batch through the
180    /// `execute_task` action and returns the worker's response metadata together
181    /// with the output batch (present on success).
182    pub async fn execute_task(
183        &mut self,
184        task: &Task,
185        input: Option<&RecordBatch>,
186    ) -> Result<(ExecuteTaskResponse, Option<RecordBatch>)> {
187        let body = wire::encode_execute_request(task, input)?;
188        let results = self
189            .do_action(wire::EXECUTE_TASK_ACTION.to_string(), body)
190            .await?;
191
192        let payload = results.first().ok_or_else(|| {
193            DistributedError::flight_rpc("execute_task returned no result payload")
194        })?;
195
196        wire::decode_execute_response(payload)
197    }
198
199    /// List all available tickets.
200    pub async fn list_tickets(&mut self) -> Result<Vec<String>> {
201        let results = self
202            .do_action("list_tickets".to_string(), Bytes::new())
203            .await?;
204
205        if results.is_empty() {
206            return Ok(Vec::new());
207        }
208
209        let tickets: Vec<String> = serde_json::from_slice(&results[0]).map_err(|e| {
210            DistributedError::flight_rpc(format!("Failed to deserialize tickets: {}", e))
211        })?;
212
213        Ok(tickets)
214    }
215
216    /// Remove a ticket from the server.
217    pub async fn remove_ticket(&mut self, ticket: String) -> Result<()> {
218        let body = Bytes::from(ticket.clone());
219        let _results = self.do_action("remove_ticket".to_string(), body).await?;
220
221        info!("Removed ticket: {}", ticket);
222        Ok(())
223    }
224
225    /// Get the server address.
226    pub fn address(&self) -> &str {
227        &self.address
228    }
229
230    /// Check if the client is connected.
231    pub async fn health_check(&mut self) -> Result<bool> {
232        match self.handshake().await {
233            Ok(_) => Ok(true),
234            Err(e) => {
235                warn!("Health check failed: {}", e);
236                Ok(false)
237            }
238        }
239    }
240}
241
242/// Connection pool for managing multiple Flight clients.
243pub struct FlightClientPool {
244    /// Available clients.
245    clients: Vec<FlightClient>,
246    /// Maximum pool size.
247    max_size: usize,
248}
249
250impl FlightClientPool {
251    /// Create a new client pool.
252    pub fn new(max_size: usize) -> Self {
253        Self {
254            clients: Vec::new(),
255            max_size,
256        }
257    }
258
259    /// Add a client to the pool.
260    pub async fn add_client(&mut self, address: String) -> Result<()> {
261        if self.clients.len() >= self.max_size {
262            return Err(DistributedError::worker_connection(
263                "Pool is at maximum capacity",
264            ));
265        }
266
267        let client = FlightClient::new(address).await?;
268        self.clients.push(client);
269        Ok(())
270    }
271
272    /// Get a client from the pool (round-robin).
273    pub fn get_client(&mut self) -> Result<&mut FlightClient> {
274        if self.clients.is_empty() {
275            return Err(DistributedError::worker_connection("No clients available"));
276        }
277
278        // Simple round-robin: rotate the first client to the back
279        self.clients.rotate_left(1);
280        let idx = self.clients.len() - 1;
281        Ok(&mut self.clients[idx])
282    }
283
284    /// Get the number of clients in the pool.
285    pub fn size(&self) -> usize {
286        self.clients.len()
287    }
288
289    /// Check health of all clients.
290    pub async fn health_check_all(&mut self) -> Result<Vec<bool>> {
291        let mut results = Vec::new();
292
293        for client in &mut self.clients {
294            let is_healthy = client.health_check().await.unwrap_or(false);
295            results.push(is_healthy);
296        }
297
298        Ok(results)
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_client_pool() {
308        let pool = FlightClientPool::new(5);
309        assert_eq!(pool.size(), 0);
310        assert_eq!(pool.max_size, 5);
311    }
312
313    #[tokio::test]
314    async fn test_client_creation_fails_for_invalid_address() {
315        let result = FlightClient::new("invalid://address".to_string()).await;
316        assert!(result.is_err());
317    }
318
319    #[test]
320    fn test_pool_get_client_empty() {
321        let mut pool = FlightClientPool::new(5);
322        let result = pool.get_client();
323        assert!(result.is_err());
324    }
325}