Skip to content

Shared RaindexClient: eliminate per-request thread spawning#58

Open
findolor wants to merge 5 commits intomainfrom
shared-raindex-client
Open

Shared RaindexClient: eliminate per-request thread spawning#58
findolor wants to merge 5 commits intomainfrom
shared-raindex-client

Conversation

@findolor
Copy link
Collaborator

@findolor findolor commented Mar 3, 2026

Dependent PRs

Motivation

Every API request previously spawned a new OS thread + Tokio runtime to create a fresh RaindexClient via run_with_client(). This was necessary because the old RaindexClient used Rc/RefCell internally and wasn't Send+Sync.

The upstream rain.orderbook now makes RaindexClient Send+Sync on native (uses Arc<Mutex<...>>), and new_native() starts the sync scheduler internally. This lets us create a single shared client at startup and pass it to all handlers via Rocket state.

Solution

  • Bump submodule to 5c78970c25 which provides Send+Sync RaindexClient
  • Rewrite RaindexProvider to hold a shared RaindexClient created at startup. Thread spawning is kept only for construction (DotrainRegistry future is not Send due to JsValue)
  • Gut sync.rs — sync is now handled internally by RaindexClient::new_native()
  • Make data source traits Send+Sync — change async_trait(?Send) to async_trait on OrderDataSource and SwapDataSource
  • Simplify all route handlers — replace run_with_client pattern with direct raindex.client() access
  • Update tests — remove tests for per-request client init failures (no longer possible)

Net result: -401 lines, +115 lines across 18 files. Local benchmarks show ~2s response times (quote/calldata), consistent with local DB performance.

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

  • Refactor

    • Reorganized orderbook client initialization flow for improved efficiency and cleaner architecture.
    • Streamlined data source construction patterns across order, swap, and trade operations.
  • Chores

    • Updated internal infrastructure and thread-safety requirements for data handling.
    • Adjusted test suite to align with refactored client initialization patterns.

findolor added 5 commits March 3, 2026 17:05
RaindexClient is now Send+Sync on native (uses Arc<Mutex<...>>
internally) and new_native() starts the sync scheduler internally.
Replace per-request thread spawning with a single client created at
startup. Thread spawning is kept only for construction (DotrainRegistry
future is not Send due to JsValue). Sync is now handled internally by
RaindexClient::new_native(), so sync.rs is gutted.
Change async_trait(?Send) to async_trait and add Send + Sync bounds
on OrderDataSource and SwapDataSource traits, since RaindexClient is
now Send+Sync and handlers no longer need non-Send futures.
Replace run_with_client pattern with direct raindex.client() access
in all route handlers. Remove per-request thread spawning overhead.
Remove tests for per-request client init failures (no longer possible
since client is created eagerly at startup). Update mock helpers.
@findolor findolor self-assigned this Mar 3, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

This PR refactors RaindexProvider to initialize and own a RaindexClient directly during load(), replacing the previous async run_with_client() helper pattern with direct client() accessor methods. The load() signature now accepts an optional db_path parameter. Trait bounds for OrderDataSource and SwapDataSource now require Send+Sync. The start_sync() function is removed as sync is now handled internally by RaindexClient.

Changes

