Repository navigation
Restore global request_id uniqueness and the missing columns on the partitioned usage_events table #1486
Description
Summary
The hash-partition migration changed the shape of usage_events in ways the application never caught up with. Uniqueness is now (request_id, developer_id) instead of request_id. BillingService never sets developer_id, so every billed row lands in one partition. The columns status_code and api_key, which PostgresUsageStore writes and reads, no longer exist. revenue_ledger.usage_event_id still references the renamed usage_events_old. This issue restores a global idempotency guarantee on request_id, makes every writer populate developer_id, and repairs the columns and foreign key so the partitioned table is the single source of truth.
Background and evidence
- Uniqueness was weakened.
migrations/0011_partition_usage_events.sqlcreates the parent withPRIMARY KEY (id, developer_id)andUNIQUE (request_id, developer_id). A partitioned table cannot have a unique key without the partition key.src/services/billing.tsrelies on "the UNIQUE constraint on request_id is a second line of defence" (header comment, and the23505handling indeductInternalanddeductBulkInternal). That guarantee no longer exists once two writers use differentdeveloper_idvalues for the samerequest_id.
- Billing writes leave
developer_idempty.runPhase1andrunPhase1Bulkinsert(user_id, api_id, endpoint_id, api_key_id, amount_usdc, request_id, created_at)with nodeveloper_id, so it defaults to''.- All billing rows hash to the same partition, which defeats the partitioning, and the
idx_usage_events_developer_createdindex is useless for them.
- Columns the code uses are gone.
migrations/005_add_persistent_store_columns.sqladdedapi_keyandstatus_codeto the flat table. The new parent created in0011has neither.PostgresUsageStore.record(src/services/usageStore.ts, about line 237) insertsstatus_code, andusageEventSelect(about line 202) readsue.api_keyandue.status_code.- Both fail with
column ... does not existon a migrated database.
- A foreign key points at the old table.
migrations/003_create_revenue_ledger.sqldeclaresusage_event_id BIGINT REFERENCES usage_events(id). After the0011rename, Postgres keeps that constraint attached tousage_events_old, so new ledger rows are validated against the wrong table, or fail. - Mixed conflict targets.
PostgresUsageStore.recordusesON CONFLICT (request_id, developer_id), while billing does not, so the two writers disagree on what a duplicate is.
Scope
In scope
- Global request-id registry.
- Add
usage_event_request_ids (request_id VARCHAR(255) PRIMARY KEY, usage_event_id BIGINT NOT NULL, developer_id VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW()). - Every insert into
usage_events(billing single and bulk,PostgresUsageStore.record,UsageEventsPgRepository) first inserts here in the same transaction. - The billing
SELECT ... FOR UPDATEidempotency check moves to this table.
- Add
- Populate
developer_id.- Resolve the API owner (
apis.developer_id) inside the Phase 1 transaction, the same wayPostgresUsageStore.recorddoes with a subselect. - Reject the deduction with a typed error if the API is unknown, instead of writing
''.
- Resolve the API owner (
- Restore the columns. Add
status_code INTEGER NOT NULL DEFAULT 200andapi_key VARCHAR(255)to the partitioned parent, or remove them fromusageStore.tsand its types. State the choice in the PR. Keepdocs/schema.mdin sync. - Repair the foreign key. Drop the
revenue_ledgerconstraint that referencesusage_events_old. Becauseusage_events.idalone is not unique on the partitioned table, either referenceusage_event_request_ids(usage_event_id)(made unique) or enforce the link in application code with a documented invariant check. - Backfill. Extend
scripts/backfill-usage-partitions.tsto populateusage_event_request_idsand to fail loudly on duplicaterequest_idvalues across developers. Write those duplicates to a report rather than silently keeping one.
Out of scope
- The amount unit change, which is a separate issue.
- Changing the number of partitions or the hash key.
- Read-replica routing.
Design constraints
- One transaction per write. The registry insert and the
usage_eventsinsert share a transaction; a failure of either rolls back both. - Concurrency. Two concurrent deductions with the same
request_idand differentdeveloper_idmust result in exactly one row. The loser gets the existing row, never a second charge. - Migration safety. Idempotent, with a
.down.sql, and it passesnpm run db:check-migrations. - Bounded hot-path cost. At most one extra indexed insert per deduction. Include an
EXPLAINof the new idempotency lookup in the PR.
Test plan
Use testcontainers Postgres for the partition and foreign-key rows (pg-mem does not support declarative partitioning). Mark those tests with the existing integration pattern under tests/integration/.
| # | Scenario | Expected |
|---|---|---|
| 1 | Deduct twice with same requestId |
one usage_events row, one registry row; second call reports alreadyProcessed |
| 2 | Same requestId, two different API owners, concurrent |
exactly one row commits; the other returns the existing row |
| 3 | Billing row developer_id |
equals apis.developer_id for the given apiId |
| 4 | Unknown apiId |
typed 4xx error, no rows written |
| 5 | PostgresUsageStore.record then listByApiKey |
succeeds on the migrated schema; status_code round-trips |
| 6 | revenue_ledger insert for a new usage event |
succeeds; FK or invariant check rejects a non-existent event id |
| 7 | Backfill with duplicate request_id across developers |
script exits non-zero and writes a report |
| 8 | Partition spread | 100 billing rows for 10 developers land in more than one partition |
Regression proof. Row 5 fails with column "status_code" does not exist and row 3 shows '' on current main.
Acceptance criteria
- No code path inserts into
usage_eventswithout also writingusage_event_request_idsin the same transaction. -
grep -n "developer_id" src/services/billing.tsshowsdeveloper_idin both Phase 1 inserts. -
npm run db:check-migrationspasses. -
npm run lint,npm run typecheck,npm run build,npm testandnpm run test:integrationpass, and CI is green. - Line coverage of at least 90% on the touched functions (
npm run test:coverage).
Getting started
git checkout -b fix/usage-events-partition-keys
npm ci && npx prisma generate
npm run test:unit -- src/services/billing src/services/usageStoreRead first: migrations/0011_partition_usage_events.sql, migrations/005_add_persistent_store_columns.sql, migrations/003_create_revenue_ledger.sql, runPhase1 / runPhase1Bulk in src/services/billing.ts, and PostgresUsageStore in src/services/usageStore.ts.
PR requirements
- Link this issue (
Closes #<n>). - Explain how global uniqueness is guaranteed despite the partition-key rule, with the
EXPLAINoutput. - Paste test output, including the concurrency test, and the coverage summary.
- No unrelated schema changes.
- Example commit:
fix(db): add global request_id registry for partitioned usage_events.
Priority rationale
High. Idempotency of money-moving deductions currently rests on a constraint that no longer exists, and two production writers fail outright on the migrated schema. The fix needs knowledge of Postgres partitioning limits, transactional design and migration safety.
Activity
Daluworks commentedon Oct 18, 2026
@Daluworks has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @Daluworks to this issue.
Joseph-tech10 commentedon Oct 19, 2026
@Joseph-tech10 has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @Joseph-tech10 to this issue.
drips-wave commentedon Oct 19, 2026
Congratulations, @Joseph-tech10! 🎉 Your application was accepted by the repo's maintainers, and the issue is due on October 22, 2026.
🧑💻 @Joseph-tech10: Please resolve the issue such that the repo's maintainers have enough time to review your contribution before the due date. You'll earn Points for completing the issue on-time, which will make you eligible for a share of the Stellar Wave Program's reward pool.
Warning
When opening a PR, please link it to this issue to ensure it gets tracked accurately. Points are awarded when this issue is marked as completed by the maintainer.
🤠 Repo maintainers: Please keep an eye on the contributor's progress and review their work before the due date. You can manage this issue, including adjusting its complexity and points, here.
🌊 Happy Wave 🌊
drips-wave commentedon Oct 24, 2026
This issue has been completed by @Joseph-tech10 as part of the Stellar Wave Program's 10th Wave 🥳
😎 @Joseph-tech10: You earned 100 Points for completing this issue! Once the Wave's rewards are finalized, you'll be eligible for a percentage of the Wave's reward pool based on the percentage of total points you've earned. Learn more here. You can also Leave a review to share your experience working on this issue.
🧑💻 Repo maintainers: How'd the contributor do? Leave a review to share your experience working with them.
greatest0fallt1me commentedon Oct 24, 2026
Reopened: the fix from #1543 was reverted in #1550 because it broke the test suite on main. Please open a new PR rebased on the current main with tests passing.