Skip to content

perf(api): recent-first routing for get_transaction (default off) - #180

Merged
igorls merged 2 commits into
devfrom
feat/get-transaction-recent-first
Jun 4, 2026
Merged

perf(api): recent-first routing for get_transaction (default off)#180
igorls merged 2 commits into
devfrom
feat/get-transaction-recent-first

Conversation

@igorls

@igorls igorls commented Jun 4, 2026

Copy link
Copy Markdown
Member

Problem

Without a block_hint, get_transaction runs a term: trx_id query against <chain>-action-* — every action partition, including older/cold-tier shards. A trx_id term carries no block range, so can_match cannot prune any shard: each one pays a term-dictionary seekExact (plus a decompress on older-codec segments). A steady stream of distinct trx_id lookups (no cache reuse) therefore loads the cold tier disproportionately.

Fix

All of a transaction's action documents share a single block, hence a single partition. So a recent-first probe is exact:

  1. Search the hot window first (resolveHotIndices, reusing hot_first_window).
  2. A non-empty result is already complete — return it (older tiers never touched).
  3. Only on a miss (an older or non-existent trx) widen to the full <chain>-action-* set.

Looking up a recent transaction never fans out to old shards. A block_hint still pins its single partition as before.

Safety

  • Opt-in via api.hot_first_transaction (default false); behaviour-preserving when disabled.
  • Correctness is unconditional: single-partition-per-trx guarantees a hot-window hit set is whole, and the widen-on-miss fallback covers everything else. If the resolver degrades to the wildcard, phase 1 is already the full search (no redundant query).
  • Residual: lookups for transactions older than the hot window (or non-existent ids) still widen to the full set on each miss; recent-first only optimizes the recent-lookup path.

Also

  • Propagate non-404 ES errors instead of dereferencing an undefined result (previously a non-404 error fell through to pResults[1]TypeError).
  • Guard the 404 check with optional chaining (e?.meta?.statusCode).

Testing

  • tsc --noEmit clean.
  • bun test tests/unit/ — 121 pass (config validation accepts the new key; resolveHotIndices already covered by hot-index.test.ts).

Ops

Enable with:

"hot_first_transaction": true,
"hot_first_window": 2

Without a block_hint, get_transaction issues a `term: trx_id` query against
`<chain>-action-*` — every action partition, including cold-tier shards. A
trx_id term has no block range, so can_match cannot prune any shard; each one
pays a term-dictionary seek (and a cold-codec decompress on old segments).
A stream of distinct trx_id lookups therefore pins the cold tier — the
dominant cold-node CPU sink observed on the WAX cluster.

All of a transaction's action documents share a single block, hence a single
partition. So a recent-first probe is exact: search the hot window first, and
only on a miss (an older or non-existent trx) widen to the full set — the only
path that can reach cold shards. A non-empty hit set is already complete.

Opt-in via api.hot_first_transaction (reuses hot_first_window); default off and
behaviour-preserving when disabled. block_hint still pins its single partition.

Also: propagate non-404 ES errors instead of dereferencing an undefined result,
and guard the 404 check with optional chaining.
Copilot AI review requested due to automatic review settings June 4, 2026 22:36

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a "recent-first" routing optimization for transaction lookups (get_transaction) via the new hot_first_transaction configuration. When enabled without a block_hint, the API first queries the hot index window before falling back to the full index pattern, reducing unnecessary fan-out to cold-tier shards. The review feedback highlights two critical issues: first, a 404 error during the phase-1 search when block_hint is absent can trigger a misleading error and halt execution instead of falling back; second, a potential crash (TypeError) could occur if $getInfo fails and resolves to null, which can be prevented using optional chaining.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +100 to +105
const runSearch = (index: string) => fastify.elastic.search<any>({
index,
size: _size,
query: {bool: {must: [{term: {trx_id: trxId}}]}},
sort: {global_sequence: "asc"}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If block_hint is not provided and hot_first_transaction is enabled, a 404 error from the phase-1 search (e.g., if the hot indices do not exist yet or are empty) will be caught by the outer try-catch block and return a misleading 'no data near block_hint' error, stopping the execution. Instead, we should treat a 404 as an empty result set (a miss) when block_hint is not present, allowing the query to fall back to the full pattern.

        const runSearch = (index: string) => fastify.elastic.search<any>({
            index,
            size: _size,
            query: {bool: {must: [{term: {trx_id: trxId}}]}},
            sort: {global_sequence: "asc"}
        }).catch(err => {
            if (err?.meta?.statusCode === 404 && !blockHint) {
                return { hits: { hits: [] } };
            }
            throw err;
        });

throw e;
}
hits = pResults[1].hits.hits;
response.lib = pResults[0].last_irreversible_block_num;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If $getInfo fails, it resolves to null (as seen in its catch block on line 43). This will cause a TypeError: Cannot read properties of null (reading 'last_irreversible_block_num') when attempting to read pResults[0].last_irreversible_block_num. Using optional chaining prevents this crash.

Suggested change
response.lib = pResults[0].last_irreversible_block_num;
response.lib = pResults[0]?.last_irreversible_block_num;

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Implements an opt-in “recent-first” Elasticsearch routing strategy for v2/history/get_transaction to avoid fanning out trx_id lookups across cold-tier shards when no block_hint is provided, while preserving correctness via a widen-on-miss fallback.

Changes:

  • Add api.hot_first_transaction config flag (default false) with schema validation support.
  • Update get_transaction to probe only the hot window indices first (when enabled) and widen to <chain>-action-* only on a miss.
  • Update the reference config JSON to include the new key.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/interfaces/hyperionConfig.ts Adds the new API config key and Zod schema validation for it.
src/api/routes/v2-history/get_transaction/get_transaction.ts Implements recent-first index resolution and widen-on-miss search; improves ES error propagation and 404 guarding.
references/config.ref.json Documents the new config key with default false.

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

throw e;
}
hits = pResults[1].hits.hits;
response.lib = pResults[0].last_irreversible_block_num;
Comment on lines +113 to 116
if (e?.meta?.statusCode === 404) {
response.error = 'no data near block_hint'
return response;
}
- Treat a phase-1 404 as a miss when there is no block_hint, so the recent-first
  probe falls back to the full pattern instead of returning a misleading
  "no data near block_hint" error. With a block_hint the 404 is still surfaced.
- Optional-chain pResults[0]?.last_irreversible_block_num — $getInfo resolves
  null on failure, which previously turned a recoverable lib lookup into a 500.
@igorls

igorls commented Jun 4, 2026

Copy link
Copy Markdown
Member Author

Thanks @gemini-code-assist and @copilot — both issues addressed in ccfdab2:

  1. Phase-1 404 with no block_hintrunSearch now catches a 404 and, when there is no block_hint, returns an empty hit set so the recent-first probe falls back to the full pattern instead of surfacing the misleading "no data near block_hint". With a block_hint the 404 is still real and surfaced by the outer catch (message now accurate, since that branch is only reachable with a hint).
  2. $getInfo resolving nullresponse.lib is now optional-chained (pResults[0]?.last_irreversible_block_num), so a recoverable get_info failure no longer 500s the lookup.

tsc clean, 121 unit tests pass.

@gemini-code-assist

Copy link
Copy Markdown

Thanks for the update, @igorls. The changes in ccfdab2 look solid and address the edge cases for the get_transaction recent-first routing correctly. The use of optional chaining for response.lib and the refined 404 handling in runSearch ensure that the API remains robust even when dealing with missing indices or recoverable failures. This approach effectively balances performance for recent lookups while maintaining correctness for older transactions.

Copilot finished work on behalf of igorls June 4, 2026 22:47
@igorls
igorls merged commit f36254f into dev Jun 4, 2026
3 checks passed
@igorls
igorls deleted the feat/get-transaction-recent-first branch June 4, 2026 22:51
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