Cohort / File(s) Summary
Submodule Update
lib/rain.orderbook
Submodule pointer updated to new commit; no observable behavioral changes.
RaindexProvider Core Architecture
src/raindex/config.rs
RaindexProvider now owns a RaindexClient field initialized during load(). Load signature extended to accept optional db_path parameter. Removed set_db_path() and run_with_client() methods; added client() and db_path() accessors. Error handling simplified with RegistryLoad, ClientInit, and WorkerPanicked variants.
Initialization & Setup
src/main.rs, src/test_helpers.rs
Updated RaindexProvider::load() calls to pass optional db_path parameter (or None in tests). Removed intermediate raindex_config setup steps.
Trait Definitions
src/routes/order/mod.rs, src/routes/swap/mod.rs
Added Send+Sync trait bounds to OrderDataSource and SwapDataSource. Updated async_trait attributes from (?Send) to default on trait and all implementations (RaindexOrderDataSource, MockOrderDataSource, RaindexSwapDataSource, MockSwapDataSource).
Order Route Handlers
src/routes/order/cancel.rs, src/routes/order/get_order.rs, src/routes/order/deploy_solver.rs, src/routes/order/deploy_dca.rs
Replaced run_with_client() wrapper with direct raindex.client() calls to construct data sources. Simplified error handling and control flow. deploy_solver and deploy_dca now contain todo!() placeholders instead of functional implementations.
Swap Route Handlers
src/routes/swap/calldata.rs, src/routes/swap/quote.rs
Migrated from run_with_client() pattern to direct client() accessor for RaindexSwapDataSource construction. Changed client field ownership from borrowed reference to owned object. Removed client initialization failure test cases.
Utility & Admin Routes
src/routes/admin.rs, src/routes/orders.rs, src/routes/trades.rs, src/routes/tests.rs
Admin route updated to pass db_path to RaindexProvider::load(). Order and trade routes replaced with todo!() placeholders. Test contract handler renamed from run_with_client_contract to shared_client_contract with updated endpoint and implementation.
Sync Infrastructure
src/sync.rs
Removed public start_sync() function and associated test module; sync is now handled internally by RaindexClient::new_native().

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • 0xgleb
  • hardyjosh

Poem

🐰 A hop through refactor, a bound to be free,
Send and Sync traits dance harmoniously,
No more run_with_client's tangled embrace,
Just client() calls keep the code at pace!
RaindexProvider now holds its own key, 🗝️

🚥 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 PR title 'Shared RaindexClient: eliminate per-request thread spawning' is clear, concise, and directly reflects the main objective of the PR—consolidating RaindexClient into a single shared instance to eliminate per-request thread spawning.
Docstring Coverage ✅ Passed Docstring coverage is 94.29% 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 shared-raindex-client

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.

Copy link

