Skip to content

perf(daemon): fire-and-forget sendRealtimeTx#402

Open
andreabadesso wants to merge 3 commits intomasterfrom
perf/daemon-async-sendrealtimetx
Open

perf(daemon): fire-and-forget sendRealtimeTx#402
andreabadesso wants to merge 3 commits intomasterfrom
perf/daemon-async-sendrealtimetx

Conversation

@andreabadesso
Copy link
Copy Markdown
Collaborator

@andreabadesso andreabadesso commented Apr 17, 2026

Motivation

Tier 2 fix #8 from #395. The SQS publish for real-time wallet tx notifications was being awaited inside the event handler, blocking sync progress on network I/O for a best-effort notification. This PR makes it fire-and-forget, matching the pattern already used for invokeOnTxPushNotificationRequestedLambda right below it.

Branches directly off master — independent of the benchmarking stack (#396#400).

Acceptance Criteria

  • sendRealtimeTx call no longer awaited in the handleVertexAccepted path
  • Errors from the async publish are logged via .catch() (same behavior as before, just doesn't block the handler)
  • All existing unit tests pass
  • No change to payload sent to SQS — only the concurrency model

Change

-        try {
-          if (seenWallets.length > 0) {
-            await sendRealtimeTx(
-              Array.from(seenWallets),
-              txData,
-            );
-          }
-        } catch (e) {
-          logger.error('Failed to send transaction to SQS queue');
-          logger.error(e);
-        }
+        // Fire-and-forget: SQS publish is a best-effort real-time
+        // notification and must not block the handler. Matches the pattern
+        // used for invokeOnTxPushNotificationRequestedLambda below.
+        if (seenWallets.length > 0) {
+          sendRealtimeTx(Array.from(seenWallets), txData)
+            .catch((err: Error) => {
+              logger.error('Failed to send transaction to SQS queue');
+              logger.error(err);
+            });
+        }

Test mock note

Also adds sendRealtimeTx: jest.fn().mockResolvedValue(undefined) to the '../../src/utils' mock in services.test.ts. It was missing from that mock before — the old await sendRealtimeTx(...) inside a try/catch was silently swallowing a TypeError: sendRealtimeTx is not a function in the affected test paths. Fire-and-forget surfaces the crash, so the mock needs the function. No production code is affected by the mock change.

Savings

Per #395: ~5–30 ms when the notification actually fires (only for txs touching wallets). Doesn't apply to the common sync-from-scratch case.

Checklist

  • If you are requesting a merge into master, confirm this code is production-ready and can be included in future releases as soon as it gets merged
  • Make sure either the unit tests and/or the QA tests are capable of testing the new features (all 214 unit tests pass)
  • Make sure you do not include new dependencies in the project unless strictly necessary and do not include dev-dependencies as production ones. (no new deps)

🤖 Generated with Claude Code

Summary by CodeRabbit

Tests

  • Updated test mocks across multiple integration and service tests to properly handle asynchronous realtime transaction operations. Transitioned from synchronous mocks to resolved promises.

Refactor

  • Modified transaction publishing behavior to execute asynchronously and independently from the handler's control flow. This improves system responsiveness by decoupling message queue operations while maintaining error handling and logging.

Tier 2 fix #8 from #395. The SQS publish for real-time wallet tx
notifications was being awaited in the handler, blocking event
processing on network I/O for a best-effort notification.

Make it fire-and-forget with a .catch() for logging, matching the
existing pattern used for invokeOnTxPushNotificationRequestedLambda
right below it. The outer try/catch is no longer needed — errors from
the async publish now flow through the .catch() handler, and the block
has no other code that can throw synchronously.

Savings only applies when the handler actually has wallets to notify
(estimated 5-30 ms when it fires, per the issue).

Also adds sendRealtimeTx to the utils mock in services.test.ts — it
was previously undefined there and worked only because the prior
try/catch swallowed the TypeError; the fire-and-forget pattern surfaces
it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

The changes make sendRealtimeTx asynchronous and non-blocking in the handleVertexAccepted handler by removing the await statement and adding error handling via .catch. Test mocks are updated to return resolved Promises instead of synchronous mock functions to align with this async behavior.

Changes

Cohort / File(s) Summary
Test Mock Updates
packages/daemon/__tests__/integration/balances.test.ts, packages/daemon/__tests__/integration/token_created_hybrid_with_reorg.test.ts, packages/daemon/__tests__/integration/token_creation.test.ts, packages/daemon/__tests__/services/services.test.ts
Updated Jest mocks for sendRealtimeTx to return resolved Promises (mockResolvedValue(undefined)) instead of synchronous mock functions, enabling asynchronous test behavior.
Core Handler Implementation
packages/daemon/src/services/index.ts
Refactored handleVertexAccepted to call sendRealtimeTx without await, converting it to a fire-and-forget async operation with error handling via .catch that logs failures independently of the handler's main control flow.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

enhancement

Suggested reviewers

  • pedroferreira1
  • luislhl

Poem

🐰 twitches nose 🌙
Promises fire but don't await,
Error catching sets the fate—
Async chains now flow so free,
Tests confirm: "Promise to be!" ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf(daemon): fire-and-forget sendRealtimeTx' is fully related to the main change—converting an awaited, blocking call to sendRealtimeTx into a fire-and-forget pattern with asynchronous error handling to improve performance by eliminating network I/O blocking.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/daemon-async-sendrealtimetx

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.

The integration tests mocked sendRealtimeTx with plain jest.fn(), which
returns undefined. The old production code awaited the call inside a
try/catch, so the undefined return was harmless (await undefined ≡
await Promise.resolve(undefined)). After the fire-and-forget refactor,
the code calls `.catch()` on the return value — which throws
TypeError: Cannot read properties of undefined (reading 'catch') when
the mock returns undefined instead of a Promise.

Without the old try/catch to swallow the error, this crashed
handleVertexAccepted, the sync hung, and integration tests timed out
after 30s.

The production sendRealtimeTx is `async` so it always returns a
Promise; the fix is just to make the mocks match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR removes an await on sendRealtimeTx within handleVertexAccepted so the daemon doesn’t block event processing on best-effort SQS publishing, while still logging async failures via .catch().

Changes:

  • Convert sendRealtimeTx to fire-and-forget in handleVertexAccepted, preserving error logging via .catch()
  • Update unit/integration test mocks so sendRealtimeTx is a resolving async mock (avoids runtime TypeError and matches new async usage)

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/daemon/src/services/index.ts Makes realtime SQS publish non-blocking and logs async failures without stalling the handler
packages/daemon/tests/services/services.test.ts Adds sendRealtimeTx to the ../../src/utils mock as an async-resolving function
packages/daemon/tests/integration/token_creation.test.ts Updates ../../src/utils/aws mock so sendRealtimeTx resolves (matches fire-and-forget async usage)
packages/daemon/tests/integration/token_created_hybrid_with_reorg.test.ts Same: ensure sendRealtimeTx mock resolves as a promise
packages/daemon/tests/integration/balances.test.ts Same: ensure sendRealtimeTx mock resolves as a promise

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
packages/daemon/src/services/index.ts (1)

531-540: Consider surfacing SQS publish failures beyond logs.

With the await removed, failures in sendRealtimeTx no longer affect the handler or state machine — they're only visible as log lines. If real-time tx delivery is user-impacting, consider emitting a metric/counter on .catch (or a span event on the active tracer) so that a sustained SQS outage is observable beyond grepping logs. Also, on shutdown, any in-flight publish is dropped silently; that's the accepted trade-off per the PR description, but worth keeping in mind for graceful shutdown draining if this becomes a hot path.

No behavioral issue with the change itself — the async declaration of sendRealtimeTx ensures any synchronous throw (e.g., the NEW_TX_SQS guard) becomes a rejection and is caught by .catch.

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

In `@packages/daemon/src/services/index.ts` around lines 531 - 540, The
fire-and-forget call to sendRealtimeTx(Array.from(seenWallets), txData) only
logs errors on rejection; update the .catch handler to also emit an
observability signal (e.g., increment a failure metric/counter or add a
span/event to the active tracer) so sustained SQS publish failures are visible
beyond logs; locate the sendRealtimeTx invocation and its .catch block and call
your metrics client or tracer API there (and include contextual tags like
seenWallets length and txData identifiers) while preserving the non-blocking
behavior and existing logger.error calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/daemon/src/services/index.ts`:
- Around line 531-540: The fire-and-forget call to
sendRealtimeTx(Array.from(seenWallets), txData) only logs errors on rejection;
update the .catch handler to also emit an observability signal (e.g., increment
a failure metric/counter or add a span/event to the active tracer) so sustained
SQS publish failures are visible beyond logs; locate the sendRealtimeTx
invocation and its .catch block and call your metrics client or tracer API there
(and include contextual tags like seenWallets length and txData identifiers)
while preserving the non-blocking behavior and existing logger.error calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8e6aba5b-129b-4d9d-886f-c2bfffda20d3

📥 Commits

Reviewing files that changed from the base of the PR and between 0525665 and 36187b1.

📒 Files selected for processing (5)
  • packages/daemon/__tests__/integration/balances.test.ts
  • packages/daemon/__tests__/integration/token_created_hybrid_with_reorg.test.ts
  • packages/daemon/__tests__/integration/token_creation.test.ts
  • packages/daemon/__tests__/services/services.test.ts
  • packages/daemon/src/services/index.ts

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.

4 participants