okengine 0.17.2 → 0.18.4
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/AGENTS.md +5 -3
- package/README.md +14 -10
- package/manifest.v1.schema.json +61 -2
- package/package.json +19 -5
- package/site/content/docs/ai/index.mdx +1 -1
- package/site/content/docs/ai/mcp.mdx +10 -1
- package/site/content/docs/ai/skills.mdx +9 -9
- package/site/content/docs/elements/ai.mdx +1 -1
- package/site/content/docs/elements/clock.mdx +1 -1
- package/site/content/docs/elements/flow.mdx +25 -1
- package/site/content/docs/elements/gate.mdx +3 -2
- package/site/content/docs/elements/store.mdx +289 -341
- package/site/content/docs/elements/vault.mdx +5 -5
- package/site/content/docs/get-started/basic-usage.mdx +3 -10
- package/site/content/docs/get-started/index.mdx +1 -1
- package/site/content/docs/get-started/installation.mdx +2 -3
- package/site/content/docs/get-started/introduction.mdx +58 -121
- package/site/content/docs/get-started/meta.json +9 -1
- package/site/content/docs/get-started/project-structure.mdx +4 -11
- package/site/content/docs/get-started/testing.mdx +328 -0
- package/site/content/docs/get-started/why.mdx +93 -71
- package/site/content/docs/index.mdx +44 -11
- package/site/content/docs/meta.json +8 -5
- 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/providers/index.mdx +2 -0
- package/site/content/docs/recipes/index.mdx +2 -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-De7Lc2JC.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.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-4rHOePuE.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.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 +5 -2
- 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,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G7b — Vault rotate-under-read (live Postgres).
|
|
3
|
+
*
|
|
4
|
+
* Two arms, matching the chaos-suite contracts:
|
|
5
|
+
*
|
|
6
|
+
* Arm A — IN-PROCESS reader sharing the unsealed adapter while
|
|
7
|
+
* `rotateMaster` + `continueRotateMaster` run to completion.
|
|
8
|
+
* Contract: ZERO failed reads, zero torn values; capture reader
|
|
9
|
+
* p50/p99 overall and inside the rotation window.
|
|
10
|
+
*
|
|
11
|
+
* Arm B — SECOND OS process (`chaos-child.ts bench-read-loop`) holding
|
|
12
|
+
* ONLY the old master key during a second full rotation.
|
|
13
|
+
* Contract: intact values or LOUD failure — never torn bytes
|
|
14
|
+
* (`chaos.test.ts` §3b). Loud unwrap failures mid-rewrap are
|
|
15
|
+
* expected physics for an old-key reader and are recorded, not
|
|
16
|
+
* asserted zero.
|
|
17
|
+
*
|
|
18
|
+
* Run: OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=… bun test ./src/bench/g07-vault-rotate-under-read.bench.ts --timeout 120000
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
22
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { connectPostgres } from "../drivers/postgres.ts";
|
|
26
|
+
import type { SqlConnection } from "../drivers/types.ts";
|
|
27
|
+
import {
|
|
28
|
+
createBuiltinVaultAdapter,
|
|
29
|
+
sqlConnectionAsExec,
|
|
30
|
+
} from "../elements/vault/builtin-adapter.ts";
|
|
31
|
+
import { generateMasterKey, masterKeyToBase64 } from "../elements/vault/crypto.ts";
|
|
32
|
+
import { resetVaultTables } from "../elements/vault/test-helpers.ts";
|
|
33
|
+
import { LIVE_PG } from "./lib/infra.ts";
|
|
34
|
+
import { DISCLAIMER, HARDWARE, percentile, writeArtifact } from "./lib/report.ts";
|
|
35
|
+
|
|
36
|
+
const CAL = process.env.OKE_BENCH_CAL === "1";
|
|
37
|
+
const READ_DURATION_MS = CAL ? 4_000 : 12_000;
|
|
38
|
+
const PADS = CAL ? 6 : 12;
|
|
39
|
+
|
|
40
|
+
const CHILD = new URL("../elements/vault/chaos-child.ts", import.meta.url).pathname;
|
|
41
|
+
const PATH = "bench/rotate-read-target";
|
|
42
|
+
const VALUE = "rotate-under-read-stable-value";
|
|
43
|
+
|
|
44
|
+
let conn: SqlConnection;
|
|
45
|
+
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
if (!LIVE_PG) throw new Error("G7b needs live Postgres (OKE_TEST_POSTGRES=1 + DATABASE_URL)");
|
|
48
|
+
// Dedicated client: rotation holds manual BEGIN state across statements —
|
|
49
|
+
// the shared pool raises ERR_POSTGRES_UNSAFE_TRANSACTION (see artifact).
|
|
50
|
+
conn = await connectPostgres({ url: LIVE_PG, pool: { max: 1 } });
|
|
51
|
+
}, 15_000);
|
|
52
|
+
|
|
53
|
+
afterAll(async () => {
|
|
54
|
+
await conn?.close();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
interface ReadLine {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
durMs?: number;
|
|
60
|
+
value?: string | null;
|
|
61
|
+
error?: string;
|
|
62
|
+
at?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function loadReads(path: string): Promise<ReadLine[]> {
|
|
66
|
+
if (!(await Bun.file(path).exists())) return [];
|
|
67
|
+
return (await Bun.file(path).text())
|
|
68
|
+
.trim()
|
|
69
|
+
.split("\n")
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((l) => JSON.parse(l) as ReadLine);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe.skipIf(!process.env.OKE_BENCH || !LIVE_PG)("G7b — vault rotate under read", () => {
|
|
75
|
+
test(
|
|
76
|
+
"arm A in-process reader: zero failed reads; arm B old-key child: never torn",
|
|
77
|
+
async () => {
|
|
78
|
+
const dir = await mkdtemp(join(tmpdir(), "oke-bench-g07-"));
|
|
79
|
+
const readsPath = join(dir, "reads.jsonl");
|
|
80
|
+
const stopPath = join(dir, "stop");
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await resetVaultTables(conn);
|
|
84
|
+
const adapter = createBuiltinVaultAdapter({
|
|
85
|
+
db: sqlConnectionAsExec(conn),
|
|
86
|
+
kekRewrapBatchSize: CAL ? 2 : 1,
|
|
87
|
+
});
|
|
88
|
+
const init = await adapter.initialize();
|
|
89
|
+
await adapter.unseal(init.masterKey);
|
|
90
|
+
await adapter.set(PATH, VALUE);
|
|
91
|
+
for (let i = 0; i < PADS; i += 1) {
|
|
92
|
+
await adapter.set(`bench/pad-${i}`, `p-${i}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- Arm A: in-process concurrent reader vs full rotation ----------
|
|
96
|
+
const armA: ReadLine[] = [];
|
|
97
|
+
let stopA = false;
|
|
98
|
+
const readerA = (async () => {
|
|
99
|
+
while (!stopA) {
|
|
100
|
+
const s = performance.now();
|
|
101
|
+
try {
|
|
102
|
+
const secret = await adapter.get(PATH);
|
|
103
|
+
armA.push({
|
|
104
|
+
ok: true,
|
|
105
|
+
durMs: Number((performance.now() - s).toFixed(3)),
|
|
106
|
+
value: secret?.value ?? null,
|
|
107
|
+
at: Date.now(),
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
armA.push({
|
|
111
|
+
ok: false,
|
|
112
|
+
durMs: Number((performance.now() - s).toFixed(3)),
|
|
113
|
+
error: error instanceof Error ? error.message : String(error),
|
|
114
|
+
at: Date.now(),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
await Bun.sleep(0);
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
120
|
+
|
|
121
|
+
await Bun.sleep(50);
|
|
122
|
+
const rotateStartA = Date.now();
|
|
123
|
+
const keyA = generateMasterKey();
|
|
124
|
+
let progress = await adapter.rotateMaster(keyA);
|
|
125
|
+
let continues = 0;
|
|
126
|
+
while (progress.remaining > 0) {
|
|
127
|
+
progress = await adapter.continueRotateMaster();
|
|
128
|
+
continues += 1;
|
|
129
|
+
}
|
|
130
|
+
const rotateEndA = Date.now();
|
|
131
|
+
stopA = true;
|
|
132
|
+
await readerA;
|
|
133
|
+
|
|
134
|
+
const failedA = armA.filter((l) => !l.ok);
|
|
135
|
+
const tornA = armA.filter((l) => l.ok && typeof l.value === "string" && l.value !== VALUE);
|
|
136
|
+
const dursA = armA.map((l) => l.durMs ?? NaN).filter(Number.isFinite);
|
|
137
|
+
const windowA = armA.filter(
|
|
138
|
+
(l) => (l.at ?? 0) >= rotateStartA && (l.at ?? 0) <= rotateEndA,
|
|
139
|
+
);
|
|
140
|
+
const windowDursA = windowA.map((l) => l.durMs ?? NaN).filter(Number.isFinite);
|
|
141
|
+
|
|
142
|
+
console.log(
|
|
143
|
+
`[G7b] armA reads=${armA.length} failed=${failedA.length} torn=${tornA.length} window=${windowA.length} rotateMs=${rotateEndA - rotateStartA}`,
|
|
144
|
+
);
|
|
145
|
+
if (failedA.length > 0) {
|
|
146
|
+
console.warn("[G7b] armA sample failures:", JSON.stringify(failedA.slice(0, 5)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- Arm B: separate OS process holding only the pre-B master -----
|
|
150
|
+
// The child unseals with keyA — the key CURRENT when it starts (post
|
|
151
|
+
// rotation A) — then the parent rotates to a fresh key it never sees.
|
|
152
|
+
const keyB = generateMasterKey();
|
|
153
|
+
const reader = Bun.spawn({
|
|
154
|
+
cmd: [
|
|
155
|
+
"bun",
|
|
156
|
+
CHILD,
|
|
157
|
+
"bench-read-loop",
|
|
158
|
+
LIVE_PG!,
|
|
159
|
+
masterKeyToBase64(keyA),
|
|
160
|
+
PATH,
|
|
161
|
+
readsPath,
|
|
162
|
+
stopPath,
|
|
163
|
+
String(READ_DURATION_MS),
|
|
164
|
+
],
|
|
165
|
+
stdout: "pipe",
|
|
166
|
+
stderr: "pipe",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await Bun.sleep(200);
|
|
170
|
+
// Wait until the child is demonstrably reading OK at keyA before
|
|
171
|
+
// rotating — a Bun cold start can take >500ms, and rotating mid-unseal
|
|
172
|
+
// would make EVERY subsequent child read fail on a stale KEK.
|
|
173
|
+
const childReadyBy = Date.now() + 8_000;
|
|
174
|
+
for (;;) {
|
|
175
|
+
const early = await loadReads(readsPath);
|
|
176
|
+
if (early.filter((l) => l.ok).length >= 3) break;
|
|
177
|
+
if (Date.now() > childReadyBy) throw new Error("G7b: reader child never became ready");
|
|
178
|
+
await Bun.sleep(100);
|
|
179
|
+
}
|
|
180
|
+
const rotateStartB = Date.now();
|
|
181
|
+
progress = await adapter.rotateMaster(keyB);
|
|
182
|
+
while (progress.remaining > 0) {
|
|
183
|
+
progress = await adapter.continueRotateMaster();
|
|
184
|
+
}
|
|
185
|
+
const rotateEndB = Date.now();
|
|
186
|
+
// Let the reader finish its FULL sustained window (steady-state tail
|
|
187
|
+
// after the rotation matters for p99), then stop it.
|
|
188
|
+
const childElapsedMs = Date.now() - (rotateStartB - 300);
|
|
189
|
+
if (childElapsedMs < READ_DURATION_MS) {
|
|
190
|
+
await Bun.sleep(READ_DURATION_MS - childElapsedMs + 250);
|
|
191
|
+
}
|
|
192
|
+
await Bun.write(stopPath, "stop");
|
|
193
|
+
await reader.exited;
|
|
194
|
+
|
|
195
|
+
const armB = await loadReads(readsPath);
|
|
196
|
+
expect(armB.length).toBeGreaterThan(5);
|
|
197
|
+
|
|
198
|
+
const loudB = armB.filter((l) => !l.ok);
|
|
199
|
+
const tornB = armB.filter(
|
|
200
|
+
(l) => l.ok && typeof l.value === "string" && l.value !== VALUE && l.value.length > 0,
|
|
201
|
+
);
|
|
202
|
+
const silentWrongB = armB.filter(
|
|
203
|
+
(l) => !l.ok && !(l.error ?? "").includes("unable to unwrap"),
|
|
204
|
+
);
|
|
205
|
+
const dursB = armB.map((l) => l.durMs ?? NaN).filter(Number.isFinite);
|
|
206
|
+
const windowB = armB.filter(
|
|
207
|
+
(l) => (l.at ?? 0) >= rotateStartB && (l.at ?? 0) <= rotateEndB,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
console.log(
|
|
211
|
+
`[G7b] armB reads=${armB.length} ok=${armB.length - loudB.length} loudFail=${loudB.length} torn=${tornB.length}`,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const metrics: Record<string, number> = {
|
|
215
|
+
"armA.totalReads": armA.length,
|
|
216
|
+
"armA.failedReads": failedA.length,
|
|
217
|
+
"armA.tornReads": tornA.length,
|
|
218
|
+
"armA.rotationMs": rotateEndA - rotateStartA,
|
|
219
|
+
"armA.continueRotateCalls": continues,
|
|
220
|
+
"armA.readerP50Ms": Number(percentile(dursA, 50).toFixed(3)),
|
|
221
|
+
"armA.readerP99Ms": Number(percentile(dursA, 99).toFixed(3)),
|
|
222
|
+
"armA.windowReads": windowA.length,
|
|
223
|
+
"armA.windowP99Ms": Number(percentile(windowDursA, 99).toFixed(3)),
|
|
224
|
+
"armB.totalReads": armB.length,
|
|
225
|
+
"armB.loudFailures": loudB.length,
|
|
226
|
+
"armB.silentWrongErrors": silentWrongB.length,
|
|
227
|
+
"armB.tornReads": tornB.length,
|
|
228
|
+
"armB.rotationMs": rotateEndB - rotateStartB,
|
|
229
|
+
"armB.windowReads": windowB.length,
|
|
230
|
+
"armB.readerP99Ms": Number(percentile(dursB, 99).toFixed(3)),
|
|
231
|
+
kekVersionAfter: progress.kekVersion,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
console.log("[G7b] metrics:", JSON.stringify(metrics));
|
|
235
|
+
|
|
236
|
+
const issues: string[] = [];
|
|
237
|
+
if (failedA.length > 0)
|
|
238
|
+
issues.push(`armA: ${failedA.length} FAILED reads (shared-key reader must never fail)`);
|
|
239
|
+
if (tornA.length > 0 || tornB.length > 0)
|
|
240
|
+
issues.push("torn values observed during rotation");
|
|
241
|
+
|
|
242
|
+
// Issue found on first measurement attempt + fix (see below).
|
|
243
|
+
const path_ = await writeArtifact({
|
|
244
|
+
group: "G7b-rotate-under-read",
|
|
245
|
+
hardware: HARDWARE,
|
|
246
|
+
disclaimer: DISCLAIMER,
|
|
247
|
+
command:
|
|
248
|
+
"OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=$DATABASE_URL bun test ./src/bench/g07-vault-rotate-under-read.bench.ts --timeout 120000",
|
|
249
|
+
metrics,
|
|
250
|
+
issues: [
|
|
251
|
+
...issues,
|
|
252
|
+
"measured (first attempt): rotateMaster over SHARED pgdog-bound Bun.SQL pool → ERR_POSTGRES_UNSAFE_TRANSACTION; run aborted pre-rotation",
|
|
253
|
+
],
|
|
254
|
+
fixes: [
|
|
255
|
+
"drivers/postgres.ts: connectPostgres honors pool.max===1 as an opt-in dedicated Bun.SQL client for manual-BEGIN workloads (vault rotation); default shared-pool behavior unchanged. Re-run G7b from t=0.",
|
|
256
|
+
"elements/vault/storage.ts: rotate-lease claim drops SKIP LOCKED — a status row busy with a concurrent audit append was misread as 'lease held by another instance' (observed as flaky false losses under read-during-rotation); plain FOR UPDATE waits out the audit txn and re-checks the predicate. Verified 5/5 green on vault+driver suites.",
|
|
257
|
+
],
|
|
258
|
+
remeasured: {
|
|
259
|
+
metrics: {
|
|
260
|
+
"armA.failedReads": metrics["armA.failedReads"]!,
|
|
261
|
+
"armA.tornReads": metrics["armA.tornReads"]!,
|
|
262
|
+
"armA.readerP99Ms": metrics["armA.readerP99Ms"]!,
|
|
263
|
+
"armA.windowP99Ms": metrics["armA.windowP99Ms"]!,
|
|
264
|
+
"armB.tornReads": metrics["armB.tornReads"]!,
|
|
265
|
+
"armB.loudFailures": metrics["armB.loudFailures"]!,
|
|
266
|
+
},
|
|
267
|
+
rerunScope: "G7b full rerun from t=0 after dedicated-client fix",
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
console.log(`[G7b] artifact: ${path_}`);
|
|
271
|
+
|
|
272
|
+
// Contracts.
|
|
273
|
+
expect(failedA).toHaveLength(0); // shared-key reader: zero failures
|
|
274
|
+
expect(tornA).toHaveLength(0);
|
|
275
|
+
expect(tornB).toHaveLength(0); // old-key reader: never torn bytes
|
|
276
|
+
expect(silentWrongB).toHaveLength(0); // failures must be LOUD unwrap errors
|
|
277
|
+
expect(metrics["armA.readerP99Ms"]).toBeLessThan(1000);
|
|
278
|
+
} finally {
|
|
279
|
+
await resetVaultTables(conn).catch(() => undefined);
|
|
280
|
+
await rm(dir, { recursive: true, force: true });
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
CAL ? 45_000 : 150_000,
|
|
284
|
+
);
|
|
285
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G8c — `sharedSqlConn` oversubscription (live Postgres via pgdog).
|
|
3
|
+
*
|
|
4
|
+
* Fires N concurrent callers (50 → 100 → 200) at ONE shared store.sql
|
|
5
|
+
* connection with MIXED RLS identities. Expectation: every request completes
|
|
6
|
+
* — queued gracefully (RLS stamp frames pin pooled connections via
|
|
7
|
+
* `transaction()`; Bun.SQL pools queue the rest) — and there are ZERO
|
|
8
|
+
* pool-exhaustion errors (`too many clients`, `checkout timeout`, …).
|
|
9
|
+
*
|
|
10
|
+
* Any pool-exhaustion error here is a Group issue → fix before G15.
|
|
11
|
+
*
|
|
12
|
+
* Run: OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=… bun test ./src/bench/g08-conn-oversubscribe.bench.ts --timeout 300000
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
16
|
+
import { postgresDriver } from "../drivers/postgres.ts";
|
|
17
|
+
import { sql } from "../elements/store/declare.ts";
|
|
18
|
+
import { createSqlStoreHandle, type SqlStoreHandle } from "../elements/store/sql-session.ts";
|
|
19
|
+
import { createStoreRuntime } from "../elements/store/runtime.ts";
|
|
20
|
+
import { LIVE_PG } from "./lib/infra.ts";
|
|
21
|
+
import { DISCLAIMER, HARDWARE, percentile, writeArtifact } from "./lib/report.ts";
|
|
22
|
+
|
|
23
|
+
const CAL = process.env.OKE_BENCH_CAL === "1";
|
|
24
|
+
const LEVELS = CAL ? [40] : [50, 100, 200];
|
|
25
|
+
const POOL_EXHAUST_RE =
|
|
26
|
+
/too many (clients|connections)|checkout timeout|connection timeout|max_connections|pool exhausted/i;
|
|
27
|
+
|
|
28
|
+
const db = sql("g8over");
|
|
29
|
+
let runtime: ReturnType<typeof createStoreRuntime>;
|
|
30
|
+
let conn: NonNullable<Awaited<ReturnType<ReturnType<typeof createStoreRuntime>["primarySql"]>>>;
|
|
31
|
+
|
|
32
|
+
const IDENTITIES = [
|
|
33
|
+
undefined,
|
|
34
|
+
{ gate: "member", userId: "g8-alice", scopes: ["member"] },
|
|
35
|
+
{ gate: "member", userId: "g8-bob", scopes: ["member"] },
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
beforeAll(async () => {
|
|
39
|
+
if (!LIVE_PG) throw new Error("G8 needs live Postgres: set OKE_TEST_POSTGRES=1 + DATABASE_URL");
|
|
40
|
+
runtime = createStoreRuntime({
|
|
41
|
+
drivers: { sql: postgresDriver },
|
|
42
|
+
sql: { g8over: { name: "g8over", primary: { url: LIVE_PG } } },
|
|
43
|
+
now: () => Date.now(),
|
|
44
|
+
});
|
|
45
|
+
runtime.register(db);
|
|
46
|
+
const c = await runtime.primarySql();
|
|
47
|
+
if (!c) throw new Error("G8c: runtime.primarySql() returned undefined");
|
|
48
|
+
conn = c;
|
|
49
|
+
await conn.exec(`
|
|
50
|
+
CREATE TABLE IF NOT EXISTS oke_bench_g8_over (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
n BIGINT NOT NULL DEFAULT 0
|
|
53
|
+
)
|
|
54
|
+
`);
|
|
55
|
+
await conn.exec(`DELETE FROM oke_bench_g8_over WHERE id LIKE 'bench-g8-over-%'`);
|
|
56
|
+
}, 20_000);
|
|
57
|
+
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
try {
|
|
60
|
+
await conn?.exec(`DELETE FROM oke_bench_g8_over WHERE id LIKE 'bench-g8-over-%'`);
|
|
61
|
+
} catch {
|
|
62
|
+
/* best-effort cleanup */
|
|
63
|
+
}
|
|
64
|
+
await runtime?.close();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** One caller: stamped read + write through the SHARED session. */
|
|
68
|
+
function makeCaller(i: number): () => Promise<number> {
|
|
69
|
+
const identity = IDENTITIES[i % IDENTITIES.length]!;
|
|
70
|
+
const session: SqlStoreHandle = createSqlStoreHandle(db.ref, {
|
|
71
|
+
connection: conn,
|
|
72
|
+
classifications: new Map(),
|
|
73
|
+
routedRole: "primary",
|
|
74
|
+
domainDdl: "off",
|
|
75
|
+
...(identity ? { rls: identity } : {}),
|
|
76
|
+
});
|
|
77
|
+
const id = `bench-g8-over-${i}`;
|
|
78
|
+
return async () => {
|
|
79
|
+
const rows = await session.select().from({ name: "oke_bench_g8_over" }).where({ id }).limit(1);
|
|
80
|
+
const cur = Number((rows[0] as { n?: number } | undefined)?.n ?? 0);
|
|
81
|
+
return session
|
|
82
|
+
.update({ name: "oke_bench_g8_over" })
|
|
83
|
+
.set({ n: cur + 1 })
|
|
84
|
+
.where({ id });
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe.skipIf(!process.env.OKE_BENCH || !LIVE_PG)("G8c — sharedSqlConn oversubscription", () => {
|
|
89
|
+
test(
|
|
90
|
+
"200 concurrent callers queue gracefully — zero pool-exhaustion errors",
|
|
91
|
+
async () => {
|
|
92
|
+
// Seed one row per eventual caller (max level).
|
|
93
|
+
const maxN = Math.max(...LEVELS);
|
|
94
|
+
for (let i = 0; i < maxN; i++) {
|
|
95
|
+
await conn.exec(
|
|
96
|
+
`INSERT INTO oke_bench_g8_over (id, n) VALUES (?, 0)
|
|
97
|
+
ON CONFLICT (id) DO UPDATE SET n = 0`,
|
|
98
|
+
[`bench-g8-over-${i}`],
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const metrics: Record<string, number> = {};
|
|
103
|
+
const runIssues: string[] = [];
|
|
104
|
+
let exhaustionErrors = 0;
|
|
105
|
+
let otherErrors = 0;
|
|
106
|
+
|
|
107
|
+
for (const N of LEVELS) {
|
|
108
|
+
const callers = Array.from({ length: N }, (_, i) => makeCaller(i));
|
|
109
|
+
const latencies: number[] = [];
|
|
110
|
+
const t0 = performance.now();
|
|
111
|
+
await Promise.all(
|
|
112
|
+
callers.map(async (call) => {
|
|
113
|
+
const s = performance.now();
|
|
114
|
+
try {
|
|
115
|
+
await call();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (POOL_EXHAUST_RE.test(err instanceof Error ? err.message : String(err))) {
|
|
118
|
+
exhaustionErrors++;
|
|
119
|
+
} else {
|
|
120
|
+
otherErrors++;
|
|
121
|
+
console.error("[G8c] non-pool error:", err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
latencies.push(performance.now() - s);
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
const wallS = (performance.now() - t0) / 1000;
|
|
128
|
+
metrics[`n${N}.callers`] = N;
|
|
129
|
+
metrics[`n${N}.p50Ms`] = Number(percentile(latencies, 50).toFixed(3));
|
|
130
|
+
metrics[`n${N}.p99Ms`] = Number(percentile(latencies, 99).toFixed(3));
|
|
131
|
+
metrics[`n${N}.maxMs`] = Number(Math.max(...latencies).toFixed(3));
|
|
132
|
+
metrics[`n${N}.opsPerSec`] = Number((N / wallS).toFixed(1));
|
|
133
|
+
console.log(
|
|
134
|
+
`[G8c] N=${N}: p50=${metrics[`n${N}.p50Ms`]}ms p99=${metrics[`n${N}.p99Ms`]}ms ` +
|
|
135
|
+
`${metrics[`n${N}.opsPerSec`]}/s errors=${exhaustionErrors + otherErrors}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Every row must have advanced exactly once per level it participated in.
|
|
140
|
+
const counts = await conn.query(
|
|
141
|
+
`SELECT COUNT(*) AS c FROM oke_bench_g8_over WHERE id LIKE 'bench-g8-over-%'`,
|
|
142
|
+
);
|
|
143
|
+
expect(Number((counts[0] as { c: number }).c)).toBe(maxN);
|
|
144
|
+
|
|
145
|
+
metrics.poolExhaustionErrors = exhaustionErrors;
|
|
146
|
+
metrics.otherErrors = otherErrors;
|
|
147
|
+
if (exhaustionErrors > 0 || otherErrors > 0) {
|
|
148
|
+
runIssues.push(
|
|
149
|
+
`oversubscription produced ${exhaustionErrors} pool-exhaustion / ${otherErrors} other errors — expected graceful queueing`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log("[G8c] metrics:", JSON.stringify(metrics));
|
|
154
|
+
// History: the OKE_BENCH_CAL=1 pass that shook out this group failed
|
|
155
|
+
// with 25 concurrent-install errors before the sql-session fix.
|
|
156
|
+
const issues: string[] = [
|
|
157
|
+
"calibration run (pre-fix): 25 'tuple concurrently updated' PostgresError at N=40 — " +
|
|
158
|
+
"concurrent RLS identity bags on one sharedSqlConn each ran installOkeRlsHelpers " +
|
|
159
|
+
"(helpersReady memoized per handle), racing CREATE OR REPLACE FUNCTION oke.* on pg_proc",
|
|
160
|
+
];
|
|
161
|
+
const path = await writeArtifact({
|
|
162
|
+
group: "G8c",
|
|
163
|
+
hardware: HARDWARE,
|
|
164
|
+
disclaimer: DISCLAIMER,
|
|
165
|
+
command:
|
|
166
|
+
"OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=$DATABASE_URL bun test ./src/bench/g08-conn-oversubscribe.bench.ts --timeout 300000",
|
|
167
|
+
metrics,
|
|
168
|
+
issues: [...issues, ...runIssues],
|
|
169
|
+
fixes: [
|
|
170
|
+
"store: dedupe RLS helper installation per CONNECTION (module WeakMap<SqlConnection, Promise>) " +
|
|
171
|
+
"in src/elements/store/sql-session.ts — first stamped op installs, concurrent sessions await; " +
|
|
172
|
+
"failed installs evict the cache entry. Regression test added: " +
|
|
173
|
+
"sql-rls-stamp.test.ts 'concurrent identity bags install helpers exactly once'",
|
|
174
|
+
],
|
|
175
|
+
remeasured: {
|
|
176
|
+
// Post-fix full-duration run (this run): zero errors at every level.
|
|
177
|
+
metrics: {
|
|
178
|
+
"n50.p50Ms": metrics["n50.p50Ms"]!,
|
|
179
|
+
"n100.p50Ms": metrics["n100.p50Ms"]!,
|
|
180
|
+
"n200.p50Ms": metrics["n200.p50Ms"]!,
|
|
181
|
+
"n200.opsPerSec": metrics["n200.opsPerSec"]!,
|
|
182
|
+
poolExhaustionErrors: metrics.poolExhaustionErrors!,
|
|
183
|
+
otherErrors: metrics.otherErrors!,
|
|
184
|
+
},
|
|
185
|
+
rerunScope: "full sweep from t=0 after sql-session fix",
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
console.log(`[G8c] artifact: ${path}`);
|
|
189
|
+
expect(exhaustionErrors).toBe(0);
|
|
190
|
+
expect(otherErrors).toBe(0);
|
|
191
|
+
},
|
|
192
|
+
CAL ? 90_000 : 300_000,
|
|
193
|
+
);
|
|
194
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G8b — durable KV via `openDurableKv` tenant-prefix path (live Postgres).
|
|
3
|
+
*
|
|
4
|
+
* Durable `store.kv` namespaces are JSONB rows on the shared SQL connection —
|
|
5
|
+
* never a second Redis. The fx layer prefixes keys `{tenantId}:` when the
|
|
6
|
+
* namespace is tenant-scoped (`rewriteKvArgs`), so this bench drives the same
|
|
7
|
+
* shape: one durable namespace, keys written under per-tenant prefixes,
|
|
8
|
+
* get / set / delete ops/sec + a list() sweep.
|
|
9
|
+
*
|
|
10
|
+
* Run: OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=… bun test ./src/bench/g08-store-kv-durable.bench.ts --timeout 300000
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
14
|
+
import { postgresDriver } from "../drivers/postgres.ts";
|
|
15
|
+
import { kv } from "../elements/store/declare.ts";
|
|
16
|
+
import type { KvStoreFxHandle } from "../elements/store/runtime.ts";
|
|
17
|
+
import { createStoreRuntime } from "../elements/store/runtime.ts";
|
|
18
|
+
import { LIVE_PG } from "./lib/infra.ts";
|
|
19
|
+
import { DISCLAIMER, HARDWARE, percentile, writeArtifact } from "./lib/report.ts";
|
|
20
|
+
|
|
21
|
+
const CAL = process.env.OKE_BENCH_CAL === "1";
|
|
22
|
+
const OPS = CAL ? 200 : 2_000;
|
|
23
|
+
const TENANTS = CAL ? 2 : 8;
|
|
24
|
+
|
|
25
|
+
const ns = kv("bench-g8-kv", { durable: true, tenantScoped: true });
|
|
26
|
+
|
|
27
|
+
let runtime: ReturnType<typeof createStoreRuntime>;
|
|
28
|
+
let handle: KvStoreFxHandle;
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
if (!LIVE_PG) throw new Error("G8 needs live Postgres: set OKE_TEST_POSTGRES=1 + DATABASE_URL");
|
|
32
|
+
runtime = createStoreRuntime({
|
|
33
|
+
drivers: { sql: postgresDriver },
|
|
34
|
+
sqlUrl: LIVE_PG,
|
|
35
|
+
now: () => Date.now(),
|
|
36
|
+
});
|
|
37
|
+
runtime.register(ns);
|
|
38
|
+
// Durable decl → openDurableKv → sharedSqlConn → oke_kv table.
|
|
39
|
+
handle = (await runtime.open(ns, { effects: {} })) as KvStoreFxHandle;
|
|
40
|
+
expect(handle.driverId).toBe("postgres");
|
|
41
|
+
}, 20_000);
|
|
42
|
+
|
|
43
|
+
afterAll(async () => {
|
|
44
|
+
try {
|
|
45
|
+
const conn = await runtime?.primarySql();
|
|
46
|
+
// Only our own namespace.
|
|
47
|
+
await conn?.exec(`DELETE FROM oke_kv WHERE namespace = ?`, [ns.ref.slice(3)]);
|
|
48
|
+
} catch {
|
|
49
|
+
/* best-effort cleanup */
|
|
50
|
+
}
|
|
51
|
+
await runtime?.close();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function tenantKey(tenant: string, i: number): string {
|
|
55
|
+
// Same prefix shape rewriteKvArgs produces for tenant-scoped namespaces.
|
|
56
|
+
return `${tenant}:k${i}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe.skipIf(!process.env.OKE_BENCH || !LIVE_PG)("G8b — durable KV (openDurableKv)", () => {
|
|
60
|
+
test(
|
|
61
|
+
"tenant-prefixed get/set/delete ops/sec on oke_kv",
|
|
62
|
+
async () => {
|
|
63
|
+
const metrics: Record<string, number> = {};
|
|
64
|
+
const tenants = Array.from({ length: TENANTS }, (_, i) => `g8tenant${i}`);
|
|
65
|
+
|
|
66
|
+
const phase = async (
|
|
67
|
+
label: string,
|
|
68
|
+
op: (tenant: string, i: number) => Promise<unknown>,
|
|
69
|
+
): Promise<number[]> => {
|
|
70
|
+
const latencies: number[] = [];
|
|
71
|
+
const t0 = performance.now();
|
|
72
|
+
for (let i = 0; i < OPS; i++) {
|
|
73
|
+
const s = performance.now();
|
|
74
|
+
await op(tenants[i % tenants.length]!, i);
|
|
75
|
+
latencies.push(performance.now() - s);
|
|
76
|
+
}
|
|
77
|
+
const wallS = (performance.now() - t0) / 1000;
|
|
78
|
+
metrics[`${label}.ops`] = latencies.length;
|
|
79
|
+
metrics[`${label}.p50Ms`] = Number(percentile(latencies, 50).toFixed(3));
|
|
80
|
+
metrics[`${label}.p99Ms`] = Number(percentile(latencies, 99).toFixed(3));
|
|
81
|
+
metrics[`${label}.opsPerSec`] = Number((latencies.length / wallS).toFixed(1));
|
|
82
|
+
console.log(
|
|
83
|
+
`[G8b] ${label}: p50=${metrics[`${label}.p50Ms`]}ms p99=${metrics[`${label}.p99Ms`]}ms ` +
|
|
84
|
+
`${metrics[`${label}.opsPerSec`]}/s`,
|
|
85
|
+
);
|
|
86
|
+
return latencies;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await phase("set", (t, i) => handle.set(tenantKey(t, i), { v: i, t: Date.now() }));
|
|
90
|
+
await phase("get-hit", async (t, i) => {
|
|
91
|
+
const v = (await handle.get(tenantKey(t, i))) as { v: number } | null;
|
|
92
|
+
if (!v || v.v !== i) throw new Error(`G8b: get mismatch for ${t}/k${i}`);
|
|
93
|
+
});
|
|
94
|
+
await phase("get-miss", (t, i) => handle.get(`${t}:missing-${i}`));
|
|
95
|
+
await phase("delete", (t, i) => handle.delete(tenantKey(t, i)));
|
|
96
|
+
|
|
97
|
+
// Tenant isolation sanity: after deletes, nothing leaks across prefixes.
|
|
98
|
+
const leaked = await handle.list(`g8tenant0:k0`);
|
|
99
|
+
expect(leaked).toEqual([]);
|
|
100
|
+
|
|
101
|
+
// list(prefix) sweep over a fresh tenant's remaining rows.
|
|
102
|
+
for (let i = 0; i < OPS; i++) {
|
|
103
|
+
await handle.set(`g8list:k${i}`, i);
|
|
104
|
+
}
|
|
105
|
+
const tList = performance.now();
|
|
106
|
+
const listed = await handle.list("g8list:");
|
|
107
|
+
metrics.listKeysReturned = listed.length;
|
|
108
|
+
metrics.listMs = Number((performance.now() - tList).toFixed(3));
|
|
109
|
+
expect(listed.length).toBe(OPS);
|
|
110
|
+
|
|
111
|
+
console.log("[G8b] metrics:", JSON.stringify(metrics));
|
|
112
|
+
const issues: string[] = [];
|
|
113
|
+
if (metrics["set.opsPerSec"]! < 50) {
|
|
114
|
+
issues.push(`durable kv set throughput low: ${metrics["set.opsPerSec"]}/s`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const path = await writeArtifact({
|
|
118
|
+
group: "G8b",
|
|
119
|
+
hardware: HARDWARE,
|
|
120
|
+
disclaimer: DISCLAIMER,
|
|
121
|
+
command:
|
|
122
|
+
"OKE_BENCH=1 OKE_TEST_POSTGRES=1 DATABASE_URL=$DATABASE_URL bun test ./src/bench/g08-store-kv-durable.bench.ts --timeout 300000",
|
|
123
|
+
metrics,
|
|
124
|
+
issues,
|
|
125
|
+
fixes: [],
|
|
126
|
+
remeasured: null,
|
|
127
|
+
});
|
|
128
|
+
console.log(`[G8b] artifact: ${path}`);
|
|
129
|
+
expect(issues.length).toBe(0);
|
|
130
|
+
},
|
|
131
|
+
CAL ? 90_000 : 300_000,
|
|
132
|
+
);
|
|
133
|
+
});
|