@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: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main.rs`:
- Around line 237-242: Before calling raindex::RaindexProvider::load (the block
that assigns raindex_config), validate local_db_path: ensure it is either an
absolute path or includes a directory component (i.e.,
Path::new(&local_db_path).is_absolute() ||
Path::new(&local_db_path).parent().and_then(|p| p.to_str()).filter(|s|
!s.is_empty()).is_some()). If the check fails (bare filename like "local.db"),
return or propagate an error / log and exit prior to invoking
RaindexProvider::load so the code does not forward invalid paths; reference the
raindex_config assignment, the local_db_path variable, and
raindex::RaindexProvider::load when adding the guard.

In `@src/raindex/config.rs`:
- Around line 14-20: The load function should reject bare filenames for db_path
at load-time: in pub(crate) async fn load(...), if let Some(db) =
db_path.clone() { check db.parent() and ensure it is Some and not empty (i.e.,
path has a directory prefix like "./data/local.db" or "db/local.db"); if parent
is None or empty, return an Err(RaindexProviderError::InvalidConfig(...)) (or
add a new RaindexProviderError variant) with a clear message so consumers cannot
pass bare "local.db". Implement this validation early in load before using db.

In `@src/routes/order/deploy_dca.rs`:
- Around line 34-35: The handler currently calls todo!() after acquiring
_raindex (shared_raindex.read().await) which panics and bypasses the ApiError
contract; replace the todo!() with proper non-panicking error handling that
returns a Result with an ApiError variant (e.g.,
Err(ApiError::InternalServerError or a more specific ApiError::BadRequest)
rather than panicking), ensure the function (deploy_dca handler) returns the
expected Result type, and propagate or map any errors from shared_raindex access
or subsequent logic into ApiError so all failures go through the ApiError enum.

In `@src/routes/order/deploy_solver.rs`:
- Around line 34-35: The handler currently calls todo!() (after awaiting
shared_raindex.read().await) which panics on every /v1/order/solver request;
replace the panic with a proper ApiError return so the error flows through typed
API error handling. Specifically, remove todo!() and return an
Err(ApiError::...) from the handler (e.g., ApiError::Internal or a more specific
variant like ServiceUnavailable) with a clear message such as "solver endpoint
not implemented" and include any context you want from shared_raindex; ensure
the function signature (the route handler that awaited
shared_raindex.read().await) still returns a Result so the Err variant is used
instead of panicking.

In `@src/routes/orders.rs`:
- Around line 37-38: The handlers currently call todo!() after acquiring the
read lock on shared_raindex (let _raindex = shared_raindex.read().await) which
panics and breaks the endpoints; replace todo!() in both order handlers with
real logic that uses the acquired read guard (use the variable name e.g.,
raindex) to perform the lookup/operation and return Result<..., ApiError>,
mapping any failure to the appropriate ApiError variant instead of panicking or
returning raw status codes; ensure all error paths convert errors into ApiError
(via ? or map_err) and that the handler signatures return the ApiError-based
Response type so the endpoints return typed API errors rather than panics.

In `@src/routes/trades.rs`:
- Around line 37-38: The handlers for /v1/trades/tx/{tx_hash} and
/v1/trades/{address} currently call todo!() and thus panic; replace the todo!()
in the blocks that acquire shared_raindex.read().await with real logic that
queries the raindex and returns results or an ApiError variant instead of
panicking. Specifically, in the function(s) handling tx_hash and address use the
read guard from shared_raindex to perform the lookup, map a missing result to
the appropriate ApiError (e.g., ApiError::NotFound or the project's canonical
error variant) and map any internal errors to ApiError::Internal or
ApiError::ServiceUnavailable as appropriate, then return Ok(response) on
success; ensure every failure path returns Err(ApiError::...) so no raw status
codes or panics remain.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6e8108 and 83d7dfb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • lib/rain.orderbook
  • src/main.rs
  • src/raindex/config.rs
  • src/routes/admin.rs
  • src/routes/order/cancel.rs
  • src/routes/order/deploy_dca.rs
  • src/routes/order/deploy_solver.rs
  • src/routes/order/get_order.rs
  • src/routes/order/mod.rs
  • src/routes/orders.rs
  • src/routes/swap/calldata.rs
  • src/routes/swap/mod.rs
  • src/routes/swap/quote.rs
  • src/routes/tests.rs
  • src/routes/trades.rs
  • src/sync.rs
  • src/test_helpers.rs

Comment on lines +237 to +242
let raindex_config = match raindex::RaindexProvider::load(
&registry_url,
Some(local_db_path),
)
.await
{
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Enforce explicit local_db_path format before loading Raindex provider.

Line 237 now always forwards local_db_path into RaindexProvider::load, but bare filenames (e.g., local.db) are still not rejected. Add an explicit validation guard before this call.

🔧 Proposed fix
 let local_db_path = std::path::PathBuf::from(&cfg.local_db_path);
+let has_directory_prefix = local_db_path
+    .parent()
+    .map(|p| !p.as_os_str().is_empty())
+    .unwrap_or(false);
+if !has_directory_prefix {
+    tracing::error!(
+        path = %cfg.local_db_path,
+        "local_db_path must include a directory prefix (e.g. data/raindex.db)"
+    );
+    drop(log_guard);
+    std::process::exit(1);
+}
 if let Some(parent) = local_db_path.parent() {

Based on learnings: In ST0x-Technology/st0x.rest.api, configuration-related Rust code should use DB paths with a directory prefix (e.g., data/raindex.db) or absolute path, never bare filenames like local.db.

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

In `@src/main.rs` around lines 237 - 242, Before calling
raindex::RaindexProvider::load (the block that assigns raindex_config), validate
local_db_path: ensure it is either an absolute path or includes a directory
component (i.e., Path::new(&local_db_path).is_absolute() ||
Path::new(&local_db_path).parent().and_then(|p| p.to_str()).filter(|s|
!s.is_empty()).is_some()). If the check fails (bare filename like "local.db"),
return or propagate an error / log and exit prior to invoking
RaindexProvider::load so the code does not forward invalid paths; reference the
raindex_config assignment, the local_db_path variable, and
raindex::RaindexProvider::load when adding the guard.

Comment on lines +14 to +20
pub(crate) async fn load(
registry_url: &str,
db_path: Option<PathBuf>,
) -> Result<Self, RaindexProviderError> {
let url = registry_url.to_string();
let (tx, rx) = tokio::sync::oneshot::channel::<Result<DotrainRegistry, WorkerError>>();
let db = db_path.clone();

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate db_path format at load-time.

Line [16] currently accepts bare filenames (for example local.db), which breaks this repo’s configuration-path convention.

Suggested validation guard
 impl RaindexProvider {
     pub(crate) async fn load(
         registry_url: &str,
         db_path: Option<PathBuf>,
     ) -> Result<Self, RaindexProviderError> {
+        if let Some(path) = db_path.as_ref() {
+            let has_parent_dir = path
+                .parent()
+                .is_some_and(|parent| !parent.as_os_str().is_empty());
+            if !has_parent_dir {
+                return Err(RaindexProviderError::ClientInit(
+                    "db_path must include a directory prefix (e.g., data/raindex.db)".into(),
+                ));
+            }
+        }
+
         let url = registry_url.to_string();
         let db = db_path.clone();

Based on learnings: "In ST0x-Technology/st0x.rest.api, Rust source files that deal with configuration should specify database paths with a directory prefix ... never use bare filenames like local.db."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub(crate) async fn load(
registry_url: &str,
db_path: Option<PathBuf>,
) -> Result<Self, RaindexProviderError> {
let url = registry_url.to_string();
let (tx, rx) = tokio::sync::oneshot::channel::<Result<DotrainRegistry, WorkerError>>();
let db = db_path.clone();
pub(crate) async fn load(
registry_url: &str,
db_path: Option<PathBuf>,
) -> Result<Self, RaindexProviderError> {
if let Some(path) = db_path.as_ref() {
let has_parent_dir = path
.parent()
.is_some_and(|parent| !parent.as_os_str().is_empty());
if !has_parent_dir {
return Err(RaindexProviderError::ClientInit(
"db_path must include a directory prefix (e.g., data/raindex.db)".into(),
));
}
}
let url = registry_url.to_string();
let db = db_path.clone();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/raindex/config.rs` around lines 14 - 20, The load function should reject
bare filenames for db_path at load-time: in pub(crate) async fn load(...), if
let Some(db) = db_path.clone() { check db.parent() and ensure it is Some and not
empty (i.e., path has a directory prefix like "./data/local.db" or
"db/local.db"); if parent is None or empty, return an
Err(RaindexProviderError::InvalidConfig(...)) (or add a new RaindexProviderError
variant) with a clear message so consumers cannot pass bare "local.db".
Implement this validation early in load before using db.

Comment on lines +34 to +35
let _raindex = shared_raindex.read().await;
todo!()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Replace todo!() in this handler before merge.

Line [35] unconditionally panics, so /v1/order/dca is effectively broken and bypasses the ApiError contract.

Immediate non-panicking fallback
-        let _raindex = shared_raindex.read().await;
-        todo!()
+        let _raindex = shared_raindex.read().await;
+        tracing::error!("DCA deploy handler is not implemented");
+        Err(ApiError::Internal(
+            "dca deployment is not implemented".into(),
+        ))

As per coding guidelines: "All API errors must go through the ApiError enum, never return raw status codes."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let _raindex = shared_raindex.read().await;
todo!()
let _raindex = shared_raindex.read().await;
tracing::error!("DCA deploy handler is not implemented");
Err(ApiError::Internal(
"dca deployment is not implemented".into(),
))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/order/deploy_dca.rs` around lines 34 - 35, The handler currently
calls todo!() after acquiring _raindex (shared_raindex.read().await) which
panics and bypasses the ApiError contract; replace the todo!() with proper
non-panicking error handling that returns a Result with an ApiError variant
(e.g., Err(ApiError::InternalServerError or a more specific
ApiError::BadRequest) rather than panicking), ensure the function (deploy_dca
handler) returns the expected Result type, and propagate or map any errors from
shared_raindex access or subsequent logic into ApiError so all failures go
through the ApiError enum.

Comment on lines +34 to +35
let _raindex = shared_raindex.read().await;
todo!()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

todo!() in the live request path is a blocker.

Line [35] panics on every /v1/order/solver request and bypasses typed API error handling.

Immediate non-panicking fallback
-        let _raindex = shared_raindex.read().await;
-        todo!()
+        let _raindex = shared_raindex.read().await;
+        tracing::error!("Solver deploy handler is not implemented");
+        Err(ApiError::Internal(
+            "solver deployment is not implemented".into(),
+        ))

As per coding guidelines: "All API errors must go through the ApiError enum, never return raw status codes."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let _raindex = shared_raindex.read().await;
todo!()
let _raindex = shared_raindex.read().await;
tracing::error!("Solver deploy handler is not implemented");
Err(ApiError::Internal(
"solver deployment is not implemented".into(),
))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/order/deploy_solver.rs` around lines 34 - 35, The handler
currently calls todo!() (after awaiting shared_raindex.read().await) which
panics on every /v1/order/solver request; replace the panic with a proper
ApiError return so the error flows through typed API error handling.
Specifically, remove todo!() and return an Err(ApiError::...) from the handler
(e.g., ApiError::Internal or a more specific variant like ServiceUnavailable)
with a clear message such as "solver endpoint not implemented" and include any
context you want from shared_raindex; ensure the function signature (the route
handler that awaited shared_raindex.read().await) still returns a Result so the
Err variant is used instead of panicking.

Comment on lines +37 to +38
let _raindex = shared_raindex.read().await;
todo!()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

todo!() in both orders handlers is a blocker.

Lines [38] and [73] panic on every request, so both endpoints are effectively down and no longer return typed API errors.

As per coding guidelines: "All API errors must go through the ApiError enum, never return raw status codes."

Also applies to: 72-73

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

In `@src/routes/orders.rs` around lines 37 - 38, The handlers currently call
todo!() after acquiring the read lock on shared_raindex (let _raindex =
shared_raindex.read().await) which panics and breaks the endpoints; replace
todo!() in both order handlers with real logic that uses the acquired read guard
(use the variable name e.g., raindex) to perform the lookup/operation and return
Result<..., ApiError>, mapping any failure to the appropriate ApiError variant
instead of panicking or returning raw status codes; ensure all error paths
convert errors into ApiError (via ? or map_err) and that the handler signatures
return the ApiError-based Response type so the endpoints return typed API errors
rather than panics.

Comment on lines +37 to +38
let _raindex = shared_raindex.read().await;
todo!()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Both trades handlers panic unconditionally (todo!()), which is a release blocker.

Lines [38] and [73] make /v1/trades/tx/{tx_hash} and /v1/trades/{address} non-functional and bypass ApiError handling.

As per coding guidelines: "All API errors must go through the ApiError enum, never return raw status codes."

Also applies to: 72-73

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

In `@src/routes/trades.rs` around lines 37 - 38, The handlers for
/v1/trades/tx/{tx_hash} and /v1/trades/{address} currently call todo!() and thus
panic; replace the todo!() in the blocks that acquire
shared_raindex.read().await with real logic that queries the raindex and returns
results or an ApiError variant instead of panicking. Specifically, in the
function(s) handling tx_hash and address use the read guard from shared_raindex
to perform the lookup, map a missing result to the appropriate ApiError (e.g.,
ApiError::NotFound or the project's canonical error variant) and map any
internal errors to ApiError::Internal or ApiError::ServiceUnavailable as
appropriate, then return Ok(response) on success; ensure every failure path
returns Err(ApiError::...) so no raw status codes or panics remain.

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