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,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bench load app — one real app process exposing every bench route needed
|
|
3
|
+
* by G1–G15. Modeled on `src/kernel/horizontal-child.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Env (resolved at runtime, never printed):
|
|
6
|
+
* DATABASE_URL / OKE_STORE_SQL_URL — live Postgres via pgdog
|
|
7
|
+
* OKE_TEST_REDIS_URL or REDIS_URL — live Redis
|
|
8
|
+
* OKE_BENCH_MAILPIT_HOST / _PORT — Mailpit for channel bulk (default 127.0.0.1:1025)
|
|
9
|
+
*
|
|
10
|
+
* Routes:
|
|
11
|
+
* GET /_/ready { ok, pid }
|
|
12
|
+
* GET /_/stats uptime, rss, counters
|
|
13
|
+
* GET /ping plain public flow
|
|
14
|
+
* GET /rate gate.rate-limited (keyBy ip)
|
|
15
|
+
* POST /_/bench/rls mixed-identity store writes through shared conn
|
|
16
|
+
* POST /_/bench/rate gate.rate-limited POST variant
|
|
17
|
+
* POST /_/bench/emit signal once emit
|
|
18
|
+
* POST /_/bench/drain signal bus drain
|
|
19
|
+
* GET /_oke/live/bench-live live SSE delivery (auto route via http.live)
|
|
20
|
+
* POST /_/bench/durable durable flow with fx.step
|
|
21
|
+
* POST /_/bench/vault-read vault runtime read
|
|
22
|
+
* POST /_/bench/stream fx.json.stream SSE response
|
|
23
|
+
* POST /_/bench/channel-bulk channel RetryTransport bulk send → Mailpit
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { openSmtpChannel } from "../drivers/channel-smtp.ts";
|
|
27
|
+
import { createPostgresCronStore } from "../drivers/clock-postgres.ts";
|
|
28
|
+
import { createPostgresJournalStore } from "../drivers/journal-postgres.ts";
|
|
29
|
+
import { memorySignalDriver } from "../drivers/signal-memory.ts";
|
|
30
|
+
import { postgresDriver } from "../drivers/postgres.ts";
|
|
31
|
+
import { redisDriver } from "../drivers/redis.ts";
|
|
32
|
+
import { channel } from "../elements/channel.ts";
|
|
33
|
+
import { createChannelRuntime, type TemplateCatalog } from "../elements/channel/runtime.ts";
|
|
34
|
+
import { clock } from "../elements/clock/declare.ts";
|
|
35
|
+
import { createClockRuntime } from "../elements/clock/runtime.ts";
|
|
36
|
+
import { gate } from "../elements/gate.ts";
|
|
37
|
+
import { createGateRuntime } from "../elements/gate/runtime.ts";
|
|
38
|
+
import { signal } from "../elements/signal/declare.ts";
|
|
39
|
+
import { createSignalRuntime } from "../elements/signal/runtime.ts";
|
|
40
|
+
import { sql } from "../elements/store/declare.ts";
|
|
41
|
+
import { createStoreRuntime } from "../elements/store/runtime.ts";
|
|
42
|
+
import { defineTable } from "../elements/store/table.ts";
|
|
43
|
+
import { vault } from "../elements/vault/declare.ts";
|
|
44
|
+
import { createVaultRuntime } from "../elements/vault/runtime.ts";
|
|
45
|
+
import { GATE_KV_NAMESPACE } from "../kernel/boot-bind/gate.ts";
|
|
46
|
+
import { oke } from "../kernel/app.ts";
|
|
47
|
+
import { flow, type AnyFlowDef } from "../kernel/flow.ts";
|
|
48
|
+
import type { Binding } from "../kernel/on.ts";
|
|
49
|
+
import { http } from "../kernel/triggers.ts";
|
|
50
|
+
import { installGracefulShutdown } from "../kernel/graceful-shutdown.ts";
|
|
51
|
+
import { okid } from "../okid.ts";
|
|
52
|
+
import { createBunRuntime } from "../runtime/bun.ts";
|
|
53
|
+
|
|
54
|
+
const mode = process.argv[2];
|
|
55
|
+
if (mode !== "serve") {
|
|
56
|
+
console.error("usage: load-app serve <instanceId> <port>");
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const instanceId = process.argv[3] ?? "bench-a";
|
|
61
|
+
const port = Number(process.argv[4] ?? "0");
|
|
62
|
+
|
|
63
|
+
const pgUrl = process.env.DATABASE_URL?.trim() || process.env.OKE_STORE_SQL_URL?.trim() || "";
|
|
64
|
+
const redisUrl = (process.env.OKE_TEST_REDIS_URL ?? process.env.REDIS_URL)?.trim() ?? "";
|
|
65
|
+
if (!pgUrl || !redisUrl) {
|
|
66
|
+
console.error("load-app: DATABASE_URL and OKE_TEST_REDIS_URL (or REDIS_URL) are required");
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const t0 = Date.now();
|
|
71
|
+
const counters = { rls: 0, emits: 0, drains: 0, durable: 0, vaultReads: 0, streams: 0, mails: 0 };
|
|
72
|
+
|
|
73
|
+
// --- Clock (perTenant cron declared for G2/G6) ------------------------------
|
|
74
|
+
const cronStore = await createPostgresCronStore({ url: pgUrl });
|
|
75
|
+
const CRON = "bench-cron";
|
|
76
|
+
const benchClock = clock(CRON, { every: "1h", perTenant: true });
|
|
77
|
+
const clockRt = createClockRuntime({ store: cronStore, instanceId, leaseMs: 300 });
|
|
78
|
+
clockRt.register(benchClock);
|
|
79
|
+
await clockRt.reconcile();
|
|
80
|
+
clockRt.onCron(CRON, async () => {});
|
|
81
|
+
|
|
82
|
+
// --- Signal (once + live physics) -------------------------------------------
|
|
83
|
+
const signalPath = `/tmp/oke-bench-signal-${process.pid}.json`;
|
|
84
|
+
const BENCH_JOB = signal("bench-job", {
|
|
85
|
+
delivery: "once",
|
|
86
|
+
retries: 3,
|
|
87
|
+
deadLetter: true,
|
|
88
|
+
optional: true,
|
|
89
|
+
});
|
|
90
|
+
const BENCH_LIVE = signal("bench-live", {
|
|
91
|
+
delivery: "live",
|
|
92
|
+
retention: { maxCount: 100 },
|
|
93
|
+
});
|
|
94
|
+
const signalRt = createSignalRuntime({
|
|
95
|
+
driver: memorySignalDriver,
|
|
96
|
+
durablePath: signalPath,
|
|
97
|
+
leaseMs: 300,
|
|
98
|
+
});
|
|
99
|
+
signalRt.register(BENCH_JOB as never);
|
|
100
|
+
signalRt.register(BENCH_LIVE as never);
|
|
101
|
+
const bus = await signalRt.start();
|
|
102
|
+
await bus.subscribe(BENCH_JOB.name, `consumer-${instanceId}`, async () => {});
|
|
103
|
+
|
|
104
|
+
// --- Gate --------------------------------------------------------------------
|
|
105
|
+
const rateGate = gate.rate({ max: 1000, per: "1m", keyBy: "ip" });
|
|
106
|
+
const kv = await redisDriver.open({
|
|
107
|
+
name: GATE_KV_NAMESPACE,
|
|
108
|
+
url: redisUrl,
|
|
109
|
+
nowMs: () => Date.now(),
|
|
110
|
+
});
|
|
111
|
+
const gateRt = createGateRuntime({
|
|
112
|
+
gates: [rateGate, gate.public],
|
|
113
|
+
kv,
|
|
114
|
+
now: () => Date.now(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// --- Store -------------------------------------------------------------------
|
|
118
|
+
const db = sql("db");
|
|
119
|
+
const benchTable = defineTable("oke_bench_rows", {
|
|
120
|
+
id: true,
|
|
121
|
+
tenant_id: true,
|
|
122
|
+
instance_id: true,
|
|
123
|
+
at: true,
|
|
124
|
+
});
|
|
125
|
+
const journalStore = await createPostgresJournalStore({ url: pgUrl });
|
|
126
|
+
await journalStore.sql.exec(`
|
|
127
|
+
CREATE TABLE IF NOT EXISTS oke_bench_rows (
|
|
128
|
+
id TEXT PRIMARY KEY,
|
|
129
|
+
tenant_id TEXT NOT NULL,
|
|
130
|
+
instance_id TEXT NOT NULL,
|
|
131
|
+
at BIGINT NOT NULL
|
|
132
|
+
)
|
|
133
|
+
`);
|
|
134
|
+
const storeRt = createStoreRuntime({
|
|
135
|
+
drivers: { sql: postgresDriver },
|
|
136
|
+
sql: { db: { name: "db", primary: { url: pgUrl } } },
|
|
137
|
+
now: () => Date.now(),
|
|
138
|
+
});
|
|
139
|
+
storeRt.register(db);
|
|
140
|
+
|
|
141
|
+
// --- Vault -------------------------------------------------------------------
|
|
142
|
+
const VAULT_SECRET = "OKE_BENCH_SECRET";
|
|
143
|
+
const vaultRt = createVaultRuntime({
|
|
144
|
+
secrets: [vault.config(VAULT_SECRET, { dev: "bench-value" })],
|
|
145
|
+
allowDevFallbacks: true,
|
|
146
|
+
});
|
|
147
|
+
await vaultRt.boot();
|
|
148
|
+
|
|
149
|
+
// --- Channel (Mailpit SMTP + RetryTransport) ----------------------------------
|
|
150
|
+
const mailHost = process.env.OKE_BENCH_MAILPIT_HOST?.trim() || "127.0.0.1";
|
|
151
|
+
const mailPort = Number(process.env.OKE_BENCH_MAILPIT_PORT ?? "1025");
|
|
152
|
+
const WELCOME = channel.email().template("bench-welcome");
|
|
153
|
+
const catalog: TemplateCatalog = {
|
|
154
|
+
"bench-welcome": { en: { subject: "Bench welcome", text: "Hello bench" } },
|
|
155
|
+
};
|
|
156
|
+
let channelRt: ReturnType<typeof createChannelRuntime> | undefined;
|
|
157
|
+
try {
|
|
158
|
+
channelRt = createChannelRuntime({
|
|
159
|
+
templates: [WELCOME],
|
|
160
|
+
drivers: [openSmtpChannel({ host: mailHost, port: mailPort })],
|
|
161
|
+
catalog,
|
|
162
|
+
retry: true,
|
|
163
|
+
now: () => Date.now(),
|
|
164
|
+
});
|
|
165
|
+
} catch (err) {
|
|
166
|
+
console.error("load-app: channel disabled", err);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Flows ---------------------------------------------------------------------
|
|
170
|
+
const ping = flow("bench.ping", { do: () => ({ ok: true as const }) });
|
|
171
|
+
|
|
172
|
+
const statsFlow = flow("bench.stats", { do: () => benchStats() });
|
|
173
|
+
|
|
174
|
+
const liveFirehose = flow("bench.live-firehose", {
|
|
175
|
+
do: (_input, fx) => fx.live(BENCH_LIVE),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const rlsWrite = flow("bench.rls", {
|
|
179
|
+
do: async (input, fx) => {
|
|
180
|
+
const body = input as { tenantId?: string; n?: number };
|
|
181
|
+
const n = Math.max(1, Math.min(64, Number(body.n ?? 1)));
|
|
182
|
+
for (let i = 0; i < n; i++) {
|
|
183
|
+
await fx
|
|
184
|
+
.store(db)
|
|
185
|
+
.insert(benchTable)
|
|
186
|
+
.values({
|
|
187
|
+
id: okid(),
|
|
188
|
+
tenant_id: body.tenantId ?? `tenant-${i}`,
|
|
189
|
+
instance_id: instanceId,
|
|
190
|
+
at: Date.now(),
|
|
191
|
+
})
|
|
192
|
+
.execute();
|
|
193
|
+
}
|
|
194
|
+
counters.rls += n;
|
|
195
|
+
return { written: n };
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const rateFlow = flow("bench.rate", { do: () => ({ ok: true as const }) });
|
|
200
|
+
|
|
201
|
+
const emit = flow("bench.emit", {
|
|
202
|
+
do: async (_input, fx) => {
|
|
203
|
+
await fx.emit(BENCH_JOB, { from: instanceId, at: Date.now() });
|
|
204
|
+
counters.emits++;
|
|
205
|
+
return { emitted: true as const };
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const emitLive = flow("bench.emit-live", {
|
|
210
|
+
do: async (_input, fx) => {
|
|
211
|
+
await fx.emit(BENCH_LIVE, { from: instanceId, at: Date.now() });
|
|
212
|
+
counters.emits++;
|
|
213
|
+
return { emitted: true as const };
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const drainFlow = flow("bench.drain", {
|
|
218
|
+
do: async () => {
|
|
219
|
+
await bus.drain();
|
|
220
|
+
counters.drains++;
|
|
221
|
+
return { drained: true as const };
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const durableFlow = flow("bench.durable", {
|
|
226
|
+
durable: true,
|
|
227
|
+
do: async (_input, fx) => {
|
|
228
|
+
await fx.step("step-one", async () => ({ at: Date.now() }));
|
|
229
|
+
await fx.step("step-two", async () => "done");
|
|
230
|
+
counters.durable++;
|
|
231
|
+
return { ok: true as const };
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const vaultRead = flow("bench.vault-read", {
|
|
236
|
+
do: async (_input, fx) => {
|
|
237
|
+
const value = await fx.vault.get(VAULT_SECRET);
|
|
238
|
+
counters.vaultReads++;
|
|
239
|
+
return { len: String(value).length };
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const streamN = flow("bench.stream", {
|
|
244
|
+
do: async (input, fx) => {
|
|
245
|
+
const chunks = Number((input as { chunks?: number }).chunks ?? 16);
|
|
246
|
+
counters.streams++;
|
|
247
|
+
return fx.json.stream(
|
|
248
|
+
(async function* () {
|
|
249
|
+
for (let i = 0; i < chunks; i++) yield { i, at: Date.now() };
|
|
250
|
+
})(),
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const channelBulk = flow("bench.channel-bulk", {
|
|
256
|
+
do: async (input) => {
|
|
257
|
+
if (!channelRt) throw new Error("channel runtime unavailable (Mailpit down?)");
|
|
258
|
+
const count = Math.max(1, Math.min(500, Number((input as { count?: number }).count ?? 10)));
|
|
259
|
+
let sent = 0;
|
|
260
|
+
for (let i = 0; i < count; i++) {
|
|
261
|
+
const r = await channelRt.send(WELCOME.name, {
|
|
262
|
+
to: `bench-${okid()}@example.test`,
|
|
263
|
+
data: { i },
|
|
264
|
+
});
|
|
265
|
+
if (r.ok) sent++;
|
|
266
|
+
}
|
|
267
|
+
counters.mails += sent;
|
|
268
|
+
return { sent };
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const bindings: Binding[] = [
|
|
273
|
+
{ trigger: http.get("/ping").public(), flow: ping as AnyFlowDef },
|
|
274
|
+
{ trigger: http.get("/_/stats").public(), flow: statsFlow as AnyFlowDef },
|
|
275
|
+
{ trigger: http.get("/rate").gate(rateGate), flow: rateFlow as AnyFlowDef },
|
|
276
|
+
{ trigger: http.post("/_/bench/rate").gate(rateGate), flow: rateFlow as AnyFlowDef },
|
|
277
|
+
{ trigger: http.post("/_/bench/rls").public(), flow: rlsWrite as AnyFlowDef },
|
|
278
|
+
{ trigger: http.post("/_/bench/emit").public(), flow: emit as AnyFlowDef },
|
|
279
|
+
{ trigger: http.post("/_/bench/emit-live").public(), flow: emitLive as AnyFlowDef },
|
|
280
|
+
{ trigger: http.post("/_/bench/drain").public(), flow: drainFlow as AnyFlowDef },
|
|
281
|
+
{ trigger: http.live(BENCH_LIVE).public(), flow: liveFirehose as AnyFlowDef },
|
|
282
|
+
{ trigger: http.post("/_/bench/durable").public(), flow: durableFlow as AnyFlowDef },
|
|
283
|
+
{ trigger: http.post("/_/bench/vault-read").public(), flow: vaultRead as AnyFlowDef },
|
|
284
|
+
{ trigger: http.post("/_/bench/stream").public(), flow: streamN as AnyFlowDef },
|
|
285
|
+
{ trigger: http.post("/_/bench/channel-bulk").public(), flow: channelBulk as AnyFlowDef },
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
const app = oke({
|
|
289
|
+
name: `bench-load-${instanceId}`,
|
|
290
|
+
env: "test",
|
|
291
|
+
startScheduler: false,
|
|
292
|
+
gate: { unguardedHttp: "allow", policies: [rateGate, gate.public] },
|
|
293
|
+
journalLeaseMs: 300,
|
|
294
|
+
bindings,
|
|
295
|
+
clocks: [benchClock],
|
|
296
|
+
signals: [BENCH_JOB as never, BENCH_LIVE as never],
|
|
297
|
+
stores: [db],
|
|
298
|
+
elements: {
|
|
299
|
+
clock: clockRt,
|
|
300
|
+
signal: signalRt,
|
|
301
|
+
gate: gateRt,
|
|
302
|
+
store: storeRt,
|
|
303
|
+
journal: { store: journalStore, instanceId, leaseMs: 300, driverId: "postgres" },
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await app.boot();
|
|
308
|
+
|
|
309
|
+
void (async () => {
|
|
310
|
+
for (;;) {
|
|
311
|
+
try {
|
|
312
|
+
await clockRt.tick();
|
|
313
|
+
await bus.drain();
|
|
314
|
+
await app.resumeDurable();
|
|
315
|
+
} catch (err) {
|
|
316
|
+
console.error("load-app tick error", err);
|
|
317
|
+
}
|
|
318
|
+
await Bun.sleep(20);
|
|
319
|
+
}
|
|
320
|
+
})();
|
|
321
|
+
|
|
322
|
+
const handle = createBunRuntime().serve(app, { port, hostname: "127.0.0.1" });
|
|
323
|
+
installGracefulShutdown({ app, handle, exit: true });
|
|
324
|
+
|
|
325
|
+
export function benchStats(): Record<string, unknown> {
|
|
326
|
+
return {
|
|
327
|
+
pid: process.pid,
|
|
328
|
+
uptimeS: Math.round((Date.now() - t0) / 1000),
|
|
329
|
+
rssMb: Math.round(process.memoryUsage.rss() / 1024 / 1024),
|
|
330
|
+
counters,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log(JSON.stringify({ ready: true, pid: process.pid, port: handle.port, instanceId }));
|
|
335
|
+
|
|
336
|
+
// Keep alive
|
|
337
|
+
await new Promise(() => {});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bench multi-mode entrypoint — argument/usage style of
|
|
3
|
+
* `src/kernel/horizontal-child.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Modes:
|
|
6
|
+
* serve <port> boot the bench load app on 127.0.0.1:<port>;
|
|
7
|
+
* prints a ready line `{ready:true,pid,port}` to stdout.
|
|
8
|
+
* flood-sse <url> <signal> <count> open N SSE connections to the live route and hold them.
|
|
9
|
+
* sustained <url> <concurrency> <durationS> [path]
|
|
10
|
+
* closed-loop HTTP loop against <url><path>.
|
|
11
|
+
* shutdown-test install graceful shutdown; drain on SIGTERM, exit 0.
|
|
12
|
+
*
|
|
13
|
+
* Env: DATABASE_URL + OKE_TEST_REDIS_URL (or REDIS_URL) required by serve.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { installGracefulShutdown } from "../kernel/graceful-shutdown.ts";
|
|
17
|
+
|
|
18
|
+
const mode = process.argv[2];
|
|
19
|
+
|
|
20
|
+
function usage(): never {
|
|
21
|
+
console.error(
|
|
22
|
+
[
|
|
23
|
+
"usage: load-child serve <port>",
|
|
24
|
+
" load-child flood-sse <url> <signal> <count>",
|
|
25
|
+
" load-child sustained <url> <concurrency> <durationS> [path]",
|
|
26
|
+
" load-child shutdown-test",
|
|
27
|
+
].join("\n"),
|
|
28
|
+
);
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!mode) usage();
|
|
33
|
+
|
|
34
|
+
if (mode === "serve") {
|
|
35
|
+
// Delegate entirely to load-app.ts so this file stays a thin dispatcher.
|
|
36
|
+
const portArg = Number(process.argv[3] ?? "0");
|
|
37
|
+
if (!Number.isFinite(portArg) || portArg < 0) usage();
|
|
38
|
+
process.argv = [process.argv[0]!, process.argv[1]!, "serve", "bench-a", String(portArg)];
|
|
39
|
+
await import("./load-app.ts");
|
|
40
|
+
} else if (mode === "flood-sse") {
|
|
41
|
+
const url = process.argv[3];
|
|
42
|
+
const sig = process.argv[4] ?? "bench-live";
|
|
43
|
+
const count = Number(process.argv[5] ?? "100");
|
|
44
|
+
if (!url) usage();
|
|
45
|
+
const target = `${url}/_oke/live/${encodeURIComponent(sig)}`;
|
|
46
|
+
console.log(JSON.stringify({ mode, target, count }));
|
|
47
|
+
const controllers: AbortController[] = [];
|
|
48
|
+
const opened = new Set<number>();
|
|
49
|
+
await Promise.all(
|
|
50
|
+
Array.from({ length: count }, (_, i) =>
|
|
51
|
+
(async () => {
|
|
52
|
+
const ac = new AbortController();
|
|
53
|
+
controllers.push(ac);
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch(target, { signal: ac.signal });
|
|
56
|
+
if (res.body) {
|
|
57
|
+
opened.add(i);
|
|
58
|
+
const reader = res.body.getReader();
|
|
59
|
+
// Hold the connection open, discarding frames.
|
|
60
|
+
for (;;) {
|
|
61
|
+
const { done } = await reader.read();
|
|
62
|
+
if (done) break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
/* aborted / conn reset — expected during teardown */
|
|
67
|
+
}
|
|
68
|
+
})(),
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
// Only reachable if every connection closed on its own.
|
|
72
|
+
console.log(JSON.stringify({ floodDone: true, opened: opened.size }));
|
|
73
|
+
} else if (mode === "sustained") {
|
|
74
|
+
const base = process.argv[3];
|
|
75
|
+
const concurrency = Number(process.argv[4] ?? "50");
|
|
76
|
+
const durationS = Number(process.argv[5] ?? "60");
|
|
77
|
+
const path = process.argv[6] ?? "/ping";
|
|
78
|
+
if (!base) usage();
|
|
79
|
+
const target = `${base}${path}`;
|
|
80
|
+
const deadline = Date.now() + durationS * 1000;
|
|
81
|
+
let requests = 0;
|
|
82
|
+
let errors = 0;
|
|
83
|
+
console.log(JSON.stringify({ mode, target, concurrency, durationS }));
|
|
84
|
+
await Promise.all(
|
|
85
|
+
Array.from({ length: concurrency }, async () => {
|
|
86
|
+
while (Date.now() < deadline) {
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch(target, { method: path.includes("ping") ? "GET" : "POST" });
|
|
89
|
+
await res.arrayBuffer();
|
|
90
|
+
if (!res.ok) errors++;
|
|
91
|
+
} catch {
|
|
92
|
+
errors++;
|
|
93
|
+
}
|
|
94
|
+
requests++;
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
console.log(JSON.stringify({ sustainedDone: true, requests, errors }));
|
|
99
|
+
} else if (mode === "shutdown-test") {
|
|
100
|
+
// Minimal app stub — shutdown-test measures signal handling + exit path,
|
|
101
|
+
// not lease release (no boot result exists in this mode).
|
|
102
|
+
installGracefulShutdown({ app: {} as never, exit: true });
|
|
103
|
+
console.log(JSON.stringify({ ready: true, pid: process.pid, mode: "shutdown-test" }));
|
|
104
|
+
// Hold until SIGTERM/SIGINT; the installed handler drains and exits 0.
|
|
105
|
+
await new Promise(() => {});
|
|
106
|
+
} else {
|
|
107
|
+
usage();
|
|
108
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bench harness smoke — validates the report/sampler/lag libs and gives
|
|
3
|
+
* `bun run bench:load` a runnable target before group benches land.
|
|
4
|
+
* Runs everywhere (no live infra required).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { measureEventLoopLag } from "./lib/event-loop-lag.ts";
|
|
9
|
+
import { percentile, summarize, DISCLAIMER, HARDWARE } from "./lib/report.ts";
|
|
10
|
+
import { rssSlopeMbPerMin, type RssSample } from "./lib/rss-sampler.ts";
|
|
11
|
+
|
|
12
|
+
describe("bench harness smoke", () => {
|
|
13
|
+
test("percentile + summarize", () => {
|
|
14
|
+
expect(percentile([1, 2, 3, 4, 5], 50)).toBe(3);
|
|
15
|
+
expect(percentile([], 50)).toBeNaN();
|
|
16
|
+
const s = summarize([10, 20, 30]);
|
|
17
|
+
expect(s.opsPerSec).toBeGreaterThan(0);
|
|
18
|
+
expect(s.p99Ms).toBeGreaterThanOrEqual(s.p50Ms);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("rss slope on synthetic samples", () => {
|
|
22
|
+
const samples: RssSample[] = Array.from({ length: 6 }, (_, i) => ({
|
|
23
|
+
tMs: i * 5000,
|
|
24
|
+
rssMb: 100 + i * 10,
|
|
25
|
+
fds: 20,
|
|
26
|
+
}));
|
|
27
|
+
// 10 MB per 5 s = 120 MB/min.
|
|
28
|
+
expect(rssSlopeMbPerMin(samples)).toBeCloseTo(120, -1);
|
|
29
|
+
expect(rssSlopeMbPerMin([])).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("event loop lag probe collects samples", async () => {
|
|
33
|
+
const probe = measureEventLoopLag(25);
|
|
34
|
+
await Bun.sleep(120);
|
|
35
|
+
probe.stop();
|
|
36
|
+
expect(probe.lags().length).toBeGreaterThan(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("artifact constants", () => {
|
|
40
|
+
expect(HARDWARE).toContain("M4");
|
|
41
|
+
expect(DISCLAIMER).toContain("not SLA");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -27,10 +27,30 @@ const FORBIDDEN_III_DEV = ["iii", ".", "dev"].join("");
|
|
|
27
27
|
const FORBIDDEN_COMPARISON_PATH = ["get-started/", "comparison"].join("");
|
|
28
28
|
/** Word-boundary classic Node framework — `-F` false-positives on `CallExpression`. */
|
|
29
29
|
const FORBIDDEN_EXPRESS = ["\\b", "Ex", "press", "\\b"].join("");
|
|
30
|
+
/** Compact-ID peer library (okid design context stays first-principles). */
|
|
31
|
+
const FORBIDDEN_NANOID = ["na", "no", "id"].join("");
|
|
32
|
+
/** Collision-resistant-ID peer library (same rule). */
|
|
33
|
+
const FORBIDDEN_CUID = ["cu", "id2"].join("");
|
|
30
34
|
|
|
31
35
|
/** Decoy `X-Powered-By` fixture value in the headers plugin tests. */
|
|
32
36
|
const EXPRESS_ALLOW = ["src/plugins/headers.test.ts:"];
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Allow-listed paths where peer-name strings are sanctioned: the okid design
|
|
40
|
+
* prompt, lockfile hashes, and the vendored Console dist bundle (Zod ships
|
|
41
|
+
* string-format validators whose registered format names collide with peer
|
|
42
|
+
* library names inside minified zod-core — vendor API identifiers, not
|
|
43
|
+
* authored comparisons).
|
|
44
|
+
*/
|
|
45
|
+
const OKID_PROMPT_ALLOW = [
|
|
46
|
+
"okid.md:",
|
|
47
|
+
"bun.lock:",
|
|
48
|
+
// Minified zod-core inside the vendored Console bundle registers string
|
|
49
|
+
// format validators whose names collide with peer library names — vendor
|
|
50
|
+
// identifiers, not authored comparisons. Hash changes on Console rebuilds.
|
|
51
|
+
"src/console/ui-next/dist/assets/index-DSEpMBPY.js:",
|
|
52
|
+
];
|
|
53
|
+
|
|
34
54
|
/**
|
|
35
55
|
* Run `git grep -F` and assert zero matches after ignoring allow-listed paths.
|
|
36
56
|
*
|
|
@@ -114,4 +134,12 @@ describe("named competitor mention removal gate", () => {
|
|
|
114
134
|
test("tracked tree has zero classic Node framework peer mentions outside decoy fixture", () => {
|
|
115
135
|
assertZeroGitGrepRegex(FORBIDDEN_EXPRESS, EXPRESS_ALLOW);
|
|
116
136
|
});
|
|
137
|
+
|
|
138
|
+
test("tracked tree has zero compact-ID peer library mentions outside design prompt", () => {
|
|
139
|
+
assertZeroGitGrepFixed(FORBIDDEN_NANOID, OKID_PROMPT_ALLOW);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("tracked tree has zero collision-resistant-ID peer library mentions outside design prompt", () => {
|
|
143
|
+
assertZeroGitGrepFixed(FORBIDDEN_CUID, OKID_PROMPT_ALLOW);
|
|
144
|
+
});
|
|
117
145
|
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oke doctor` file-descriptor headroom probe.
|
|
3
|
+
*
|
|
4
|
+
* A live-SSE app holds one fd per open subscriber stream; a low `ulimit -n`
|
|
5
|
+
* soft limit collapses under load long before CPU or memory do. Doctor
|
|
6
|
+
* estimates peak concurrent fds from the manifest and compares against the
|
|
7
|
+
* process soft limit.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import type { Manifest } from "../manifest/types.ts";
|
|
12
|
+
|
|
13
|
+
/** Conservative fallback when the soft limit cannot be read. */
|
|
14
|
+
const DEFAULT_SOFT_FD_LIMIT = 256;
|
|
15
|
+
|
|
16
|
+
/** Baseline fds every runtime needs (stdin/out/err, event loop, listeners). */
|
|
17
|
+
const BASE_FDS = 64;
|
|
18
|
+
|
|
19
|
+
/** Expected concurrent subscribers per live signal / SSE route. */
|
|
20
|
+
const EXPECTED_SUBSCRIBERS_PER_LIVE = 64;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Measured server-side fd cost per open SSE subscriber.
|
|
24
|
+
*
|
|
25
|
+
* Calibrated by the G3b bench (`src/bench/g03-signal-sse-memory.bench.ts`):
|
|
26
|
+
* 1.53 fds/subscriber observed on macOS/Bun across a 100→500 subscriber
|
|
27
|
+
* scale-up. The original formula implicitly budgeted 64 fds/subscriber —
|
|
28
|
+
* off by >20% against this measurement. Remaining conservatism lives in
|
|
29
|
+
* `BASE_FDS` / `HTTP_CONCURRENCY`, not here.
|
|
30
|
+
*/
|
|
31
|
+
export const FD_COST_PER_SUBSCRIBER: number = 1.5;
|
|
32
|
+
|
|
33
|
+
/** Pool connections per SQL driver. */
|
|
34
|
+
const SQL_CONNECTIONS = 2;
|
|
35
|
+
|
|
36
|
+
/** Connections held by Redis-style KV drivers. */
|
|
37
|
+
const KV_CONNECTIONS = 2;
|
|
38
|
+
|
|
39
|
+
/** Concurrent in-flight HTTP requests to budget for. */
|
|
40
|
+
const HTTP_CONCURRENCY = 128;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read the current soft `NOFILE` limit for this process.
|
|
44
|
+
*
|
|
45
|
+
* Prefers `process.getrlimit` when Bun exposes it; falls back to spawning
|
|
46
|
+
* `sh -c 'ulimit -n'` (which reports the shell's own limit, a close proxy).
|
|
47
|
+
* Returns `null` when neither path works — callers treat that as the
|
|
48
|
+
* conservative {@link DEFAULT_SOFT_FD_LIMIT}.
|
|
49
|
+
*/
|
|
50
|
+
export function getSoftFdLimit(): number | null {
|
|
51
|
+
const proc = process as typeof process & {
|
|
52
|
+
getrlimit?: (name: string) => { readonly soft: number; readonly hard: number };
|
|
53
|
+
};
|
|
54
|
+
if (typeof proc.getrlimit === "function") {
|
|
55
|
+
try {
|
|
56
|
+
const rl = proc.getrlimit("NOFILE");
|
|
57
|
+
if (rl && Number.isFinite(rl.soft)) return rl.soft;
|
|
58
|
+
} catch {
|
|
59
|
+
// fall through to shell
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const out = spawnSync("sh", ["-c", "ulimit -n"], { encoding: "utf8" });
|
|
64
|
+
const parsed = Number.parseInt(out.stdout?.trim() ?? "", 10);
|
|
65
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
66
|
+
} catch {
|
|
67
|
+
// unreadable — caller falls back
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Estimate peak concurrent file descriptors from a manifest.
|
|
74
|
+
*
|
|
75
|
+
* Budgets base runtime fds, expected live-signal subscribers (`signals`
|
|
76
|
+
* with `delivery === "live"` plus flows with an http trigger and `live`),
|
|
77
|
+
* pool connections, and HTTP concurrency. Without a manifest, returns a
|
|
78
|
+
* conservative baseline only.
|
|
79
|
+
*
|
|
80
|
+
* @param manifest - Loaded manifest, or `null` when none was found.
|
|
81
|
+
*/
|
|
82
|
+
export function estimatePeakFds(manifest: Manifest | null): number {
|
|
83
|
+
let need = BASE_FDS + SQL_CONNECTIONS + KV_CONNECTIONS + HTTP_CONCURRENCY;
|
|
84
|
+
if (!manifest) return need;
|
|
85
|
+
const liveSignals = Object.values(manifest.signals ?? {}).filter(
|
|
86
|
+
(signal) => signal.delivery === "live",
|
|
87
|
+
).length;
|
|
88
|
+
const sseRoutes = Object.values(manifest.flows ?? {}).filter((flow) => {
|
|
89
|
+
if (!flow.live) return false;
|
|
90
|
+
const trigger = flow.trigger?.http;
|
|
91
|
+
return (
|
|
92
|
+
trigger !== undefined &&
|
|
93
|
+
(trigger.method === "GET" || trigger.method === "HEAD" || trigger.method === "QUERY")
|
|
94
|
+
);
|
|
95
|
+
}).length;
|
|
96
|
+
// Live subscribers are the dominant term; budget the busiest signal/route
|
|
97
|
+
// at the expected concurrency times the measured per-subscriber fd cost.
|
|
98
|
+
need += Math.max(liveSignals, sseRoutes) * EXPECTED_SUBSCRIBERS_PER_LIVE * FD_COST_PER_SUBSCRIBER;
|
|
99
|
+
return need;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compare the estimated peak fd need against the soft limit.
|
|
104
|
+
*
|
|
105
|
+
* @param manifest - Loaded manifest, or `null` when none was found.
|
|
106
|
+
*/
|
|
107
|
+
export function checkFileDescriptorHeadroom(manifest: Manifest | null): {
|
|
108
|
+
ok: boolean;
|
|
109
|
+
softLimit: number;
|
|
110
|
+
estimatedNeed: number;
|
|
111
|
+
headroom: number;
|
|
112
|
+
} {
|
|
113
|
+
const softLimit = getSoftFdLimit() ?? DEFAULT_SOFT_FD_LIMIT;
|
|
114
|
+
const estimatedNeed = estimatePeakFds(manifest);
|
|
115
|
+
const headroom = softLimit - estimatedNeed;
|
|
116
|
+
return { ok: headroom >= 0, softLimit, estimatedNeed, headroom };
|
|
117
|
+
}
|