|
14 | 14 | // KIND, either express or implied. See the License for the |
15 | 15 | // specific language governing permissions and limitations |
16 | 16 | // under the License. |
17 | | -use std::{collections::HashMap, sync::Arc}; |
| 17 | +use std::{ |
| 18 | + collections::HashMap, |
| 19 | + sync::{Arc, OnceLock}, |
| 20 | +}; |
18 | 21 |
|
19 | 22 | use arrow_schema::DataType; |
20 | 23 | use pyo3::prelude::*; |
@@ -61,27 +64,46 @@ fn stringify_options( |
61 | 64 | .collect() |
62 | 65 | } |
63 | 66 |
|
| 67 | +/// The process-wide Tokio runtime shared by every [`InternalContext`]. |
| 68 | +/// |
| 69 | +/// One multi-threaded runtime backs all contexts so the worker and |
| 70 | +/// blocking-thread pools are bounded per process rather than per session. |
| 71 | +/// Building a runtime per `connect()` meant opening N sessions spawned N |
| 72 | +/// runtimes' worth of threads; under a free-threaded interpreter (no GIL to |
| 73 | +/// serialize the Python-touching workers) those piled up across short-lived |
| 74 | +/// sessions and exhausted the OS per-process thread limit. A single shared |
| 75 | +/// runtime keeps full `num_cpus` parallelism for queries while making a new |
| 76 | +/// session cost no additional threads. It lives for the process, so it is |
| 77 | +/// never torn down on an interpreter thread. |
| 78 | +fn shared_runtime() -> Result<Arc<RuntimeHandle>, PySedonaError> { |
| 79 | + static SHARED: OnceLock<Arc<RuntimeHandle>> = OnceLock::new(); |
| 80 | + if let Some(runtime) = SHARED.get() { |
| 81 | + return Ok(runtime.clone()); |
| 82 | + } |
| 83 | + let runtime = tokio::runtime::Builder::new_multi_thread() |
| 84 | + .enable_all() |
| 85 | + .build() |
| 86 | + .map_err(|e| PySedonaError::SedonaPython(format!("Failed to build shared runtime: {e}")))?; |
| 87 | + // On the rare initialization race, the losing thread's freshly built handle |
| 88 | + // is dropped here (draining on a janitor thread, never the caller's), and |
| 89 | + // the winner's runtime is returned. |
| 90 | + let handle = Arc::new(RuntimeHandle::new(runtime)); |
| 91 | + Ok(SHARED.get_or_init(|| handle).clone()) |
| 92 | +} |
| 93 | + |
64 | 94 | #[pymethods] |
65 | 95 | impl InternalContext { |
66 | 96 | #[new] |
67 | 97 | #[pyo3(signature = (options=HashMap::new()))] |
68 | 98 | fn new(py: Python, options: HashMap<String, String>) -> Result<Self, PySedonaError> { |
69 | | - let runtime = tokio::runtime::Builder::new_multi_thread() |
70 | | - .enable_all() |
71 | | - .build() |
72 | | - .map_err(|e| { |
73 | | - PySedonaError::SedonaPython(format!("Failed to build multithreaded runtime: {e}")) |
74 | | - })?; |
| 99 | + let runtime = shared_runtime()?; |
75 | 100 |
|
76 | 101 | let builder = SedonaContextBuilder::from_options(&options) |
77 | 102 | .map_err(|e| PySedonaError::SedonaPython(e.to_string()))?; |
78 | 103 |
|
79 | 104 | let inner = wait_for_future(py, &runtime, builder.build())??; |
80 | 105 |
|
81 | | - Ok(Self { |
82 | | - inner, |
83 | | - runtime: Arc::new(RuntimeHandle::new(runtime)), |
84 | | - }) |
| 106 | + Ok(Self { inner, runtime }) |
85 | 107 | } |
86 | 108 |
|
87 | 109 | pub fn view<'py>( |
|
0 commit comments