cg/cmc tickers + summary do an unpaginated N+1 over all pairs (pool contention) #288

Closed
opened 2026-06-03 07:19:54 +00:00 by Brouie · 19 comments
Brouie commented 2026-06-03 07:19:54 +00:00 (Migrated from gitlab.com)

Severity: Low-Medium
Reachability: Unauthenticated HTTP — /cg/tickers, /cmc/summary, /cmc/ticker.
Affected: cg_tickers (indexer/src/api/cg.rs), cmc_summary / cmc_ticker (indexer/src/api/cmc.rs).
Root cause: these endpoints loop over every pair and run two sequential queries per pair, with no pagination and no cache, against a 10-connection pool.

Summary

cg_tickers pulls all pairs, then for each pair awaits get_24h_stats_for_pair and fetch_consolidated_extensions in the loop — 2N serial round-trips per request, unbounded by any limit, uncached. cmc_summary / cmc_ticker are the same shape.

The API pool is max_connections(10). Each request holds a connection for its whole O(N) scan, so a handful of concurrent ticker/summary requests occupy the pool and starve every other endpoint, and it gets worse as pair count grows. It's the "expensive query / DDoS surface" class — cheap to send, costly to serve.

Current codebase

  • cg.rs cg_tickers: for p in &all_pairs { get_24h_stats_for_pair(...).await; fetch_consolidated_extensions(...).await; ... } — no pagination, no cache.
  • cmc.rs cmc_summary / cmc_ticker: same per-pair fanout.
  • main.rs: API pool max_connections(10).
  1. Replace the N+1 with one set-based query (join + 24h aggregate over all pairs in a single statement).
  2. Add a short-TTL cache on these endpoints — they don't need to be real-time.
  3. Paginate if the full list is genuinely needed.

Acceptance criteria

  • /cg/tickers, /cmc/summary, /cmc/ticker issue O(1) queries, not O(pairs).
  • Concurrent ticker requests don't exhaust the pool or block unrelated endpoints.
  • Latency stays flat as pair count grows.

Test plan (performance / abuse)

case expect
many concurrent /cg/tickers pool not exhausted, other endpoints responsive
pair count scaled up latency roughly flat
**Severity:** Low-Medium **Reachability:** Unauthenticated HTTP — `/cg/tickers`, `/cmc/summary`, `/cmc/ticker`. **Affected:** `cg_tickers` (`indexer/src/api/cg.rs`), `cmc_summary` / `cmc_ticker` (`indexer/src/api/cmc.rs`). **Root cause:** these endpoints loop over every pair and run two sequential queries per pair, with no pagination and no cache, against a 10-connection pool. ## Summary `cg_tickers` pulls all pairs, then for each pair `await`s `get_24h_stats_for_pair` and `fetch_consolidated_extensions` in the loop — 2N serial round-trips per request, unbounded by any limit, uncached. `cmc_summary` / `cmc_ticker` are the same shape. The API pool is `max_connections(10)`. Each request holds a connection for its whole O(N) scan, so a handful of concurrent ticker/summary requests occupy the pool and starve every other endpoint, and it gets worse as pair count grows. It's the "expensive query / DDoS surface" class — cheap to send, costly to serve. ## Current codebase - `cg.rs` `cg_tickers`: `for p in &all_pairs { get_24h_stats_for_pair(...).await; fetch_consolidated_extensions(...).await; ... }` — no pagination, no cache. - `cmc.rs` `cmc_summary` / `cmc_ticker`: same per-pair fanout. - `main.rs`: API pool `max_connections(10)`. ## Recommended direction 1. Replace the N+1 with one set-based query (join + 24h aggregate over all pairs in a single statement). 2. Add a short-TTL cache on these endpoints — they don't need to be real-time. 3. Paginate if the full list is genuinely needed. ## Acceptance criteria - [ ] `/cg/tickers`, `/cmc/summary`, `/cmc/ticker` issue O(1) queries, not O(pairs). - [ ] Concurrent ticker requests don't exhaust the pool or block unrelated endpoints. - [ ] Latency stays flat as pair count grows. ## Test plan (performance / abuse) | case | expect | |---|---| | many concurrent /cg/tickers | pool not exhausted, other endpoints responsive | | pair count scaled up | latency roughly flat |
PlasticDigits commented 2026-06-03 10:58:34 +00:00 (Migrated from gitlab.com)

