Skip to content

Commit f7dae0a

Browse files
committed
chore: rivetkit core/napi/typescript follow up review
1 parent d7cd40d commit f7dae0a

81 files changed

Lines changed: 6989 additions & 1018 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
import { spawn, type ChildProcess } from "node:child_process";
2+
import { randomUUID } from "node:crypto";
3+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4+
import { tmpdir } from "node:os";
5+
import { join, resolve } from "node:path";
6+
import { createServer } from "node:net";
7+
import { Hono } from "hono";
8+
import { serve } from "@hono/node-server";
9+
import { createClient } from "rivetkit/client";
10+
import { registry } from "../examples/kitchen-sink/src/index.ts";
11+
12+
const TOKEN = "dev";
13+
const HOST = "127.0.0.1";
14+
let lastEngineOutput = "";
15+
16+
function freePort(): Promise<number> {
17+
return new Promise((resolvePort, reject) => {
18+
const server = createServer();
19+
server.once("error", reject);
20+
server.listen(0, HOST, () => {
21+
const address = server.address();
22+
if (!address || typeof address === "string") {
23+
server.close(() => reject(new Error("failed to allocate port")));
24+
return;
25+
}
26+
const port = address.port;
27+
server.close(() => resolvePort(port));
28+
});
29+
});
30+
}
31+
32+
async function waitForOk(url: string, timeoutMs: number): Promise<void> {
33+
const deadline = Date.now() + timeoutMs;
34+
let lastError: unknown;
35+
while (Date.now() < deadline) {
36+
try {
37+
const res = await fetch(url);
38+
if (res.ok) return;
39+
lastError = new Error(`${res.status} ${await res.text()}`);
40+
} catch (error) {
41+
lastError = error;
42+
}
43+
await new Promise((resolve) => setTimeout(resolve, 250));
44+
}
45+
throw new Error(`timed out waiting for ${url}: ${String(lastError)}`);
46+
}
47+
48+
async function readJson<T>(res: Response): Promise<T> {
49+
const text = await res.text();
50+
if (!res.ok) {
51+
throw new Error(`${res.status} ${text}`);
52+
}
53+
return JSON.parse(text) as T;
54+
}
55+
56+
async function fetchWithTimeout(
57+
input: string,
58+
init?: RequestInit,
59+
timeoutMs = 15_000,
60+
): Promise<Response> {
61+
const controller = new AbortController();
62+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
63+
try {
64+
return await fetch(input, { ...init, signal: init?.signal ?? controller.signal });
65+
} finally {
66+
clearTimeout(timeout);
67+
}
68+
}
69+
70+
function logStep(step: string, details?: Record<string, unknown>) {
71+
console.error(JSON.stringify({ kind: "step", step, ...details }));
72+
}
73+
74+
async function main() {
75+
const guardPort = await freePort();
76+
const apiPeerPort = await freePort();
77+
const metricsPort = await freePort();
78+
const servicePort = await freePort();
79+
const endpoint = `http://${HOST}:${guardPort}`;
80+
const serviceUrl = `http://${HOST}:${servicePort}/api/rivet`;
81+
const namespace = `serverless-e2e-${randomUUID()}`;
82+
const runnerName = `kitchen-sink-${randomUUID()}`;
83+
const dbRoot = mkdtempSync(join(tmpdir(), "rivetkit-serverless-e2e-"));
84+
const configPath = join(dbRoot, "engine.json");
85+
let engine: ChildProcess | undefined;
86+
let service: ReturnType<typeof serve> | undefined;
87+
88+
try {
89+
writeFileSync(
90+
configPath,
91+
JSON.stringify({
92+
topology: {
93+
datacenter_label: 1,
94+
datacenters: {
95+
default: {
96+
datacenter_label: 1,
97+
is_leader: true,
98+
public_url: endpoint,
99+
peer_url: `http://${HOST}:${apiPeerPort}`,
100+
},
101+
},
102+
},
103+
}),
104+
);
105+
106+
engine = spawn(resolve("target/debug/rivet-engine"), ["--config", configPath, "start"], {
107+
env: {
108+
...process.env,
109+
RIVET__GUARD__HOST: HOST,
110+
RIVET__GUARD__PORT: guardPort.toString(),
111+
RIVET__API_PEER__HOST: HOST,
112+
RIVET__API_PEER__PORT: apiPeerPort.toString(),
113+
RIVET__METRICS__HOST: HOST,
114+
RIVET__METRICS__PORT: metricsPort.toString(),
115+
RIVET__FILE_SYSTEM__PATH: join(dbRoot, "db"),
116+
},
117+
stdio: ["ignore", "pipe", "pipe"],
118+
});
119+
120+
engine.stdout?.on("data", (chunk) => {
121+
lastEngineOutput += chunk.toString();
122+
});
123+
engine.stderr?.on("data", (chunk) => {
124+
lastEngineOutput += chunk.toString();
125+
});
126+
127+
logStep("wait-engine", { endpoint });
128+
await waitForOk(`${endpoint}/health`, 90_000);
129+
130+
registry.config.test = { ...registry.config.test, enabled: true };
131+
registry.config.startEngine = false;
132+
registry.config.endpoint = endpoint;
133+
registry.config.token = TOKEN;
134+
registry.config.namespace = namespace;
135+
registry.config.envoy = {
136+
...registry.config.envoy,
137+
poolName: runnerName,
138+
};
139+
140+
const app = new Hono();
141+
app.all("/api/rivet/*", async (c) => {
142+
const res = await registry.handler(c.req.raw);
143+
console.error(
144+
JSON.stringify({
145+
kind: "serverless-request",
146+
method: c.req.method,
147+
path: new URL(c.req.url).pathname,
148+
status: res.status,
149+
endpoint: c.req.header("x-rivet-endpoint"),
150+
poolName: c.req.header("x-rivet-pool-name"),
151+
namespace: c.req.header("x-rivet-namespace-name"),
152+
hasToken: Boolean(c.req.header("x-rivet-token")),
153+
}),
154+
);
155+
return res;
156+
});
157+
app.get("/health", (c) => c.json({ ok: true }));
158+
service = serve({ fetch: app.fetch, hostname: HOST, port: servicePort });
159+
logStep("wait-service", { serviceUrl });
160+
await waitForOk(`http://${HOST}:${servicePort}/health`, 10_000);
161+
162+
logStep("metadata");
163+
const serviceMetadata = await readJson<{ runtime: string; actorNames: unknown }>(
164+
await fetchWithTimeout(`${serviceUrl}/metadata`),
165+
);
166+
if (serviceMetadata.runtime !== "rivetkit") {
167+
throw new Error(`unexpected metadata runtime ${serviceMetadata.runtime}`);
168+
}
169+
170+
logStep("create-namespace", { namespace });
171+
await readJson(
172+
await fetchWithTimeout(`${endpoint}/namespaces`, {
173+
method: "POST",
174+
headers: {
175+
Authorization: `Bearer ${TOKEN}`,
176+
"Content-Type": "application/json",
177+
},
178+
body: JSON.stringify({
179+
name: namespace,
180+
display_name: namespace,
181+
}),
182+
}),
183+
);
184+
185+
logStep("get-datacenters", { namespace });
186+
const datacenters = await readJson<{ datacenters: Array<{ name: string }> }>(
187+
await fetchWithTimeout(`${endpoint}/datacenters?namespace=${namespace}`, {
188+
headers: { Authorization: `Bearer ${TOKEN}` },
189+
}),
190+
);
191+
const dc = datacenters.datacenters[0]?.name;
192+
if (!dc) throw new Error("engine returned no datacenters");
193+
194+
logStep("serverless-health-check", { serviceUrl });
195+
const healthCheck = await readJson<{ success?: { version: string }; failure?: unknown }>(
196+
await fetchWithTimeout(
197+
`${endpoint}/runner-configs/serverless-health-check?namespace=${namespace}`,
198+
{
199+
method: "POST",
200+
headers: {
201+
Authorization: `Bearer ${TOKEN}`,
202+
"Content-Type": "application/json",
203+
},
204+
body: JSON.stringify({ url: serviceUrl, headers: {} }),
205+
},
206+
),
207+
);
208+
if (!("success" in healthCheck)) {
209+
throw new Error(`serverless health check failed: ${JSON.stringify(healthCheck)}`);
210+
}
211+
212+
logStep("put-runner-config", { runnerName, dc });
213+
await readJson(
214+
await fetchWithTimeout(
215+
`${endpoint}/runner-configs/${encodeURIComponent(runnerName)}?namespace=${namespace}`,
216+
{
217+
method: "PUT",
218+
headers: {
219+
Authorization: `Bearer ${TOKEN}`,
220+
"Content-Type": "application/json",
221+
},
222+
body: JSON.stringify({
223+
datacenters: {
224+
[dc]: {
225+
serverless: {
226+
url: serviceUrl,
227+
headers: { "x-rivet-token": TOKEN },
228+
request_lifespan: 30,
229+
max_concurrent_actors: 8,
230+
drain_grace_period: 10,
231+
slots_per_runner: 8,
232+
min_runners: 0,
233+
max_runners: 8,
234+
runners_margin: 0,
235+
metadata_poll_interval: 1000,
236+
},
237+
drain_on_version_upgrade: true,
238+
},
239+
},
240+
}),
241+
},
242+
),
243+
);
244+
245+
const client = createClient<typeof registry>({
246+
endpoint,
247+
namespace,
248+
token: TOKEN,
249+
poolName: runnerName,
250+
disableMetadataLookup: true,
251+
});
252+
try {
253+
logStep("actor-increment");
254+
const handle = client.counter.getOrCreate(["serverless-e2e"]);
255+
const count = await Promise.race([
256+
handle.increment(7),
257+
new Promise<never>((_, reject) =>
258+
setTimeout(() => reject(new Error("actor increment timed out")), 60_000),
259+
),
260+
]);
261+
if (count !== 7) {
262+
throw new Error(`expected counter result 7, received ${count}`);
263+
}
264+
} finally {
265+
await client.dispose();
266+
}
267+
268+
console.log(
269+
JSON.stringify({
270+
ok: true,
271+
endpoint,
272+
namespace,
273+
runnerName,
274+
serviceUrl,
275+
}),
276+
);
277+
278+
if (engine.exitCode !== null) {
279+
throw new Error(`engine exited early:\n${lastEngineOutput}`);
280+
}
281+
} finally {
282+
service?.close();
283+
if (engine && engine.exitCode === null) {
284+
engine.kill("SIGTERM");
285+
await new Promise((resolve) => setTimeout(resolve, 1000));
286+
if (engine.exitCode === null) engine.kill("SIGKILL");
287+
}
288+
rmSync(dbRoot, { recursive: true, force: true });
289+
}
290+
}
291+
292+
main()
293+
.then(() => process.exit(0))
294+
.catch((error) => {
295+
console.error(error);
296+
console.error(lastEngineOutput);
297+
process.exit(1);
298+
});

0 commit comments

Comments
 (0)