perf(clubdata): scale club refresh to fiberCap; bound fiberCap by DB pool#89
Merged
Conversation
…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>
This was referenced Jun 18, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 75msmin-request-delaypacer caps admission at ~13.3 req/s regardless of permit count. Two concurrency issues surfaced:ClubDataAppwas the lone holdout at a hardcodedwithParallelism(8)— the oldmaxPermits, frozen, never tracking config. The 8→16 permit bump never reached it; peak concurrency pinned at 8.ApiConcurrency.fiberCapwas a pure2 * 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), wheredbMaxPoolSizereadsdatabase.pool.maximumPoolSizefrom config (the same direct-config shortcutTablesuses for cache settings — noPostgresClientthreaded 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)
min(32, 20)= 20 — closes the pool oversubscription.Tests
797 passed, 0 failed. The pool cap is3under test config; verified it breaks no timing/throttling suite.🤖 Generated with Claude Code