Skip to content

Commit c2b8dd4

Browse files
authored
fix(gateway): prune retired quota state during reconciliation (#64)
## Summary - prune only product-retired quota feature rows during lazy Durable Object reconciliation - preserve and structurally validate all current `sandbox_hours` and `composio_calls` data - perform pruning, strict-schema rebuild, version marking, and exact-schema verification in one rollback-safe transaction ## Why Production quota objects still contain dormant rows for quota features that were removed from the product. The exact-schema reconciler correctly rejected those rows, leaving `/v1/me/usage`, activity, and run admission unavailable. Earlier runtime code intentionally deleted these retired rows, so the forward migration must retain that product policy while continuing to reject malformed data. ## Verification - `pnpm lint` - `pnpm typecheck` - `pnpm build` - `pnpm --filter @cheatcode/gateway-worker typecheck` - `pnpm --filter @cheatcode/gateway-worker build` - `pnpm knip` ## Risk and rollback The migration deletes only rows whose text feature key is outside the two current quota features. It validates current rows, proves their counts survive the rebuild, and runs entirely inside one Durable Object SQLite transaction. Any error rolls back the migration.
1 parent d1f2ef2 commit c2b8dd4

1 file changed

Lines changed: 54 additions & 22 deletions

File tree

apps/gateway-worker/src/durable-objects/quota-tracker-storage.ts

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const USAGE_EVENT_INDEX_SQL =
4646
"CREATE INDEX usage_event_feature_time_idx ON usage_event(feature, recorded_at)";
4747
const QUOTA_OPERATION_INDEX_SQL =
4848
"CREATE INDEX quota_operation_feature_time_idx ON quota_operation(feature, recorded_at)";
49+
const QUOTA_TABLES = ["counter", "limit_override", "usage_event", "quota_operation"] as const;
50+
const CURRENT_QUOTA_FEATURES = [QUOTA_FEATURES.composioCalls, QUOTA_FEATURES.sandboxHours] as const;
4951

5052
const QUOTA_STORAGE_SCHEMA: readonly ExpectedSqliteObject[] = [
5153
{ name: "counter", sql: COUNTER_SQL, tableName: "counter", type: "table" },
@@ -96,19 +98,29 @@ export function ensureQuotaTrackerStorage(ctx: DurableObjectState): void {
9698
);
9799
}
98100

99-
/** Force-normalizes all quota tables while preserving every valid source row. */
101+
/**
102+
* Force-normalizes quota storage while preserving every current product row.
103+
*
104+
* Quotas removed from the product are intentionally discarded before the
105+
* exact-schema rebuild. Feature values must still be valid text so corrupt
106+
* rows cannot be mistaken for safely retired data.
107+
*/
100108
export function reconcileQuotaTrackerStorage(ctx: DurableObjectState): void {
101-
ensureSourceTables(ctx);
102-
const limitColumns = ctx.storage.sql.exec("PRAGMA table_info(limit_override)").toArray();
103-
if (!hasColumn(limitColumns, "feature") || !hasColumn(limitColumns, "limit_val")) {
104-
throw new Error("Unsupported quota limit schema; refusing lossy evolution.");
105-
}
106-
const hasEntitlementVersion = hasColumn(limitColumns, "entitlement_version");
107-
assertQuotaSourceRows(ctx, hasEntitlementVersion);
108-
const entitlementVersion = hasEntitlementVersion ? "entitlement_version" : "0";
109-
ctx.storage.transactionSync(() => rebuildQuotaTables(ctx, entitlementVersion));
110-
setCurrentSqliteStorageVersion(ctx);
111-
assertQuotaTrackerStorage(ctx);
109+
ctx.storage.transactionSync(() => {
110+
ensureSourceTables(ctx);
111+
const limitColumns = ctx.storage.sql.exec("PRAGMA table_info(limit_override)").toArray();
112+
if (!hasColumn(limitColumns, "feature") || !hasColumn(limitColumns, "limit_val")) {
113+
throw new Error("Unsupported quota limit schema; refusing lossy evolution.");
114+
}
115+
const hasEntitlementVersion = hasColumn(limitColumns, "entitlement_version");
116+
assertQuotaFeatureTypes(ctx);
117+
deleteRetiredQuotaRows(ctx);
118+
assertQuotaSourceRows(ctx, hasEntitlementVersion);
119+
const entitlementVersion = hasEntitlementVersion ? "entitlement_version" : "0";
120+
rebuildQuotaTables(ctx, entitlementVersion);
121+
setCurrentSqliteStorageVersion(ctx);
122+
assertQuotaTrackerStorage(ctx);
123+
});
112124
}
113125

114126
export function assertQuotaTrackerStorage(ctx: DurableObjectState): void {
@@ -144,14 +156,14 @@ function ensureSourceTables(ctx: DurableObjectState): void {
144156
function rebuildQuotaTables(ctx: DurableObjectState, entitlementVersion: string): void {
145157
ctx.storage.sql.exec("DROP INDEX IF EXISTS usage_event_feature_time_idx");
146158
ctx.storage.sql.exec("DROP INDEX IF EXISTS quota_operation_feature_time_idx");
147-
for (const table of ["counter", "limit_override", "usage_event", "quota_operation"] as const) {
159+
for (const table of QUOTA_TABLES) {
148160
ctx.storage.sql.exec(`ALTER TABLE ${table} RENAME TO ${table}_reconcile_source`);
149161
}
150162
for (const sql of [COUNTER_SQL, LIMIT_OVERRIDE_SQL, USAGE_EVENT_SQL, QUOTA_OPERATION_SQL]) {
151163
ctx.storage.sql.exec(sql);
152164
}
153165
copyQuotaRows(ctx, entitlementVersion);
154-
for (const table of ["counter", "limit_override", "usage_event", "quota_operation"] as const) {
166+
for (const table of QUOTA_TABLES) {
155167
assertSqliteRowCountPreserved(ctx, `${table}_reconcile_source`, table);
156168
ctx.storage.sql.exec(`DROP TABLE ${table}_reconcile_source`);
157169
}
@@ -182,40 +194,61 @@ function copyQuotaRows(ctx: DurableObjectState, entitlementVersion: string): voi
182194
);
183195
}
184196

197+
function assertQuotaFeatureTypes(ctx: DurableObjectState): void {
198+
for (const table of QUOTA_TABLES) {
199+
if (
200+
ctx.storage.sql
201+
.exec(`SELECT 1 FROM ${table} WHERE typeof(feature) <> 'text' LIMIT 1`)
202+
.toArray().length > 0
203+
) {
204+
throw new Error(`Quota ${table} contains a malformed feature; refusing lossy evolution.`);
205+
}
206+
}
207+
}
208+
209+
function deleteRetiredQuotaRows(ctx: DurableObjectState): void {
210+
for (const table of QUOTA_TABLES) {
211+
ctx.storage.sql.exec(
212+
`DELETE FROM ${table} WHERE feature NOT IN (?, ?)`,
213+
...CURRENT_QUOTA_FEATURES,
214+
);
215+
}
216+
}
217+
185218
function assertQuotaSourceRows(ctx: DurableObjectState, hasEntitlementVersion: boolean): void {
186-
const features = [QUOTA_FEATURES.composioCalls, QUOTA_FEATURES.sandboxHours] as const;
187219
assertNoInvalidRows(
188220
ctx,
221+
"counter",
189222
`SELECT 1 FROM counter WHERE
190223
typeof(feature) <> 'text' OR feature NOT IN (?, ?) OR
191224
typeof(period_key) <> 'text' OR length(period_key) <> 7 OR
192225
period_key NOT GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]' OR
193226
typeof(used) NOT IN ('integer', 'real') OR used < 0 OR abs(used) > ${MAX_FINITE_REAL} OR
194227
typeof(updated_at) <> 'integer' OR updated_at < 0 LIMIT 1`,
195-
features,
196228
);
197229
const entitlementPredicate = hasEntitlementVersion
198230
? " OR typeof(entitlement_version) <> 'integer' OR entitlement_version < 0"
199231
: "";
200232
assertNoInvalidRows(
201233
ctx,
234+
"limit_override",
202235
`SELECT 1 FROM limit_override WHERE
203236
typeof(feature) <> 'text' OR feature NOT IN (?, ?) OR
204237
typeof(limit_val) NOT IN ('integer', 'real') OR limit_val < 0 OR
205238
abs(limit_val) > ${MAX_FINITE_REAL}${entitlementPredicate} LIMIT 1`,
206-
features,
207239
);
208240
assertNoInvalidRows(
209241
ctx,
242+
"usage_event",
210243
`SELECT 1 FROM usage_event WHERE
211244
typeof(id) <> 'integer' OR typeof(feature) <> 'text' OR feature NOT IN (?, ?) OR
212245
typeof(amount) NOT IN ('integer', 'real') OR amount <= 0 OR
213246
abs(amount) > ${MAX_FINITE_REAL} OR typeof(recorded_at) <> 'integer' OR
214247
recorded_at < 0 LIMIT 1`,
215-
features,
216248
);
217249
assertNoInvalidRows(
218250
ctx,
251+
"quota_operation",
219252
`SELECT 1 FROM quota_operation WHERE
220253
typeof(event_id) <> 'text' OR length(event_id) NOT BETWEEN 1 AND 200 OR
221254
typeof(operation) <> 'text' OR operation NOT IN ('record', 'try-consume') OR
@@ -230,17 +263,16 @@ function assertQuotaSourceRows(ctx: DurableObjectState, hasEntitlementVersion: b
230263
abs(remaining) > ${MAX_FINITE_REAL} OR typeof(used) NOT IN ('integer', 'real') OR
231264
used < 0 OR abs(used) > ${MAX_FINITE_REAL} OR
232265
typeof(recorded_at) <> 'integer' OR recorded_at < 0 LIMIT 1`,
233-
features,
234266
);
235267
}
236268

237269
function assertNoInvalidRows(
238270
ctx: DurableObjectState,
271+
table: (typeof QUOTA_TABLES)[number],
239272
sql: string,
240-
features: readonly [string, string],
241273
): void {
242-
if (ctx.storage.sql.exec(sql, ...features).toArray().length > 0) {
243-
throw new Error("Quota storage contains invalid or retired data; refusing lossy evolution.");
274+
if (ctx.storage.sql.exec(sql, ...CURRENT_QUOTA_FEATURES).toArray().length > 0) {
275+
throw new Error(`Quota ${table} contains invalid current data; refusing lossy evolution.`);
244276
}
245277
}
246278

0 commit comments

Comments
 (0)