Sync→async runtime bridge for Heddle.
[RuntimeBridge] is a worker thread that owns a private current-thread
Tokio runtime. Synchronous code hands futures to the worker over an
async mpsc channel; the worker tokio::spawns each future on its own
runtime and the caller blocks on a per-request reply channel until the
task completes. The caller's runtime (if any) is never re-entered.
Why
Heddle has several ObjectStore / OpLogBackend / RefBackend
implementations whose trait surface is synchronous but whose underlying
I/O is async (aws-sdk-s3, sqlx, etc.). A naive bridge —
Handle::current().block_on(...) or
tokio::task::block_in_place(|| Handle::current().block_on(...)) —
breaks in caller-flavor-dependent ways:
Handle::current().block_on(...)panics with "Cannot start a runtime from within a runtime" when the caller is already on a Tokio runtime.block_in_place(...)panics with "can call blocking only when running on the multi-threaded runtime" when the caller is on a current-thread runtime (e.g.#[tokio::test(flavor = "current_thread")]).- Neither works at all when the caller is on a non-Tokio thread.
Routing through this bridge sidesteps all three: the future runs on the bridge's private runtime regardless of who calls it, and the caller's thread simply blocks on a reply channel.
Concurrency
The worker dispatches each request via [tokio::spawn] rather than
awaiting it inline, so concurrent callers sharing one bridge can
progress in parallel on the worker's runtime. This preserves the
connection-level parallelism of pools like sqlx::PgPool instead of
head-of-line blocking every caller behind the slowest in-flight query.
Error recovery
[RuntimeBridge::block_on] returns [Result<T, BridgeError>] so a dead
worker surfaces as a recoverable error in the caller's Result-typed
API rather than escalating into a process-level panic. A bridged task
that panics aborts only that task: its reply channel is dropped and the
waiting caller observes [BridgeError::ResponseLost]; the worker keeps
serving other requests.
Shutdown
Dropping the bridge drops the Sender; the worker's Receiver::recv
then returns None, the loop breaks, and the runtime is dropped on
the worker thread. The JoinHandle is retained on the bridge so the
thread isn't reaped before its in-flight requests drain.