feat: relay snapshot DB reconcile for stale order status#67
Conversation
When listings expire or cancel on the daemon while Mostrix is offline, DB rows could stay non-terminal and skew My Trades. Fetch full relay snapshots (not pending-only), merge latest per order id, update local rows for terminal statuses via should_apply_status_transition, and keep the Orders tab on pending+currency filtered subset. Also rename create_seven_days_filter to create_mostro_list_fetch_filter with accurate docs and MOSTRO_LIST_FETCH_EVENT_LIMIT (no since window was ever applied). Co-authored-by: Cursor <cursoragent@cursor.com>
WalkthroughThreads SqlitePool into the fetch scheduler, replaces seven-day filters with an event-limit filter, adds aggregation/fetch helpers, introduces a relay-to-DB reconciliation module (batch + targeted ticks), updates startup/UI wiring to run reconciliation, and adds tests and a models helper for targeted IDs. ChangesOrder Reconciliation & Scheduler
Sequence DiagramsequenceDiagram
participant Client as Relay Client
participant Scheduler as Fetch Scheduler
participant Helper as Order Helpers
participant Reconcile as Reconciliation Module
participant DB as SQLite DB
Scheduler->>Helper: fetch_mostro_order_events(client, mostro_pubkey)
Helper->>Client: query events (create_mostro_list_fetch_filter)
Client-->>Helper: event batch
Helper-->>Scheduler: aggregated latest_by_id map
Scheduler->>Reconcile: reconcile_terminal_order_statuses_from_relay(pool, latest_map)
Reconcile->>DB: load/update orders by id
DB-->>Reconcile: update results
Reconcile-->>Scheduler: Ok(())
Scheduler->>Helper: pending_orders_for_book(latest_map)
Helper-->>Scheduler: Vec<SmallOrder>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/util/order_utils/helper.rs (1)
320-329:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPrefer terminal updates when two order events share the same second.
Line 325 only replaces the stored order when
new_ts > old_ts. Becausecreated_atis second-granularity here, apendingevent and a laterexpired/canceledevent emitted in the same second will keep whichever one was seen first. That can leave the snapshot non-terminal and bypass the DB reconcile this PR is adding.🔧 Localized fix
latest_by_id .entry(order_id) .and_modify(|existing| { let new_ts = order.created_at.unwrap_or(0); let old_ts = existing.created_at.unwrap_or(0); - if new_ts > old_ts { + let prefer_new = new_ts > old_ts + || (new_ts == old_ts + && matches!(order.status, Some(status) if is_terminal_trade_status(status)) + && !matches!(existing.status, Some(status) if is_terminal_trade_status(status))); + if prefer_new { *existing = order.clone(); } }) .or_insert(order);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/util/order_utils/helper.rs` around lines 320 - 329, The current update in latest_by_id only replaces on new_ts > old_ts, which leaves earlier-seen non-terminal events when two events share created_at second; change the logic inside the and_modify closure for latest_by_id (the order_id / existing / order handling) to also prefer terminal events when timestamps are equal: replace the condition with one that updates when new_ts > old_ts OR (new_ts == old_ts AND is_terminal(&order) && !is_terminal(existing)); add or reuse a small is_terminal(Order) helper if needed to detect terminal states (expired/canceled) so terminal events win ties even within the same second.src/ui/key_handler/async_tasks.rs (1)
215-229:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAwait relay→DB reconcile before re-hydrating active-order DM state.
This respawns the scheduler and then immediately calls
hydrate_startup_active_order_dm_state(pool), but the terminal-status reconcile now runs in the background insidespawn_fetch_scheduler_loops. After an offline period, these reload paths can still hydrate chats for orders that the relay already expired/canceled because the DB has not been reconciled yet. Startup was fixed by awaitingrun_relay_order_db_reconcile_once;apply_pending_fetch_scheduler_reloadandreload_runtime_session_after_reconnectneed the same sequencing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ui/key_handler/async_tasks.rs` around lines 215 - 229, Ensure the relay→DB reconciliation completes before rehydrating active-order DM state: call and await run_relay_order_db_reconcile_once(...) and only after it finishes invoke hydrate_startup_active_order_dm_state(pool).apply the same sequencing change to apply_pending_fetch_scheduler_reload and reload_runtime_session_after_reconnect so they await run_relay_order_db_reconcile_once(...) before calling any hydrate or reload logic; reference spawn_fetch_scheduler_loops, hydrate_startup_active_order_dm_state, run_relay_order_db_reconcile_once, apply_pending_fetch_scheduler_reload, and reload_runtime_session_after_reconnect to locate the spots to reorder.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/util/order_utils/relay_order_db_reconcile.rs`:
- Around line 210-213: The test currently only ensures
reconcile_terminal_order_statuses_from_relay(&pool,
&relay_latest).await.unwrap() doesn't panic; add a postcondition that queries
the DB via the same test pool to assert no rows were inserted or updated (e.g.,
fetch count or fetch orders by relay id and assert it equals 0 or equals the
original pre-reconcile state). Use the existing pool handle and relay_latest
identifiers in your assertion (call the repository/select helper used elsewhere
in tests or run a SELECT COUNT(*) on the orders table for relay_latest entries)
immediately after the reconcile call to verify no changes occurred.
- Around line 51-53: The current use of Order::get_by_id(pool,
&order_id.to_string()).await silently ignores all errors; change it to match the
Result so you only treat RowNotFound as the benign case and surface or log any
other DB errors. Specifically, replace the let Ok(...) else { return } pattern
by matching the result of Order::get_by_id, keep the early return for
RowNotFound, but for other Err(e) either return an Err/propagate the error or
call the reconciler logger (include e) so non-RowNotFound failures are not
swallowed; refer to Order::get_by_id, pool, order_id and the RowNotFound variant
when implementing this handling.
- Around line 78-82: The in-memory SQLite tests use async fn test_pool() which
currently calls SqlitePool::connect("sqlite::memory:") causing multiple pooled
connections to see different databases; change test_pool to create the pool with
SqlitePoolOptions::new().max_connections(1).connect("sqlite::memory:").await
(preserving the expect message) so the pool is limited to a single connection
and the tests (which call test_pool) will see the same in-memory DB.
---
Outside diff comments:
In `@src/ui/key_handler/async_tasks.rs`:
- Around line 215-229: Ensure the relay→DB reconciliation completes before
rehydrating active-order DM state: call and await
run_relay_order_db_reconcile_once(...) and only after it finishes invoke
hydrate_startup_active_order_dm_state(pool).apply the same sequencing change to
apply_pending_fetch_scheduler_reload and reload_runtime_session_after_reconnect
so they await run_relay_order_db_reconcile_once(...) before calling any hydrate
or reload logic; reference spawn_fetch_scheduler_loops,
hydrate_startup_active_order_dm_state, run_relay_order_db_reconcile_once,
apply_pending_fetch_scheduler_reload, and reload_runtime_session_after_reconnect
to locate the spots to reorder.
In `@src/util/order_utils/helper.rs`:
- Around line 320-329: The current update in latest_by_id only replaces on
new_ts > old_ts, which leaves earlier-seen non-terminal events when two events
share created_at second; change the logic inside the and_modify closure for
latest_by_id (the order_id / existing / order handling) to also prefer terminal
events when timestamps are equal: replace the condition with one that updates
when new_ts > old_ts OR (new_ts == old_ts AND is_terminal(&order) &&
!is_terminal(existing)); add or reuse a small is_terminal(Order) helper if
needed to detect terminal states (expired/canceled) so terminal events win ties
even within the same second.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e479ff5e-6ff5-41bd-86c5-aac620dcb43a
📒 Files selected for processing (9)
docs/CODING_STANDARDS.mdsrc/main.rssrc/ui/key_handler/async_tasks.rssrc/util/filters.rssrc/util/mod.rssrc/util/order_utils/fetch_scheduler.rssrc/util/order_utils/helper.rssrc/util/order_utils/mod.rssrc/util/order_utils/relay_order_db_reconcile.rs
There was a problem hiding this comment.
I reviewed the PR carefully, and I do see a blocker in the current reconciliation design.
Blocker
The new relay-to-DB reconcile path fixes the previous pending-only bug, but it now depends entirely on the global capped snapshot returned by create_mostro_list_fetch_filter().
That filter no longer uses a time window and instead applies only:
- author
- kind
MOSTRO_LIST_FETCH_EVENT_LIMIT = 500
Then both startup reconcile and periodic reconcile do:
- fetch latest global order events
- aggregate latest event per order id
- reconcile local DB rows from that bounded snapshot
The problem is that this makes local DB correctness for a given stale order depend on whether that order’s terminal event happens to be inside the relay’s most recent 500 order events overall.
So the PR fixes one class of stale-status bug, but it also means another perfectly valid stale local row can now remain unreconciled indefinitely simply because enough unrelated order traffic pushed its terminal event out of the global window.
I know the PR description mentions this as a limitation, but in this case it is not just a limitation of presentation. It directly affects the correctness of the new reconciliation mechanism, because the feature claim is specifically about aligning stale local rows with relay truth.
I think this needs a more targeted fetch path for the local rows that are actually candidates for reconciliation, instead of relying on a generic global capped list snapshot.
For example, reconciling local non-terminal rows by querying specific order identifiers (or another targeted relay lookup strategy) would avoid this false-negative window.
Because this can silently leave the DB in the exact stale state the PR is trying to solve, I am marking it as request changes.
|
I reviewed the PR in detail and left a formal request changes review. My main concern is not the specific Right now the local DB reconcile depends on a global capped relay snapshot ( That means a stale local order can still remain stale indefinitely if its terminal relay event falls outside the latest global window due to unrelated traffic. So even though this PR improves the previous |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Re-reviewed the latest changes.
The main blocker I raised is addressed now.
The PR no longer relies only on the global capped relay snapshot for DB correctness. It keeps the snapshot-based reconcile as a cheap broad pass, but adds a targeted per-order relay reconcile for local non-terminal rows, with bounded work per tick and round-robin progression.
That hybrid approach is the right tradeoff here:
- global snapshot for normal list refresh
- targeted fetches for eventual DB correctness on locally-stale trades
I also checked the updated selection logic for targeted candidates and the startup/scheduler wiring, and I do not see a new blocker in the revised design.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/util/order_utils/relay_order_db_reconcile.rs (1)
23-31: 💤 Low valueConsider
Ok(())removal — function is infallible.
reconcile_terminal_order_statuses_from_relayalways returnsOk(())becausereconcile_one_order_if_terminalis()-typed (errors are logged internally). TheResult<()>signature is misleading at call sites: callers?-propagate something that can't fail. Either make this()-returning or have the inner function propagate errors meaningfully (e.g., aggregate and surface counts of failures).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/util/order_utils/relay_order_db_reconcile.rs` around lines 23 - 31, The function reconcile_terminal_order_statuses_from_relay currently returns Result<()> but never fails because reconcile_one_order_if_terminal returns () and logs errors internally; change reconcile_terminal_order_statuses_from_relay to be infallible by updating its signature to return () (remove Result), delete the final Ok(()) and keep the for-loop calling reconcile_one_order_if_terminal, and then update any callers that currently use ? to stop propagating a Result (or handle accordingly). If you prefer surfacing failures instead, alternatively make reconcile_one_order_if_terminal return Result<()> and have reconcile_terminal_order_statuses_from_relay collect/propagate errors (e.g., aggregate failure count) so the Result return becomes meaningful.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/util/order_utils/relay_order_db_reconcile.rs`:
- Around line 23-31: The function reconcile_terminal_order_statuses_from_relay
currently returns Result<()> but never fails because
reconcile_one_order_if_terminal returns () and logs errors internally; change
reconcile_terminal_order_statuses_from_relay to be infallible by updating its
signature to return () (remove Result), delete the final Ok(()) and keep the
for-loop calling reconcile_one_order_if_terminal, and then update any callers
that currently use ? to stop propagating a Result (or handle accordingly). If
you prefer surfacing failures instead, alternatively make
reconcile_one_order_if_terminal return Result<()> and have
reconcile_terminal_order_statuses_from_relay collect/propagate errors (e.g.,
aggregate failure count) so the Result return becomes meaningful.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5e215d0e-c34c-45ca-8bf6-94ff5849a06f
📒 Files selected for processing (6)
src/main.rssrc/models.rssrc/util/order_utils/fetch_scheduler.rssrc/util/order_utils/helper.rssrc/util/order_utils/mod.rssrc/util/order_utils/relay_order_db_reconcile.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- src/util/order_utils/mod.rs
- src/util/order_utils/fetch_scheduler.rs
Summary
Fixes local SQLite
ordersrows staying non-terminal (e.g.pending) when Mostro already published expired / canceled (etc.) nostr order events while Mostrix was closed. The Orders tab reconcile previously fetched pending-only, so the latest terminal state from the relay was dropped before it could touch the DB.What changed
SmallOrderper order id (no status filter), then:should_apply_status_transition+Order::update_status);run_relay_order_db_reconcile_oncewhen relays are reachable, before user order chat hydration.SqlitePoolthroughstart_fetch_scheduler/spawn_fetch_scheduler_loops(main + key reload + scheduler reload + reconnect paths).create_seven_days_filterwithcreate_mostro_list_fetch_filterand document that there is nosince—only author + kind +MOSTRO_LIST_FETCH_EVENT_LIMIT(50).Limitations
Relay list snapshots are still capped by
MOSTRO_LIST_FETCH_EVENT_LIMIT; very old terminal updates might not appear in the fetched window until a targeted query exists.Breaking changes
create_seven_days_filterremoved frommostrix::util; usecreate_mostro_list_fetch_filterand/orMOSTRO_LIST_FETCH_EVENT_LIMIT.Summary by CodeRabbit
New Features
Refactor
Bug Fixes
Documentation
Tests