Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export async function getRunnerConfig(
return apiCall<never, RunnerConfigsResponse>(
config,
"GET",
`/runner-configs?runner_name=${name}`,
`/runner-configs?runner_name=${encodeURIComponent(name)}`,
);
}

Expand Down
75 changes: 75 additions & 0 deletions rivetkit-typescript/packages/rivetkit/src/serverless/configure.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { convertRegistryConfigToClientConfig } from "@/client/config";

Check failure on line 1 in rivetkit-typescript/packages/rivetkit/src/serverless/configure.ts

View workflow job for this annotation

GitHub Actions / RivetKit / Quality Check

format

Formatter would have printed the following content:
import type { ClientConfig } from "@/client/config";
import { stringifyError } from "@/common/utils";
import { RivetError } from "@/actor/errors";
import {
getDatacenters,
getRunnerConfig,
updateRunnerConfig,
} from "@/engine-client/api-endpoints";
import type { RegistryConfig } from "@/registry/config";
import { logger } from "@/registry/log";
import { isDev } from "@/utils/env-vars";

const DEFAULT_CONFIGURE_TIMEOUT_MS = 60_000;
const CONFIGURE_RETRY_DELAY_MS = 1_000;
const LOCAL_HANDLER_PROBE_TIMEOUT_MS = 1_000;

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
Expand Down Expand Up @@ -62,6 +67,11 @@
const clientConfig = convertRegistryConfigToClientConfig(config);
const dcsRes = await getDatacenters(clientConfig);
const poolName = customConfig.name ?? "default";
await assertLocalServerlessHandlerOwnership(
clientConfig,
poolName,
customConfig.url,
);
const serverlessToken = config.token ?? config.publicToken;
const headers = {
...(serverlessToken
Expand Down Expand Up @@ -101,6 +111,13 @@
});
return;
} catch (error) {
if (
error instanceof RivetError &&
error.group === "rivetkit" &&
error.code === "local_serverless_handler_conflict"
) {
throw error;
}
lastError = error;
logger().warn({
msg: "serverless pool configuration attempt failed",
Expand All @@ -118,3 +135,61 @@
});
throw lastError;
}

async function assertLocalServerlessHandlerOwnership(
config: ClientConfig,
poolName: string,
requestedUrl: string,
): Promise<void> {
if (!isDev()) return;

const response = await getRunnerConfig(config, poolName);
const datacenters = response.runner_configs[poolName]?.datacenters ?? {};
const existingUrls = new Set(
Object.values(datacenters)
.map((runnerConfig) => runnerConfig.serverless?.url)
.filter((url): url is string => url !== undefined),
);

for (const existingUrl of existingUrls) {
if (normalizeHandlerUrl(existingUrl) === normalizeHandlerUrl(requestedUrl)) {
continue;
}
if (!(await handlerIsLive(existingUrl))) continue;

throw new RivetError(
"rivetkit",
"local_serverless_handler_conflict",
`namespace \`${config.namespace}\` and pool \`${poolName}\` already use the live serverless handler ${existingUrl}. Stop the other project, set RIVET_NAMESPACE to a different namespace, or set RIVET_RUN_ENGINE_PORT to a different Engine port. See https://rivet.dev/docs/general/environment-variables/`,
{
public: true,
metadata: {
namespace: config.namespace,
poolName,
existingUrl,
requestedUrl,
},
},
);
}
}

export function normalizeHandlerUrl(value: string): string {
const url = new URL(value);
url.hash = "";
url.pathname = url.pathname.replace(/\/+$/, "");
return url.toString();
}

async function handlerIsLive(handlerUrl: string): Promise<boolean> {
const metadataUrl = new URL(handlerUrl);
metadataUrl.pathname = `${metadataUrl.pathname.replace(/\/+$/, "")}/metadata`;
try {
const response = await fetch(metadataUrl, {
signal: AbortSignal.timeout(LOCAL_HANDLER_PROBE_TIMEOUT_MS),
});
return response.ok;
} catch {
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, test } from "vitest";
import { normalizeHandlerUrl } from "../src/serverless/configure";

describe("local serverless handler ownership", () => {
test("normalizes equivalent hot-reload handler URLs", () => {
expect(normalizeHandlerUrl("http://LOCALHOST:3000/api/rivet/")).toBe(
normalizeHandlerUrl("http://localhost:3000/api/rivet"),
);
});

test("preserves distinct handler ports", () => {
expect(normalizeHandlerUrl("http://localhost:3000/api/rivet")).not.toBe(
normalizeHandlerUrl("http://localhost:3001/api/rivet"),
);
});
});
Loading