Skip to content

Make RaindexClient Send+Sync on native targets#2489

Open
findolor wants to merge 6 commits into2026-03-03-raindex-client-localdb-refactorfrom
2026-03-03-raindex-client-send-sync
Open

Make RaindexClient Send+Sync on native targets#2489
findolor wants to merge 6 commits into2026-03-03-raindex-client-localdb-refactorfrom
2026-03-03-raindex-client-send-sync

Conversation

@findolor
Copy link
Collaborator

@findolor findolor commented Mar 3, 2026

Dependent PRs

Motivation

Following the local DB refactor in #2488, RaindexClient and its dependency graph (LocalDb, LocalDbState, LocalDbQueryExecutor) still used Rc/RefCell everywhere, making them !Send + !Sync on native targets. This blocks using RaindexClient across threads in CLI/server contexts.

Solution

1. LocalDbQueryExecutor trait — conditional Send bound

The trait now uses async_trait(?Send) on WASM and async_trait (with Send) on native, via #[cfg_attr].

2. LocalDbArc-based internals on native

  • FnPtr<T> alias: Rc<T> on WASM, Arc<T> on native
  • Function pointer types (ExecuteBatchFn, etc.) require Send + Sync on native
  • Two impl LocalDb blocks with platform-appropriate bounds

3. LocalDbState — thread-safe fields on native

  • db field: Rc<RefCell<...>> on WASM, Arc<Mutex<...>> on native
  • scheduler field: same split
  • Poison-safe lock access throughout (.ok(), if let Ok(...), .unwrap_or(false))

4. ClientRef type alias

  • Rc<RaindexClient> on WASM, Arc<RaindexClient> on native
  • Used across all data sources (orders, vaults, trades, add_orders, remove_orders)

5. WASM export isolation

  • clear_tables_wasm and get_sync_status moved into #[cfg(target_family = "wasm")] mod wasm blocks
  • JsCallbackExecutor module gated to WASM-only

6. Test updates

  • All LocalDbQueryExecutor mock impls updated with async_trait cfg gate
  • RefCellMutex, RcArc in test code

Checks

By submitting this for review, I'm confirming I've done the following:

  • made this PR as small as possible
  • unit-tested any new functionality
  • linked any relevant issues or PRs
  • included screenshots (if this involves a front-end change)

Summary by CodeRabbit

Release Notes

  • Refactor
    • Optimized WebAssembly and native build support with platform-specific concurrency models for improved performance and thread-safety across targets.
    • Improved database client reference management with unified interface abstractions for consistent behavior across WebAssembly and native environments.

findolor added 6 commits March 3, 2026 15:40
- Conditionally use async_trait(?Send) for WASM, async_trait for native
  on LocalDbQueryExecutor trait
- Add FnPtr<T> alias (Rc on WASM, Arc on native) for LocalDb internals
- Split LocalDb::new into WASM and native impl blocks with correct bounds
- Add ClientRef type alias (Rc on WASM, Arc on native)
- Gate JsCallbackExecutor module and impl to WASM-only
- Split db/scheduler fields: Rc<RefCell<...>> on WASM, Arc<Mutex<...>> on native
- Split LocalDbState::new into per-target impl blocks
- Use poison-safe lock access (ok()/if-let) in db(), mark_ready(),
  is_ready(), and Drop impl
- Replace Rc::new/Rc::clone with ClientRef::new/ClientRef::clone
- Update OrdersDataSource, VaultsDataSource traits to use async_trait
  cfg gate
- Update LocalDbOrders, LocalDbVaults to accept ClientRef
- Update RaindexOrder, RaindexVault to store ClientRef
- Move clear_tables_wasm into #[cfg(target_family = "wasm")] mod wasm
- Move get_sync_status into #[cfg(target_family = "wasm")] mod wasm
- Keep core logic functions ungated for both targets
- Apply async_trait cfg gate to all LocalDbQueryExecutor test impls
- Replace RefCell with Mutex in test executors
- Replace Rc with Arc in test code
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

