Skip to content

Commit 431be00

Browse files
committed
Add shared store runtime wrapper
Add a crate-local runtime wrapper for store backends that need to keep their I/O isolated while shutting down safely from async contexts. Co-Authored-By: HAL 9000
1 parent 458d660 commit 431be00

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

src/runtime.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
// accordance with one or both of these licenses.
77

88
use std::future::Future;
9+
use std::io;
10+
use std::sync::atomic::{AtomicUsize, Ordering};
911
use std::sync::{Arc, Mutex};
1012
use std::time::Duration;
1113

@@ -223,6 +225,90 @@ enum RuntimeMode {
223225
Handle(tokio::runtime::Handle),
224226
}
225227

228+
/// Runtime used by async store backends while ldk-node still exposes synchronous APIs.
229+
///
230+
/// This is a temporary bridge for store implementations that need Tokio-driven I/O, such as VSS
231+
/// and PostgreSQL. Many public ldk-node methods are still synchronous, so they call
232+
/// [`Runtime::block_on`] when they need to wait for async persistence. If that persistence work is
233+
/// driven by the same Tokio runtime as the synchronous caller, a blocking call can deadlock in a
234+
/// narrow but realistic scheduler state.
235+
///
236+
/// The failure mode is that `block_on` parks the current worker with `block_in_place` while it
237+
/// waits for an async store operation. Suppose that store operation is waiting for an I/O future,
238+
/// and the connection driver or I/O driver task that can make the future progress is assigned to
239+
/// the same worker thread that just entered `block_in_place`. The blocked sync caller is waiting
240+
/// for the persistence future to complete, while the persistence future is waiting for an I/O task
241+
/// that cannot be polled because its worker is occupied by the blocking caller. With no worker
242+
/// driving that I/O resource, neither side can make progress.
243+
///
244+
/// A simple example is a synchronous node API calling `block_on(store.write(...))` for a
245+
/// tokio-postgres-backed store. The write future may wait for the postgres connection task or
246+
/// socket readiness. If the runtime worker that should poll that connection task is also the
247+
/// worker currently blocked in the synchronous API, the write cannot complete, and the synchronous
248+
/// API cannot unblock.
249+
///
250+
/// `StoreRuntime` gives each such store backend its own small runtime, workers, and I/O driver.
251+
/// Synchronous node APIs may still block the node runtime while waiting for persistence, but the
252+
/// persistence tasks they wait on are driven independently and can continue polling sockets and
253+
/// connection drivers.
254+
///
255+
/// Once ldk-node switches the remaining store-backed APIs to be fully async, callers will await
256+
/// persistence directly and these `block_on` bridges will be disallowed. At that point the store
257+
/// runtimes should be removed again and store I/O can run on the node runtime directly.
258+
pub(crate) struct StoreRuntime {
259+
runtime: Option<tokio::runtime::Runtime>,
260+
}
261+
262+
impl StoreRuntime {
263+
pub(crate) fn new(
264+
thread_name_prefix: &'static str, worker_threads: usize, runtime_name: &'static str,
265+
) -> io::Result<Self> {
266+
let runtime = tokio::runtime::Builder::new_multi_thread()
267+
.enable_all()
268+
.thread_name_fn(move || {
269+
static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
270+
let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
271+
format!("{}-{}", thread_name_prefix, id)
272+
})
273+
.worker_threads(worker_threads)
274+
.max_blocking_threads(worker_threads)
275+
.build()
276+
.map_err(|e| {
277+
io::Error::new(
278+
io::ErrorKind::Other,
279+
format!("Failed to build {runtime_name} runtime: {e}"),
280+
)
281+
})?;
282+
Ok(Self { runtime: Some(runtime) })
283+
}
284+
285+
pub(crate) fn handle(&self) -> &tokio::runtime::Handle {
286+
self.runtime.as_ref().expect("store runtime must be available").handle()
287+
}
288+
289+
pub(crate) fn spawn<F>(&self, future: F) -> JoinHandle<F::Output>
290+
where
291+
F: Future + Send + 'static,
292+
F::Output: Send + 'static,
293+
{
294+
self.handle().spawn(future)
295+
}
296+
297+
pub(crate) fn shutdown_background(mut self) {
298+
if let Some(runtime) = self.runtime.take() {
299+
runtime.shutdown_background();
300+
}
301+
}
302+
}
303+
304+
impl Drop for StoreRuntime {
305+
fn drop(&mut self) {
306+
if let Some(runtime) = self.runtime.take() {
307+
runtime.shutdown_background();
308+
}
309+
}
310+
}
311+
226312
pub(crate) struct RuntimeSpawner {
227313
runtime: Arc<Runtime>,
228314
}

0 commit comments

Comments
 (0)