okengine 0.17.2 → 0.18.3
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/README.md +9 -5
- package/manifest.v1.schema.json +61 -2
- package/package.json +18 -4
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/store.mdx +287 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/installation.mdx +1 -2
- package/site/content/docs/get-started/introduction.mdx +54 -110
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +94 -70
- package/site/content/docs/index.mdx +1 -1
- package/site/content/docs/plugins/apple.mdx +151 -0
- package/site/content/docs/plugins/discord.mdx +139 -0
- package/site/content/docs/plugins/facebook.mdx +134 -0
- package/site/content/docs/plugins/figma.mdx +138 -0
- package/site/content/docs/plugins/github.mdx +138 -0
- package/site/content/docs/plugins/google.mdx +153 -0
- package/site/content/docs/plugins/index.mdx +47 -1
- package/site/content/docs/plugins/meta.json +10 -0
- package/site/content/docs/plugins/microsoft.mdx +151 -0
- package/site/content/docs/plugins/oauth.mdx +188 -0
- package/site/content/docs/plugins/x.mdx +125 -0
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/client.mdx +58 -1
- package/site/content/docs/reference/configuration.mdx +2 -4
- package/site/content/docs/reference/fx.mdx +3 -1
- package/site/content/docs/reference/index.mdx +0 -5
- package/site/content/docs/reference/meta.json +2 -2
- package/site/content/docs/reference/okid.mdx +137 -0
- package/src/auth/bindings.ts +1 -1
- package/src/auth/config.ts +9 -0
- package/src/auth/identity-sql.ts +314 -0
- package/src/auth/identity.ts +140 -2
- package/src/auth/index.ts +17 -1
- package/src/auth/method-context.ts +3 -0
- package/src/auth/oauth-as/cimd.ts +132 -0
- package/src/auth/oauth-as/crypto.test.ts +101 -0
- package/src/auth/oauth-as/crypto.ts +393 -0
- package/src/auth/oauth-as/errors.ts +68 -0
- package/src/auth/oauth-as/http.test.ts +419 -0
- package/src/auth/oauth-as/http.ts +842 -0
- package/src/auth/oauth-as/stores.ts +61 -0
- package/src/auth/oauth-as/tables.ts +142 -0
- package/src/auth/tables.ts +0 -11
- package/src/bench/README.md +83 -0
- package/src/bench/REPORT.md +176 -0
- package/src/bench/g01-rls-stamp.bench.ts +194 -0
- package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
- package/src/bench/g03-signal-once.bench.ts +157 -0
- package/src/bench/g03-signal-reconnect.bench.ts +254 -0
- package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
- package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
- package/src/bench/g05-sustained-full.bench.ts +265 -0
- package/src/bench/g06-mixed-load.bench.ts +260 -0
- package/src/bench/g07-vault-crypto.bench.ts +100 -0
- package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
- package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
- package/src/bench/g08-store-kv-durable.bench.ts +133 -0
- package/src/bench/g08-store-sql.bench.ts +178 -0
- package/src/bench/g09-journal-sustained.bench.ts +203 -0
- package/src/bench/g10-observability-contention.bench.ts +246 -0
- package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
- package/src/bench/g13-elements.bench.ts +427 -0
- package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
- package/src/bench/g15-postgres-degradation.bench.ts +264 -0
- package/src/bench/g16-live-query-fanout.bench.ts +206 -0
- package/src/bench/lib/event-loop-lag.ts +26 -0
- package/src/bench/lib/infra.ts +60 -0
- package/src/bench/lib/report.ts +52 -0
- package/src/bench/lib/rss-sampler.ts +61 -0
- package/src/bench/lib/signal-pg.ts +88 -0
- package/src/bench/load-app.ts +337 -0
- package/src/bench/load-child.ts +108 -0
- package/src/bench/smoke.bench.ts +43 -0
- package/src/cli/competitor-mention-removal.test.ts +28 -0
- package/src/cli/doctor-fd.ts +117 -0
- package/src/cli/doctor.test.ts +192 -0
- package/src/cli/doctor.ts +129 -1
- package/src/client/create.ts +95 -1
- package/src/client/index.ts +9 -2
- package/src/client/transport.ts +11 -4
- package/src/client/use-live-query.ts +154 -0
- package/src/client-react/index.ts +15 -1
- package/src/client-react/live-resource.ts +246 -0
- package/src/client-react/use-live-query.test.ts +475 -0
- package/src/client-react/use-live-query.ts +530 -0
- package/src/compiler/extract.test.ts +518 -0
- package/src/compiler/extract.ts +386 -19
- package/src/console/server/invoke-user-flow.ts +2 -1
- package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-DY4N6nnk.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-CsPDMrVM.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-CcTDXHuz.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-CKR595wP.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
- package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-CpPFFKyE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-BsMf-9_W.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
- package/src/drivers/cdc-outbox.ts +389 -0
- package/src/drivers/memory.ts +20 -0
- package/src/drivers/oauth-apple.ts +156 -0
- package/src/drivers/oauth-discord.ts +79 -0
- package/src/drivers/oauth-facebook.ts +80 -0
- package/src/drivers/oauth-figma.ts +116 -0
- package/src/drivers/oauth-github.ts +92 -0
- package/src/drivers/oauth-google.ts +142 -0
- package/src/drivers/oauth-microsoft.ts +174 -0
- package/src/drivers/oauth-oidc.ts +293 -0
- package/src/drivers/oauth-shared.ts +326 -0
- package/src/drivers/oauth-types.ts +159 -0
- package/src/drivers/oauth-x.ts +77 -0
- package/src/drivers/oauth2-common.ts +95 -0
- package/src/drivers/oauth2-token.ts +61 -0
- package/src/drivers/pg-rls-row-passes.ts +251 -0
- package/src/drivers/pg-rls.ts +2 -0
- package/src/drivers/postgres.ts +45 -2
- package/src/drivers/signal-postgres.ts +2 -1
- package/src/elements/channel/runtime.ts +29 -2
- package/src/elements/channel.test.ts +52 -0
- package/src/elements/gate/boot.ts +29 -2
- package/src/elements/store/emit-drizzle.ts +147 -14
- package/src/elements/store/field-ddl.test.ts +118 -0
- package/src/elements/store/field-types.test.ts +455 -0
- package/src/elements/store/list-query.golden.json +777 -0
- package/src/elements/store/list-query.parity.test.ts +396 -0
- package/src/elements/store/list-query.ts +792 -0
- package/src/elements/store/live-default.test.ts +136 -0
- package/src/elements/store/live-http.test.ts +160 -0
- package/src/elements/store/live-isolation.test.ts +291 -0
- package/src/elements/store/live-query-runtime.test.ts +323 -0
- package/src/elements/store/live-query-runtime.ts +403 -0
- package/src/elements/store/live-query-server.test.ts +377 -0
- package/src/elements/store/live-query-server.ts +102 -0
- package/src/elements/store/live-query.ts +97 -0
- package/src/elements/store/resource.ts +189 -680
- package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
- package/src/elements/store/schema-decl.ts +539 -41
- package/src/elements/store/sql-rls-stamp.test.ts +27 -0
- package/src/elements/store/sql-session.ts +297 -35
- package/src/elements/store/table.ts +102 -21
- package/src/elements/store.test.ts +3 -1
- package/src/elements/store.ts +12 -1
- package/src/elements/vault/chaos-child.ts +74 -1
- package/src/elements/vault/chaos.test.ts +4 -2
- package/src/elements/vault/storage.ts +4 -2
- package/src/index.ts +4 -1
- package/src/kernel/app-auth.ts +1 -0
- package/src/kernel/app.ts +116 -2
- package/src/kernel/auth-sharing.test.ts +196 -0
- package/src/kernel/boot.test.ts +3 -3
- package/src/kernel/errors.ts +8 -0
- package/src/kernel/fx.test.ts +1 -0
- package/src/kernel/fx.ts +14 -2
- package/src/kernel/horizontal-child.ts +2 -1
- package/src/kernel/http-resource.ts +33 -7
- package/src/kernel/identity-host-persist.test.ts +119 -0
- package/src/kernel/instance-id.ts +4 -2
- package/src/kernel/journal.ts +2 -1
- package/src/kernel/mcp-tool.test.ts +95 -0
- package/src/kernel/on.ts +9 -0
- package/src/kernel/realtime-bind.ts +326 -0
- package/src/kernel/resource-live.ts +117 -0
- package/src/kernel/triggers.ts +86 -4
- package/src/manifest/diff.ts +37 -0
- package/src/manifest/types.ts +64 -2
- package/src/okid.bench.test.ts +64 -0
- package/src/okid.test.ts +338 -0
- package/src/okid.ts +245 -0
- package/src/plugins/anonymous.ts +19 -1
- package/src/plugins/auth/shared.ts +15 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +10 -8
- package/src/plugins/mcp-oauth.ts +208 -0
- package/src/plugins/oauth/flow-store.ts +117 -0
- package/src/plugins/oauth/link.ts +69 -0
- package/src/plugins/oauth/shared.ts +108 -0
- package/src/plugins/oauth/token-vault.ts +100 -0
- package/src/plugins/oauth.security.test.ts +535 -0
- package/src/plugins/oauth.ts +532 -0
- package/src/plugins/otp.ts +48 -6
- package/src/plugins/passkey.ts +20 -1
- package/src/plugins/two-factor.ts +11 -0
- package/src/plugins/username.ts +40 -7
- package/src/release/build-lib.ts +7 -1
- package/src/release/measure.ts +1 -0
- package/src/release/official-plugins.ts +4 -1
- package/src/runs/collect.ts +2 -1
- package/src/runs/drivers/files.ts +2 -1
- package/src/test/create-test-app.ts +114 -5
- package/src/test/export-bundle.test.ts +33 -0
- package/src/test/live-signals.test.ts +83 -0
- package/src/test/tenant-isolation.test.ts +175 -0
- package/src/testing.ts +26 -0
- package/src/upgrade/codemods.ts +1 -1
- package/site/content/docs/reference/migrating-environments.mdx +0 -158
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G14 — graceful shutdown under load.
|
|
3
|
+
*
|
|
4
|
+
* Extends the `instances-chaos-child.ts` graceful-hold pattern through
|
|
5
|
+
* `load-child shutdown-test` semantics, but with the REAL app: a full
|
|
6
|
+
* `load-child serve` boot installs `installGracefulShutdown`
|
|
7
|
+
* (`src/kernel/graceful-shutdown.ts`) over its server handle.
|
|
8
|
+
*
|
|
9
|
+
* Scenario: 220 held SSE subscribers (`load-child flood-sse`) + sustained
|
|
10
|
+
* HTTP load mixing `/ping` and sharedSqlConn RLS stamp writes. Then a FROZEN
|
|
11
|
+
* in-flight batch of 50 RLS stamp writes is launched, SIGTERM lands while
|
|
12
|
+
* they are mid-handler, and their outcomes classify the drain exactly:
|
|
13
|
+
* - completed vs dropped requests,
|
|
14
|
+
* - time-to-exit,
|
|
15
|
+
* - pending sharedSqlConn stamp ops completing AFTER the signal (drain).
|
|
16
|
+
*
|
|
17
|
+
* No request is initiated after SIGTERM, so every error is a genuinely
|
|
18
|
+
* dropped in-flight request — no new-connection-refusal noise.
|
|
19
|
+
*
|
|
20
|
+
* Run: OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=… OKE_TEST_REDIS_URL=… bun test ./src/bench/g14-graceful-shutdown.bench.ts --timeout 120000
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, expect, test } from "bun:test";
|
|
24
|
+
import { LIVE_PG } from "./lib/infra.ts";
|
|
25
|
+
import { DISCLAIMER, HARDWARE, writeArtifact } from "./lib/report.ts";
|
|
26
|
+
|
|
27
|
+
const CAL = process.env.OKE_BENCH_CAL === "1";
|
|
28
|
+
const PORT = 6679;
|
|
29
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
30
|
+
const SSE_COUNT = CAL ? 60 : 220;
|
|
31
|
+
const PING_WORKERS = 20;
|
|
32
|
+
const RLS_WORKERS = 40;
|
|
33
|
+
const INFLIGHT_BATCH = CAL ? 20 : 50;
|
|
34
|
+
const WARM_MS = CAL ? 2_000 : 4_000;
|
|
35
|
+
const LOAD_MS = CAL ? 2_000 : 4_000;
|
|
36
|
+
|
|
37
|
+
type Server = Bun.Subprocess<"ignore", "pipe", "ignore">;
|
|
38
|
+
|
|
39
|
+
function spawnServe(): Server {
|
|
40
|
+
return Bun.spawn(["bun", "run", "src/bench/load-child.ts", "serve", String(PORT)], {
|
|
41
|
+
stdout: "pipe",
|
|
42
|
+
stderr: "ignore",
|
|
43
|
+
env: process.env,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function spawnFloodSse(count: number): Bun.Subprocess<"ignore", "pipe", "ignore"> {
|
|
48
|
+
return Bun.spawn(
|
|
49
|
+
["bun", "run", "src/bench/load-child.ts", "flood-sse", BASE, "bench-live", String(count)],
|
|
50
|
+
{ stdout: "pipe", stderr: "ignore", env: process.env },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readReadyPid(proc: Server): Promise<number> {
|
|
55
|
+
const reader = proc.stdout.getReader();
|
|
56
|
+
const dec = new TextDecoder();
|
|
57
|
+
let buf = "";
|
|
58
|
+
for (;;) {
|
|
59
|
+
const { done, value } = await reader.read();
|
|
60
|
+
if (done) throw new Error("server died before ready");
|
|
61
|
+
buf += dec.decode(value, { stream: true });
|
|
62
|
+
const nl = buf.indexOf("\n");
|
|
63
|
+
if (nl >= 0) {
|
|
64
|
+
const line = buf.slice(0, nl).trim();
|
|
65
|
+
if (line.startsWith("{")) {
|
|
66
|
+
void (async () => {
|
|
67
|
+
try {
|
|
68
|
+
for (;;) {
|
|
69
|
+
const r = await reader.read();
|
|
70
|
+
if (r.done) break;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
/* closed */
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
return (JSON.parse(line) as { pid: number }).pid;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe.skipIf(!process.env.OKE_BENCH || !LIVE_PG)("G14 — graceful shutdown under load", () => {
|
|
83
|
+
test(
|
|
84
|
+
`${SSE_COUNT} SSE + ${PING_WORKERS + RLS_WORKERS} workers → frozen in-flight batch → SIGTERM → drain`,
|
|
85
|
+
async () => {
|
|
86
|
+
const server = spawnServe();
|
|
87
|
+
const pid = await readReadyPid(server);
|
|
88
|
+
console.log(`[G14] server ready pid=${pid}`);
|
|
89
|
+
await Bun.sleep(WARM_MS);
|
|
90
|
+
|
|
91
|
+
// Hold SSE subscribers in a separate child so the test process only
|
|
92
|
+
// runs the closing HTTP load.
|
|
93
|
+
const flood = spawnFloodSse(SSE_COUNT);
|
|
94
|
+
await Bun.sleep(1_500); // let subscribers attach
|
|
95
|
+
|
|
96
|
+
let stopLoad = false;
|
|
97
|
+
const c = { ping: 0, rls: 0, errorsBeforeSignal: 0 };
|
|
98
|
+
|
|
99
|
+
const pingLoop = async (): Promise<void> => {
|
|
100
|
+
while (!stopLoad) {
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetch(`${BASE}/ping`);
|
|
103
|
+
await res.arrayBuffer();
|
|
104
|
+
if (!res.ok) c.errorsBeforeSignal++;
|
|
105
|
+
else c.ping++;
|
|
106
|
+
} catch {
|
|
107
|
+
if (!stopLoad) c.errorsBeforeSignal++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const rlsLoop = async (): Promise<void> => {
|
|
112
|
+
while (!stopLoad) {
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetch(`${BASE}/_/bench/rls`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "content-type": "application/json" },
|
|
117
|
+
body: JSON.stringify({ tenantId: `g14-${Math.floor(Math.random() * 8)}`, n: 1 }),
|
|
118
|
+
});
|
|
119
|
+
await res.arrayBuffer();
|
|
120
|
+
if (!res.ok) c.errorsBeforeSignal++;
|
|
121
|
+
else c.rls++;
|
|
122
|
+
} catch {
|
|
123
|
+
if (!stopLoad) c.errorsBeforeSignal++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const running = Promise.all([
|
|
129
|
+
...Array.from({ length: PING_WORKERS }, pingLoop),
|
|
130
|
+
...Array.from({ length: RLS_WORKERS }, rlsLoop),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
await Bun.sleep(LOAD_MS);
|
|
134
|
+
// Freeze the sustained load; wait for its last requests to settle so
|
|
135
|
+
// nothing except the frozen batch is in flight at signal time.
|
|
136
|
+
stopLoad = true;
|
|
137
|
+
await running;
|
|
138
|
+
expect(c.errorsBeforeSignal).toBe(0);
|
|
139
|
+
|
|
140
|
+
// Frozen in-flight batch — multi-roundtrip sharedSqlConn stamp writes.
|
|
141
|
+
// SIGTERM lands while all of these are mid-handler.
|
|
142
|
+
let batchSettled = false;
|
|
143
|
+
let batchDrained = 0;
|
|
144
|
+
let batchDropped = 0;
|
|
145
|
+
const batchErrors: string[] = [];
|
|
146
|
+
const batch = Promise.all(
|
|
147
|
+
Array.from({ length: INFLIGHT_BATCH }, async () => {
|
|
148
|
+
try {
|
|
149
|
+
const res = await fetch(`${BASE}/_/bench/rls`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { "content-type": "application/json" },
|
|
152
|
+
body: JSON.stringify({ tenantId: "g14-inflight", n: 1 }),
|
|
153
|
+
});
|
|
154
|
+
await res.arrayBuffer();
|
|
155
|
+
if (res.ok && !batchSettled) batchDrained++;
|
|
156
|
+
else if (!batchSettled) batchDropped++;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (!batchSettled) {
|
|
159
|
+
batchDropped++;
|
|
160
|
+
if (batchErrors.length < 3) batchErrors.push(String(err));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await Bun.sleep(15); // batch is now mid-handler behind the RLS stamps
|
|
167
|
+
const tSignal = performance.now();
|
|
168
|
+
server.kill("SIGTERM");
|
|
169
|
+
await batch;
|
|
170
|
+
batchSettled = true;
|
|
171
|
+
|
|
172
|
+
const exitRace = await Promise.race([
|
|
173
|
+
server.exited.then(() => "exited" as const),
|
|
174
|
+
Bun.sleep(20_000).then(() => "timeout" as const),
|
|
175
|
+
]);
|
|
176
|
+
const timeToExitMs =
|
|
177
|
+
exitRace === "exited" ? Number((performance.now() - tSignal).toFixed(1)) : -1;
|
|
178
|
+
if (exitRace === "timeout") {
|
|
179
|
+
server.kill("SIGKILL");
|
|
180
|
+
await server.exited.catch(() => {});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Flood child exits (printing floodDone) once every SSE stream ends.
|
|
184
|
+
const floodExit = await Promise.race([
|
|
185
|
+
flood.exited.then(() => "closed" as const),
|
|
186
|
+
Bun.sleep(10_000).then(() => "timeout" as const),
|
|
187
|
+
]);
|
|
188
|
+
if (floodExit === "timeout") flood.kill();
|
|
189
|
+
|
|
190
|
+
const metrics: Record<string, number> = {
|
|
191
|
+
sseSubscribers: SSE_COUNT,
|
|
192
|
+
sseClosedOnShutdown: floodExit === "closed" ? 1 : 0,
|
|
193
|
+
inflightBatchSize: INFLIGHT_BATCH,
|
|
194
|
+
requestsCompletedBeforeSignal: c.ping + c.rls,
|
|
195
|
+
pingCompletedBeforeSignal: c.ping,
|
|
196
|
+
rlsStampWritesBeforeSignal: c.rls,
|
|
197
|
+
inflightDrainedAfterSignal: batchDrained,
|
|
198
|
+
inflightDroppedAfterSignal: batchDropped,
|
|
199
|
+
droppedRequests: batchDropped,
|
|
200
|
+
timeToExitMs,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const issues: string[] = [
|
|
204
|
+
// Calibration-run history: the first pass classified post-SIGTERM
|
|
205
|
+
// *new* connection attempts ("unable to connect") as dropped
|
|
206
|
+
// in-flight work. Methodology was corrected to a frozen in-flight
|
|
207
|
+
// batch before the signal — measurement fix, not a product fix.
|
|
208
|
+
"note: first CAL pass used a load-loop that kept opening new requests during the SIGTERM window and miscounted connection refusals as dropped in-flight work; replaced with the frozen in-flight-batch method (no product change)",
|
|
209
|
+
];
|
|
210
|
+
if (batchDropped > 0) {
|
|
211
|
+
issues.push(
|
|
212
|
+
`${batchDropped}/${INFLIGHT_BATCH} in-flight sharedSqlConn stamp writes dropped across SIGTERM — graceful shutdown did not fully drain: ${batchErrors.join("; ")}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
if (timeToExitMs < 0) {
|
|
216
|
+
issues.push("server did not exit within 20s of SIGTERM");
|
|
217
|
+
} else if (timeToExitMs > 10_000) {
|
|
218
|
+
issues.push(`time-to-exit ${timeToExitMs}ms exceeds 10s`);
|
|
219
|
+
}
|
|
220
|
+
if (floodExit !== "closed")
|
|
221
|
+
issues.push("SSE subscribers not all closed within 10s of shutdown");
|
|
222
|
+
|
|
223
|
+
console.log("[G14] metrics:", JSON.stringify(metrics));
|
|
224
|
+
if (issues.length > 0) console.log("[G14] issues:", issues);
|
|
225
|
+
const path = await writeArtifact({
|
|
226
|
+
group: "G14-graceful-shutdown",
|
|
227
|
+
hardware: HARDWARE,
|
|
228
|
+
disclaimer: DISCLAIMER,
|
|
229
|
+
command:
|
|
230
|
+
"OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=$DATABASE_URL OKE_TEST_REDIS_URL=$OKE_TEST_REDIS_URL bun test ./src/bench/g14-graceful-shutdown.bench.ts --timeout 120000",
|
|
231
|
+
metrics,
|
|
232
|
+
issues,
|
|
233
|
+
fixes: [],
|
|
234
|
+
remeasured: null,
|
|
235
|
+
});
|
|
236
|
+
console.log(`[G14] artifact: ${path}`);
|
|
237
|
+
|
|
238
|
+
expect(c.ping + c.rls).toBeGreaterThan(100);
|
|
239
|
+
expect(c.rls).toBeGreaterThan(10);
|
|
240
|
+
expect(batchDrained).toBeGreaterThanOrEqual(INFLIGHT_BATCH - 2);
|
|
241
|
+
},
|
|
242
|
+
CAL ? 60_000 : 120_000,
|
|
243
|
+
);
|
|
244
|
+
});
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G15 — Postgres degradation (in-process backpressure).
|
|
3
|
+
*
|
|
4
|
+
* Test-only driver wrapper honors `OKE_BENCH_PG_DELAY_MS`: every
|
|
5
|
+
* query/exec awaits `Bun.sleep(delay)` BEFORE hitting live Postgres
|
|
6
|
+
* (preferred over pgdog network delays for reproducibility). Live PG only —
|
|
7
|
+
* never PGlite.
|
|
8
|
+
*
|
|
9
|
+
* Open-loop arrivals hammer ONE sharedSqlConn through RLS-stamped sessions
|
|
10
|
+
* while SQL is stalled at 500ms/query. Watch: unbounded `withRlsStampLock`
|
|
11
|
+
* queue growth / OOM vs loud bounded errors.
|
|
12
|
+
*
|
|
13
|
+
* MANDATORY HONESTY NOTE (carried in the artifact): this is an IN-PROCESS
|
|
14
|
+
* BACKPRESSURE TEST ONLY — it does NOT simulate network jitter, disk
|
|
15
|
+
* contention, or pooler failure modes.
|
|
16
|
+
*
|
|
17
|
+
* Run: OKE_BENCH=1 OKE_BENCH_PG_DELAY_MS=500 OKE_TEST_POSTGRES=1 DATABASE_URL=… bun test ./src/bench/g15-postgres-degradation.bench.ts --timeout 300000
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, expect, test } from "bun:test";
|
|
21
|
+
import { connectPostgres, postgresDriver } from "../drivers/postgres.ts";
|
|
22
|
+
import type { SqlDriver, SqlConnection } from "../drivers/types.ts";
|
|
23
|
+
import { sql } from "../elements/store/declare.ts";
|
|
24
|
+
import { createSqlStoreHandle, type SqlStoreHandle } from "../elements/store/sql-session.ts";
|
|
25
|
+
import { createStoreRuntime } from "../elements/store/runtime.ts";
|
|
26
|
+
import { LIVE_PG } from "./lib/infra.ts";
|
|
27
|
+
import { measureEventLoopLag } from "./lib/event-loop-lag.ts";
|
|
28
|
+
import { DISCLAIMER, HARDWARE, percentile, writeArtifact } from "./lib/report.ts";
|
|
29
|
+
|
|
30
|
+
const CAL = process.env.OKE_BENCH_CAL === "1";
|
|
31
|
+
const DELAY_MS = Math.max(0, Number(process.env.OKE_BENCH_PG_DELAY_MS ?? "500"));
|
|
32
|
+
const DURATION_S = CAL ? 12 : Math.max(10, Number(process.env.OKE_BENCH_G15_S ?? "30"));
|
|
33
|
+
const ARRIVALS_PER_SEC = CAL ? 6 : Math.max(1, Number(process.env.OKE_BENCH_G15_RATE ?? "10"));
|
|
34
|
+
const SAMPLE_MS = 250;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Test-only driver wrapper: injects `Bun.sleep(DELAY_MS)` before every
|
|
38
|
+
* query/exec on connections this driver opens. Transaction pinning and
|
|
39
|
+
* role routing pass through untouched.
|
|
40
|
+
*/
|
|
41
|
+
function delayedPostgresDriver(delayMs: number): SqlDriver {
|
|
42
|
+
if (delayMs <= 0) return postgresDriver;
|
|
43
|
+
return {
|
|
44
|
+
id: "postgres",
|
|
45
|
+
facet: "sql",
|
|
46
|
+
connect: async (options): Promise<SqlConnection> => {
|
|
47
|
+
const conn = await connectPostgres(options);
|
|
48
|
+
const slowed: SqlConnection = {
|
|
49
|
+
driverId: conn.driverId,
|
|
50
|
+
role: conn.role,
|
|
51
|
+
async query(text, params = []) {
|
|
52
|
+
await Bun.sleep(delayMs);
|
|
53
|
+
return conn.query(text, params);
|
|
54
|
+
},
|
|
55
|
+
async exec(text, params = []) {
|
|
56
|
+
await Bun.sleep(delayMs);
|
|
57
|
+
return conn.exec(text, params);
|
|
58
|
+
},
|
|
59
|
+
transaction(fn) {
|
|
60
|
+
if (!conn.transaction) {
|
|
61
|
+
throw new Error("G15: underlying postgres connection lacks transaction()");
|
|
62
|
+
}
|
|
63
|
+
return conn.transaction(fn);
|
|
64
|
+
},
|
|
65
|
+
close() {
|
|
66
|
+
return conn.close();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
return slowed;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe.skipIf(!process.env.OKE_BENCH || !LIVE_PG)(
|
|
75
|
+
"G15 — Postgres degradation (in-process backpressure)",
|
|
76
|
+
() => {
|
|
77
|
+
test(
|
|
78
|
+
`open-loop ${ARRIVALS_PER_SEC}/s vs sharedSqlConn @ ${DELAY_MS}ms/query for ${DURATION_S}s`,
|
|
79
|
+
async () => {
|
|
80
|
+
const db = sql("g15degrade");
|
|
81
|
+
const runtime = createStoreRuntime({
|
|
82
|
+
drivers: { sql: delayedPostgresDriver(DELAY_MS) },
|
|
83
|
+
sql: { g15degrade: { name: "g15degrade", primary: { url: LIVE_PG! } } },
|
|
84
|
+
now: () => Date.now(),
|
|
85
|
+
});
|
|
86
|
+
runtime.register(db);
|
|
87
|
+
const maybeConn = await runtime.primarySql();
|
|
88
|
+
if (!maybeConn) throw new Error("G15: runtime.primarySql() returned undefined");
|
|
89
|
+
const c: SqlConnection = maybeConn;
|
|
90
|
+
// DDL is stamp-exempt — set up before the stall matters anyway.
|
|
91
|
+
await c.exec(`CREATE TABLE IF NOT EXISTS oke_bench_g15 (
|
|
92
|
+
id TEXT PRIMARY KEY, n BIGINT NOT NULL DEFAULT 0)`);
|
|
93
|
+
await c.exec(
|
|
94
|
+
`INSERT INTO oke_bench_g15 (id, n) VALUES ('g15-counter', 0)
|
|
95
|
+
ON CONFLICT (id) DO UPDATE SET n = 0`,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const IDENTITIES = [
|
|
99
|
+
undefined,
|
|
100
|
+
{ gate: "member", userId: "g15-alice", scopes: ["member"] },
|
|
101
|
+
{ gate: "member", userId: "g15-bob", scopes: ["member"] },
|
|
102
|
+
] as const;
|
|
103
|
+
let handleIdx = 0;
|
|
104
|
+
function nextSession(): SqlStoreHandle {
|
|
105
|
+
const identity = IDENTITIES[handleIdx % IDENTITIES.length]!;
|
|
106
|
+
handleIdx++;
|
|
107
|
+
return createSqlStoreHandle(db.ref, {
|
|
108
|
+
connection: c,
|
|
109
|
+
classifications: new Map(),
|
|
110
|
+
routedRole: "primary",
|
|
111
|
+
domainDdl: "off",
|
|
112
|
+
...(identity ? { rls: identity } : {}),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let inflight = 0;
|
|
117
|
+
let maxInflight = 0;
|
|
118
|
+
let completed = 0;
|
|
119
|
+
let failed = 0;
|
|
120
|
+
const failureSamples: string[] = [];
|
|
121
|
+
|
|
122
|
+
async function oneOp(): Promise<void> {
|
|
123
|
+
inflight++;
|
|
124
|
+
if (inflight > maxInflight) maxInflight = inflight;
|
|
125
|
+
try {
|
|
126
|
+
const session = nextSession();
|
|
127
|
+
const rows = await session
|
|
128
|
+
.select()
|
|
129
|
+
.from({ name: "oke_bench_g15" })
|
|
130
|
+
.where({ id: "g15-counter" })
|
|
131
|
+
.limit(1);
|
|
132
|
+
const cur = Number((rows[0] as { n?: number } | undefined)?.n ?? 0);
|
|
133
|
+
await session
|
|
134
|
+
.update({ name: "oke_bench_g15" })
|
|
135
|
+
.set({ n: cur + 1 })
|
|
136
|
+
.where({ id: "g15-counter" });
|
|
137
|
+
completed++;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
failed++;
|
|
140
|
+
if (failureSamples.length < 5) {
|
|
141
|
+
failureSamples.push(err instanceof Error ? err.message : String(err));
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
inflight--;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Background samplers.
|
|
149
|
+
const rssSamples: { tMs: number; rssMb: number }[] = [];
|
|
150
|
+
const inflightSamples: { tMs: number; inflight: number }[] = [];
|
|
151
|
+
const t0 = performance.now();
|
|
152
|
+
const samplerId = setInterval(() => {
|
|
153
|
+
const tMs = performance.now() - t0;
|
|
154
|
+
rssSamples.push({ tMs, rssMb: process.memoryUsage.rss() / 1024 / 1024 });
|
|
155
|
+
inflightSamples.push({ tMs, inflight });
|
|
156
|
+
}, SAMPLE_MS);
|
|
157
|
+
const lag = measureEventLoopLag(100);
|
|
158
|
+
|
|
159
|
+
// Open-loop arrivals: fixed cadence regardless of completion.
|
|
160
|
+
const arrivalId = setInterval(() => void oneOp(), 1000 / ARRIVALS_PER_SEC);
|
|
161
|
+
await Bun.sleep(DURATION_S * 1000);
|
|
162
|
+
clearInterval(arrivalId);
|
|
163
|
+
clearInterval(samplerId);
|
|
164
|
+
lag.stop();
|
|
165
|
+
|
|
166
|
+
// Let the CURRENTLY ARRIVING tick settle, then freeze the picture.
|
|
167
|
+
await Bun.sleep(50);
|
|
168
|
+
const finalInflight = inflight;
|
|
169
|
+
|
|
170
|
+
const rssStart = rssSamples[0]?.rssMb ?? 0;
|
|
171
|
+
const rssEnd = rssSamples.at(-1)?.rssMb ?? 0;
|
|
172
|
+
// Least-squares MB/min over all samples.
|
|
173
|
+
const n = rssSamples.length;
|
|
174
|
+
let rssSlope = 0;
|
|
175
|
+
if (n >= 2) {
|
|
176
|
+
const sx = rssSamples.reduce((a, s) => a + s.tMs, 0);
|
|
177
|
+
const sy = rssSamples.reduce((a, s) => a + s.rssMb, 0);
|
|
178
|
+
const sxy = rssSamples.reduce((a, s) => a + s.tMs * s.rssMb, 0);
|
|
179
|
+
const sxx = rssSamples.reduce((a, s) => a + s.tMs * s.tMs, 0);
|
|
180
|
+
rssSlope = ((n * sxy - sx * sy) / (n * sxx - sx * sx || 1)) * 60_000;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const lags = lag.lags();
|
|
184
|
+
const metrics: Record<string, number> = {
|
|
185
|
+
pgDelayMs: DELAY_MS,
|
|
186
|
+
durationS: DURATION_S,
|
|
187
|
+
arrivalsPerSec: ARRIVALS_PER_SEC,
|
|
188
|
+
completedOps: completed,
|
|
189
|
+
failedOps: failed,
|
|
190
|
+
errorRatePct: Number(((failed / (completed + failed || 1)) * 100).toFixed(3)),
|
|
191
|
+
opsPerSec: Number((completed / DURATION_S).toFixed(2)),
|
|
192
|
+
maxInflight: maxInflight,
|
|
193
|
+
finalInflight: finalInflight,
|
|
194
|
+
avgInflight: Number(
|
|
195
|
+
(
|
|
196
|
+
inflightSamples.reduce((a, s) => a + s.inflight, 0) / (inflightSamples.length || 1)
|
|
197
|
+
).toFixed(1),
|
|
198
|
+
),
|
|
199
|
+
rssStartMb: Number(rssStart.toFixed(1)),
|
|
200
|
+
rssEndMb: Number(rssEnd.toFixed(1)),
|
|
201
|
+
rssGrowthMb: Number((rssEnd - rssStart).toFixed(1)),
|
|
202
|
+
rssSlopeMbPerMin: Number(rssSlope.toFixed(2)),
|
|
203
|
+
lagP50Ms: Number(percentile(lags, 50).toFixed(2)),
|
|
204
|
+
lagP99Ms: Number(percentile(lags, 99).toFixed(2)),
|
|
205
|
+
lagMaxMs: lags.length > 0 ? Number(Math.max(...lags).toFixed(2)) : 0,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const issues: string[] = [];
|
|
209
|
+
if (failed > 0) {
|
|
210
|
+
issues.push(
|
|
211
|
+
`${failed} op(s) failed under stall — sample errors: ${failureSamples.join(" | ")}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (metrics.rssSlopeMbPerMin! > 100 || metrics.rssGrowthMb! > 300) {
|
|
215
|
+
issues.push(
|
|
216
|
+
`RSS grew ${metrics.rssGrowthMb}MB (${metrics.rssSlopeMbPerMin}MB/min) — OOM-shaped growth while the withRlsStampLock queue built up`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (metrics.lagP99Ms! > 250) {
|
|
220
|
+
issues.push(
|
|
221
|
+
`event-loop lag p99 ${metrics.lagP99Ms}ms — backpressure is leaking onto the loop`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (issues.length === 0 && finalInflight > ARRIVALS_PER_SEC) {
|
|
225
|
+
issues.push(
|
|
226
|
+
`note (expected physics): withRlsStampLock queue grew UNBOUNDED without any loud bounded error — ` +
|
|
227
|
+
`finalInflight=${finalInflight} at cutoff (arrivals ${ARRIVALS_PER_SEC}/s outpace serialized service ~${metrics.opsPerSec}/s at ${DELAY_MS}ms/query). ` +
|
|
228
|
+
`Memory stayed flat (+${metrics.rssGrowthMb}MB), so this is unbounded LATENCY exposure, not OOM — callers wait forever instead of failing fast`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log("[G15] metrics:", JSON.stringify(metrics));
|
|
233
|
+
if (issues.length > 0) console.log("[G15] issues:", issues);
|
|
234
|
+
|
|
235
|
+
const path = await writeArtifact({
|
|
236
|
+
group: "G15-postgres-degradation",
|
|
237
|
+
hardware: HARDWARE,
|
|
238
|
+
disclaimer: DISCLAIMER,
|
|
239
|
+
command:
|
|
240
|
+
"OKE_BENCH=1 OKE_BENCH_PG_DELAY_MS=500 OKE_TEST_POSTGRES=1 DATABASE_URL=$DATABASE_URL bun test ./src/bench/g15-postgres-degradation.bench.ts --timeout 300000",
|
|
241
|
+
metrics,
|
|
242
|
+
issues: [
|
|
243
|
+
// Mandatory honesty note — in the artifact, not just the header.
|
|
244
|
+
"HONESTY NOTE: in-process backpressure test only — does not simulate network jitter, disk contention, or pooler failure modes",
|
|
245
|
+
...issues,
|
|
246
|
+
],
|
|
247
|
+
fixes: [],
|
|
248
|
+
remeasured: null,
|
|
249
|
+
});
|
|
250
|
+
console.log(`[G15] artifact: ${path}`);
|
|
251
|
+
|
|
252
|
+
expect(failed).toBe(0);
|
|
253
|
+
expect(completed).toBeGreaterThan(0);
|
|
254
|
+
// Backpressure must be observable: the queue builds instead of
|
|
255
|
+
// silently dropping work.
|
|
256
|
+
expect(maxInflight).toBeGreaterThan(ARRIVALS_PER_SEC);
|
|
257
|
+
|
|
258
|
+
// Do not drain the multi-minute queue — abandon it with the runtime.
|
|
259
|
+
await runtime.close().catch(() => {});
|
|
260
|
+
},
|
|
261
|
+
CAL ? 90_000 : 300_000,
|
|
262
|
+
);
|
|
263
|
+
},
|
|
264
|
+
);
|