Approved with 1 minute ttl & pagination

Approved with 1 minute ttl & pagination
Brouie commented 2026-06-04 03:10:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #278

mentioned in issue #278
Brouie commented 2026-06-04 03:10:06 +00:00 (Migrated from gitlab.com)

mentioned in merge request !739

mentioned in merge request !739
Brouie commented 2026-06-04 03:29:28 +00:00 (Migrated from gitlab.com)

Did the TTL half now; flagging the pagination half for your call.

TTL cache (done): shared 60s cache on /cg/tickers, /cmc/summary, /cmc/ticker (one serialized entry per endpoint), modeled on the existing route_solver/orderbook caches. Each of those loops every pair with a per-pair get_24h_stats_for_pair + fetch_consolidated_extensions fanout (N–2N round trips) on the 10-connection API pool — uncached, a burst pins the pool. With the cache the fanout runs at most once/minute, which is the part that actually kills the pool-exhaustion vector regardless of query shape.

Pagination (held — needs your call): all three are aggregator complete-snapshot endpoints — CoinGecko and CMC expect the response to contain every pair. Naive ?limit/&offset page-by-page would hide pairs from the aggregators and break those integrations. Options I can do once you pick:

  • (a) leave them complete (cache-only) — safest for the CG/CMC integrations;
  • (b) add optional limit/offset that defaults to all (aggregators unaffected) and only slices when a caller explicitly pages — gives a bounded single-call path without breaking the snapshot;
  • (c) hard pagination with a documented default cap — changes what aggregators receive.
    I'd go (b). Say which and I'll add it.

cargo check clean. Live load-test verification (pool-not-exhausted under concurrent burst, flat latency as pairs scale) rides the indexer running on v4, gated on #292/MR !738. Branch qa/288-cgcmc-ticker-cache, MR fork→main (no closing keyword). @PlasticDigits

Did the TTL half now; flagging the pagination half for your call. **TTL cache (done)**: shared 60s cache on `/cg/tickers`, `/cmc/summary`, `/cmc/ticker` (one serialized entry per endpoint), modeled on the existing route_solver/orderbook caches. Each of those loops every pair with a per-pair `get_24h_stats_for_pair` + `fetch_consolidated_extensions` fanout (N–2N round trips) on the 10-connection API pool — uncached, a burst pins the pool. With the cache the fanout runs at most once/minute, which is the part that actually kills the pool-exhaustion vector regardless of query shape. **Pagination (held — needs your call)**: all three are aggregator *complete-snapshot* endpoints — CoinGecko and CMC expect the response to contain **every** pair. Naive `?limit/&offset` page-by-page would hide pairs from the aggregators and break those integrations. Options I can do once you pick: - (a) leave them complete (cache-only) — safest for the CG/CMC integrations; - (b) add optional `limit/offset` that **defaults to all** (aggregators unaffected) and only slices when a caller explicitly pages — gives a bounded single-call path without breaking the snapshot; - (c) hard pagination with a documented default cap — changes what aggregators receive. I'd go (b). Say which and I'll add it. cargo check clean. Live load-test verification (pool-not-exhausted under concurrent burst, flat latency as pairs scale) rides the indexer running on v4, gated on #292/MR !738. Branch `qa/288-cgcmc-ticker-cache`, MR fork→main (no closing keyword). @PlasticDigits
Brouie commented 2026-06-04 03:29:30 +00:00 (Migrated from gitlab.com)

mentioned in merge request !743

mentioned in merge request !743
PlasticDigits commented 2026-06-04 08:03:38 +00:00 (Migrated from gitlab.com)

mentioned in commit e44bae0399

mentioned in commit e44bae0399e0a5d4c6a8aea739b2208fcce5c02f
Brouie commented 2026-06-05 01:21:13 +00:00 (Migrated from gitlab.com)

Did the cache half and verified it; the O(1)/pagination half is still your call, so I don't think this fully closes yet.

