Skip to content

perf(clubdata): scale club refresh to fiberCap; bound fiberCap by DB pool#89

Merged
Sootopolis merged 1 commit into
mainfrom
wip
Jun 18, 2026
Merged

perf(clubdata): scale club refresh to fiberCap; bound fiberCap by DB pool#89
Sootopolis merged 1 commit into
mainfrom
wip

Conversation

@Sootopolis

Copy link
Copy Markdown
Owner

Context

Investigating recent client runs at maxPermits = 16 (config 6, recovery_tiers [2,4,6,8,12,16]): safe — 126,769 requests, zero 429s, zero Cloudflare 403s, zero throttle-downs. But throughput on the network-bound apps stayed flat, because the global 75ms min-request-delay pacer caps admission at ~13.3 req/s regardless of permit count. Two concurrency issues surfaced:

  1. ClubDataApp was the lone holdout at a hardcoded withParallelism(8) — the old maxPermits, frozen, never tracking config. The 8→16 permit bump never reached it; peak concurrency pinned at 8.
  2. ApiConcurrency.fiberCap was a pure 2 * maxPermits — the same bump silently pushed every other API app to 32 fibers, oversubscribing the 20-connection Hikari pool.

Changes

  • ClubDataApp: withParallelism(8)withParallelism(ApiConcurrency.fiberCap(client)). Now consistent with every other API-bound app; auto-tracks config.
  • ApiConcurrency.fiberCap: min(2 * maxPermits, dbMaxPoolSize), where dbMaxPoolSize reads database.pool.maximumPoolSize from config (the same direct-config shortcut Tables uses for cache settings — no PostgresClient threaded through the 12 callsites). Absent config → no DB cap (test-safe). 1-arg signature unchanged → all callsites pool-aware for free.

Effect (default config: maxPermits 16, pool 20)

  • All API apps cap at min(32, 20) = 20 — closes the pool oversubscription.
  • clubdata: 8 → 20. Fills the gate on slow-upstream days instead of bottlenecking at 8. Normal-day throughput unchanged (pacer-bound).

Tests

797 passed, 0 failed. The pool cap is 3 under test config; verified it breaks no timing/throttling suite.

🤖 Generated with Claude Code

…pool

ClubDataApp fanned out club refresh at a hardcoded withParallelism(8) — the
old maxPermits value, frozen and never tracking config. Every other API-bound
app uses ApiConcurrency.fiberCap(client); clubdata was the lone holdout, so the
config-6 maxPermits bump (8 -> 16) never reached it and its peak concurrency
stayed pinned at 8. Switch it to fiberCap.

fiberCap was a pure 2 * maxPermits, which the same bump pushed to 32 —
oversubscribing the 20-connection Hikari pool. Add a DB-pool ceiling:
min(2 * maxPermits, database.pool.maximumPoolSize), read from config the same
way Tables reads cache settings (no PostgresClient threaded through the 12
callsites). With the default config every API app now caps at min(32, 20) = 20;
clubdata goes 8 -> 20.

Throughput on normal-latency days is unchanged — the global 75ms
min-request-delay pacer caps admission at ~13.3 req/s regardless of permit count
— but the wider window lets clubdata fill the gate on slow-upstream days instead
of bottlenecking at 8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Sootopolis Sootopolis merged commit ae35cb0 into main Jun 18, 2026
1 check passed
Sootopolis added a commit that referenced this pull request Jun 23, 2026
Two latent hardening follow-ups on ApiConcurrency.fiberCap from #89.

Item 1 — connection margin. With the prod config (maxPermits 16, pool 20)
fiberCap was min(32, 20) = 20, i.e. the entire pool. A single app under a
DB-phase burst could momentarily check out every connection, leaving none for
health/readiness probes or ad-hoc queries, which then queue on connectionTimeout.
Now reserve PoolReserve (2) connections from the DB ceiling — but only when the
pool is more than twice the reserve, so a tiny pool (the size-3 test pool) keeps
its full width and fan-out concurrency there is unchanged. Prod goes 20 -> 18.

Item 2 — prefix coupling. dbMaxPoolSize reads the 'database' prefix directly;
a PostgresClient under another prefix would silently lose the DB cap. All current
callers use the default prefix, so this is latent and intentional — documented as
such, with threading the resolved prefix named as the upgrade trigger if a second
prefix is ever introduced (the per-callsite plumbing #89 deliberately avoided).

The pure arithmetic is extracted into cappedFor so the rule is deterministically
unit-testable without ConfigFactory (new TestApiConcurrency, 9 cases).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGcaTyiNJyAJyhc2oVGyyc
Sootopolis added a commit that referenced this pull request Jun 23, 2026
Two latent hardening follow-ups on ApiConcurrency.fiberCap from #89.

Item 1 — connection margin. With the prod config (maxPermits 16, pool 20)
fiberCap was min(32, 20) = 20, i.e. the entire pool. A single app under a
DB-phase burst could momentarily check out every connection, leaving none for
health/readiness probes or ad-hoc queries, which then queue on connectionTimeout.
Now reserve PoolReserve (2) connections from the DB ceiling — but only when the
pool is more than twice the reserve, so a tiny pool (the size-3 test pool) keeps
its full width and fan-out concurrency there is unchanged. Prod goes 20 -> 18.

Item 2 — prefix coupling. dbMaxPoolSize reads the 'database' prefix directly;
a PostgresClient under another prefix would silently lose the DB cap. All current
callers use the default prefix, so this is latent and intentional — documented as
such, with threading the resolved prefix named as the upgrade trigger if a second
prefix is ever introduced (the per-callsite plumbing #89 deliberately avoided).

The pure arithmetic is extracted into cappedFor so the rule is deterministically
unit-testable without ConfigFactory (new TestApiConcurrency, 9 cases).


Claude-Session: https://claude.ai/code/session_01JGcaTyiNJyAJyhc2oVGyyc

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant