perf(api): recent-first routing for get_transaction (default off) - #180
Conversation
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.
There was a problem hiding this comment.
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.
| const runSearch = (index: string) => fastify.elastic.search<any>({ | ||
| index, | ||
| size: _size, | ||
| query: {bool: {must: [{term: {trx_id: trxId}}]}}, | ||
| sort: {global_sequence: "asc"} | ||
| }); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
| response.lib = pResults[0].last_irreversible_block_num; | |
| response.lib = pResults[0]?.last_irreversible_block_num; |
There was a problem hiding this comment.
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_transactionconfig flag (defaultfalse) with schema validation support. - Update
get_transactionto 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; |
| 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.
|
Thanks @gemini-code-assist and @copilot — both issues addressed in ccfdab2:
|
|
Thanks for the update, @igorls. The changes in |
Problem
Without a
block_hint,get_transactionruns aterm: trx_idquery against<chain>-action-*— every action partition, including older/cold-tier shards. Atrx_idterm carries no block range, socan_matchcannot prune any shard: each one pays a term-dictionaryseekExact(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:
resolveHotIndices, reusinghot_first_window).<chain>-action-*set.Looking up a recent transaction never fans out to old shards. A
block_hintstill pins its single partition as before.Safety
api.hot_first_transaction(defaultfalse); behaviour-preserving when disabled.Also
pResults[1]→TypeError).e?.meta?.statusCode).Testing
tsc --noEmitclean.bun test tests/unit/— 121 pass (config validation accepts the new key;resolveHotIndicesalready covered byhot-index.test.ts).Ops
Enable with: