Skip to content

Speed up get_unconfirmed_messages with denormalized first_confirmed_at#1132

Merged
odesenfans merged 1 commit into
mainfrom
perf/unconfirmed-messages-first-confirmed-at
May 13, 2026
Merged

Speed up get_unconfirmed_messages with denormalized first_confirmed_at#1132
odesenfans merged 1 commit into
mainfrom
perf/unconfirmed-messages-first-confirmed-at

Conversation

@odesenfans
Copy link
Copy Markdown
Collaborator

Summary

  • Replace the NOT EXISTS anti-join against message_confirmations + chain_txs in get_unconfirmed_messages with a direct check on messages.first_confirmed_at IS NULL. The column is already trigger-maintained (migration 0049), so no schema change is needed.
  • Drop the now-unused chain parameter and update the two callers (chains/ethereum.py, chains/nuls2.py) accordingly.

Performance

Measured against a production-sized dataset (~22k unconfirmed signed messages):

Before (anti-join): ~10s
After (denorm column, using existing ix_messages_first_confirmed): ~300ms cold, ~50ms warm. No new index needed.

Limit  (cost=26259.44..26260.69 rows=500 width=1275) (actual time=48.696..48.796 rows=500 loops=1)
  ->  Sort  (Sort Key: reception_time, Sort Method: top-N heapsort  Memory: 738kB)
        ->  Bitmap Heap Scan on messages (Recheck Cond: first_confirmed_at IS NULL, Filter: signature IS NOT NULL)
              ->  Bitmap Index Scan on ix_messages_first_confirmed (Index Cond: first_confirmed_at IS NULL)
Execution Time: 48.856 ms

Semantic note

The previous implementation filtered confirmations by chain (e.g. chain=Chain.ETH for the ETH packer). first_confirmed_at is set by a confirmation on any chain. In production only ETH confirmations are recorded, so the behavior is equivalent today. If multi-chain confirmations are reintroduced, the unconfirmed query will need to reconsider this.

Test plan

  • hatch run testing:test tests/db/test_messages.py::test_get_unconfirmed_messages tests/db/test_messages.py::test_get_unconfirmed_messages_trusted_messages -v
  • Spot-check ETH and NULS2 packer paths still call get_unconfirmed_messages without chain=.

Replace the NOT EXISTS anti-join against message_confirmations +
chain_txs with a direct check on messages.first_confirmed_at, which is
already populated by the trg_update_first_confirmed trigger added in
migration 0049. Query time on production-sized data drops from ~10s to
~50ms warm, using the existing ix_messages_first_confirmed index.

The chain parameter is dropped: it filtered by which chain confirmed a
message, but first_confirmed_at is set by any chain. In production only
ETH confirmations are recorded, so the two are equivalent today.
Copy link
Copy Markdown

@foxpatch-aleph foxpatch-aleph left a comment

Choose a reason for hiding this comment

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

⚠️ autodev could not generate a review for this PR (empty output from the model).

Click Re-request review to retry.

@odesenfans odesenfans requested a review from foxpatch-aleph May 13, 2026 13:28
Copy link
Copy Markdown

@foxpatch-aleph foxpatch-aleph left a comment

Choose a reason for hiding this comment

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

Clean, well-motivated refactor replacing a slow anti-join with a direct IS NULL check on the trigger-maintained first_confirmed_at column. Removes the now-unnecessary chain parameter and handles both callers. The semantic change (any chain's confirmation now counts) is actually more correct than the old chain-filtered behavior.

src/aleph/db/accessors/messages.py (line 421): Excellent simplification. The old branching logic between chain=None and chain= was a source of complexity for a feature (per-chain unconfirmed queries) that had no real use case.

@odesenfans odesenfans merged commit f479e02 into main May 13, 2026
4 checks passed
@odesenfans odesenfans deleted the perf/unconfirmed-messages-first-confirmed-at branch May 13, 2026 20:56
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.

2 participants