Repository navigation
perf(gamification): add xp_action_counts cache table to fix O(n) COUNT(*) on every XP event (#12546) - #13116
perf(gamification): add xp_action_counts cache table to fix O(n) COUNT(*) on every XP event (#12546)#13116KooshaPari wants to merge 4 commits into
Conversation
…T(*) on every XP event (cheaper-inference#12546, cheaper-inference#12712) Per Diego's spec on the original issue + his review feedback on the first PR (cheaper-inference#12712, closed for base drift + column mismatch): - New table xp_action_counts(api_key_id, action, count, updated_at) - Migration 174 adds the table + backfills via GROUP BY api_key_id, action (NOT user_id - that's the column mismatch the first PR had) - incrementActionCount() runs in the SAME transaction as the xp_audit_log INSERT (atomic). Wrapped in try/catch so a cache write failure doesn't fail the whole XP grant (cache is a performance optimization, not the source of truth). - getActionCountByType() reads from the cache, falls back to COUNT(*) on cache miss (predates migration 174). Also try/catch so a cache read failure falls back rather than throwing. - getActionCount() in badges.ts is now a 3-line wrapper. - checkActionCountBadges() in events.ts calls the cached reader with a comment explaining the O(1) hot-path. Test: tests/unit/gamification/db-gamification.test.ts - addXp increments the cache - incrementActionCount is idempotent (same api_key+action) - getActionCountByType falls back to COUNT(*) on cache miss Fixes cheaper-inference#12546 Resubmission of cheaper-inference#12712 (corrected base + correct column)
|
Thanks for the Folded into a local recut of Resolution on the recut only: rename to Purpose of the recut: ship current |
…T(*) on every XP event Fixes cheaper-inference#12546. Resubmission of cheaper-inference#12712 (closed for base drift + column mismatch). XP-earning hot path was running COUNT(*) on every event. Introduces incremental counter cache keyed on api_key_id matching xp_audit_log schema. Cherry-picked from cd7a090 and rebased on upstream/release/v3.8.51.
cd7a090 to
c542fd3
Compare
…ts.sql Per HouMinXi's review on cheaper-inference#13116: the recut of release/v3.8.51 already has 174_server_tool_executions.sql (cheaper-inference#12867) and 176_provider_connection_synced_models_at.sql (cheaper-inference#13248), so this PR's 174_xp_action_counts.sql trips the migration-runner collision guard. Resolution: rename to 177 so it merges vs tip directly. Migration numbers are file-name-based in this repo (no central registry), so the only other reference was the test description (also updated). Verification: 6/6 node --test tests/unit/gamification/db-gamification.test.ts pass.
|
Shipped the rename directly in PR
Verification: The branch is now mergeable vs tip without any rebase / re-cut on your end. Re: your recut branch — happy to mirror the same rename there if you'd prefer to land the rename via your recut rather than this PR; just let me know which path is cleaner for the merge. |
Merge origin/pr/resubmit-12712-xp-cache into local. Resolves conflict in tests/unit/gamification/db-gamification.test.ts (174 -> 177 in describe() block, matching the file rename). Drops the stale src/lib/db/migrations/ 174_xp_action_counts.sql from the fork's view since the canonical file is now 177_xp_action_counts.sql. 6/6 gamification tests pass.
#13248) Merged after renumbering. `176_provider_connection_synced_models_at.sql` collided with `176_xp_action_counts.sql` (#12651), which made the migration runner abort on every DB open. Renamed to **177**; the doc count moves 173 → 174 across README.md, AGENTS.md, llm.txt and the i18n mirrors (operator-approved, 206 numeric substitutions and nothing else). - `check:migration-numbering`: OK, 174 migrations, no duplicates - `check:docs-counts` migrations: ✓ - 84/84 across the NVIDIA suite plus the seven DB-touching suites the collision had taken down - ESLint and `typecheck:core`: exit 0 Heads-up for whoever lands next: **177 is claimed by eight other open PRs** (#13610, #13602, #13580, #13554, #13405, #13331, #13177, #13116) and 176 by #13373 and #13102. With this merged, all of them need to renumber at merge time — `check:migration-numbering` forbids new gaps, so the next free number is always the only valid one.⚠️ base-red inherited: #12732
|
Thanks for the O(1) action-count cache. The same fix already landed in the release branch through #12651: a durable #12651 also keeps the |
…r-inference#12849) (cheaper-inference#13248) Merged after renumbering. `176_provider_connection_synced_models_at.sql` collided with `176_xp_action_counts.sql` (cheaper-inference#12651), which made the migration runner abort on every DB open. Renamed to **177**; the doc count moves 173 → 174 across README.md, AGENTS.md, llm.txt and the i18n mirrors (operator-approved, 206 numeric substitutions and nothing else). - `check:migration-numbering`: OK, 174 migrations, no duplicates - `check:docs-counts` migrations: ✓ - 84/84 across the NVIDIA suite plus the seven DB-touching suites the collision had taken down - ESLint and `typecheck:core`: exit 0 Heads-up for whoever lands next: **177 is claimed by eight other open PRs** (cheaper-inference#13610, cheaper-inference#13602, cheaper-inference#13580, cheaper-inference#13554, cheaper-inference#13405, cheaper-inference#13331, cheaper-inference#13177, cheaper-inference#13116) and 176 by cheaper-inference#13373 and cheaper-inference#13102. With this merged, all of them need to renumber at merge time — `check:migration-numbering` forbids new gaps, so the next free number is always the only valid one.⚠️ base-red inherited: cheaper-inference#12732
Summary
Fixes #12546. Resubmission of #12712 (which was closed for base drift +
user_idvsapi_key_idcolumn mismatch).The XP-earning hot path was running
SELECT COUNT(*) FROM xp_audit_log ... GROUP BY ...on every event. With largexp_audit_logtables this is the slow path that triggered base-red CI badges.Fix
Introduces an incremental counter cache keyed on
api_key_id(matching thexp_audit_logschema):xp_action_counts(api_key_id, action, count, updated_at)via migration 174 (next free after 173 onrelease/v3.8.51)GROUP BY api_key_id, actionfrom existingxp_audit_logrowsincrementActionCount()runs in the same transaction as thexp_audit_logINSERT (atomic) — and is wrapped intry/catchso a cache write failure doesn't fail the whole XP grant (cache is a performance optimization, not the source of truth)getActionCountByType()reads from the cache, falls back toCOUNT(*)on cache miss (user predates migration 174), also wrapped intry/catchso a cache read failure falls back rather than throwsgetActionCount()inbadges.tsis now a 3-line wrapper calling the cached readercheckActionCountBadges()inevents.tsuses the cached reader — O(1) hot pathBehavior
COUNT(*)COUNT(*)— same as beforeCOUNT(*)— no regressionFiles
src/lib/db/migrations/174_xp_action_counts.sqlsrc/lib/db/gamification.tssrc/lib/gamification/badges.tssrc/lib/gamification/events.tstests/unit/gamification/db-gamification.test.tsTests
tests/unit/gamification/db-gamification.test.ts:addXp increments the cacheincrementActionCount is idempotent (same api_key+action)getActionCountByType falls back to COUNT(*) on cache missAll 6 tests pass.
Why this matters
The XP-earning path is hit on every successful LLM request — currently O(audit_log) per call. With migration 174 in place it's O(1), which:
release/v3.8.51Fixes #12546.