Skip to content

Commit e5467bd

Browse files
committed
fix(rivetkit): properly apply clientEndpoint from metadata lookup
1 parent b540cc6 commit e5467bd

File tree

5 files changed

+89
-58
lines changed

5 files changed

+89
-58
lines changed

rivetkit-openapi/openapi.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"openapi": "3.0.0",
33
"info": {
4-
"version": "2.0.22",
4+
"version": "2.0.23",
55
"title": "RivetKit API"
66
},
77
"components": {

rivetkit-typescript/packages/rivetkit/src/client/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const ClientConfigSchema = z.object({
4141
getUpgradeWebSocket: z.custom<GetUpgradeWebSocket>().optional(),
4242

4343
/** Whether to automatically perform health checks when the client is created. */
44-
disableHealthCheck: z.boolean().optional().default(false),
44+
disableMetadataLookup: z.boolean().optional().default(false),
4545
});
4646

4747
export type ClientConfig = z.infer<typeof ClientConfigSchema>;

rivetkit-typescript/packages/rivetkit/src/registry/mod.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class Registry<A extends RegistryActors> {
8080
// a serverless runner request, so we do not know what to health check
8181
if (config.runnerKind === "serverless") {
8282
logger().debug("disabling health check since using serverless");
83-
config.disableHealthCheck = true;
83+
config.disableMetadataLookup = true;
8484
}
8585

8686
// Auto-configure serverless runner if not in prod
@@ -290,7 +290,7 @@ async function configureServerlessRunner(config: RunnerConfig): Promise<void> {
290290
transport: config.transport,
291291
headers: config.headers,
292292
getUpgradeWebSocket: config.getUpgradeWebSocket,
293-
disableHealthCheck: true, // We don't need health check for this operation
293+
disableMetadataLookup: true, // We don't need health check for this operation
294294
};
295295

296296
// Fetch all datacenters
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import pRetry from "p-retry";
2+
import type { ClientConfig } from "@/client/client";
3+
import type { MetadataResponse } from "@/common/router";
4+
import { stringifyError } from "@/common/utils";
5+
import { getMetadata } from "./api-endpoints";
6+
import { getEndpoint } from "./api-utils";
7+
import { logger } from "./log";
8+
9+
// Global cache to store metadata check promises for each endpoint
10+
const metadataLookupCache = new Map<string, Promise<MetadataResponse>>();
11+
12+
export async function lookupMetadataCached(
13+
config: ClientConfig,
14+
): Promise<MetadataResponse> {
15+
const endpoint = getEndpoint(config);
16+
17+
// Check if metadata lookup is already in progress or completed for this endpoint
18+
const existingPromise = metadataLookupCache.get(endpoint);
19+
if (existingPromise) {
20+
return existingPromise;
21+
}
22+
23+
// Create and store the promise immediately to prevent racing requests
24+
const metadataLookupPromise = pRetry(
25+
async () => {
26+
logger().debug({
27+
msg: "fetching metadata",
28+
endpoint,
29+
});
30+
31+
const metadataData = await getMetadata(config);
32+
33+
logger().debug({
34+
msg: "received metadata",
35+
endpoint,
36+
clientEndpoint: metadataData.clientEndpoint,
37+
});
38+
39+
return metadataData;
40+
},
41+
{
42+
forever: true,
43+
minTimeout: 500,
44+
maxTimeout: 15_000,
45+
onFailedAttempt: (error) => {
46+
logger().warn({
47+
msg: "failed to fetch metadata, retrying",
48+
endpoint,
49+
attempt: error.attemptNumber,
50+
error: stringifyError(error),
51+
});
52+
},
53+
},
54+
);
55+
56+
metadataLookupCache.set(endpoint, metadataLookupPromise);
57+
return metadataLookupPromise;
58+
}

rivetkit-typescript/packages/rivetkit/src/remote-manager-driver/mod.ts

Lines changed: 27 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import invariant from "invariant";
44
import { deserializeActorKey, serializeActorKey } from "@/actor/keys";
55
import { generateRandomString } from "@/actor/utils";
66
import type { ClientConfig } from "@/client/client";
7+
import type { MetadataResponse } from "@/common/router";
78
import { noopNext, stringifyError } from "@/common/utils";
89
import type {
910
ActorOutput,
@@ -32,6 +33,7 @@ import {
3233
} from "./api-endpoints";
3334
import { EngineApiError, getEndpoint } from "./api-utils";
3435
import { logger } from "./log";
36+
import { lookupMetadataCached } from "./metadata";
3537
import { createWebSocketProxy } from "./ws-proxy";
3638

3739
// TODO:
@@ -48,9 +50,6 @@ import { createWebSocketProxy } from "./ws-proxy";
4850
// };
4951
// })();
5052

51-
// Global cache to store metadata check promises for each endpoint
52-
const metadataCheckCache = new Map<string, Promise<void>>();
53-
5453
export class RemoteManagerDriver implements ManagerDriver {
5554
#config: ClientConfig;
5655
#metadataPromise: Promise<void> | undefined;
@@ -63,64 +62,38 @@ export class RemoteManagerDriver implements ManagerDriver {
6362
logger().info(
6463
"detected next.js build phase, disabling health check",
6564
);
66-
runConfig.disableHealthCheck = true;
65+
runConfig.disableMetadataLookup = true;
6766
}
6867

69-
this.#config = runConfig;
68+
// Clone config so we can mutate the endpoint in #metadataPromise
69+
// NOTE: This is a shallow clone, so mutating nested properties will not do anything
70+
this.#config = { ...runConfig };
7071

7172
// Perform metadata check if enabled
72-
if (!runConfig.disableHealthCheck) {
73-
this.#metadataPromise = this.#performMetadataCheck(runConfig);
74-
this.#metadataPromise.catch((error) => {
75-
logger().error({
76-
msg: "metadata check failed",
77-
error:
78-
error instanceof Error ? error.message : String(error),
79-
});
80-
});
81-
}
82-
}
83-
84-
async #performMetadataCheck(config: ClientConfig): Promise<void> {
85-
const endpoint = getEndpoint(config);
86-
87-
// Check if metadata check is already in progress or completed for this endpoint
88-
const existingPromise = metadataCheckCache.get(endpoint);
89-
if (existingPromise) {
90-
return existingPromise;
91-
}
92-
93-
// Create and store the promise immediately to prevent racing requests
94-
const metadataCheckPromise = (async () => {
95-
try {
96-
const metadataData = await getMetadata(config);
73+
if (!runConfig.disableMetadataLookup) {
74+
// This should never error, since it uses pRetry. If it does for
75+
// any reason, we'll surface the error anywhere #metadataPromise is
76+
// awaited.
77+
this.#metadataPromise = lookupMetadataCached(this.#config).then(
78+
(metadataData) => {
79+
// Override endpoint for all future requests
80+
if (metadataData.clientEndpoint) {
81+
this.#config.endpoint = metadataData.clientEndpoint;
82+
logger().info({
83+
msg: "overriding cached client endpoint",
84+
endpoint: metadataData.clientEndpoint,
85+
});
86+
}
9787

98-
if (metadataData.clientEndpoint) {
9988
logger().info({
100-
msg: "received new client endpoint from metadata",
101-
endpoint: metadataData.clientEndpoint,
89+
msg: "connected to rivetkit manager",
90+
runtime: metadataData.runtime,
91+
version: metadataData.version,
92+
runner: metadataData.runner,
10293
});
103-
this.#config.endpoint = metadataData.clientEndpoint;
104-
}
105-
106-
// Log successful metadata check with runtime and version info
107-
logger().info({
108-
msg: "connected to rivetkit manager",
109-
runtime: metadataData.runtime,
110-
version: metadataData.version,
111-
runner: metadataData.runner,
112-
});
113-
} catch (error) {
114-
logger().error({
115-
msg: "health check failed, validate the Rivet endpoint is configured correctly",
116-
endpoint,
117-
error: stringifyError(error),
118-
});
119-
}
120-
})();
121-
122-
metadataCheckCache.set(endpoint, metadataCheckPromise);
123-
return metadataCheckPromise;
94+
},
95+
);
96+
}
12497
}
12598

12699
async getForId({

0 commit comments

Comments
 (0)