Walkthrough

This PR introduces wasm/non-wasm specialization across the codebase. It replaces platform-agnostic synchronization primitives with conditional compilation: Rc/RefCell on single-threaded wasm and Arc/Mutex on multi-threaded native. It introduces a ClientRef type alias to abstract pointer types, updates async_trait attributes conditionally per target, and restructures LocalDb and LocalDbState with per-target constructors and field types.

Changes

Cohort / File(s) Summary
Conditional async_trait attributes (attribute-only changes)
crates/cli/src/commands/local_db/pipeline/bootstrap.rs, crates/common/src/local_db/query/executor.rs, crates/common/src/local_db/executor.rs, crates/common/src/local_db/pipeline/adapters/*.rs (4 files), crates/common/src/raindex_client/local_db/pipeline/bootstrap.rs, crates/common/src/raindex_client/local_db/pipeline/runner/*, crates/common/src/raindex_client/local_db/add_orders.rs, crates/common/src/raindex_client/remove_orders.rs
Replaced #[async_trait(?Send)] with conditional cfg_attr attributes: #[cfg_attr(target_family = "wasm", async_trait(?Send))] and #[cfg_attr(not(target_family = "wasm"), async_trait)]. No functional changes; purely conditional attribute application across ~13 files.
Mutex/Arc specialization in test infrastructure
crates/common/src/local_db/export.rs, crates/common/src/local_db/pipeline/engine.rs
Replaced RefCell with Mutex and updated access patterns from borrow()/borrow_mut() to lock().unwrap() in test fixtures. Changes struct fields MockExecutor.response and MockExecutor.statements from RefCell to Mutex.
ClientRef type alias and raindex_client unification
crates/common/src/raindex_client/mod.rs
Introduced new ClientRef type alias: Rc<RaindexClient> on wasm, Arc<RaindexClient> on non-wasm. Added conditional imports and updated non-wasm scheduler storage to Arc<Mutex<...>>.
Major LocalDb refactoring
crates/common/src/raindex_client/local_db/mod.rs
Introduced per-target function pointer type aliases (ExecuteBatchFn, QueryTextFn, etc.) with Send/Sync bounds only on non-wasm. Created FnPtr<T> wrapper macro expanding to Rc<T> (wasm) or Arc<T> (non-wasm). Changed LocalDb fields to use FnPtr wrapper. Added wasm-specific from_js_callback constructor. Specialized LocalDb::new with Send+Sync bounds only on non-wasm. Added public wasm executor module.
ClientRef propagation across orders/vaults
crates/common/src/raindex_client/orders.rs, crates/common/src/raindex_client/vaults.rs, crates/common/src/raindex_client/local_db/orders.rs, crates/common/src/raindex_client/local_db/vaults.rs, crates/common/src/raindex_client/trades.rs
Replaced Rc<RaindexClient> with ClientRef in struct fields, constructor parameters, and all clone sites across Orders and Vaults data sources. Updated from_local_db_order, try_from_sg_order, and similar converters to accept ClientRef.
LocalDbState wasm/non-wasm specialization
crates/common/src/raindex_client/local_db/state.rs
Conditionally defined struct fields: db and scheduler use Rc<RefCell<...>> on wasm, Arc<Mutex<...>> on non-wasm. Added per-target new constructors. Updated SyncReadiness with Rc<RefCell<...>> (wasm) or Arc<RwLock<...>> (non-wasm). Adjusted Drop and access methods (mark_ready, is_ready) per target. Updated test helpers with per-target scheduler types.
Wasm module scoping for exported functions
crates/common/src/raindex_client/local_db/query/clear_tables.rs, crates/common/src/raindex_client/local_db/query/fetch_last_synced_block.rs
Moved wasm-exported functions (clear_tables_wasm, get_sync_status) into #[cfg(target_family = "wasm")] modules. Adjusted imports and test references to JsValue from wasm_bindgen_utils within wasm modules. Functions remain publicly accessible via module path.
Conditional JsCallbackExecutor implementation
crates/common/src/raindex_client/local_db/executor.rs
Added #[cfg(target_family = "wasm")] gating to the entire impl LocalDbQueryExecutor for JsCallbackExecutor block, excluding it from non-wasm builds.
Registry non-wasm method
crates/js_api/src/registry.rs
Added new get_raindex_client_native method (non-wasm only) to DotrainRegistry, constructing a native RaindexClient with optional DB path. Gated with #[cfg(not(target_family = "wasm"))].

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main objective of the PR: making RaindexClient Send+Sync on native targets, which is the central theme across all file changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 2026-03-03-raindex-client-send-sync

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@findolor
Copy link
Collaborator Author

findolor commented Mar 3, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 3, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/common/src/raindex_client/local_db/mod.rs (1)

337-340: ⚠️ Potential issue | 🔴 Critical

Fix WASM test compile failure from missing RaindexClient import.

Line [391] uses RaindexClient without bringing it into scope, matching the current CI error.
Line [338] also has an unused TimeoutFuture import warning.

Suggested patch
 #[cfg(all(test, target_family = "wasm", feature = "browser-tests"))]
 mod wasm_tests {
     use super::*;
-    use gloo_timers::future::TimeoutFuture;
+    use crate::raindex_client::RaindexClient;
     use rain_orderbook_app_settings::spec_version::SpecVersion;

Also applies to: 391-393

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/common/src/raindex_client/local_db/mod.rs` around lines 337 - 340, Add
a missing import for RaindexClient and remove the unused TimeoutFuture import:
bring RaindexClient into scope (e.g., add a use statement referencing
RaindexClient so the code that references RaindexClient at lines ~391–393
compiles), and delete the unused use gloo_timers::future::TimeoutFuture; line
(or gate it behind a wasm-only cfg if it will be used conditionally) to silence
the unused-import warning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/common/src/raindex_client/local_db/state.rs`:
- Around line 91-96: The code currently swallows poisoned Mutex locks (e.g., in
the db() accessor and the other lock sites referenced) by mapping .lock().ok()
to None/false; instead recover the inner guard from a poisoned lock so local DB
routing and cleanup remain deterministic. Replace .lock().ok().and_then(|guard|
guard.as_ref().cloned()) with lock handling that matches on self.db.lock(): on
Ok(guard) use guard.as_ref().cloned(); on Err(poisoned) call
poisoned.into_inner() and then guard.as_ref().cloned(); apply the same pattern
to the other mutex accesses (the places around the other boolean/guard checks in
the file) so that poison does not silently fall back to subgraph or skip
scheduler stop. Ensure the logic still clones/returns LocalDb (or boolean) as
before after recovering the inner guard.

---

Outside diff comments:
In `@crates/common/src/raindex_client/local_db/mod.rs`:
- Around line 337-340: Add a missing import for RaindexClient and remove the
unused TimeoutFuture import: bring RaindexClient into scope (e.g., add a use
statement referencing RaindexClient so the code that references RaindexClient at
lines ~391–393 compiles), and delete the unused use
gloo_timers::future::TimeoutFuture; line (or gate it behind a wasm-only cfg if
it will be used conditionally) to silence the unused-import warning.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48b4d4d and 5c78970.

📒 Files selected for processing (26)
  • crates/cli/src/commands/local_db/pipeline/bootstrap.rs
  • crates/common/src/local_db/executor.rs
  • crates/common/src/local_db/export.rs
  • crates/common/src/local_db/pipeline/adapters/apply.rs
  • crates/common/src/local_db/pipeline/adapters/bootstrap.rs
  • crates/common/src/local_db/pipeline/adapters/tokens.rs
  • crates/common/src/local_db/pipeline/adapters/window.rs
  • crates/common/src/local_db/pipeline/engine.rs
  • crates/common/src/local_db/query/executor.rs
  • crates/common/src/raindex_client/add_orders.rs
  • crates/common/src/raindex_client/local_db/executor.rs
  • crates/common/src/raindex_client/local_db/mod.rs
  • crates/common/src/raindex_client/local_db/orders.rs
  • crates/common/src/raindex_client/local_db/pipeline/bootstrap.rs
  • crates/common/src/raindex_client/local_db/pipeline/runner/mod.rs
  • crates/common/src/raindex_client/local_db/pipeline/runner/scheduler/native.rs
  • crates/common/src/raindex_client/local_db/query/clear_tables.rs
  • crates/common/src/raindex_client/local_db/query/fetch_last_synced_block.rs
  • crates/common/src/raindex_client/local_db/state.rs
  • crates/common/src/raindex_client/local_db/vaults.rs
  • crates/common/src/raindex_client/mod.rs
  • crates/common/src/raindex_client/orders.rs
  • crates/common/src/raindex_client/remove_orders.rs
  • crates/common/src/raindex_client/trades.rs
  • crates/common/src/raindex_client/vaults.rs
  • crates/js_api/src/registry.rs

Comment on lines +91 to +96
fn db(&self) -> Option<LocalDb> {
self.db
.lock()
.ok()
.and_then(|guard| guard.as_ref().cloned())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not silently degrade behavior on poisoned locks.

On native, lock poison currently maps to None/false/no-op (Line 93, Line 193, Line 205), which can route queries to subgraph or skip scheduler stop after a panic. Prefer recovering with into_inner() so local DB routing and cleanup remain deterministic.

Suggested fix
     fn db(&self) -> Option<LocalDb> {
-        self.db
-            .lock()
-            .ok()
-            .and_then(|guard| guard.as_ref().cloned())
+        match self.db.lock() {
+            Ok(guard) => guard.as_ref().cloned(),
+            Err(poisoned) => poisoned.into_inner().as_ref().cloned(),
+        }
     }
@@
-                if let Ok(mut guard) = self.scheduler.lock() {
-                    if let Some(handle) = guard.take() {
-                        handle.stop();
-                    }
-                }
+                let mut guard = self
+                    .scheduler
+                    .lock()
+                    .unwrap_or_else(|poisoned| poisoned.into_inner());
+                if let Some(handle) = guard.take() {
+                    handle.stop();
+                }
             }
         }
@@
-        if let Ok(mut guard) = self.ready.write() {
-            guard.insert(chain_id);
-        }
+        let mut guard = self
+            .ready
+            .write()
+            .unwrap_or_else(|poisoned| poisoned.into_inner());
+        guard.insert(chain_id);
@@
-        return self
-            .ready
-            .read()
-            .map(|guard| guard.contains(&chain_id))
-            .unwrap_or(false);
+        let guard = self
+            .ready
+            .read()
+            .unwrap_or_else(|poisoned| poisoned.into_inner());
+        return guard.contains(&chain_id);

Based on learnings: when local DB is enabled and available in crates/common/src/raindex_client, it should be used exclusively without subgraph fallback, because mixing sources can cause inconsistencies.

Also applies to: 160-165, 193-206

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/common/src/raindex_client/local_db/state.rs` around lines 91 - 96, The
code currently swallows poisoned Mutex locks (e.g., in the db() accessor and the
other lock sites referenced) by mapping .lock().ok() to None/false; instead
recover the inner guard from a poisoned lock so local DB routing and cleanup
remain deterministic. Replace .lock().ok().and_then(|guard|
guard.as_ref().cloned()) with lock handling that matches on self.db.lock(): on
Ok(guard) use guard.as_ref().cloned(); on Err(poisoned) call
poisoned.into_inner() and then guard.as_ref().cloned(); apply the same pattern
to the other mutex accesses (the places around the other boolean/guard checks in
the file) so that poison does not silently fall back to subgraph or skip
scheduler stop. Ensure the logic still clones/returns LocalDb (or boolean) as
before after recovering the inner guard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant