1use super::*;
21use std::time::Duration;
22use std::sync::atomic::{AtomicU64, Ordering};
23
24pub struct RiGrpcClient {
25 channel: Option<tonic::transport::Channel>,
26 endpoint: String,
27 timeout: Duration,
28 stats: Arc<RwLock<RiGrpcStats>>,
29 request_id: Arc<AtomicU64>,
30 connected: Arc<RwLock<bool>>,
31 retry_count: u32,
32 retry_delay: Duration,
33}
34
35#[cfg(feature = "pyo3")]
36#[pyclass]
37pub struct RiGrpcClientPy {
38 inner: RiGrpcClient,
39}
40
41#[cfg(feature = "pyo3")]
42#[pymethods]
43impl RiGrpcClientPy {
44 #[new]
45 fn new(endpoint: String) -> Self {
46 Self {
47 inner: RiGrpcClient::new(endpoint),
48 }
49 }
50
51 #[pyo3(signature = (timeout_secs=30))]
52 fn with_timeout(&mut self, timeout_secs: u64) {
53 self.inner.timeout = Duration::from_secs(timeout_secs);
54 }
55
56 #[pyo3(signature = (count=3, delay_ms=100))]
57 fn with_retry(&mut self, count: u32, delay_ms: u64) {
58 self.inner.retry_count = count;
59 self.inner.retry_delay = Duration::from_millis(delay_ms);
60 }
61
62 fn get_stats(&self) -> RiGrpcStats {
63 self.inner.get_stats()
64 }
65
66 fn is_connected(&self) -> bool {
67 self.inner.channel.is_some()
68 }
69
70 fn get_endpoint(&self) -> String {
71 self.inner.endpoint.clone()
72 }
73
74 fn connect(&mut self) -> PyResult<()> {
75 let rt = crate::py_runtime::py_runtime();
76
77 rt.block_on(async {
78 self.inner.connect().await
79 }).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
80 }
81
82 fn disconnect(&mut self) {
83 let rt = crate::py_runtime::py_runtime();
84 rt.block_on(async {
85 self.inner.disconnect().await
86 });
87 }
88
89 #[pyo3(signature = (service_name, method, data))]
90 fn call(&mut self, service_name: String, method: String, data: Vec<u8>) -> PyResult<Vec<u8>> {
91 let rt = crate::py_runtime::py_runtime();
92
93 rt.block_on(async {
94 self.inner.call(&service_name, &method, &data).await
95 }).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
96 }
97}
98
99impl RiGrpcClient {
100 pub fn new(endpoint: String) -> Self {
101 Self {
102 channel: None,
103 endpoint,
104 timeout: Duration::from_secs(30),
105 stats: Arc::new(RwLock::new(RiGrpcStats::new())),
106 request_id: Arc::new(AtomicU64::new(0)),
107 connected: Arc::new(RwLock::new(false)),
108 retry_count: 3,
109 retry_delay: Duration::from_millis(100),
110 }
111 }
112
113 pub fn with_timeout(mut self, timeout: Duration) -> Self {
114 self.timeout = timeout;
115 self
116 }
117
118 pub fn with_retry(mut self, count: u32, delay: Duration) -> Self {
119 self.retry_count = count;
120 self.retry_delay = delay;
121 self
122 }
123
124 pub fn get_stats(&self) -> RiGrpcStats {
125 self.stats.try_read()
126 .map(|guard| guard.clone())
127 .unwrap_or_else(|_| RiGrpcStats::new())
128 }
129
130 pub async fn connect(&mut self) -> RiResult<()> {
131 let endpoint = tonic::transport::Endpoint::from_shared(self.endpoint.clone())
132 .map_err(|e| GrpcError::ConnectionFailed {
133 message: format!("Invalid endpoint: {}", e)
134 })?
135 .connect_timeout(self.timeout)
136 .timeout(self.timeout);
137
138 let channel = endpoint.connect()
139 .await
140 .map_err(|e| GrpcError::ConnectionFailed {
141 message: format!("Connection failed: {}", e)
142 })?;
143
144 self.channel = Some(channel);
145 *self.connected.write().await = true;
146
147 tracing::info!("gRPC client connected to {}", self.endpoint);
148 Ok(())
149 }
150
151 pub async fn is_connected(&self) -> bool {
152 *self.connected.read().await && self.channel.is_some()
153 }
154
155 fn generate_request_id(&self) -> u64 {
156 self.request_id.fetch_add(1, Ordering::SeqCst)
157 }
158
159 pub async fn call(&mut self, service_name: &str, method: &str, data: &[u8]) -> RiResult<Vec<u8>> {
160 let channel = match &self.channel {
161 Some(ch) => ch.clone(),
162 None => {
163 return Err(GrpcError::Client {
164 message: "Not connected to gRPC server".to_string()
165 }.into());
166 }
167 };
168
169 if !*self.connected.read().await {
170 return Err(GrpcError::Client {
171 message: "gRPC client not connected".to_string()
172 }.into());
173 }
174
175 let request_id = self.generate_request_id();
176 let path = format!("/{}/{}", service_name, method);
177
178 tracing::debug!("gRPC call: {} (request_id={})", path, request_id);
179
180 let mut last_error: Option<RiError> = None;
181 for attempt in 0..=self.retry_count {
182 if attempt > 0 {
183 tokio::time::sleep(self.retry_delay).await;
184 tracing::warn!("Retrying gRPC call (attempt {}/{})", attempt, self.retry_count);
185 }
186
187 match Self::execute_unary_call(channel.clone(), &path, data).await {
188 Ok(response) => {
189 let mut stats = self.stats.write().await;
190 stats.record_request(data.len());
191 stats.record_response(response.len());
192 return Ok(response);
193 }
194 Err(e) => {
195 last_error = Some(e.clone());
196 let mut stats = self.stats.write().await;
197 stats.record_error();
198
199 if !Self::is_retryable_error(&e) {
200 return Err(e);
201 }
202 }
203 }
204 }
205
206 Err(last_error.unwrap_or_else(|| GrpcError::Client {
207 message: "Unknown error after retries".to_string()
208 }.into()))
209 }
210
211 async fn execute_unary_call(
212 channel: tonic::transport::Channel,
213 path: &str,
214 data: &[u8],
215 ) -> RiResult<Vec<u8>> {
216 use tonic::client::Grpc;
217 use tonic::codec::ProstCodec;
218
219 let codec = ProstCodec::<Vec<u8>, Vec<u8>>::new();
220 let mut client = Grpc::new(channel);
221
222 let request = tonic::Request::new(data.to_vec());
223 let path_and_query: http::uri::PathAndQuery = path.parse()
224 .map_err(|e| GrpcError::Client {
225 message: format!("Invalid path: {}", e)
226 })?;
227
228 let response = client.unary(request, path_and_query, codec)
229 .await
230 .map_err(|e| GrpcError::Client {
231 message: format!("RPC call failed: {}", e)
232 })?;
233
234 Ok(response.into_inner())
235 }
236
237 fn is_retryable_error(error: &RiError) -> bool {
238 let error_str = error.to_string();
239 error_str.contains("UNAVAILABLE") ||
240 error_str.contains("DEADLINE_EXCEEDED") ||
241 error_str.contains("RESOURCE_EXHAUSTED")
242 }
243
244 pub async fn disconnect(&mut self) {
245 self.channel.take();
246 *self.connected.write().await = false;
247 tracing::info!("gRPC client disconnected from {}", self.endpoint);
248 }
249}
250
251impl Drop for RiGrpcClient {
252 fn drop(&mut self) {
253 if self.channel.is_some() {
254 let rt = crate::py_runtime::py_runtime();
255 rt.block_on(async {
256 self.disconnect().await;
257 });
258 }
259 }
260}
261
262impl Default for RiGrpcClient {
263 fn default() -> Self {
264 Self::new("http://127.0.0.1:50051".to_string())
265 }
266}
267
268impl Clone for RiGrpcClient {
269 fn clone(&self) -> Self {
270 Self {
271 channel: self.channel.clone(),
272 endpoint: self.endpoint.clone(),
273 timeout: self.timeout,
274 stats: self.stats.clone(),
275 request_id: self.request_id.clone(),
276 connected: self.connected.clone(),
277 retry_count: self.retry_count,
278 retry_delay: self.retry_delay,
279 }
280 }
281}