chalk-remote-call-python 1.10.7__tar.gz → 1.10.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {chalk_remote_call_python-1.10.7/chalk_remote_call_python.egg-info → chalk_remote_call_python-1.10.9}/PKG-INFO +1 -1
  2. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/Cargo.lock +1 -0
  3. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml +3 -0
  4. chalk_remote_call_python-1.10.9/chalk-remote-call-rs/chalk-remote-call-server/build.rs +15 -0
  5. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-server/src/python_bridge.rs +243 -14
  6. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-server/src/self_consumer.rs +1 -1
  7. chalk_remote_call_python-1.10.9/chalk_remote_call/_version.py +1 -0
  8. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9/chalk_remote_call_python.egg-info}/PKG-INFO +1 -1
  9. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call_python.egg-info/SOURCES.txt +1 -0
  10. chalk_remote_call_python-1.10.7/chalk_remote_call/_version.py +0 -1
  11. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/MANIFEST.in +0 -0
  12. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/README.md +0 -0
  13. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/Cargo.toml +0 -0
  14. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-proto/Cargo.toml +0 -0
  15. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.auth.v1.rs +0 -0
  16. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.common.v1.rs +0 -0
  17. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.rs +0 -0
  18. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.tonic.rs +0 -0
  19. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.utils.v1.rs +0 -0
  20. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-proto/src/gen/descriptor.bin +0 -0
  21. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-proto/src/lib.rs +0 -0
  22. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-server/src/async_service.rs +0 -0
  23. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-server/src/coalesce.rs +0 -0
  24. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-server/src/fnq_client.rs +0 -0
  25. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-server/src/lib.rs +0 -0
  26. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-server/src/metrics.rs +0 -0
  27. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-server/src/server.rs +0 -0
  28. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/chalk-remote-call-server/src/service.rs +0 -0
  29. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk-remote-call-rs/rust-toolchain.toml +0 -0
  30. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/__init__.py +0 -0
  31. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/__main__.py +0 -0
  32. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/__init__.py +0 -0
  33. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/__init__.py +0 -0
  34. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/auth/__init__.py +0 -0
  35. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/auth/v1/__init__.py +0 -0
  36. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2.py +0 -0
  37. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/auth/v1/permissions_pb2_grpc.py +0 -0
  38. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/common/__init__.py +0 -0
  39. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/common/v1/__init__.py +0 -0
  40. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2.py +0 -0
  41. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/common/v1/chalk_error_pb2_grpc.py +0 -0
  42. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/runtime/__init__.py +0 -0
  43. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/runtime/v1/__init__.py +0 -0
  44. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/runtime/v1/fnq_consumer_pb2.py +0 -0
  45. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/runtime/v1/fnq_consumer_pb2_grpc.py +0 -0
  46. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2.py +0 -0
  47. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/runtime/v1/remote_python_call_pb2_grpc.py +0 -0
  48. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/utils/__init__.py +0 -0
  49. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/utils/v1/__init__.py +0 -0
  50. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2.py +0 -0
  51. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/utils/v1/encoding_pb2_grpc.py +0 -0
  52. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2.py +0 -0
  53. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/utils/v1/field_change_pb2_grpc.py +0 -0
  54. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/utils/v1/large_pb2.py +0 -0
  55. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/utils/v1/large_pb2_grpc.py +0 -0
  56. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2.py +0 -0
  57. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_gen/chalk/utils/v1/sensitive_pb2_grpc.py +0 -0
  58. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/_native.pyi +0 -0
  59. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/arrow_utils.py +0 -0
  60. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/cli.py +0 -0
  61. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/errors.py +0 -0
  62. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/handler_loader.py +0 -0
  63. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/input_transform.py +0 -0
  64. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/logging.py +0 -0
  65. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/server.py +0 -0
  66. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/servicer.py +0 -0
  67. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call/tracing.py +0 -0
  68. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call_python.egg-info/dependency_links.txt +0 -0
  69. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call_python.egg-info/entry_points.txt +0 -0
  70. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call_python.egg-info/requires.txt +0 -0
  71. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/chalk_remote_call_python.egg-info/top_level.txt +0 -0
  72. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/pyproject.toml +0 -0
  73. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/setup.cfg +0 -0
  74. {chalk_remote_call_python-1.10.7 → chalk_remote_call_python-1.10.9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.10.7
3
+ Version: 1.10.9
4
4
  Summary: Chalk remote call Python runtime interface client
5
5
  Author: Chalk AI, Inc.
6
6
  Project-URL: Homepage, https://chalk.ai
@@ -548,6 +548,7 @@ dependencies = [
548
548
  "futures-util",
549
549
  "prost 0.13.5",
550
550
  "pyo3",
551
+ "pyo3-build-config",
551
552
  "redis",
552
553
  "serde_json",
553
554
  "tokio",
@@ -11,6 +11,9 @@ crate-type = ["cdylib"]
11
11
  default = ["extension-module"]
12
12
  extension-module = ["pyo3/extension-module"]
13
13
 
14
+ [build-dependencies]
15
+ pyo3-build-config = { version = "0.29", features = ["resolve-config"] }
16
+
14
17
  [dependencies]
15
18
  chalk-remote-call-proto = { path = "../chalk-remote-call-proto" }
16
19
  redis = { version = "1.0.2", features = ["aio", "tokio-comp", "streams", "connection-manager"] }
@@ -0,0 +1,15 @@
1
+ fn main() {
2
+ println!("cargo:rerun-if-env-changed=CARGO_FEATURE_EXTENSION_MODULE");
3
+ if std::env::var_os("CARGO_FEATURE_EXTENSION_MODULE").is_some() {
4
+ return;
5
+ }
6
+
7
+ let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
8
+ if matches!(target_os.as_str(), "linux" | "macos") {
9
+ // Embedding test binaries need the same Python library that PyO3 links,
10
+ // even when a managed interpreter installs it outside system paths.
11
+ if let Some(lib_dir) = pyo3_build_config::get().lib_dir() {
12
+ println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}");
13
+ }
14
+ }
15
+ }
@@ -1,3 +1,4 @@
1
+ use std::cell::OnceCell;
1
2
  use std::collections::HashMap;
2
3
  use std::sync::{Arc, Mutex};
3
4
 
@@ -8,6 +9,46 @@ use pyo3::types::{PyBytes, PyDict, PyList};
8
9
  use tokio::sync::{mpsc, Notify};
9
10
  use tonic::Status;
10
11
 
12
+ thread_local! {
13
+ // This value belongs only to its OS thread, including its destructor.
14
+ static THREAD_ATTACHMENT: OnceCell<ThreadAttachment> = const { OnceCell::new() };
15
+ }
16
+
17
+ struct ThreadAttachment {
18
+ state: pyo3::ffi::PyGILState_STATE,
19
+ }
20
+
21
+ impl ThreadAttachment {
22
+ fn new(_py: Python<'_>) -> Self {
23
+ // SAFETY: the Python token guarantees an attached thread. This nested
24
+ // Ensure returns LOCKED and pins its state after the outer guard detaches.
25
+ // The outer PyO3 guard still releases the GIL between invocations.
26
+ let state = unsafe { pyo3::ffi::PyGILState_Ensure() };
27
+ Self { state }
28
+ }
29
+ }
30
+
31
+ impl Drop for ThreadAttachment {
32
+ fn drop(&mut self) {
33
+ // Tokio joins its blocking threads while start_server's caller is detached.
34
+ // Reattach before releasing our matching LOCKED token; PyO3 then releases
35
+ // its outer token, deleting the state without retaining the GIL.
36
+ let _ = Python::try_attach(|_| unsafe {
37
+ // SAFETY: creation and destruction run on this same OS thread under
38
+ // the GIL, and this releases exactly the nested Ensure above.
39
+ pyo3::ffi::PyGILState_Release(self.state);
40
+ });
41
+ }
42
+ }
43
+
44
+ fn retain_thread_state(py: Python<'_>) {
45
+ // pybind11 can cache this state in native TLS. Its lifetime must match the
46
+ // blocking worker rather than an individual Python invocation.
47
+ THREAD_ATTACHMENT.with(|attachment| {
48
+ attachment.get_or_init(|| ThreadAttachment::new(py));
49
+ });
50
+ }
51
+
11
52
  /// Holds references to the Python handler and bridge function.
12
53
  pub struct PythonHandler {
13
54
  /// The user's handler callable.
@@ -45,7 +86,7 @@ impl PythonHandler {
45
86
  /// into a shared buffer). The only difference between the two paths is which
46
87
  /// emitter object `process_batches` is handed.
47
88
  async fn call_with_emitter<F>(
48
- &self,
89
+ self: &Arc<Self>,
49
90
  ipc_bytes: Vec<u8>,
50
91
  function_name: String,
51
92
  metadata: HashMap<String, String>,
@@ -55,12 +96,14 @@ impl PythonHandler {
55
96
  where
56
97
  F: FnOnce(Python<'_>) -> PyResult<Py<PyAny>> + Send + 'static,
57
98
  {
58
- let (handler, process_fn) =
59
- Python::attach(|py| (self.handler.clone_ref(py), self.process_fn.clone_ref(py)));
60
- let arg_names = self.arg_names.clone();
61
-
99
+ // GIL acquisition can block, so only clone the Rust owner on the runtime worker.
100
+ let owner = Arc::clone(self);
62
101
  tokio::task::spawn_blocking(move || {
63
102
  Python::attach(|py| -> Result<(), PythonError> {
103
+ retain_thread_state(py);
104
+ let handler = owner.handler.clone_ref(py);
105
+ let process_fn = &owner.process_fn;
106
+ let arg_names = &owner.arg_names;
64
107
  let py_bytes = PyBytes::new(py, &ipc_bytes).into_any().unbind();
65
108
  let py_ctx = build_context(py, &function_name, &metadata, &peer)?
66
109
  .into_any()
@@ -81,7 +124,7 @@ impl PythonHandler {
81
124
  /// Non-batched streaming dispatch. Calls `process_batches(..., emit)` so
82
125
  /// generator results are sent to the gRPC stream as each yield is encoded.
83
126
  pub async fn call_streaming(
84
- &self,
127
+ self: &Arc<Self>,
85
128
  ipc_bytes: Vec<u8>,
86
129
  function_name: String,
87
130
  metadata: HashMap<String, String>,
@@ -100,7 +143,7 @@ impl PythonHandler {
100
143
  /// value, so completion never depends on emitter teardown timing (which is
101
144
  /// what caused a Python 3.10 hang in the channel-drain version).
102
145
  pub async fn call_into_buffer(
103
- &self,
146
+ self: &Arc<Self>,
104
147
  ipc_bytes: Vec<u8>,
105
148
  function_name: String,
106
149
  metadata: HashMap<String, String>,
@@ -117,15 +160,17 @@ impl PythonHandler {
117
160
  /// `process_batches_coalesced`. Returns one IPC-encoded response per
118
161
  /// caller, in input order.
119
162
  pub async fn call_coalesced(
120
- &self,
163
+ self: &Arc<Self>,
121
164
  callers: Vec<CallerInput>,
122
165
  ) -> Result<Vec<Vec<u8>>, PythonError> {
123
- let (handler, process_fn) =
124
- Python::attach(|py| (self.handler.clone_ref(py), self.process_fn.clone_ref(py)));
125
- let arg_names = self.arg_names.clone();
126
-
166
+ // GIL acquisition can block, so only clone the Rust owner on the runtime worker.
167
+ let owner = Arc::clone(self);
127
168
  tokio::task::spawn_blocking(move || {
128
169
  Python::attach(|py| -> Result<Vec<Vec<u8>>, PythonError> {
170
+ retain_thread_state(py);
171
+ let handler = &owner.handler;
172
+ let process_fn = &owner.process_fn;
173
+ let arg_names = &owner.arg_names;
129
174
  let py_inputs = PyList::empty(py);
130
175
  let py_contexts = PyList::empty(py);
131
176
  for c in &callers {
@@ -139,8 +184,8 @@ impl PythonHandler {
139
184
  let py_arg_names = build_arg_names(py, arg_names.as_deref())?;
140
185
  invoke_process_fn(
141
186
  py,
142
- &process_fn,
143
- &handler,
187
+ process_fn,
188
+ handler,
144
189
  py_inputs.into_any().unbind(),
145
190
  py_arg_names,
146
191
  py_contexts.into_any().unbind(),
@@ -389,3 +434,187 @@ fn format_python_traceback(py: Python<'_>, err: &PyErr) -> PyResult<String> {
389
434
  let lines: Vec<String> = formatted.extract()?;
390
435
  Ok(lines.concat().trim_end().to_string())
391
436
  }
437
+
438
+ #[cfg(test)]
439
+ mod tests {
440
+ use super::*;
441
+ use std::future::{poll_fn, Future};
442
+ use std::task::Poll;
443
+ use std::time::Duration;
444
+
445
+ #[derive(Clone, Copy)]
446
+ enum Dispatch {
447
+ Streaming,
448
+ Buffered,
449
+ Coalesced,
450
+ }
451
+
452
+ // Polling a dispatch must yield even while another thread owns the GIL:
453
+ // otherwise Python contention blocks the worker that also serves health RPCs.
454
+ fn dispatch_yields_while_gil_is_held(dispatch: Dispatch) {
455
+ Python::initialize();
456
+ let (yielded, worker) = Python::attach(|py| {
457
+ let handler = Arc::new(PythonHandler::new(
458
+ py.None(),
459
+ py.eval(pyo3::ffi::c_str!("lambda *args: []"), None, None)
460
+ .unwrap()
461
+ .unbind(),
462
+ None,
463
+ ));
464
+ let (polled_tx, polled_rx) = std::sync::mpsc::channel();
465
+ let worker = std::thread::spawn(move || {
466
+ let runtime = tokio::runtime::Builder::new_current_thread()
467
+ .build()
468
+ .unwrap();
469
+ runtime.block_on(async move {
470
+ let call = async {
471
+ match dispatch {
472
+ Dispatch::Streaming => {
473
+ let (tx, _rx) = mpsc::channel(1);
474
+ handler
475
+ .call_streaming(
476
+ Vec::new(),
477
+ String::new(),
478
+ HashMap::new(),
479
+ String::new(),
480
+ tx,
481
+ )
482
+ .await
483
+ }
484
+ Dispatch::Buffered => {
485
+ handler
486
+ .call_into_buffer(
487
+ Vec::new(),
488
+ String::new(),
489
+ HashMap::new(),
490
+ String::new(),
491
+ ChunkBuffer::new().into_shared(),
492
+ )
493
+ .await
494
+ }
495
+ Dispatch::Coalesced => {
496
+ handler.call_coalesced(Vec::new()).await.map(|_| ())
497
+ }
498
+ }
499
+ };
500
+ tokio::pin!(call);
501
+ poll_fn(|cx| {
502
+ assert!(call.as_mut().poll(cx).is_pending());
503
+ let _ = polled_tx.send(());
504
+ Poll::Ready(())
505
+ })
506
+ .await;
507
+ call.await.unwrap();
508
+ });
509
+ });
510
+ // No Python bytecode runs here, so CPython cannot periodically release
511
+ // the GIL and accidentally let a blocking attach pass the test.
512
+ let yielded = polled_rx.recv_timeout(Duration::from_secs(5)).is_ok();
513
+ (yielded, worker)
514
+ });
515
+ // Release the GIL before joining so even a regressed implementation exits.
516
+ worker.join().unwrap();
517
+ assert!(
518
+ yielded,
519
+ "dispatch blocked its Tokio worker acquiring the GIL"
520
+ );
521
+ }
522
+
523
+ #[test]
524
+ fn streaming_dispatch_yields_under_gil_contention() {
525
+ dispatch_yields_while_gil_is_held(Dispatch::Streaming);
526
+ }
527
+
528
+ #[test]
529
+ fn buffered_dispatch_yields_under_gil_contention() {
530
+ dispatch_yields_while_gil_is_held(Dispatch::Buffered);
531
+ }
532
+
533
+ #[test]
534
+ fn coalesced_dispatch_yields_under_gil_contention() {
535
+ dispatch_yields_while_gil_is_held(Dispatch::Coalesced);
536
+ }
537
+ fn contains_thread_state(_py: Python<'_>, id: i64) -> bool {
538
+ // SAFETY: interpreter state iteration and ID access occur under the GIL.
539
+ unsafe {
540
+ let interpreter =
541
+ pyo3::ffi::PyThreadState_GetInterpreter(pyo3::ffi::PyThreadState_Get());
542
+ let mut state = pyo3::ffi::PyInterpreterState_ThreadHead(interpreter);
543
+ while !state.is_null() {
544
+ if pyo3::ffi::PyThreadState_GetID(state) == id {
545
+ return true;
546
+ }
547
+ state = pyo3::ffi::PyThreadState_Next(state);
548
+ }
549
+ }
550
+ false
551
+ }
552
+
553
+ async fn blocking_thread_state_id() -> i64 {
554
+ tokio::task::spawn_blocking(|| {
555
+ Python::attach(|py| {
556
+ retain_thread_state(py);
557
+ // SAFETY: this call runs under the live attachment above.
558
+ unsafe { pyo3::ffi::PyThreadState_GetID(pyo3::ffi::PyThreadState_Get()) }
559
+ })
560
+ })
561
+ .await
562
+ .unwrap()
563
+ }
564
+
565
+ #[test]
566
+ fn blocking_worker_reuses_state_and_releases_it_at_shutdown() {
567
+ Python::initialize();
568
+ let runtime = tokio::runtime::Builder::new_current_thread()
569
+ .max_blocking_threads(1)
570
+ .build()
571
+ .unwrap();
572
+ let ids = runtime.block_on(async {
573
+ let mut ids = Vec::new();
574
+ for _ in 0..40 {
575
+ ids.push(blocking_thread_state_id().await);
576
+ }
577
+ ids
578
+ });
579
+ assert!(ids.iter().all(|id| *id == ids[0]));
580
+ assert!(Python::attach(|py| contains_thread_state(py, ids[0])));
581
+ let failed = runtime.block_on(async {
582
+ tokio::task::spawn_blocking(|| {
583
+ Python::attach(|py| {
584
+ retain_thread_state(py);
585
+ panic!("test blocking job panic");
586
+ });
587
+ })
588
+ .await
589
+ .unwrap_err()
590
+ });
591
+ assert!(failed.is_panic());
592
+ assert_eq!(ids[0], runtime.block_on(blocking_thread_state_id()));
593
+ // Joining must happen with this test thread detached so TLS can clean up.
594
+ drop(runtime);
595
+ assert!(!Python::attach(|py| contains_thread_state(py, ids[0])));
596
+ }
597
+
598
+ #[test]
599
+ fn idle_blocking_worker_releases_state_before_replacement() {
600
+ Python::initialize();
601
+ let runtime = tokio::runtime::Builder::new_current_thread()
602
+ .max_blocking_threads(1)
603
+ .thread_keep_alive(Duration::from_millis(20))
604
+ .build()
605
+ .unwrap();
606
+ let original = runtime.block_on(blocking_thread_state_id());
607
+ let deadline = std::time::Instant::now() + Duration::from_secs(5);
608
+ while Python::attach(|py| contains_thread_state(py, original)) {
609
+ assert!(
610
+ std::time::Instant::now() < deadline,
611
+ "idle worker state was not released"
612
+ );
613
+ std::thread::sleep(Duration::from_millis(5));
614
+ }
615
+ let replacement = runtime.block_on(blocking_thread_state_id());
616
+ assert_ne!(original, replacement);
617
+ drop(runtime);
618
+ assert!(!Python::attach(|py| contains_thread_state(py, replacement)));
619
+ }
620
+ }
@@ -1351,7 +1351,7 @@ fn ensure_traceparent(headers: &mut HashMap<String, String>) -> String {
1351
1351
  /// `end`, or `error`) to the call's result stream, updating meta status along
1352
1352
  /// the way.
1353
1353
  async fn execute_and_write(
1354
- handler: &PythonHandler,
1354
+ handler: &Arc<PythonHandler>,
1355
1355
  output: &mut CallOutput,
1356
1356
  work: QueuedCall,
1357
1357
  flush_timeout: Duration,
@@ -0,0 +1 @@
1
+ __version__ = "1.10.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chalk-remote-call-python
3
- Version: 1.10.7
3
+ Version: 1.10.9
4
4
  Summary: Chalk remote call Python runtime interface client
5
5
  Author: Chalk AI, Inc.
6
6
  Project-URL: Homepage, https://chalk.ai
@@ -14,6 +14,7 @@ chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.runtime.v1.tonic.rs
14
14
  chalk-remote-call-rs/chalk-remote-call-proto/src/gen/chalk.utils.v1.rs
15
15
  chalk-remote-call-rs/chalk-remote-call-proto/src/gen/descriptor.bin
16
16
  chalk-remote-call-rs/chalk-remote-call-server/Cargo.toml
17
+ chalk-remote-call-rs/chalk-remote-call-server/build.rs
17
18
  chalk-remote-call-rs/chalk-remote-call-server/src/async_service.rs
18
19
  chalk-remote-call-rs/chalk-remote-call-server/src/coalesce.rs
19
20
  chalk-remote-call-rs/chalk-remote-call-server/src/fnq_client.rs
@@ -1 +0,0 @@
1
- __version__ = "1.10.7"