fix(osis): make CRUD handlers async — drop spawn_blocking and PR #130 blocking shims #133
No reviewers
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_rpc!133
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "osis-handlers-async"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Replaces #130 (which is being closed in favor of this architectural fix). hero_rpc#127 wired OSIS handler bodies through an
OsisIndexerfacade that held its owntokio::runtime::Runtimeandblock_oned everyhero_indexer_sdkcall. The moment a generated handler fired from inside the jsonrpsee server's worker, that nestedblock_onpanicked: "Cannot start a runtime from within a runtime."#130 hid the panic behind a thread-pool detour shim (
index_document_blocking+ a bridge runtime). That worked but was a hack — every CRUD path still crossed sync<->async twice per request.This PR fixes the root cause: every link in the chain is async, so the dispatch pipeline is one continuous
.awaitstack from the network closure to the indexer socket.Before / after
Zero sync<->async crossings inside any CRUD handler. The legacy Axum-based dispatch in
osis/src/rpc/server.rskeeps itsspawn_blockingshells around the legacyOsisRpcHandlertrait surface (still sync — out of scope), and the modern hot path viarpc2_adapterusesregister_async_methoddirectly.What changed
crates/osis/src/index/remote.rs—OsisIndexerfully async (tokio::sync::OnceCell+get_or_try_init). Public methodsindex_document/delete_document/search/try_initializeareasync fn. The ownedRuntime+ bridge-runtime shims are gone.crates/generator/src/rust/rust_osis.rs— emits every CRUD method (_new,_get,_set,_delete,_list,_list_full,_exists,_find) asasync fn; same for the_rpc_*dispatchers,handle_rpc,handle_rpc_call,handle_service_call. Indexer write-through inside_new/_set/_deletebecomes.await.crates/osis/src/rpc/server.rs—OsisAppRpcHandlertrait becomes async via RPITIT (impl Future + Send).OsisAppWrapper'sOsisRpcHandlerbridge usesHandle::current().block_on(...)— safe because the legacy dispatch calls it from insidespawn_blocking.crates/osis/src/rpc/rpc2_adapter.rs— switches fromregister_methodtoregister_async_methodand awaits the trait call directly. Zerospawn_blockingon the CRUD hot path.crates/osis/tests/indexer_smoke.rs— async smoke test (#[tokio::test]). The*_blockingshim regression test is gone.crates/osis_benches/benches/index_perf.rs— bench-ownedtokio::runtime::Runtimedrives the now-async CRUD methods viablock_on(criterion harness fns can't beasync fn). Reverts the #130 server-wrapping fixture back to the in-process tempdir model that existed pre-#130.examples/walkthrough/src/main.rs— hand-writtenOsisAppRpcHandlerimpl marked async to match the new trait shape.What stays
OsisRpcHandlertrait incrates/osis/src/rpc/handler.rsstays sync — modern production code goes throughrpc2_adapterand never crosses it.spawn_blockingaround the legacyhandler.handle_requestsites inserver.rs(lines ~905/992/1401/1870) stays — bridges async Axum router to the sync legacy trait.spawn_blockingaround service-method dispatch (lines ~1443/1920) stays. User-implemented service trait methods may legitimately be sync.Validation
cargo build --workspace— clean.cargo test -p hero_rpc_osis --features rpc --lib --tests— 85 lib + 1 smoke, all pass. Smoke degrades gracefully when hero_indexer is unreachable.cargo test -p hero_rpc_generator --lib— 140 generator self-tests pass.cargo test -p hero_rpc2 --lib— 11 hero_rpc2 tests pass.Release bench numbers weren't captured (validation worktree disk filled). Expect a small win vs #130 from the dropped
spawn_blockingoverhead.Closeout
Before this change the dispatch pipeline crossed sync<->async boundaries twice and panicked at the second crossing whenever a write-through to the hero_indexer fired from inside the jsonrpsee server's worker: RPC server (async) -> tokio::task::spawn_blocking <-- 1st boundary OSIS handler (SYNC) -> indexer.index_document() (sync facade) -> Handle::block_on(async sdk) <-- 2nd boundary, PANIC PR #130 hid the panic behind a thread-pool detour shim (`index_document_blocking` etc.) that crossed the same boundary on a worker thread the calling runtime didn't own. That worked but was a hack — every CRUD path still made two unnecessary sync<->async hops per request. This change makes the handler async end-to-end: RPC server (async) -> handler.handle_rpc_call_with_context().await <-- direct await OSIS handler (ASYNC) -> indexer.index_document().await <-- direct await Concretely: - `crates/generator/src/rust/rust_osis.rs` emits every CRUD method (`_new` / `_get` / `_set` / `_delete` / `_list` / `_list_full` / `_exists` / `_find`) and the JSON-RPC dispatcher arms (`_rpc_*`, `handle_rpc`, `handle_rpc_call`, `handle_service_call`) as `async fn`. Indexer write-through is now `.await` not a sync facade call. - `OsisAppRpcHandler` (the public trait every codegen-emitted impl satisfies) becomes async via RPITIT (`impl Future + Send`). - `crates/osis/src/rpc/rpc2_adapter.rs` switches from `register_method` to `register_async_method` and awaits the trait call directly — zero spawn_blocking on the CRUD hot path. Service- method dispatch follows the same shape (the user-implemented service trait method may itself be sync; the outer `handle_service_call` impl is async because it threads through the async `handle_rpc`). - `OsisAppWrapper`, the bridge from the (still-sync) legacy `OsisRpcHandler` trait to the new async `OsisAppRpcHandler`, uses `Handle::current().block_on(...)`. This is safe: the Axum- based legacy server in `osis/src/rpc/server.rs` already calls the wrapper from inside `tokio::task::spawn_blocking`, so by the time we land at the bridge we're on a tokio blocking worker thread — exactly the context `Handle::block_on` is designed for. - The legacy Axum server in `osis/src/rpc/server.rs` keeps its `spawn_blocking` shells around `OsisRpcHandler::handle_request` (legacy trait, stays sync) and around service-method dispatch (the user-trait surface stays sync). The new hot path through `rpc2_adapter` doesn't touch any of those. - Examples / benches / walkthrough: the in-process bench harness owns a `tokio::runtime::Runtime` and `block_on`s the now-async CRUD methods (criterion harness functions can't be `async fn`); the walkthrough's hand-written `OsisAppRpcHandler` impl is marked async; the committed-in generated example file gets its trait method signatures async-fied with sync-handle_rpc bridging. Net effect: every CRUD request from network closure to indexer socket is one continuous async stack with zero spawn_blocking hops and zero nested-runtime panics. PR #130's blocking shims and bridge runtime are gone.