1use std::{
14 collections::{BTreeMap, HashMap},
15 sync::{
16 atomic::{AtomicU64, Ordering},
17 Arc, Mutex,
18 },
19 time::Duration,
20};
21
22use tokio::sync::oneshot;
23
24use crate::{
25 bridge::{
26 protocol::{Frame, Message, Payload, ProtocolError},
27 transport::Writer,
28 },
29 error::{codes, Error, Result},
30};
31
32pub type ChannelSink = Arc<dyn Fn(Payload) + Send + Sync>;
36
37pub trait EventSink: Send + Sync + 'static {
41 fn emit(&self, event: &str, target: Option<&str>, payload: Payload);
43 fn exited(&self, code: Option<i32>);
45}
46
47#[derive(Debug, Clone, Default)]
49pub struct CallOptions {
50 pub client_id: Option<String>,
55 pub timeout: Option<Duration>,
57 pub window: Option<String>,
59}
60
61pub const MAX_TIMEOUT_FACTOR: u32 = 10;
63
64type ClientKey = (String, String);
66
67struct Pending {
68 tx: oneshot::Sender<Result<Payload>>,
69 channels: Vec<u64>,
70 client: Option<ClientKey>,
71}
72
73#[derive(Default)]
77pub struct Bridge {
78 next_id: AtomicU64,
79 next_channel: AtomicU64,
80 pending: Mutex<HashMap<u64, Pending>>,
81 clients: Mutex<HashMap<ClientKey, u64>>,
82 channels: Mutex<HashMap<u64, ChannelSink>>,
83}
84
85fn cancel_frame(id: u64, reason: &str) -> Frame {
86 Frame::new(Message::Cancel { id, reason: reason.into() })
87}
88
89impl Bridge {
90 pub fn new() -> Self {
92 Self::default()
93 }
94
95 pub async fn call(
112 &self,
113 writer: &Writer,
114 cmd: &str,
115 args: Payload,
116 channels: Vec<(String, ChannelSink)>,
117 opts: CallOptions,
118 default_timeout: Duration,
119 ) -> Result<Payload> {
120 let id = self.next_id.fetch_add(1, Ordering::Relaxed) + 1;
121 let client = opts
122 .client_id
123 .as_ref()
124 .map(|client_id| (opts.window.clone().unwrap_or_default(), client_id.clone()));
125 if let Some(key) = &client {
126 let mut clients = self.clients.lock().unwrap();
127 if clients.contains_key(key) {
128 return Err(Error::new(
129 codes::INVALID_ARGS,
130 format!("call id '{}' is already pending in this window", key.1),
131 ));
132 }
133 clients.insert(key.clone(), id);
134 }
135 let mut channel_ids = BTreeMap::new();
136 let mut registered = Vec::with_capacity(channels.len());
137 {
138 let mut map = self.channels.lock().unwrap();
139 for (name, sink) in channels {
140 let channel_id = self.next_channel.fetch_add(1, Ordering::Relaxed) + 1;
141 map.insert(channel_id, sink);
142 channel_ids.insert(name, channel_id);
143 registered.push(channel_id);
144 }
145 }
146
147 let (tx, rx) = oneshot::channel();
148 self.pending.lock().unwrap().insert(id, Pending { tx, channels: registered, client });
149
150 let (payload, body) = args.split();
151 let message = Message::Request { id, cmd: cmd.into(), channels: channel_ids, window: opts.window.clone(), payload };
152 if let Err(e) = writer.send(Frame { message, body }) {
153 self.finish(id);
154 return Err(e);
155 }
156
157 let timeout = opts
158 .timeout
159 .map_or(default_timeout, |t| t.min(default_timeout.saturating_mul(MAX_TIMEOUT_FACTOR)));
160 match tokio::time::timeout(timeout, rx).await {
161 Ok(Ok(result)) => result,
162 Ok(Err(_)) => Err(Error::new(codes::RUNTIME_EXITED, "python runtime exited")),
163 Err(_) => {
164 if self.finish(id).is_some() {
166 let _ = writer.send(cancel_frame(id, "timeout"));
167 }
168 Err(Error::new(
169 codes::TIMEOUT,
170 format!("command '{cmd}' timed out after {}ms", timeout.as_millis()),
171 ))
172 }
173 }
174 }
175
176 fn finish(&self, id: u64) -> Option<Pending> {
178 let pending = self.pending.lock().unwrap().remove(&id)?;
179 if let Some(key) = &pending.client {
180 let mut clients = self.clients.lock().unwrap();
181 if clients.get(key) == Some(&id) {
183 clients.remove(key);
184 }
185 }
186 let mut channels = self.channels.lock().unwrap();
187 for channel_id in &pending.channels {
188 channels.remove(channel_id);
189 }
190 Some(pending)
191 }
192
193 pub fn cancel(&self, writer: Option<&Writer>, window: &str, client_id: &str) -> bool {
198 let key = (window.to_string(), client_id.to_string());
199 let id = { self.clients.lock().unwrap().get(&key).copied() };
200 let Some(id) = id else { return false };
201 let Some(pending) = self.finish(id) else { return false };
202 if let Some(writer) = writer {
203 let _ = writer.send(cancel_frame(id, "client"));
204 }
205 let _ = pending.tx.send(Err(Error::new(codes::CANCELLED, "call was cancelled")));
206 true
207 }
208
209 pub fn handle(&self, frame: Frame, sink: &dyn EventSink) {
214 if let Err(e) = self.dispatch(frame, sink) {
215 log::warn!(target: "py-runtime", "dropped frame from python: {e}");
216 }
217 }
218
219 fn dispatch(&self, frame: Frame, sink: &dyn EventSink) -> std::result::Result<(), ProtocolError> {
220 let Frame { message, body } = frame;
221 match message {
222 Message::Response { id, payload } => {
223 if let Some(pending) = self.finish(id) {
224 let _ = pending.tx.send(payload.join(body).map_err(Error::from));
225 }
226 }
227 Message::Error { id, code, message, data } => {
228 if let Some(pending) = self.finish(id) {
229 let code = code.unwrap_or_else(|| codes::INTERNAL.into());
230 let _ = pending.tx.send(Err(Error { code, message: message.unwrap_or_default(), data }));
231 }
232 }
233 Message::Event { event, target, payload, .. } => sink.emit(&event, target.as_deref(), payload.join(body)?),
234 Message::ChannelData { channel, payload } => {
235 let channel = self.channels.lock().unwrap().get(&channel).cloned();
236 if let Some(channel) = channel {
237 channel(payload.join(body)?);
238 }
239 }
240 Message::ChannelClose { channel } => {
241 self.channels.lock().unwrap().remove(&channel);
242 }
243 other => log::warn!(target: "py-runtime", "unexpected {} frame from python", <&str>::from(&other)),
244 }
245 Ok(())
246 }
247
248 pub fn fail_all(&self, code: &str, message: &str) {
252 let drained: Vec<Pending> = self.pending.lock().unwrap().drain().map(|(_, p)| p).collect();
253 self.clients.lock().unwrap().clear();
254 self.channels.lock().unwrap().clear();
255 for pending in drained {
256 let _ = pending.tx.send(Err(Error::new(code, message)));
257 }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::bridge::protocol::{Inline, DEFAULT_MAX_FRAME_SIZE};
265 use bytes::Bytes;
266 use serde_json::{json, Value};
267 use tokio::sync::mpsc::UnboundedReceiver;
268
269 struct NullSink;
270 impl EventSink for NullSink {
271 fn emit(&self, _: &str, _: Option<&str>, _: Payload) {}
272 fn exited(&self, _: Option<i32>) {}
273 }
274
275 fn setup() -> (Arc<Bridge>, Writer, UnboundedReceiver<Bytes>) {
276 let (writer, rx) = Writer::channel(DEFAULT_MAX_FRAME_SIZE);
277 (Arc::new(Bridge::new()), writer, rx)
278 }
279
280 fn spawn_call(
281 bridge: &Arc<Bridge>,
282 writer: &Writer,
283 cmd: &str,
284 opts: CallOptions,
285 channels: Vec<(String, ChannelSink)>,
286 ) -> tokio::task::JoinHandle<Result<Payload>> {
287 let (bridge, writer, cmd) = (bridge.clone(), writer.clone(), cmd.to_string());
288 tokio::spawn(async move {
289 bridge.call(&writer, &cmd, Payload::None, channels, opts, Duration::from_secs(5)).await
290 })
291 }
292
293 async fn recv(rx: &mut UnboundedReceiver<Bytes>) -> Value {
295 let frame = Frame::decode(&rx.recv().await.unwrap()).unwrap();
296 serde_json::to_value(frame.message).unwrap()
297 }
298
299 fn response(id: u64, payload: Payload) -> Frame {
300 let (payload, body) = payload.split();
301 Frame { message: Message::Response { id, payload }, body }
302 }
303
304 fn message(value: Value) -> Frame {
305 Frame::new(serde_json::from_value(value).unwrap())
306 }
307
308 #[tokio::test]
309 async fn response_resolves_call() {
310 let (bridge, writer, mut rx) = setup();
311 let call = spawn_call(&bridge, &writer, "hello", CallOptions { window: Some("main".into()), ..Default::default() }, vec![]);
312 let req = recv(&mut rx).await;
313 assert_eq!(req, json!({"type": "request", "id": 1, "cmd": "hello", "window": "main"}));
314 bridge.handle(response(1, Payload::Json(json!("hi"))), &NullSink);
315 assert_eq!(call.await.unwrap().unwrap(), Payload::Json(json!("hi")));
316 }
317
318 #[tokio::test]
319 async fn out_of_order_responses() {
320 let (bridge, writer, mut rx) = setup();
321 let first = spawn_call(&bridge, &writer, "a", CallOptions::default(), vec![]);
322 let id_a = recv(&mut rx).await["id"].as_u64().unwrap();
323 let second = spawn_call(&bridge, &writer, "b", CallOptions::default(), vec![]);
324 let id_b = recv(&mut rx).await["id"].as_u64().unwrap();
325 bridge.handle(response(id_b, Payload::Json(json!(2))), &NullSink);
326 assert_eq!(second.await.unwrap().unwrap(), Payload::Json(json!(2)));
327 bridge.handle(response(id_a, Payload::Json(json!(1))), &NullSink);
328 assert_eq!(first.await.unwrap().unwrap(), Payload::Json(json!(1)));
329 }
330
331 #[tokio::test]
332 async fn response_with_inconsistent_payload_fails_call() {
333 let (bridge, writer, mut rx) = setup();
334 let call = spawn_call(&bridge, &writer, "x", CallOptions::default(), vec![]);
335 let id = recv(&mut rx).await["id"].as_u64().unwrap();
336 let mut bad = response(id, Payload::Json(json!(1)));
337 bad.body = vec![1];
338 bridge.handle(bad, &NullSink);
339 assert_eq!(call.await.unwrap().unwrap_err().code, codes::INTERNAL);
340 }
341
342 #[tokio::test]
343 async fn error_frame_maps_to_error() {
344 let (bridge, writer, mut rx) = setup();
345 let call = spawn_call(&bridge, &writer, "x", CallOptions::default(), vec![]);
346 let id = recv(&mut rx).await["id"].as_u64().unwrap();
347 bridge.handle(
348 message(json!({"type": "error", "id": id, "code": "custom", "message": "bad", "data": {"k": 1}})),
349 &NullSink,
350 );
351 let err = call.await.unwrap().unwrap_err();
352 assert_eq!(err, Error::new("custom", "bad").with_data(json!({"k": 1})));
353 }
354
355 #[tokio::test]
356 async fn timeout_sends_cancel() {
357 let (bridge, writer, mut rx) = setup();
358 let opts = CallOptions { timeout: Some(Duration::from_millis(50)), ..Default::default() };
359 let call = spawn_call(&bridge, &writer, "slow", opts, vec![]);
360 let id = recv(&mut rx).await["id"].as_u64().unwrap();
361 assert_eq!(call.await.unwrap().unwrap_err().code, codes::TIMEOUT);
362 assert_eq!(recv(&mut rx).await, json!({"type": "cancel", "id": id, "reason": "timeout"}));
363 }
364
365 fn client_opts(window: &str, client_id: &str) -> CallOptions {
366 CallOptions { client_id: Some(client_id.into()), window: Some(window.into()), ..Default::default() }
367 }
368
369 #[tokio::test]
370 async fn client_cancel() {
371 let (bridge, writer, mut rx) = setup();
372 let call = spawn_call(&bridge, &writer, "slow", client_opts("main", "c1"), vec![]);
373 rx.recv().await.unwrap();
374 assert!(!bridge.cancel(Some(&writer), "other", "c1"));
375 assert!(bridge.cancel(Some(&writer), "main", "c1"));
376 assert!(!bridge.cancel(Some(&writer), "main", "c1"));
377 assert_eq!(call.await.unwrap().unwrap_err().code, codes::CANCELLED);
378 assert_eq!(recv(&mut rx).await["reason"], "client");
379 }
380
381 #[tokio::test]
382 async fn duplicate_pending_client_id_is_rejected() {
383 let (bridge, writer, mut rx) = setup();
384 let first = spawn_call(&bridge, &writer, "slow", client_opts("main", "c1"), vec![]);
385 let id = recv(&mut rx).await["id"].as_u64().unwrap();
386 let dup = spawn_call(&bridge, &writer, "slow", client_opts("main", "c1"), vec![]);
387 assert_eq!(dup.await.unwrap().unwrap_err().code, codes::INVALID_ARGS);
388 assert!(rx.try_recv().is_err(), "duplicate call must not reach python");
389 bridge.handle(response(id, Payload::None), &NullSink);
390 first.await.unwrap().unwrap();
391 let again = spawn_call(&bridge, &writer, "slow", client_opts("main", "c1"), vec![]);
392 rx.recv().await.unwrap();
393 assert!(bridge.cancel(Some(&writer), "main", "c1"));
394 assert_eq!(again.await.unwrap().unwrap_err().code, codes::CANCELLED);
395 }
396
397 #[tokio::test]
398 async fn same_client_id_in_different_windows_is_independent() {
399 let (bridge, writer, mut rx) = setup();
400 let a = spawn_call(&bridge, &writer, "slow", client_opts("a", "c1"), vec![]);
401 let id_a = recv(&mut rx).await["id"].as_u64().unwrap();
402 let b = spawn_call(&bridge, &writer, "slow", client_opts("b", "c1"), vec![]);
403 let id_b = recv(&mut rx).await["id"].as_u64().unwrap();
404 assert!(bridge.cancel(Some(&writer), "b", "c1"));
405 assert_eq!(b.await.unwrap().unwrap_err().code, codes::CANCELLED);
406 assert_eq!(recv(&mut rx).await["id"], id_b);
407 assert!(bridge.cancel(Some(&writer), "a", "c1"));
408 assert_eq!(a.await.unwrap().unwrap_err().code, codes::CANCELLED);
409 assert_eq!(recv(&mut rx).await["id"], id_a);
410 }
411
412 #[test]
413 fn finish_keeps_newer_client_mapping() {
414 let bridge = Bridge::new();
415 let key = ("main".to_string(), "c1".to_string());
416 let (tx, _rx) = oneshot::channel();
417 bridge.pending.lock().unwrap().insert(1, Pending { tx, channels: vec![], client: Some(key.clone()) });
418 bridge.clients.lock().unwrap().insert(key.clone(), 2);
419 assert!(bridge.finish(1).is_some());
420 assert_eq!(bridge.clients.lock().unwrap().get(&key), Some(&2));
421 }
422
423 #[tokio::test]
424 async fn per_call_timeout_is_capped() {
425 let (bridge, writer, mut rx) = setup();
426 let opts = CallOptions { timeout: Some(Duration::from_secs(60)), ..Default::default() };
427 let call = {
428 let (bridge, writer) = (bridge.clone(), writer.clone());
429 tokio::spawn(async move {
430 bridge.call(&writer, "slow", Payload::None, vec![], opts, Duration::from_millis(10)).await
431 })
432 };
433 rx.recv().await.unwrap();
434 let err = tokio::time::timeout(Duration::from_secs(2), call).await.expect("cap not applied").unwrap().unwrap_err();
435 assert_eq!(err.code, codes::TIMEOUT);
436 assert!(err.message.contains("100ms"), "{}", err.message);
437 }
438
439 #[tokio::test]
440 async fn channel_data_is_routed_until_response() {
441 let (bridge, writer, mut rx) = setup();
442 let received = Arc::new(Mutex::new(Vec::new()));
443 let sink_store = received.clone();
444 let sink: ChannelSink = Arc::new(move |p| sink_store.lock().unwrap().push(p));
445 let call = spawn_call(&bridge, &writer, "count", CallOptions::default(), vec![("onItem".into(), sink)]);
446 let req = recv(&mut rx).await;
447 let id = req["id"].as_u64().unwrap();
448 let channel = req["channels"]["onItem"].as_u64().unwrap();
449 let data = |n: u64| {
450 Frame::new(Message::ChannelData { channel, payload: Inline { value: Some(json!(n)), raw: false } })
451 };
452 bridge.handle(data(0), &NullSink);
453 bridge.handle(data(1), &NullSink);
454 bridge.handle(response(id, Payload::None), &NullSink);
455 call.await.unwrap().unwrap();
456 bridge.handle(data(2), &NullSink);
457 assert_eq!(*received.lock().unwrap(), vec![Payload::Json(json!(0)), Payload::Json(json!(1))]);
458 }
459
460 #[tokio::test]
461 async fn fail_all_rejects_pending_calls() {
462 let (bridge, writer, mut rx) = setup();
463 let call = spawn_call(&bridge, &writer, "x", CallOptions::default(), vec![]);
464 rx.recv().await.unwrap();
465 bridge.fail_all(codes::RUNTIME_EXITED, "gone");
466 assert_eq!(call.await.unwrap().unwrap_err().code, codes::RUNTIME_EXITED);
467 }
468}