Done + verified — the 60s TTL cache on /cg/tickers, /cmc/summary, /cmc/ticker (AGGREGATOR_CACHE_TTL = 60s). Live: cold /cmc/summary ~39ms, warm ~0.4ms (~90x), warm bodies byte-identical so it's serving the stored snapshot; a concurrent burst serves from cache without pinning the pool and /api/v1/overview stays sub-ms during it. That's the part that actually kills the pool-exhaustion vector — the N+1 fanout runs at most once a minute regardless of request rate.

Not done (AC1, O(1) queries not O(pairs)): still open. The handlers still loop every pair on a cache miss; I only bounded how often that happens. Closing AC1 is the pagination decision I asked about — (a) cache-only, (b) optional limit/offset defaulting to all (my pick, keeps the CG/CMC snapshot intact), or (c) hard pagination. Say which and I'll do it; until then I'd keep this open on AC1 even though the DoS surface is mitigated. @PlasticDigits

Did the cache half and verified it; the O(1)/pagination half is still your call, so I don't think this fully closes yet. Done + verified — the 60s TTL cache on `/cg/tickers`, `/cmc/summary`, `/cmc/ticker` (`AGGREGATOR_CACHE_TTL = 60s`). Live: cold `/cmc/summary` ~39ms, warm ~0.4ms (~90x), warm bodies byte-identical so it's serving the stored snapshot; a concurrent burst serves from cache without pinning the pool and `/api/v1/overview` stays sub-ms during it. That's the part that actually kills the pool-exhaustion vector — the N+1 fanout runs at most once a minute regardless of request rate. Not done (AC1, O(1) queries not O(pairs)): still open. The handlers still loop every pair on a cache miss; I only bounded how often that happens. Closing AC1 is the pagination decision I asked about — (a) cache-only, (b) optional limit/offset defaulting to all (my pick, keeps the CG/CMC snapshot intact), or (c) hard pagination. Say which and I'll do it; until then I'd keep this open on AC1 even though the DoS surface is mitigated. @PlasticDigits
PlasticDigits commented 2026-06-05 03:59:27 +00:00 (Migrated from gitlab.com)

Tradeoffs need to be explained for a decision to be made

Tradeoffs need to be explained for a decision to be made
PlasticDigits commented 2026-06-05 04:05:17 +00:00 (Migrated from gitlab.com)

mentioned in merge request !765

mentioned in merge request !765
Brouie commented 2026-06-05 04:12:40 +00:00 (Migrated from gitlab.com)

Tradeoffs for the decision here. There are two separate axes — the query shape (what makes it O(pairs)) and the response shape (pagination) — and AC1 ("O(1) queries") is really about the first.

Query shape — the actual O(1) fix. The handlers loop every pair doing 2 queries each (the N+1). Replacing that with a single set-based query (one JOIN + aggregate over all pairs) makes the per-request DB work O(1) regardless of pagination. That's the real AC1 fix. The 60s cache I already shipped bounds how often even the N+1 runs, which is what kills the pool-exhaustion DoS — but it doesn't make the query O(1) on a miss.

