Skip to content

Commit 3036854

Browse files
committed
fix(gateway): prune retired quota state during reconciliation
1 parent d1f2ef2 commit 3036854

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)