From 9bbc18a57d17f69dc2a8f5eb0c8950fc4cf84293 Mon Sep 17 00:00:00 2001 From: NayiemW Date: Fri, 15 May 2026 11:26:45 +0200 Subject: [PATCH] api: estimate counts by default, cache /blockchain burn totals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit estimateTotalCount in defaults previously evaluated to false when CORE_API_ESTIMATED_TOTAL_COUNT was unset, which routed every list endpoint through a real COUNT(*) inside a REPEATABLE READ transaction on the api connection. statement_timeout (3s) kills COUNT(*) on a mainnet-sized blocks or transactions table; the catch returns totalCount: 0 with an empty data array under a 200, so clients see silently empty results. Flip the default to true and treat the env var as an explicit opt-out (=false). BlockchainController.index calls two unbounded SUMs over transactions per request, each ~30s on mainnet. Under explorer polling these pile up holding api connections (load average 8+ observed under load). Cache the result in-process with a 10-minute TTL and a single-flight background refresh — values only change on blocks carrying burned fees or burn transactions, and the endpoint's main consumers are dashboards where that staleness is fine. --- packages/api/src/controllers/blockchain.ts | 43 +++++++++++++++++++++- packages/api/src/defaults.ts | 2 +- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/api/src/controllers/blockchain.ts b/packages/api/src/controllers/blockchain.ts index 65a8b39b2..3aa39fd75 100644 --- a/packages/api/src/controllers/blockchain.ts +++ b/packages/api/src/controllers/blockchain.ts @@ -8,6 +8,28 @@ import { WalletSearchResource } from "../resources-new"; import { WalletSearchService } from "../services"; import { Controller } from "./controller"; +// Burn totals only change when a new block carrying burned fees or burn +// transactions is forged, so recomputing them on every /blockchain request +// is wasteful — both queries are unbounded SUMs over the transactions table +// and take tens of seconds on a mature mainnet. Cache the result and refresh +// lazily in the background; a stale-but-fresh-on-read pattern is fine for an +// endpoint whose primary consumers are explorer dashboards. +interface BurnCache { + fees: string; + transactions: string; + ts: number; + refreshing: Promise | null; +} + +const burnCache: BurnCache = { + fees: "0", + transactions: "0", + ts: 0, + refreshing: null, +}; + +const BURN_CACHE_TTL_MS = 10 * 60 * 1000; + export class BlockchainController extends Controller { @Container.inject(Container.Identifiers.BlockHistoryService) @Container.tagged("connection", "api") @@ -34,8 +56,25 @@ export class BlockchainController extends Controller { public async index(_: Hapi.Request, h: Hapi.ResponseToolkit) { const { data } = this.stateStore.getLastBlock(); - const fees = Utils.BigNumber.make(await this.transactionRepository.getFeesBurned()); - const transactions = Utils.BigNumber.make(await this.transactionRepository.getBurnTransactionTotal()); + if (Date.now() - burnCache.ts > BURN_CACHE_TTL_MS && !burnCache.refreshing) { + const repo = this.transactionRepository; + burnCache.refreshing = (async () => { + try { + const [feesRaw, txRaw] = await Promise.all([ + repo.getFeesBurned(), + repo.getBurnTransactionTotal(), + ]); + burnCache.fees = String(feesRaw); + burnCache.transactions = String(txRaw); + burnCache.ts = Date.now(); + } finally { + burnCache.refreshing = null; + } + })(); + } + + const fees = Utils.BigNumber.make(burnCache.fees); + const transactions = Utils.BigNumber.make(burnCache.transactions); const total = fees.plus(transactions); return { diff --git a/packages/api/src/defaults.ts b/packages/api/src/defaults.ts index 31b2a7f07..5a2ce49f5 100644 --- a/packages/api/src/defaults.ts +++ b/packages/api/src/defaults.ts @@ -69,7 +69,7 @@ export const defaults = { }, options: { basePath: "/api", - estimateTotalCount: !!process.env.CORE_API_ESTIMATED_TOTAL_COUNT, + estimateTotalCount: process.env.CORE_API_ESTIMATED_TOTAL_COUNT !== "false", }, ws: { banSeconds: 10,