orez 0.4.24 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/worker/cf-do-shim.d.ts +55 -0
- package/dist/worker/cf-do-shim.d.ts.map +1 -0
- package/dist/worker/cf-do-shim.js +58 -0
- package/dist/worker/cf-do-shim.js.map +1 -0
- package/dist/worker/zero-cache-do-sqlite.d.ts +39 -0
- package/dist/worker/zero-cache-do-sqlite.d.ts.map +1 -0
- package/dist/worker/zero-cache-do-sqlite.js +64 -0
- package/dist/worker/zero-cache-do-sqlite.js.map +1 -0
- package/dist/worker/zero-cache-replica-repair.d.ts +66 -0
- package/dist/worker/zero-cache-replica-repair.d.ts.map +1 -0
- package/dist/worker/zero-cache-replica-repair.js +185 -0
- package/dist/worker/zero-cache-replica-repair.js.map +1 -0
- package/dist/worker/zero-sql-write-circuit.d.ts +46 -0
- package/dist/worker/zero-sql-write-circuit.d.ts.map +1 -0
- package/dist/worker/zero-sql-write-circuit.js +128 -0
- package/dist/worker/zero-sql-write-circuit.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* namespace routing primitives for the Cloudflare Durable Object deploy.
|
|
3
|
+
*
|
|
4
|
+
* a multi-tenant CF/orez deploy shards data into one DO instance per tenant
|
|
5
|
+
* namespace (`ns:<scope>-<id>`) plus a control-plane `singleton`. the worker
|
|
6
|
+
* tiers carry the chosen namespace in a header (and re-stamp it so an inbound
|
|
7
|
+
* value is never trusted) and must validate its shape before routing — an
|
|
8
|
+
* unvalidated namespace would let a client mint unbounded DO instances.
|
|
9
|
+
*
|
|
10
|
+
* the deployed worker entry classes are bundled strings in the consumer's
|
|
11
|
+
* deploy integration (awkward to unit-test), so the routing decision and the
|
|
12
|
+
* security-relevant shape validation live here as pure functions, tested in
|
|
13
|
+
* cf-do-shim.test.ts. consumers import these into their worker shims instead of
|
|
14
|
+
* copy-pasting the validation regex at every routing site.
|
|
15
|
+
*/
|
|
16
|
+
export interface NamespaceRoutingOptions {
|
|
17
|
+
/** allowed namespace scope prefixes. default `['proj', 'test']`. */
|
|
18
|
+
scopes?: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* namespace values that resolve to the control-plane `singleton` instance,
|
|
21
|
+
* in addition to the empty string (which is always the singleton).
|
|
22
|
+
*/
|
|
23
|
+
controlPlaneNamespaces?: readonly string[];
|
|
24
|
+
/** request header the worker tiers carry the namespace in. default `x-orez-ns`. */
|
|
25
|
+
nsHeader?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* true when `ns` is a structurally valid tenant namespace: a configured scope
|
|
29
|
+
* prefix followed by a 1-64 char `[A-Za-z0-9_-]` id. this is the gate that
|
|
30
|
+
* keeps a stray header from minting an unbounded DO instance, so every routing
|
|
31
|
+
* site that forwards a namespace should run it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isValidNamespace(ns: string, opts?: NamespaceRoutingOptions): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* resolve a raw namespace string to a DO instance name:
|
|
36
|
+
* - `''` or a control-plane alias -> `'singleton'`
|
|
37
|
+
* - a valid tenant namespace -> `'ns:<ns>'`
|
|
38
|
+
* - anything else -> `null` (caller should reject the request)
|
|
39
|
+
*/
|
|
40
|
+
export declare function doInstanceName(ns: string, opts?: NamespaceRoutingOptions): string | null;
|
|
41
|
+
interface HeaderReader {
|
|
42
|
+
get(name: string): string | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* read the namespace from a request (the configured header, falling back to the
|
|
46
|
+
* `?ns=` query param) and resolve it to a DO instance name. returns `null` for a
|
|
47
|
+
* structurally invalid namespace so the worker can reply 400 instead of routing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function doInstanceNameForRequest(request: {
|
|
50
|
+
headers: HeaderReader;
|
|
51
|
+
}, url: {
|
|
52
|
+
searchParams: HeaderReader;
|
|
53
|
+
}, opts?: NamespaceRoutingOptions): string | null;
|
|
54
|
+
export {};
|
|
55
|
+
//# sourceMappingURL=cf-do-shim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cf-do-shim.d.ts","sourceRoot":"","sources":["../../src/worker/cf-do-shim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B;;;OAGG;IACH,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1C,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAWD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,EACV,IAAI,GAAE,uBAA4B,GACjC,OAAO,CAET;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,MAAM,EACV,IAAI,GAAE,uBAA4B,GACjC,MAAM,GAAG,IAAI,CAKf;AAED,UAAU,YAAY;IACpB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CACjC;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE;IAAE,OAAO,EAAE,YAAY,CAAA;CAAE,EAClC,GAAG,EAAE;IAAE,YAAY,EAAE,YAAY,CAAA;CAAE,EACnC,IAAI,GAAE,uBAA4B,GACjC,MAAM,GAAG,IAAI,CAIf"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* namespace routing primitives for the Cloudflare Durable Object deploy.
|
|
3
|
+
*
|
|
4
|
+
* a multi-tenant CF/orez deploy shards data into one DO instance per tenant
|
|
5
|
+
* namespace (`ns:<scope>-<id>`) plus a control-plane `singleton`. the worker
|
|
6
|
+
* tiers carry the chosen namespace in a header (and re-stamp it so an inbound
|
|
7
|
+
* value is never trusted) and must validate its shape before routing — an
|
|
8
|
+
* unvalidated namespace would let a client mint unbounded DO instances.
|
|
9
|
+
*
|
|
10
|
+
* the deployed worker entry classes are bundled strings in the consumer's
|
|
11
|
+
* deploy integration (awkward to unit-test), so the routing decision and the
|
|
12
|
+
* security-relevant shape validation live here as pure functions, tested in
|
|
13
|
+
* cf-do-shim.test.ts. consumers import these into their worker shims instead of
|
|
14
|
+
* copy-pasting the validation regex at every routing site.
|
|
15
|
+
*/
|
|
16
|
+
/** default namespace scope prefixes (`proj-<id>`, `test-<id>`). */
|
|
17
|
+
const DEFAULT_SCOPES = ['proj', 'test'];
|
|
18
|
+
function escapeForRegExp(value) {
|
|
19
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
20
|
+
}
|
|
21
|
+
function namespacePattern(scopes) {
|
|
22
|
+
const group = scopes.map(escapeForRegExp).join('|');
|
|
23
|
+
return new RegExp(`^(?:${group})-[A-Za-z0-9_-]{1,64}$`);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* true when `ns` is a structurally valid tenant namespace: a configured scope
|
|
27
|
+
* prefix followed by a 1-64 char `[A-Za-z0-9_-]` id. this is the gate that
|
|
28
|
+
* keeps a stray header from minting an unbounded DO instance, so every routing
|
|
29
|
+
* site that forwards a namespace should run it.
|
|
30
|
+
*/
|
|
31
|
+
export function isValidNamespace(ns, opts = {}) {
|
|
32
|
+
return namespacePattern(opts.scopes ?? DEFAULT_SCOPES).test(ns);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* resolve a raw namespace string to a DO instance name:
|
|
36
|
+
* - `''` or a control-plane alias -> `'singleton'`
|
|
37
|
+
* - a valid tenant namespace -> `'ns:<ns>'`
|
|
38
|
+
* - anything else -> `null` (caller should reject the request)
|
|
39
|
+
*/
|
|
40
|
+
export function doInstanceName(ns, opts = {}) {
|
|
41
|
+
if (!ns)
|
|
42
|
+
return 'singleton';
|
|
43
|
+
if ((opts.controlPlaneNamespaces ?? []).includes(ns))
|
|
44
|
+
return 'singleton';
|
|
45
|
+
if (!isValidNamespace(ns, opts))
|
|
46
|
+
return null;
|
|
47
|
+
return 'ns:' + ns;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* read the namespace from a request (the configured header, falling back to the
|
|
51
|
+
* `?ns=` query param) and resolve it to a DO instance name. returns `null` for a
|
|
52
|
+
* structurally invalid namespace so the worker can reply 400 instead of routing.
|
|
53
|
+
*/
|
|
54
|
+
export function doInstanceNameForRequest(request, url, opts = {}) {
|
|
55
|
+
const ns = request.headers.get(opts.nsHeader ?? 'x-orez-ns') || url.searchParams.get('ns') || '';
|
|
56
|
+
return doInstanceName(ns, opts);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=cf-do-shim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cf-do-shim.js","sourceRoot":"","sources":["../../src/worker/cf-do-shim.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,mEAAmE;AACnE,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,MAAM,CAAU,CAAA;AAchD,SAAS,eAAe,CAAC,KAAa;IACpC,OAAO,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAA;AACrD,CAAC;AAED,SAAS,gBAAgB,CAAC,MAAyB;IACjD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACnD,OAAO,IAAI,MAAM,CAAC,OAAO,KAAK,wBAAwB,CAAC,CAAA;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,EAAU,EACV,OAAgC,EAAE;IAElC,OAAO,gBAAgB,CAAC,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AACjE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,EAAU,EACV,OAAgC,EAAE;IAElC,IAAI,CAAC,EAAE;QAAE,OAAO,WAAW,CAAA;IAC3B,IAAI,CAAC,IAAI,CAAC,sBAAsB,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,WAAW,CAAA;IACxE,IAAI,CAAC,gBAAgB,CAAC,EAAE,EAAE,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAC5C,OAAO,KAAK,GAAG,EAAE,CAAA;AACnB,CAAC;AAMD;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CACtC,OAAkC,EAClC,GAAmC,EACnC,OAAgC,EAAE;IAElC,MAAM,EAAE,GACN,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,WAAW,CAAC,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IACvF,OAAO,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,CAAA;AACjC,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare DO-SQLite compatibility for a zero-cache embed.
|
|
3
|
+
*
|
|
4
|
+
* zero-cache's embedded SQLite issues storage-engine statements (VACUUM, ATTACH,
|
|
5
|
+
* journal/checkpoint PRAGMAs) that a Durable Object's `ctx.storage.sql` does NOT
|
|
6
|
+
* own — the DO manages journaling + checkpointing itself and rejects them with
|
|
7
|
+
* SQLITE_AUTH, which aborts `/sync`. these helpers make those statements no-op so
|
|
8
|
+
* the embed boots unmodified, while every real read/write passes straight
|
|
9
|
+
* through. pure over the minimal `exec` shape (no `@cloudflare/...` types),
|
|
10
|
+
* tested in zero-cache-do-sqlite.test.ts.
|
|
11
|
+
*/
|
|
12
|
+
interface DoExecSql {
|
|
13
|
+
exec(sql: string, ...params: unknown[]): unknown;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
interface DoStorageCtx {
|
|
17
|
+
storage: {
|
|
18
|
+
sql: DoExecSql;
|
|
19
|
+
transactionSync?: (...args: unknown[]) => unknown;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare const DO_FORBIDDEN_SQLITE: RegExp;
|
|
23
|
+
export declare function isDoForbiddenSqlite(sql: unknown): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* patch `sql.exec` in place so EVERY caller (the embed, its sqlite shim,
|
|
26
|
+
* transactionSync callbacks) skips DO-forbidden statements instead of throwing
|
|
27
|
+
* SQLITE_AUTH. idempotent per handle.
|
|
28
|
+
*/
|
|
29
|
+
export declare function installDoForbiddenSqliteGuard(sql: DoExecSql): void;
|
|
30
|
+
/**
|
|
31
|
+
* wrap a DO's `storage.sql` for the zero-cache embed: DO-forbidden statements
|
|
32
|
+
* no-op, and `transactionSync` is bound through when the platform exposes it.
|
|
33
|
+
*/
|
|
34
|
+
export declare function doSqliteStorage(ctx: DoStorageCtx): {
|
|
35
|
+
exec: (sql: string, ...params: unknown[]) => unknown;
|
|
36
|
+
transactionSync: ((...args: unknown[]) => unknown) | undefined;
|
|
37
|
+
};
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=zero-cache-do-sqlite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zero-cache-do-sqlite.d.ts","sourceRoot":"","sources":["../../src/worker/zero-cache-do-sqlite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,UAAU,SAAS;IACjB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAChD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,UAAU,YAAY;IACpB,OAAO,EAAE;QACP,GAAG,EAAE,SAAS,CAAA;QACd,eAAe,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAA;KAClD,CAAA;CACF;AAGD,eAAO,MAAM,mBAAmB,QAC+H,CAAA;AAE/J,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEzD;AAID;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAgBlE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,YAAY,GAAG;IAClD,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAA;IACpD,eAAe,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,SAAS,CAAA;CAC/D,CAqBA"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare DO-SQLite compatibility for a zero-cache embed.
|
|
3
|
+
*
|
|
4
|
+
* zero-cache's embedded SQLite issues storage-engine statements (VACUUM, ATTACH,
|
|
5
|
+
* journal/checkpoint PRAGMAs) that a Durable Object's `ctx.storage.sql` does NOT
|
|
6
|
+
* own — the DO manages journaling + checkpointing itself and rejects them with
|
|
7
|
+
* SQLITE_AUTH, which aborts `/sync`. these helpers make those statements no-op so
|
|
8
|
+
* the embed boots unmodified, while every real read/write passes straight
|
|
9
|
+
* through. pure over the minimal `exec` shape (no `@cloudflare/...` types),
|
|
10
|
+
* tested in zero-cache-do-sqlite.test.ts.
|
|
11
|
+
*/
|
|
12
|
+
// storage-engine statements the DO rejects (it owns journaling/checkpointing).
|
|
13
|
+
export const DO_FORBIDDEN_SQLITE = /^\s*(?:VACUUM\b|ATTACH\b|PRAGMA\s+(?:journal_mode|synchronous|page_size|mmap_size|wal_checkpoint|wal_autocheckpoint|locking_mode|temp_store|cache_size)\b)/i;
|
|
14
|
+
export function isDoForbiddenSqlite(sql) {
|
|
15
|
+
return typeof sql === 'string' && DO_FORBIDDEN_SQLITE.test(sql);
|
|
16
|
+
}
|
|
17
|
+
const GUARD_MARK = '__orezDoSqliteGuarded';
|
|
18
|
+
/**
|
|
19
|
+
* patch `sql.exec` in place so EVERY caller (the embed, its sqlite shim,
|
|
20
|
+
* transactionSync callbacks) skips DO-forbidden statements instead of throwing
|
|
21
|
+
* SQLITE_AUTH. idempotent per handle.
|
|
22
|
+
*/
|
|
23
|
+
export function installDoForbiddenSqliteGuard(sql) {
|
|
24
|
+
if (!sql || sql[GUARD_MARK])
|
|
25
|
+
return;
|
|
26
|
+
const rawExec = sql.exec.bind(sql);
|
|
27
|
+
sql.exec = (statement, ...params) => {
|
|
28
|
+
if (isDoForbiddenSqlite(statement)) {
|
|
29
|
+
return {
|
|
30
|
+
toArray: () => [],
|
|
31
|
+
rowsRead: 0,
|
|
32
|
+
rowsWritten: 0,
|
|
33
|
+
columnNames: [],
|
|
34
|
+
[Symbol.iterator]: () => [][Symbol.iterator](),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return rawExec(statement, ...params);
|
|
38
|
+
};
|
|
39
|
+
sql[GUARD_MARK] = true;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* wrap a DO's `storage.sql` for the zero-cache embed: DO-forbidden statements
|
|
43
|
+
* no-op, and `transactionSync` is bound through when the platform exposes it.
|
|
44
|
+
*/
|
|
45
|
+
export function doSqliteStorage(ctx) {
|
|
46
|
+
const rawExec = ctx.storage.sql.exec.bind(ctx.storage.sql);
|
|
47
|
+
const exec = (sql, ...params) => {
|
|
48
|
+
if (isDoForbiddenSqlite(sql)) {
|
|
49
|
+
return {
|
|
50
|
+
toArray: () => [],
|
|
51
|
+
rowsRead: 0,
|
|
52
|
+
[Symbol.iterator]: () => [][Symbol.iterator](),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return rawExec(sql, ...params);
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
exec,
|
|
59
|
+
transactionSync: typeof ctx.storage.transactionSync === 'function'
|
|
60
|
+
? ctx.storage.transactionSync.bind(ctx.storage)
|
|
61
|
+
: undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=zero-cache-do-sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zero-cache-do-sqlite.js","sourceRoot":"","sources":["../../src/worker/zero-cache-do-sqlite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAcH,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAC9B,6JAA6J,CAAA;AAE/J,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjE,CAAC;AAED,MAAM,UAAU,GAAG,uBAAuB,CAAA;AAE1C;;;;GAIG;AACH,MAAM,UAAU,6BAA6B,CAAC,GAAc;IAC1D,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC;QAAE,OAAM;IACnC,MAAM,OAAO,GAAI,GAAG,CAAC,IAAuD,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACtF,GAAG,CAAC,IAAI,GAAG,CAAC,SAAiB,EAAE,GAAG,MAAiB,EAAE,EAAE;QACrD,IAAI,mBAAmB,CAAC,SAAS,CAAC,EAAE,CAAC;YACnC,OAAO;gBACL,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE;gBACjB,QAAQ,EAAE,CAAC;gBACX,WAAW,EAAE,CAAC;gBACd,WAAW,EAAE,EAAE;gBACf,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE;aAC/C,CAAA;QACH,CAAC;QACD,OAAO,OAAO,CAAC,SAAS,EAAE,GAAG,MAAM,CAAC,CAAA;IACtC,CAAC,CAAA;IACD,GAAG,CAAC,UAAU,CAAC,GAAG,IAAI,CAAA;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,GAAiB;IAI/C,MAAM,OAAO,GACX,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IACjB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACvB,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,GAAG,MAAiB,EAAE,EAAE;QACjD,IAAI,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,OAAO;gBACL,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE;gBACjB,QAAQ,EAAE,CAAC;gBACX,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE;aAC/C,CAAA;QACH,CAAC;QACD,OAAO,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,CAAA;IAChC,CAAC,CAAA;IACD,OAAO;QACL,IAAI;QACJ,eAAe,EACb,OAAO,GAAG,CAAC,OAAO,CAAC,eAAe,KAAK,UAAU;YAC/C,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAC/C,CAAC,CAAC,SAAS;KAChB,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* self-heal for a zero-cache embed's DO-SQLite replica.
|
|
3
|
+
*
|
|
4
|
+
* zero-cache snapshots its publication into a replica (here, the Durable
|
|
5
|
+
* Object's `ctx.storage.sql`, `ZERO_REPLICA_FILE=':do-sqlite:'`). on a real
|
|
6
|
+
* Postgres that replica lives in a normal file with normal transaction
|
|
7
|
+
* semantics; inside a DO the sqlite shim makes BEGIN/COMMIT/ROLLBACK no-ops
|
|
8
|
+
* (the object auto-commits per I/O turn) and any boot can be killed across an
|
|
9
|
+
* await (the 120s ready-timeout, an eviction, an OOM). that combination leaves
|
|
10
|
+
* three distinct corrupt-replica states that each wedge `/sync` permanently
|
|
11
|
+
* until healed. these functions detect and repair each one; every one of them
|
|
12
|
+
* fixed a specific production incident (see the per-function comments).
|
|
13
|
+
*
|
|
14
|
+
* the replica is *derived* data — the upstream rows live in the SQL DO and are
|
|
15
|
+
* untouched by any wipe here, so dropping the replica only forces zero-cache to
|
|
16
|
+
* re-run initial sync. pure logic over minimal `exec`/`get`/`put` shapes (no
|
|
17
|
+
* `@cloudflare/...` types); the decision logic is unit-tested in
|
|
18
|
+
* zero-cache-replica-repair.test.ts against simulated replica states. consumers
|
|
19
|
+
* call these from their ZeroCacheDO boot sequence with their own storage key +
|
|
20
|
+
* log prefix.
|
|
21
|
+
*/
|
|
22
|
+
export interface ReplicaSqlResult {
|
|
23
|
+
toArray(): Array<Record<string, unknown>>;
|
|
24
|
+
}
|
|
25
|
+
export interface ReplicaSqlStorage {
|
|
26
|
+
exec(sql: string, ...params: unknown[]): ReplicaSqlResult;
|
|
27
|
+
}
|
|
28
|
+
export interface ReplicaKvStorage {
|
|
29
|
+
get(key: string): Promise<unknown>;
|
|
30
|
+
put(key: string, value: unknown): Promise<unknown>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* run a SQL statement against the SQL-DO backend (pg-over-DO `/exec`), returning
|
|
34
|
+
* its `{ rows, error }` body. consumers wrap their backend fetch in this shape;
|
|
35
|
+
* an `{ error }` is surfaced (never treated as "no rows", which would silently
|
|
36
|
+
* disable the guards).
|
|
37
|
+
*/
|
|
38
|
+
export type BackendExec = (sql: string, params?: unknown[]) => Promise<{
|
|
39
|
+
rows?: Array<Record<string, unknown>>;
|
|
40
|
+
error?: string;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* drop every non-internal table from the replica SQLite. SQLite internals
|
|
44
|
+
* (`sqlite_*`) and Cloudflare's DO tables (`_cf_*`) are skipped: the DO storage
|
|
45
|
+
* authorizer rejects DROP on `_cf_*` with SQLITE_AUTH, which would abort the
|
|
46
|
+
* whole reset. returns the number of tables dropped.
|
|
47
|
+
*/
|
|
48
|
+
export declare function dropReplicaTables(sql: ReplicaSqlStorage): number;
|
|
49
|
+
export declare function resetReplicaIfTableSetChanged(sql: ReplicaSqlStorage, storage: ReplicaKvStorage, opts: {
|
|
50
|
+
schemaVersion: string;
|
|
51
|
+
tables?: Iterable<string>;
|
|
52
|
+
/** durable storage key the last-applied tag is persisted under. */
|
|
53
|
+
tagKey: string;
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
export declare function repairPartialReplicaInit(sql: ReplicaSqlStorage, opts?: {
|
|
56
|
+
logPrefix?: string;
|
|
57
|
+
}): void;
|
|
58
|
+
export declare function resetReplicaIfChangeLogPoisoned(sql: ReplicaSqlStorage, backendExec: BackendExec, opts: {
|
|
59
|
+
appId: string;
|
|
60
|
+
logPrefix?: string;
|
|
61
|
+
}): Promise<void>;
|
|
62
|
+
export declare function clearChangeStreamerStateIfReplicaUninitialized(sql: ReplicaSqlStorage, backendExec: BackendExec, opts: {
|
|
63
|
+
appId: string;
|
|
64
|
+
logPrefix?: string;
|
|
65
|
+
}): Promise<void>;
|
|
66
|
+
//# sourceMappingURL=zero-cache-replica-repair.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zero-cache-replica-repair.d.ts","sourceRoot":"","sources":["../../src/worker/zero-cache-replica-repair.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAC1C;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAA;CAC1D;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACnD;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,OAAO,EAAE,KACf,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAEvE;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAUhE;AAWD,wBAAsB,6BAA6B,CACjD,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE;IACJ,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACzB,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAA;CACf,GACA,OAAO,CAAC,IAAI,CAAC,CAWf;AAeD,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,iBAAiB,EACtB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAChC,IAAI,CA8BN;AASD,wBAAsB,+BAA+B,CACnD,GAAG,EAAE,iBAAiB,EACtB,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,IAAI,CAAC,CAoDf;AASD,wBAAsB,8CAA8C,CAClE,GAAG,EAAE,iBAAiB,EACtB,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* self-heal for a zero-cache embed's DO-SQLite replica.
|
|
3
|
+
*
|
|
4
|
+
* zero-cache snapshots its publication into a replica (here, the Durable
|
|
5
|
+
* Object's `ctx.storage.sql`, `ZERO_REPLICA_FILE=':do-sqlite:'`). on a real
|
|
6
|
+
* Postgres that replica lives in a normal file with normal transaction
|
|
7
|
+
* semantics; inside a DO the sqlite shim makes BEGIN/COMMIT/ROLLBACK no-ops
|
|
8
|
+
* (the object auto-commits per I/O turn) and any boot can be killed across an
|
|
9
|
+
* await (the 120s ready-timeout, an eviction, an OOM). that combination leaves
|
|
10
|
+
* three distinct corrupt-replica states that each wedge `/sync` permanently
|
|
11
|
+
* until healed. these functions detect and repair each one; every one of them
|
|
12
|
+
* fixed a specific production incident (see the per-function comments).
|
|
13
|
+
*
|
|
14
|
+
* the replica is *derived* data — the upstream rows live in the SQL DO and are
|
|
15
|
+
* untouched by any wipe here, so dropping the replica only forces zero-cache to
|
|
16
|
+
* re-run initial sync. pure logic over minimal `exec`/`get`/`put` shapes (no
|
|
17
|
+
* `@cloudflare/...` types); the decision logic is unit-tested in
|
|
18
|
+
* zero-cache-replica-repair.test.ts against simulated replica states. consumers
|
|
19
|
+
* call these from their ZeroCacheDO boot sequence with their own storage key +
|
|
20
|
+
* log prefix.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* drop every non-internal table from the replica SQLite. SQLite internals
|
|
24
|
+
* (`sqlite_*`) and Cloudflare's DO tables (`_cf_*`) are skipped: the DO storage
|
|
25
|
+
* authorizer rejects DROP on `_cf_*` with SQLITE_AUTH, which would abort the
|
|
26
|
+
* whole reset. returns the number of tables dropped.
|
|
27
|
+
*/
|
|
28
|
+
export function dropReplicaTables(sql) {
|
|
29
|
+
const rows = sql.exec("SELECT name FROM sqlite_master WHERE type='table'").toArray();
|
|
30
|
+
let dropped = 0;
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
const name = String(row.name);
|
|
33
|
+
if (name.startsWith('sqlite_') || name.startsWith('_cf_'))
|
|
34
|
+
continue;
|
|
35
|
+
sql.exec('DROP TABLE IF EXISTS "' + name.replaceAll('"', '""') + '"');
|
|
36
|
+
dropped++;
|
|
37
|
+
}
|
|
38
|
+
return dropped;
|
|
39
|
+
}
|
|
40
|
+
// zero-cache snapshots the publication's tables into its replica ONCE during
|
|
41
|
+
// initial sync and never picks up a table OR COLUMN added afterward — ALTER only
|
|
42
|
+
// feeds the change stream, not the existing snapshot. so a redeploy that evolves
|
|
43
|
+
// the schema leaves the persisted replica stuck on the old shape and every
|
|
44
|
+
// client fails SchemaVersionNotSupported (2026-06-10: file.title/description
|
|
45
|
+
// columns — table set unchanged, so a tables-only tag never reset). key the tag
|
|
46
|
+
// on schemaVersion (a hash of the full deploy-time DDL batch — any
|
|
47
|
+
// table/column/type change) plus the table set, and wipe the replica on change
|
|
48
|
+
// so zero-cache re-runs initial sync over the full publication.
|
|
49
|
+
export async function resetReplicaIfTableSetChanged(sql, storage, opts) {
|
|
50
|
+
const tag = JSON.stringify([opts.schemaVersion, [...(opts.tables || [])].sort()]);
|
|
51
|
+
const lastTag = await storage.get(opts.tagKey);
|
|
52
|
+
// reset whenever the tag differs — including the no-baseline case (lastTag
|
|
53
|
+
// undefined), which covers a DO whose replica was initialized by a deploy that
|
|
54
|
+
// predates this tracking. on a brand-new DO the replica is empty so the drop
|
|
55
|
+
// loop is a no-op; on a stale one it forces a full re-sync.
|
|
56
|
+
if (lastTag !== tag) {
|
|
57
|
+
dropReplicaTables(sql);
|
|
58
|
+
}
|
|
59
|
+
await storage.put(opts.tagKey, tag);
|
|
60
|
+
}
|
|
61
|
+
// repair a PARTIALLY-INITIALIZED replica left by an interrupted embed boot.
|
|
62
|
+
// zero-cache's runSchemaMigrations wraps initial-sync (createReplicationStateTables
|
|
63
|
+
// + the versionHistory row write) in one BEGIN EXCLUSIVE/COMMIT, expecting it to
|
|
64
|
+
// be atomic. but on a CF DO the sqlite shim makes BEGIN/COMMIT/ROLLBACK NO-OPS
|
|
65
|
+
// (the DO auto-commits per I/O turn), and the setup migration is async (it awaits
|
|
66
|
+
// initialSync, which yields across turns). so if the boot is killed mid-migration
|
|
67
|
+
// — the 120s ready-timeout, a DO eviction, an OOM — the _zero.* tables auto-commit
|
|
68
|
+
// but the closing versionHistory INSERT never runs. next boot: getVersionHistory
|
|
69
|
+
// reads an empty table => dataVersion 0 => it re-runs the setup migration =>
|
|
70
|
+
// CREATE TABLE "_zero.replicationConfig" => "already exists" SQLITE_ERROR, and
|
|
71
|
+
// /sync never reaches ready (editor stuck on "loading files"). detect that exact
|
|
72
|
+
// inconsistency (replica data tables present but no versionHistory row) and wipe
|
|
73
|
+
// the _zero.* replica so the embed re-runs initial sync cleanly.
|
|
74
|
+
export function repairPartialReplicaInit(sql, opts = {}) {
|
|
75
|
+
const logPrefix = opts.logPrefix ?? '[orez]';
|
|
76
|
+
const hasConfig = sql
|
|
77
|
+
.exec("SELECT 1 FROM sqlite_master WHERE type='table' AND name='_zero.replicationConfig'")
|
|
78
|
+
.toArray().length;
|
|
79
|
+
if (!hasConfig)
|
|
80
|
+
return;
|
|
81
|
+
let versionRows = 0;
|
|
82
|
+
try {
|
|
83
|
+
versionRows = sql
|
|
84
|
+
.exec('SELECT 1 FROM "_zero.versionHistory" LIMIT 1')
|
|
85
|
+
.toArray().length;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// versionHistory table missing entirely is also an inconsistent state
|
|
89
|
+
versionRows = 0;
|
|
90
|
+
}
|
|
91
|
+
if (versionRows > 0)
|
|
92
|
+
return;
|
|
93
|
+
// inconsistent: the replica's _zero.* control tables exist but version tracking
|
|
94
|
+
// is empty. the ENTIRE replica is half-initialized — initial-sync creates the
|
|
95
|
+
// _zero.* control tables AND the published app tables in the same interrupted
|
|
96
|
+
// run, so re-running setup also fails on a duplicate app table. drop every
|
|
97
|
+
// replica table so the next boot initial-syncs the whole set from scratch.
|
|
98
|
+
const dropped = dropReplicaTables(sql);
|
|
99
|
+
console.log(logPrefix +
|
|
100
|
+
' repaired partial replica init: dropped ' +
|
|
101
|
+
dropped +
|
|
102
|
+
' replica tables (no versionHistory row) so initial sync re-runs');
|
|
103
|
+
}
|
|
104
|
+
// a changeLog transaction group without a commit entry is an interrupted storer
|
|
105
|
+
// write (zero stores each replicated tx inside one pg transaction; real pg rolls
|
|
106
|
+
// a crashed tx back, but the DO sqlite shim auto-commits per turn, so a kill
|
|
107
|
+
// persists the partial group). catchup replays it as begin->data->begin and the
|
|
108
|
+
// replicator dies on "Already in a transaction" on every boot. wiping the replica
|
|
109
|
+
// here makes the uninitialized-replica guard clear cdc state, forcing a clean
|
|
110
|
+
// initial sync.
|
|
111
|
+
export async function resetReplicaIfChangeLogPoisoned(sql, backendExec, opts) {
|
|
112
|
+
const logPrefix = opts.logPrefix ?? '[orez]';
|
|
113
|
+
const initialized = sql
|
|
114
|
+
.exec("SELECT 1 FROM sqlite_master WHERE type='table' AND name='_zero.replicationConfig'")
|
|
115
|
+
.toArray().length;
|
|
116
|
+
if (!initialized)
|
|
117
|
+
return;
|
|
118
|
+
// /exec parses pg SQL: placeholders are $1-style, never '?'. an {error}
|
|
119
|
+
// response must be surfaced — treating it as "no rows" silently disables the
|
|
120
|
+
// guard (the exact failure mode this guard exists to prevent).
|
|
121
|
+
const listBody = await backendExec("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE $1", [opts.appId + '_%/cdc_changeLog']);
|
|
122
|
+
if (listBody.error) {
|
|
123
|
+
console.error(logPrefix + ' changeLog poison check failed (list): ' + listBody.error);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const names = (listBody.rows || []).map((row) => String(row.name));
|
|
127
|
+
for (const name of names) {
|
|
128
|
+
const checkBody = await backendExec('SELECT watermark FROM "' +
|
|
129
|
+
name.replaceAll('"', '""') +
|
|
130
|
+
"\" GROUP BY watermark HAVING SUM(CASE WHEN json_extract(change, '$.tag') = 'commit' THEN 1 ELSE 0 END) = 0 LIMIT 1");
|
|
131
|
+
if (checkBody.error) {
|
|
132
|
+
console.error(logPrefix + ' changeLog poison check failed (scan): ' + checkBody.error);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const rows = checkBody.rows || [];
|
|
136
|
+
console.log(logPrefix +
|
|
137
|
+
' changeLog poison scan: ' +
|
|
138
|
+
name +
|
|
139
|
+
' -> ' +
|
|
140
|
+
(rows.length ? 'POISONED at ' + rows[0].watermark : 'clean'));
|
|
141
|
+
if (!rows.length)
|
|
142
|
+
continue;
|
|
143
|
+
const dropped = dropReplicaTables(sql);
|
|
144
|
+
console.log(logPrefix +
|
|
145
|
+
' cdc changeLog has a partial transaction at watermark ' +
|
|
146
|
+
rows[0].watermark +
|
|
147
|
+
' (interrupted storer write): dropped ' +
|
|
148
|
+
dropped +
|
|
149
|
+
' replica tables so cdc state clears and initial sync re-runs');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// a replica without its init marker must not reuse the cdc subscription state,
|
|
154
|
+
// or initial sync never re-runs. the change-streamer's subscription state lives
|
|
155
|
+
// in the SQL DO and SURVIVES a replica wipe (the resets above, or an OOM
|
|
156
|
+
// eviction); a wiped replica + surviving subscription state makes zero-cache
|
|
157
|
+
// skip initial sync ("already synced") and serve an EMPTY replica that only ever
|
|
158
|
+
// receives catchup changes. when the replica has no init marker, clear the cdc
|
|
159
|
+
// state so the embed re-runs initial sync from scratch.
|
|
160
|
+
export async function clearChangeStreamerStateIfReplicaUninitialized(sql, backendExec, opts) {
|
|
161
|
+
const logPrefix = opts.logPrefix ?? '[orez]';
|
|
162
|
+
const initialized = sql
|
|
163
|
+
.exec("SELECT 1 FROM sqlite_master WHERE type='table' AND name='_zero.replicationConfig'")
|
|
164
|
+
.toArray().length;
|
|
165
|
+
if (initialized)
|
|
166
|
+
return;
|
|
167
|
+
// /exec parses pg SQL: $1-style placeholders, never '?' (a '?' is a parse
|
|
168
|
+
// error whose {error} body silently disabled this guard).
|
|
169
|
+
const body = await backendExec("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE $1", [opts.appId + '_%/cdc_%']);
|
|
170
|
+
if (body.error) {
|
|
171
|
+
console.error(logPrefix + ' cdc state clear failed (list): ' + body.error);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const names = (body.rows || []).map((row) => String(row.name));
|
|
175
|
+
for (const name of names) {
|
|
176
|
+
await backendExec('DROP TABLE IF EXISTS "' + name.replaceAll('"', '""') + '"');
|
|
177
|
+
}
|
|
178
|
+
if (names.length) {
|
|
179
|
+
console.log(logPrefix +
|
|
180
|
+
' replica uninitialized: cleared ' +
|
|
181
|
+
names.length +
|
|
182
|
+
' cdc state tables so initial sync re-runs');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=zero-cache-replica-repair.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zero-cache-replica-repair.js","sourceRoot":"","sources":["../../src/worker/zero-cache-replica-repair.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runaway-write circuit breaker for a Durable Object's SQLite storage.
|
|
3
|
+
*
|
|
4
|
+
* a ZeroSqlDO (orez `ZeroDO`) runs the pg-over-DO backend in-instance, so a
|
|
5
|
+
* buggy or malicious mutation flow can write unbounded rows into the DO's
|
|
6
|
+
* SQLite and blow past the platform's per-object storage + billing limits
|
|
7
|
+
* before anything notices. this wraps `sql.exec` to meter rows written per
|
|
8
|
+
* rolling window and trip — refusing further writes — once the rate stays over
|
|
9
|
+
* a soft threshold for a sustained period, or instantly past a hard threshold.
|
|
10
|
+
*
|
|
11
|
+
* the meter state lives in a single-row table in the same SQLite, so a tripped
|
|
12
|
+
* breaker survives DO eviction (the object stays bricked-for-writes until an
|
|
13
|
+
* operator clears the row). reads are never gated. the wrap is idempotent per
|
|
14
|
+
* `sql` handle.
|
|
15
|
+
*
|
|
16
|
+
* pure logic over the minimal `DurableSqlStorage` shape (no `@cloudflare/...`
|
|
17
|
+
* types), unit-tested in zero-sql-write-circuit.test.ts. consumers install it
|
|
18
|
+
* from their ZeroSqlDO constructor with their own table/log prefix.
|
|
19
|
+
*/
|
|
20
|
+
export interface DurableSqlCursor {
|
|
21
|
+
one(): Record<string, unknown> | undefined;
|
|
22
|
+
rowsWritten?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface DurableSqlStorage {
|
|
25
|
+
exec(sql: string, ...params: unknown[]): DurableSqlCursor;
|
|
26
|
+
}
|
|
27
|
+
export interface WriteCircuitOptions {
|
|
28
|
+
/** single-row meter table name. default `_orez_write_circuit`. */
|
|
29
|
+
table?: string;
|
|
30
|
+
/** soft cap: rows/window above which the sustained timer starts. default 2,000,000. */
|
|
31
|
+
rowsPerWindow?: number;
|
|
32
|
+
/** hard cap: rows/window that trips instantly. default 10,000,000. */
|
|
33
|
+
hardRowsPerWindow?: number;
|
|
34
|
+
/** rolling window length in ms. default 60,000. */
|
|
35
|
+
windowMs?: number;
|
|
36
|
+
/** how long the rate must stay over the soft cap before tripping, in ms. default 180,000. */
|
|
37
|
+
sustainedMs?: number;
|
|
38
|
+
/** log prefix for the trip diagnostics. default `[orez]`. */
|
|
39
|
+
logPrefix?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* wrap `sql.exec` with the runaway-write circuit breaker. idempotent: a second
|
|
43
|
+
* call on the same `sql` handle is a no-op.
|
|
44
|
+
*/
|
|
45
|
+
export declare function installZeroSqlWriteCircuitBreaker(sql: DurableSqlStorage, opts?: WriteCircuitOptions): void;
|
|
46
|
+
//# sourceMappingURL=zero-sql-write-circuit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zero-sql-write-circuit.d.ts","sourceRoot":"","sources":["../../src/worker/zero-sql-write-circuit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAA;CAC1D;AAED,MAAM,WAAW,mBAAmB;IAClC,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6FAA6F;IAC7F,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAQD;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,GAAG,EAAE,iBAAiB,EACtB,IAAI,GAAE,mBAAwB,GAC7B,IAAI,CA+HN"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runaway-write circuit breaker for a Durable Object's SQLite storage.
|
|
3
|
+
*
|
|
4
|
+
* a ZeroSqlDO (orez `ZeroDO`) runs the pg-over-DO backend in-instance, so a
|
|
5
|
+
* buggy or malicious mutation flow can write unbounded rows into the DO's
|
|
6
|
+
* SQLite and blow past the platform's per-object storage + billing limits
|
|
7
|
+
* before anything notices. this wraps `sql.exec` to meter rows written per
|
|
8
|
+
* rolling window and trip — refusing further writes — once the rate stays over
|
|
9
|
+
* a soft threshold for a sustained period, or instantly past a hard threshold.
|
|
10
|
+
*
|
|
11
|
+
* the meter state lives in a single-row table in the same SQLite, so a tripped
|
|
12
|
+
* breaker survives DO eviction (the object stays bricked-for-writes until an
|
|
13
|
+
* operator clears the row). reads are never gated. the wrap is idempotent per
|
|
14
|
+
* `sql` handle.
|
|
15
|
+
*
|
|
16
|
+
* pure logic over the minimal `DurableSqlStorage` shape (no `@cloudflare/...`
|
|
17
|
+
* types), unit-tested in zero-sql-write-circuit.test.ts. consumers install it
|
|
18
|
+
* from their ZeroSqlDO constructor with their own table/log prefix.
|
|
19
|
+
*/
|
|
20
|
+
// any statement that can write rows (so reads never pay the meter cost).
|
|
21
|
+
const MUTATION_RE = /^\s*(?:insert|update|delete|replace|create|alter|drop|truncate|vacuum|reindex|with)\b/i;
|
|
22
|
+
const INSTALLED = new WeakSet();
|
|
23
|
+
/**
|
|
24
|
+
* wrap `sql.exec` with the runaway-write circuit breaker. idempotent: a second
|
|
25
|
+
* call on the same `sql` handle is a no-op.
|
|
26
|
+
*/
|
|
27
|
+
export function installZeroSqlWriteCircuitBreaker(sql, opts = {}) {
|
|
28
|
+
if (!sql || INSTALLED.has(sql))
|
|
29
|
+
return;
|
|
30
|
+
const table = opts.table ?? '_orez_write_circuit';
|
|
31
|
+
const rowsPerWindow = opts.rowsPerWindow ?? 2_000_000;
|
|
32
|
+
const hardRowsPerWindow = opts.hardRowsPerWindow ?? 10_000_000;
|
|
33
|
+
const windowMs = opts.windowMs ?? 60 * 1000;
|
|
34
|
+
const sustainedMs = opts.sustainedMs ?? 3 * 60 * 1000;
|
|
35
|
+
const logPrefix = opts.logPrefix ?? '[orez]';
|
|
36
|
+
const rawExec = sql.exec.bind(sql);
|
|
37
|
+
let ready = false;
|
|
38
|
+
const ensureReady = () => {
|
|
39
|
+
if (ready)
|
|
40
|
+
return;
|
|
41
|
+
rawExec('CREATE TABLE IF NOT EXISTS ' +
|
|
42
|
+
table +
|
|
43
|
+
' (id INTEGER PRIMARY KEY CHECK (id = 1), window_start INTEGER NOT NULL DEFAULT 0, rows_in_window INTEGER NOT NULL DEFAULT 0, first_over_at INTEGER NOT NULL DEFAULT 0, tripped_at INTEGER NOT NULL DEFAULT 0, last_statement TEXT)');
|
|
44
|
+
rawExec('INSERT OR IGNORE INTO ' +
|
|
45
|
+
table +
|
|
46
|
+
' (id, window_start, rows_in_window, first_over_at, tripped_at, last_statement) VALUES (1, 0, 0, 0, 0, ?)', '');
|
|
47
|
+
ready = true;
|
|
48
|
+
};
|
|
49
|
+
const readState = () => {
|
|
50
|
+
ensureReady();
|
|
51
|
+
return (rawExec('SELECT window_start, rows_in_window, first_over_at, tripped_at FROM ' +
|
|
52
|
+
table +
|
|
53
|
+
' WHERE id = 1').one() || {});
|
|
54
|
+
};
|
|
55
|
+
const clippedStatement = (statement) => String(statement || '')
|
|
56
|
+
.replace(/\s+/g, ' ')
|
|
57
|
+
.trim()
|
|
58
|
+
.slice(0, 500);
|
|
59
|
+
const assertOpen = (statement) => {
|
|
60
|
+
const state = readState();
|
|
61
|
+
const trippedAt = Number(state.tripped_at || 0);
|
|
62
|
+
if (trippedAt > 0) {
|
|
63
|
+
throw new Error(logPrefix +
|
|
64
|
+
' ZeroSqlDO write circuit breaker tripped at ' +
|
|
65
|
+
new Date(trippedAt).toISOString() +
|
|
66
|
+
'; refusing SQL write: ' +
|
|
67
|
+
clippedStatement(statement));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const recordRowsWritten = (rowsWritten, statement) => {
|
|
71
|
+
const rows = Number(rowsWritten || 0);
|
|
72
|
+
if (!Number.isFinite(rows) || rows <= 0)
|
|
73
|
+
return false;
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const state = readState();
|
|
76
|
+
let windowStart = Number(state.window_start || 0);
|
|
77
|
+
let rowsInWindow = Number(state.rows_in_window || 0);
|
|
78
|
+
let firstOverAt = Number(state.first_over_at || 0);
|
|
79
|
+
const trippedAt = Number(state.tripped_at || 0);
|
|
80
|
+
if (trippedAt > 0)
|
|
81
|
+
return true;
|
|
82
|
+
const windowAgeMs = windowStart ? now - windowStart : 0;
|
|
83
|
+
if (!windowStart || windowAgeMs >= windowMs) {
|
|
84
|
+
const previousWindowWasOver = windowAgeMs < windowMs * 2 && rowsInWindow > rowsPerWindow;
|
|
85
|
+
windowStart = now;
|
|
86
|
+
rowsInWindow = 0;
|
|
87
|
+
if (!previousWindowWasOver)
|
|
88
|
+
firstOverAt = 0;
|
|
89
|
+
}
|
|
90
|
+
rowsInWindow += rows;
|
|
91
|
+
let nextTrippedAt = 0;
|
|
92
|
+
if (rowsInWindow > rowsPerWindow) {
|
|
93
|
+
if (!firstOverAt)
|
|
94
|
+
firstOverAt = now;
|
|
95
|
+
if (rowsInWindow > hardRowsPerWindow || now - firstOverAt >= sustainedMs) {
|
|
96
|
+
nextTrippedAt = now;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
rawExec('UPDATE ' +
|
|
100
|
+
table +
|
|
101
|
+
' SET window_start = ?, rows_in_window = ?, first_over_at = ?, tripped_at = ?, last_statement = ? WHERE id = 1', windowStart, rowsInWindow, firstOverAt, nextTrippedAt, clippedStatement(statement));
|
|
102
|
+
if (nextTrippedAt) {
|
|
103
|
+
console.error(logPrefix +
|
|
104
|
+
' ZeroSqlDO write circuit breaker tripped: rows_in_window=' +
|
|
105
|
+
rowsInWindow +
|
|
106
|
+
', rows_written=' +
|
|
107
|
+
rows +
|
|
108
|
+
', statement=' +
|
|
109
|
+
clippedStatement(statement));
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
sql.exec = (statement, ...params) => {
|
|
115
|
+
const text = String(statement || '');
|
|
116
|
+
const isCircuitStatement = text.includes(table);
|
|
117
|
+
const isMutation = MUTATION_RE.test(text) && !isCircuitStatement;
|
|
118
|
+
if (isMutation)
|
|
119
|
+
assertOpen(text);
|
|
120
|
+
const cursor = rawExec(statement, ...params);
|
|
121
|
+
if (isMutation && recordRowsWritten(cursor && cursor.rowsWritten, text)) {
|
|
122
|
+
assertOpen(text);
|
|
123
|
+
}
|
|
124
|
+
return cursor;
|
|
125
|
+
};
|
|
126
|
+
INSTALLED.add(sql);
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=zero-sql-write-circuit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zero-sql-write-circuit.js","sourceRoot":"","sources":["../../src/worker/zero-sql-write-circuit.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orez",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.26",
|
|
4
4
|
"description": "PGlite-powered zero-sync development backend. No Docker required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"@electric-sql/pglite": "0.4.1",
|
|
88
88
|
"@electric-sql/pglite-tools": "^0.3.1",
|
|
89
89
|
"@pgsql/traverse": "17.2.6",
|
|
90
|
-
"bedrock-sqlite": "0.4.
|
|
90
|
+
"bedrock-sqlite": "0.4.26",
|
|
91
91
|
"citty": "^0.2.0",
|
|
92
92
|
"pg-gateway": "0.3.0-beta.4",
|
|
93
93
|
"pgsql-parser": "^17.9.11",
|