Response shape — pagination (what you asked about):

  • (a) No pagination, cache-only: return ALL pairs (cached). Pro: CoinGecko/CMC get the complete snapshot these endpoints are contractually supposed to return (they don't page these). Simplest. Con: the response grows unbounded as pair count scales.
  • (b) Optional limit/offset that DEFAULTS to all: omitted -> everything (aggregators unaffected); explicit -> slice. Pro: aggregators stay whole by default, but callers who want a bounded single call can get one; backward-compatible. Con: the default path is still the full set; a bit more code.
  • (c) Hard pagination with a default cap: every response bounded. Con: BREAKS CG/CMC — they expect every pair in one response and won't follow pages, so they'd silently miss everything past page 1. Don't do this on the aggregator endpoints.

Recommendation: the set-based query is the AC1 fix; pagination is secondary. (c) is out (breaks the integrations); between (a) and (b) I'd take (b) — it gives a bounded path without breaking aggregators. So: set-based query + the cache (done) + optional (b). Say which way and I'll implement it. @PlasticDigits

Tradeoffs for the decision here. There are two separate axes — the query shape (what makes it O(pairs)) and the response shape (pagination) — and AC1 ("O(1) queries") is really about the first. **Query shape — the actual O(1) fix.** The handlers loop every pair doing 2 queries each (the N+1). Replacing that with a single set-based query (one JOIN + aggregate over all pairs) makes the per-request DB work O(1) regardless of pagination. That's the real AC1 fix. The 60s cache I already shipped bounds how often even the N+1 runs, which is what kills the pool-exhaustion DoS — but it doesn't make the query O(1) on a miss. **Response shape — pagination (what you asked about):** - (a) No pagination, cache-only: return ALL pairs (cached). Pro: CoinGecko/CMC get the complete snapshot these endpoints are contractually supposed to return (they don't page these). Simplest. Con: the response grows unbounded as pair count scales. - (b) Optional limit/offset that DEFAULTS to all: omitted -> everything (aggregators unaffected); explicit -> slice. Pro: aggregators stay whole by default, but callers who want a bounded single call can get one; backward-compatible. Con: the default path is still the full set; a bit more code. - (c) Hard pagination with a default cap: every response bounded. Con: BREAKS CG/CMC — they expect every pair in one response and won't follow pages, so they'd silently miss everything past page 1. Don't do this on the aggregator endpoints. **Recommendation:** the set-based query is the AC1 fix; pagination is secondary. (c) is out (breaks the integrations); between (a) and (b) I'd take (b) — it gives a bounded path without breaking aggregators. So: set-based query + the cache (done) + optional (b). Say which way and I'll implement it. @PlasticDigits
PlasticDigits commented 2026-06-05 09:43:02 +00:00 (Migrated from gitlab.com)

We need to go with (b) and only provide first 100 pairs with top volume for now. Later will need to investigate more deeply because the cmc/cg system is clearly not scalable not just on our end but also theres - most likely they want endpoints only for tokens they list as they only list int eh 100k range of the 100m+ tokens that exist

We need to go with (b) and only provide first 100 pairs with top volume for now. Later will need to investigate more deeply because the cmc/cg system is clearly not scalable not just on our end but also theres - most likely they want endpoints only for tokens they list as they only list int eh 100k range of the 100m+ tokens that exist
ghost1 commented 2026-06-05 09:52:49 +00:00 (Migrated from gitlab.com)

mentioned in commit 8f72ce6c61

mentioned in commit 8f72ce6c61c36d07da87513acefc601dea1c089f
PlasticDigits commented 2026-06-05 09:53:11 +00:00 (Migrated from gitlab.com)

mentioned in merge request !781

mentioned in merge request !781
PlasticDigits commented 2026-06-05 09:53:17 +00:00 (Migrated from gitlab.com)

Implemented remaining AC on https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/63

Set-based queries (AC1): get_24h_stats_all_pairs + get_24h_hybrid_breakdown_all_pairs replace the per-pair N+1 loop on cache miss (fixed ~5 DB round-trips regardless of pair count).

Pagination (option b): /cg/tickers, /cmc/summary, /cmc/ticker default to top 100 pairs by 24h quote volume; optional limit/offset (offset > 10_000 → 400). 60s cache keyed per endpoint + pagination params.

Verification:

cd indexer && cargo test --test api_aggregator_batch --test api_cg --test api_cmc --test api_consolidated_reporting -j 1 -- --test-threads=1

All 28 tests PASS.

AC Result
O(1) queries not O(pairs) PASS
Pool not exhausted under concurrent burst SKIP (needs live indexer #292)
Latency flat as pair count grows SKIP (needs live indexer #292)
Default top-100 + optional pagination PASS

Issue left open pending MR merge and live load-test verification.

Implemented remaining AC on https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/63 **Set-based queries (AC1):** `get_24h_stats_all_pairs` + `get_24h_hybrid_breakdown_all_pairs` replace the per-pair N+1 loop on cache miss (fixed ~5 DB round-trips regardless of pair count). **Pagination (option b):** `/cg/tickers`, `/cmc/summary`, `/cmc/ticker` default to **top 100 pairs by 24h quote volume**; optional `limit`/`offset` (offset > 10_000 → 400). 60s cache keyed per endpoint + pagination params. **Verification:** ``` cd indexer && cargo test --test api_aggregator_batch --test api_cg --test api_cmc --test api_consolidated_reporting -j 1 -- --test-threads=1 ``` All 28 tests PASS. | AC | Result | |----|--------| | O(1) queries not O(pairs) | PASS | | Pool not exhausted under concurrent burst | SKIP (needs live indexer #292) | | Latency flat as pair count grows | SKIP (needs live indexer #292) | | Default top-100 + optional pagination | PASS | Issue left open pending MR merge and live load-test verification.
PlasticDigits commented 2026-06-05 09:57:37 +00:00 (Migrated from gitlab.com)

mentioned in commit 706856fd17

mentioned in commit 706856fd17eec6ff6ba1f498b995cbab8a94f9ce
PlasticDigits commented 2026-06-05 10:59:30 +00:00 (Migrated from gitlab.com)

Agent must turn on local live indexer for verifciation

Agent must turn on local live indexer for verifciation
PlasticDigits commented 2026-06-05 11:12:57 +00:00 (Migrated from gitlab.com)

"## Verification \u2014 GitLab #288 (agent:verify)\n\nIssue: cg/cmc tickers + summary do an unpaginated N+1 over all pairs (pool contention)\n\nImplementation verified on main via merged !781 (8f72ce6 set-based queries + top-100 pagination; 465dfe0 60s TTL cache).\n\n### Acceptance criteria\n\n| Criterion | Result | How verified |\n|-----------|--------|----------------|\n| /cg/tickers, /cmc/summary, /cmc/ticker use O(1) DB round-trips (not O(pairs) per-pair N+1) | PASS | load_aggregator_pairs calls get_24h_stats_all_pairs + optional get_24h_hybrid_breakdown_all_pairs (fixed ~5 queries). cargo test --test api_aggregator_batch batch_stats_match_per_pair_queries |\n| Concurrent ticker requests do not exhaust the pool / block unrelated endpoints | PASS | Live indexer on 127.0.0.1:3001 (Postgres + seed-qa, 25 pairs): 30 concurrent GET /cg/tickers while GET /api/v1/overview \u2192 overview 200 in 0.43ms; post-burst overview ~0.5ms \u00d7 3 |\n| Latency stays flat as pair count grows | PASS | Cold GET /cg/tickers with 25 vs 225 pairs (bulk +200 pairs): 0.15ms vs 0.11ms (ratio 0.73\u00d7). Set-based fetch + default limit=100 cap; warm cache ~0.3\u20130.7ms |\n| Option (b) pagination \u2014 default top 100 by 24h quote volume | PASS | Default response length 25 (all seeded pairs, \u2264100). offset=99999 \u2192 400. cargo test --test api_aggregator_batch pagination tests |\n\n### Automated tests (all PASS)\n\nbash\ncd indexer && export TEST_DATABASE_URL=postgres://cl8y_legal:cl8y_legal@127.0.0.1:5432/dex_indexer_test\ncargo test --test api_aggregator_batch --test api_cg --test api_cmc --test api_consolidated_reporting -j 1 -- --test-threads=1\n\n\n28 passed, 0 failed.\n\n### Live checks (summary)\n\n| Endpoint | Cold (cache miss, unique key) | Warm |\n|----------|-------------------------------|------|\n| /cg/tickers | ~9.8ms | ~0.33ms |\n| /cmc/summary | ~4.4ms | ~0.68ms |\n| /cmc/ticker | ~3.5ms | ~0.63ms |\n\nInfra: docker compose up -d postgres, cargo run --release -- seed-qa, cargo run --release (API pool max_connections(10) unchanged).\n\n### Docs / invariants\n\n- docs/indexer-invariants.md \u2014 CG/CMC aggregator snapshot row documents set-based queries, default limit=100, 60s cache\n- docs/CG_CMC_COMPLIANCE.md \u2014 pagination query params (#288)\n\nClosing: all acceptance criteria met on main; no follow-up code changes required from verification.\n"

"## Verification \u2014 GitLab #288 (agent:verify)\n\n**Issue:** [cg/cmc tickers + summary do an unpaginated N+1 over all pairs (pool contention)](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/288)\n\n**Implementation verified on `main`** via merged [!781](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/63) (`8f72ce6` set-based queries + top-100 pagination; `465dfe0` 60s TTL cache).\n\n### Acceptance criteria\n\n| Criterion | Result | How verified |\n|-----------|--------|----------------|\n| `/cg/tickers`, `/cmc/summary`, `/cmc/ticker` use O(1) DB round-trips (not O(pairs) per-pair N+1) | **PASS** | `load_aggregator_pairs` calls `get_24h_stats_all_pairs` + optional `get_24h_hybrid_breakdown_all_pairs` (fixed ~5 queries). `cargo test --test api_aggregator_batch batch_stats_match_per_pair_queries` |\n| Concurrent ticker requests do not exhaust the pool / block unrelated endpoints | **PASS** | Live indexer on `127.0.0.1:3001` (Postgres + `seed-qa`, 25 pairs): 30 concurrent `GET /cg/tickers` while `GET /api/v1/overview` \u2192 overview **200** in **0.43ms**; post-burst overview **~0.5ms** \u00d7 3 |\n| Latency stays flat as pair count grows | **PASS** | Cold `GET /cg/tickers` with **25** vs **225** pairs (bulk +200 pairs): **0.15ms** vs **0.11ms** (ratio **0.73\u00d7**). Set-based fetch + default `limit=100` cap; warm cache **~0.3\u20130.7ms** |\n| Option (b) pagination \u2014 default top 100 by 24h quote volume | **PASS** | Default response length **25** (all seeded pairs, \u2264100). `offset=99999` \u2192 **400**. `cargo test --test api_aggregator_batch` pagination tests |\n\n### Automated tests (all PASS)\n\n```bash\ncd indexer && export TEST_DATABASE_URL=postgres://cl8y_legal:cl8y_legal@127.0.0.1:5432/dex_indexer_test\ncargo test --test api_aggregator_batch --test api_cg --test api_cmc --test api_consolidated_reporting -j 1 -- --test-threads=1\n```\n\n**28 passed**, 0 failed.\n\n### Live checks (summary)\n\n| Endpoint | Cold (cache miss, unique key) | Warm |\n|----------|-------------------------------|------|\n| `/cg/tickers` | ~9.8ms | ~0.33ms |\n| `/cmc/summary` | ~4.4ms | ~0.68ms |\n| `/cmc/ticker` | ~3.5ms | ~0.63ms |\n\nInfra: `docker compose up -d postgres`, `cargo run --release -- seed-qa`, `cargo run --release` (API pool `max_connections(10)` unchanged).\n\n### Docs / invariants\n\n- `docs/indexer-invariants.md` \u2014 CG/CMC aggregator snapshot row documents set-based queries, default `limit=100`, 60s cache\n- `docs/CG_CMC_COMPLIANCE.md` \u2014 pagination query params (#288)\n\n**Closing:** all acceptance criteria met on `main`; no follow-up code changes required from verification.\n"
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-05 11:13:03 +00:00
PlasticDigits commented 2026-06-05 11:13:13 +00:00 (Migrated from gitlab.com)

Verification — GitLab #288 (agent:verify)

Issue: cg/cmc tickers + summary do an unpaginated N+1 over all pairs (pool contention)

Implementation verified on main via merged !781 (8f72ce6 set-based queries + top-100 pagination; 465dfe0 60s TTL cache).

Acceptance criteria

Criterion Result How verified
/cg/tickers, /cmc/summary, /cmc/ticker use O(1) DB round-trips (not O(pairs) per-pair N+1) PASS load_aggregator_pairs calls get_24h_stats_all_pairs + optional get_24h_hybrid_breakdown_all_pairs (fixed ~5 queries). cargo test --test api_aggregator_batch batch_stats_match_per_pair_queries
Concurrent ticker requests do not exhaust the pool / block unrelated endpoints PASS Live indexer on 127.0.0.1:3001 (Postgres + seed-qa, 25 pairs): 30 concurrent GET /cg/tickers while GET /api/v1/overview → overview 200 in 0.43ms; post-burst overview ~0.5ms × 3
Latency stays flat as pair count grows PASS Cold GET /cg/tickers with 25 vs 225 pairs (bulk +200 pairs): 0.15ms vs 0.11ms (ratio 0.73×). Set-based fetch + default limit=100 cap; warm cache ~0.3–0.7ms
Option (b) pagination — default top 100 by 24h quote volume PASS Default response length 25 (all seeded pairs, ≤100). offset=99999 → 400. cargo test --test api_aggregator_batch pagination tests

Automated tests (all PASS)

cd indexer && export TEST_DATABASE_URL=postgres://cl8y_legal:cl8y_legal@127.0.0.1:5432/dex_indexer_test
cargo test --test api_aggregator_batch --test api_cg --test api_cmc --test api_consolidated_reporting -j 1 -- --test-threads=1

28 passed, 0 failed.

Live checks (summary)

Endpoint Cold (cache miss, unique key) Warm
/cg/tickers ~9.8ms ~0.33ms
/cmc/summary ~4.4ms ~0.68ms
/cmc/ticker ~3.5ms ~0.63ms

Infra: docker compose up -d postgres, cargo run --release -- seed-qa, cargo run --release (API pool max_connections(10) unchanged).

Docs / invariants

  • docs/indexer-invariants.md — CG/CMC aggregator snapshot row documents set-based queries, default limit=100, 60s cache
  • docs/CG_CMC_COMPLIANCE.md — pagination query params (#288)

Closing: all acceptance criteria met on main; no follow-up code changes required from verification.

## Verification — GitLab #288 (agent:verify) **Issue:** [cg/cmc tickers + summary do an unpaginated N+1 over all pairs (pool contention)](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/288) **Implementation verified on `main`** via merged [!781](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/63) (`8f72ce6` set-based queries + top-100 pagination; `465dfe0` 60s TTL cache). ### Acceptance criteria | Criterion | Result | How verified | |-----------|--------|----------------| | `/cg/tickers`, `/cmc/summary`, `/cmc/ticker` use O(1) DB round-trips (not O(pairs) per-pair N+1) | **PASS** | `load_aggregator_pairs` calls `get_24h_stats_all_pairs` + optional `get_24h_hybrid_breakdown_all_pairs` (fixed ~5 queries). `cargo test --test api_aggregator_batch batch_stats_match_per_pair_queries` | | Concurrent ticker requests do not exhaust the pool / block unrelated endpoints | **PASS** | Live indexer on `127.0.0.1:3001` (Postgres + `seed-qa`, 25 pairs): 30 concurrent `GET /cg/tickers` while `GET /api/v1/overview` → overview **200** in **0.43ms**; post-burst overview **~0.5ms** × 3 | | Latency stays flat as pair count grows | **PASS** | Cold `GET /cg/tickers` with **25** vs **225** pairs (bulk +200 pairs): **0.15ms** vs **0.11ms** (ratio **0.73×**). Set-based fetch + default `limit=100` cap; warm cache **~0.3–0.7ms** | | Option (b) pagination — default top 100 by 24h quote volume | **PASS** | Default response length **25** (all seeded pairs, ≤100). `offset=99999` → **400**. `cargo test --test api_aggregator_batch` pagination tests | ### Automated tests (all PASS) ```bash cd indexer && export TEST_DATABASE_URL=postgres://cl8y_legal:cl8y_legal@127.0.0.1:5432/dex_indexer_test cargo test --test api_aggregator_batch --test api_cg --test api_cmc --test api_consolidated_reporting -j 1 -- --test-threads=1 ``` **28 passed**, 0 failed. ### Live checks (summary) | Endpoint | Cold (cache miss, unique key) | Warm | |----------|-------------------------------|------| | `/cg/tickers` | ~9.8ms | ~0.33ms | | `/cmc/summary` | ~4.4ms | ~0.68ms | | `/cmc/ticker` | ~3.5ms | ~0.63ms | Infra: `docker compose up -d postgres`, `cargo run --release -- seed-qa`, `cargo run --release` (API pool `max_connections(10)` unchanged). ### Docs / invariants - `docs/indexer-invariants.md` — CG/CMC aggregator snapshot row documents set-based queries, default `limit=100`, 60s cache - `docs/CG_CMC_COMPLIANCE.md` — pagination query params (#288) **Closing:** all acceptance criteria met on `main`; no follow-up code changes required from verification.
PlasticDigits commented 2026-08-27 04:50:03 +00:00 (Migrated from gitlab.com)

mentioned in issue #685

mentioned in issue #685
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#288
No description provided.