@rivetkit/engine-runner 2.0.23 → 2.0.24
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/.turbo/turbo-build.log +10 -10
- package/dist/mod.cjs +1460 -812
- package/dist/mod.cjs.map +1 -1
- package/dist/mod.d.cts +263 -17
- package/dist/mod.d.ts +263 -17
- package/dist/mod.js +1454 -806
- package/dist/mod.js.map +1 -1
- package/package.json +2 -2
- package/src/actor.ts +196 -0
- package/src/mod.ts +409 -177
- package/src/stringify.ts +182 -12
- package/src/tunnel.ts +822 -428
- package/src/utils.ts +93 -0
- package/src/websocket-tunnel-adapter.ts +340 -357
- package/tests/utils.test.ts +194 -0
package/src/tunnel.ts
CHANGED
|
@@ -1,30 +1,44 @@
|
|
|
1
1
|
import type * as protocol from "@rivetkit/engine-runner-protocol";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
GatewayId,
|
|
4
|
+
MessageId,
|
|
5
|
+
RequestId,
|
|
6
|
+
} from "@rivetkit/engine-runner-protocol";
|
|
3
7
|
import type { Logger } from "pino";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
import {
|
|
9
|
+
parse as uuidparse,
|
|
10
|
+
stringify as uuidstringify,
|
|
11
|
+
v4 as uuidv4,
|
|
12
|
+
} from "uuid";
|
|
13
|
+
import type { Runner, RunnerActor } from "./mod";
|
|
7
14
|
import {
|
|
8
15
|
stringifyToClientTunnelMessageKind,
|
|
9
16
|
stringifyToServerTunnelMessageKind,
|
|
10
17
|
} from "./stringify";
|
|
11
|
-
import { unreachable } from "./utils";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const WEBSOCKET_STATE_PERSIST_TIMEOUT = 30000; // 30 seconds
|
|
18
|
+
import { arraysEqual, idToStr, unreachable } from "./utils";
|
|
19
|
+
import {
|
|
20
|
+
HIBERNATABLE_SYMBOL,
|
|
21
|
+
WebSocketTunnelAdapter,
|
|
22
|
+
} from "./websocket-tunnel-adapter";
|
|
17
23
|
|
|
18
|
-
interface PendingRequest {
|
|
24
|
+
export interface PendingRequest {
|
|
19
25
|
resolve: (response: Response) => void;
|
|
20
26
|
reject: (error: Error) => void;
|
|
21
27
|
streamController?: ReadableStreamDefaultController<Uint8Array>;
|
|
22
28
|
actorId?: string;
|
|
29
|
+
gatewayId?: GatewayId;
|
|
30
|
+
requestId?: RequestId;
|
|
31
|
+
clientMessageIndex: number;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
|
-
interface
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
export interface HibernatingWebSocketMetadata {
|
|
35
|
+
gatewayId: GatewayId;
|
|
36
|
+
requestId: RequestId;
|
|
37
|
+
clientMessageIndex: number;
|
|
38
|
+
serverMessageIndex: number;
|
|
39
|
+
|
|
40
|
+
path: string;
|
|
41
|
+
headers: Record<string, string>;
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
class RunnerShutdownError extends Error {
|
|
@@ -36,15 +50,19 @@ class RunnerShutdownError extends Error {
|
|
|
36
50
|
export class Tunnel {
|
|
37
51
|
#runner: Runner;
|
|
38
52
|
|
|
39
|
-
/**
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
#pendingTunnelMessages: Map<string, PendingTunnelMessage> = new Map();
|
|
53
|
+
/** Maps request IDs to actor IDs for lookup */
|
|
54
|
+
#requestToActor: Array<{
|
|
55
|
+
gatewayId: GatewayId;
|
|
56
|
+
requestId: RequestId;
|
|
57
|
+
actorId: string;
|
|
58
|
+
}> = [];
|
|
46
59
|
|
|
47
|
-
|
|
60
|
+
/** Buffer for messages when not connected */
|
|
61
|
+
#bufferedMessages: Array<{
|
|
62
|
+
gatewayId: GatewayId;
|
|
63
|
+
requestId: RequestId;
|
|
64
|
+
messageKind: protocol.ToServerTunnelMessageKind;
|
|
65
|
+
}> = [];
|
|
48
66
|
|
|
49
67
|
get log(): Logger | undefined {
|
|
50
68
|
return this.#runner.log;
|
|
@@ -55,70 +73,473 @@ export class Tunnel {
|
|
|
55
73
|
}
|
|
56
74
|
|
|
57
75
|
start(): void {
|
|
58
|
-
|
|
76
|
+
// No-op - kept for compatibility
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
resendBufferedEvents(): void {
|
|
80
|
+
if (this.#bufferedMessages.length === 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.log?.info({
|
|
85
|
+
msg: "resending buffered tunnel messages",
|
|
86
|
+
count: this.#bufferedMessages.length,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const messages = this.#bufferedMessages;
|
|
90
|
+
this.#bufferedMessages = [];
|
|
91
|
+
|
|
92
|
+
for (const { gatewayId, requestId, messageKind } of messages) {
|
|
93
|
+
this.#sendMessage(gatewayId, requestId, messageKind);
|
|
94
|
+
}
|
|
59
95
|
}
|
|
60
96
|
|
|
61
97
|
shutdown() {
|
|
62
98
|
// NOTE: Pegboard WS already closed at this point, cannot send
|
|
63
99
|
// anything. All teardown logic is handled by pegboard-runner.
|
|
64
100
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
101
|
+
// Reject all pending requests and close all WebSockets for all actors
|
|
102
|
+
// RunnerShutdownError will be explicitly ignored
|
|
103
|
+
for (const [_actorId, actor] of this.#runner.actors) {
|
|
104
|
+
// Reject all pending requests for this actor
|
|
105
|
+
for (const entry of actor.pendingRequests) {
|
|
106
|
+
entry.request.reject(new RunnerShutdownError());
|
|
107
|
+
}
|
|
108
|
+
actor.pendingRequests = [];
|
|
109
|
+
|
|
110
|
+
// Close all WebSockets for this actor
|
|
111
|
+
// The WebSocket close event with retry is automatically sent when the
|
|
112
|
+
// runner WS closes, so we only need to notify the client that the WS
|
|
113
|
+
// closed:
|
|
114
|
+
// https://github.com/rivet-dev/rivet/blob/00d4f6a22da178a6f8115e5db50d96c6f8387c2e/engine/packages/pegboard-runner/src/lib.rs#L157
|
|
115
|
+
for (const entry of actor.webSockets) {
|
|
116
|
+
// Only close non-hibernatable websockets to prevent sending
|
|
117
|
+
// unnecessary close messages for websockets that will be hibernated
|
|
118
|
+
if (!entry.ws[HIBERNATABLE_SYMBOL]) {
|
|
119
|
+
entry.ws._closeWithoutCallback(1000, "ws.tunnel_shutdown");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
actor.webSockets = [];
|
|
68
123
|
}
|
|
69
124
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
125
|
+
// Clear the request-to-actor mapping
|
|
126
|
+
this.#requestToActor = [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async restoreHibernatingRequests(
|
|
130
|
+
actorId: string,
|
|
131
|
+
metaEntries: HibernatingWebSocketMetadata[],
|
|
132
|
+
) {
|
|
133
|
+
const actor = this.#runner.getActor(actorId);
|
|
134
|
+
if (!actor) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Actor ${actorId} not found for restoring hibernating requests`,
|
|
137
|
+
);
|
|
75
138
|
}
|
|
76
|
-
this.#actorPendingRequests.clear();
|
|
77
139
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
140
|
+
if (actor.hibernationRestored) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Actor ${actorId} already restored hibernating requests`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.log?.debug({
|
|
147
|
+
msg: "restoring hibernating requests",
|
|
148
|
+
actorId,
|
|
149
|
+
requests: actor.hibernatingRequests.length,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Track all background operations
|
|
153
|
+
const backgroundOperations: Promise<void>[] = [];
|
|
154
|
+
|
|
155
|
+
// Process connected WebSockets
|
|
156
|
+
let connectedButNotLoadedCount = 0;
|
|
157
|
+
let restoredCount = 0;
|
|
158
|
+
for (const { gatewayId, requestId } of actor.hibernatingRequests) {
|
|
159
|
+
const requestIdStr = idToStr(requestId);
|
|
160
|
+
const meta = metaEntries.find(
|
|
161
|
+
(entry) =>
|
|
162
|
+
arraysEqual(entry.gatewayId, gatewayId) &&
|
|
163
|
+
arraysEqual(entry.requestId, requestId),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (!meta) {
|
|
167
|
+
// Connected but not loaded (not persisted) - close it
|
|
168
|
+
//
|
|
169
|
+
// This may happen if the metadata was not successfully persisted
|
|
170
|
+
this.log?.warn({
|
|
171
|
+
msg: "closing websocket that is not persisted",
|
|
172
|
+
requestId: requestIdStr,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
176
|
+
tag: "ToServerWebSocketClose",
|
|
177
|
+
val: {
|
|
178
|
+
code: 1000,
|
|
179
|
+
reason: "ws.meta_not_found_during_restore",
|
|
180
|
+
hibernate: false,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
connectedButNotLoadedCount++;
|
|
185
|
+
} else {
|
|
186
|
+
// Both connected and persisted - restore it
|
|
187
|
+
const request = buildRequestForWebSocket(
|
|
188
|
+
meta.path,
|
|
189
|
+
meta.headers,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// This will call `runner.config.websocket` under the hood to
|
|
193
|
+
// attach the event listeners to the WebSocket.
|
|
194
|
+
// Track this operation to ensure it completes
|
|
195
|
+
const restoreOperation = this.#createWebSocket(
|
|
196
|
+
actorId,
|
|
197
|
+
gatewayId,
|
|
198
|
+
requestId,
|
|
199
|
+
requestIdStr,
|
|
200
|
+
meta.serverMessageIndex,
|
|
201
|
+
true,
|
|
202
|
+
true,
|
|
203
|
+
request,
|
|
204
|
+
meta.path,
|
|
205
|
+
meta.headers,
|
|
206
|
+
false,
|
|
207
|
+
)
|
|
208
|
+
.then(() => {
|
|
209
|
+
// Create a PendingRequest entry to track the message index
|
|
210
|
+
const actor = this.#runner.getActor(actorId);
|
|
211
|
+
if (actor) {
|
|
212
|
+
actor.createPendingRequest(
|
|
213
|
+
gatewayId,
|
|
214
|
+
requestId,
|
|
215
|
+
meta.clientMessageIndex,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.log?.info({
|
|
220
|
+
msg: "connection successfully restored",
|
|
221
|
+
actorId,
|
|
222
|
+
requestId: requestIdStr,
|
|
223
|
+
});
|
|
224
|
+
})
|
|
225
|
+
.catch((err) => {
|
|
226
|
+
this.log?.error({
|
|
227
|
+
msg: "error creating websocket during restore",
|
|
228
|
+
requestId: requestIdStr,
|
|
229
|
+
err,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Close the WebSocket on error
|
|
233
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
234
|
+
tag: "ToServerWebSocketClose",
|
|
235
|
+
val: {
|
|
236
|
+
code: 1011,
|
|
237
|
+
reason: "ws.restore_error",
|
|
238
|
+
hibernate: false,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
backgroundOperations.push(restoreOperation);
|
|
244
|
+
restoredCount++;
|
|
89
245
|
}
|
|
90
246
|
}
|
|
91
|
-
|
|
247
|
+
|
|
248
|
+
// Process loaded but not connected (stale) - remove them
|
|
249
|
+
let loadedButNotConnectedCount = 0;
|
|
250
|
+
for (const meta of metaEntries) {
|
|
251
|
+
const requestIdStr = idToStr(meta.requestId);
|
|
252
|
+
const isConnected = actor.hibernatingRequests.some(
|
|
253
|
+
(req) =>
|
|
254
|
+
arraysEqual(req.gatewayId, meta.gatewayId) &&
|
|
255
|
+
arraysEqual(req.requestId, meta.requestId),
|
|
256
|
+
);
|
|
257
|
+
if (!isConnected) {
|
|
258
|
+
this.log?.warn({
|
|
259
|
+
msg: "removing stale persisted websocket",
|
|
260
|
+
requestId: requestIdStr,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const request = buildRequestForWebSocket(
|
|
264
|
+
meta.path,
|
|
265
|
+
meta.headers,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Create adapter to register user's event listeners.
|
|
269
|
+
// Pass engineAlreadyClosed=true so close callback won't send tunnel message.
|
|
270
|
+
// Track this operation to ensure it completes
|
|
271
|
+
const cleanupOperation = this.#createWebSocket(
|
|
272
|
+
actorId,
|
|
273
|
+
meta.gatewayId,
|
|
274
|
+
meta.requestId,
|
|
275
|
+
requestIdStr,
|
|
276
|
+
meta.serverMessageIndex,
|
|
277
|
+
true,
|
|
278
|
+
true,
|
|
279
|
+
request,
|
|
280
|
+
meta.path,
|
|
281
|
+
meta.headers,
|
|
282
|
+
true,
|
|
283
|
+
)
|
|
284
|
+
.then((adapter) => {
|
|
285
|
+
// Close the adapter normally - this will fire user's close event handler
|
|
286
|
+
// (which should clean up persistence) and trigger the close callback
|
|
287
|
+
// (which will clean up maps but skip sending tunnel message)
|
|
288
|
+
adapter.close(1000, "ws.stale_metadata");
|
|
289
|
+
})
|
|
290
|
+
.catch((err) => {
|
|
291
|
+
this.log?.error({
|
|
292
|
+
msg: "error creating stale websocket during restore",
|
|
293
|
+
requestId: requestIdStr,
|
|
294
|
+
err,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
backgroundOperations.push(cleanupOperation);
|
|
299
|
+
loadedButNotConnectedCount++;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Wait for all background operations to complete before finishing
|
|
304
|
+
await Promise.allSettled(backgroundOperations);
|
|
305
|
+
|
|
306
|
+
// Mark restoration as complete
|
|
307
|
+
actor.hibernationRestored = true;
|
|
308
|
+
|
|
309
|
+
this.log?.info({
|
|
310
|
+
msg: "restored hibernatable websockets",
|
|
311
|
+
actorId,
|
|
312
|
+
restoredCount,
|
|
313
|
+
connectedButNotLoadedCount,
|
|
314
|
+
loadedButNotConnectedCount,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Called from WebSocketOpen message and when restoring hibernatable WebSockets.
|
|
320
|
+
*
|
|
321
|
+
* engineAlreadyClosed will be true if this is only being called to trigger
|
|
322
|
+
* the close callback and not to send a close message to the server. This
|
|
323
|
+
* is used specifically to clean up zombie WebSocket connections.
|
|
324
|
+
*/
|
|
325
|
+
async #createWebSocket(
|
|
326
|
+
actorId: string,
|
|
327
|
+
gatewayId: GatewayId,
|
|
328
|
+
requestId: RequestId,
|
|
329
|
+
requestIdStr: string,
|
|
330
|
+
serverMessageIndex: number,
|
|
331
|
+
isHibernatable: boolean,
|
|
332
|
+
isRestoringHibernatable: boolean,
|
|
333
|
+
request: Request,
|
|
334
|
+
path: string,
|
|
335
|
+
headers: Record<string, string>,
|
|
336
|
+
engineAlreadyClosed: boolean,
|
|
337
|
+
): Promise<WebSocketTunnelAdapter> {
|
|
338
|
+
this.log?.debug({
|
|
339
|
+
msg: "createWebSocket creating adapter",
|
|
340
|
+
actorId,
|
|
341
|
+
requestIdStr,
|
|
342
|
+
isHibernatable,
|
|
343
|
+
path,
|
|
344
|
+
});
|
|
345
|
+
// Create WebSocket adapter
|
|
346
|
+
const adapter = new WebSocketTunnelAdapter(
|
|
347
|
+
this,
|
|
348
|
+
actorId,
|
|
349
|
+
requestIdStr,
|
|
350
|
+
serverMessageIndex,
|
|
351
|
+
isHibernatable,
|
|
352
|
+
isRestoringHibernatable,
|
|
353
|
+
request,
|
|
354
|
+
(data: ArrayBuffer | string, isBinary: boolean) => {
|
|
355
|
+
// Send message through tunnel
|
|
356
|
+
const dataBuffer =
|
|
357
|
+
typeof data === "string"
|
|
358
|
+
? (new TextEncoder().encode(data).buffer as ArrayBuffer)
|
|
359
|
+
: data;
|
|
360
|
+
|
|
361
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
362
|
+
tag: "ToServerWebSocketMessage",
|
|
363
|
+
val: {
|
|
364
|
+
data: dataBuffer,
|
|
365
|
+
binary: isBinary,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
},
|
|
369
|
+
(code?: number, reason?: string) => {
|
|
370
|
+
// Send close through tunnel if engine doesn't already know it's closed
|
|
371
|
+
if (!engineAlreadyClosed) {
|
|
372
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
373
|
+
tag: "ToServerWebSocketClose",
|
|
374
|
+
val: {
|
|
375
|
+
code: code || null,
|
|
376
|
+
reason: reason || null,
|
|
377
|
+
hibernate: false,
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Clean up actor tracking
|
|
383
|
+
const actor = this.#runner.getActor(actorId);
|
|
384
|
+
if (actor) {
|
|
385
|
+
actor.deleteWebSocket(gatewayId, requestId);
|
|
386
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Clean up request-to-actor mapping
|
|
390
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// Get actor and add websocket to it
|
|
395
|
+
const actor = this.#runner.getActor(actorId);
|
|
396
|
+
if (!actor) {
|
|
397
|
+
throw new Error(`Actor ${actorId} not found`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
actor.setWebSocket(gatewayId, requestId, adapter);
|
|
401
|
+
this.addRequestToActor(gatewayId, requestId, actorId);
|
|
402
|
+
|
|
403
|
+
// Call WebSocket handler. This handler will add event listeners
|
|
404
|
+
// for `open`, etc.
|
|
405
|
+
await this.#runner.config.websocket(
|
|
406
|
+
this.#runner,
|
|
407
|
+
actorId,
|
|
408
|
+
adapter,
|
|
409
|
+
gatewayId,
|
|
410
|
+
requestId,
|
|
411
|
+
request,
|
|
412
|
+
path,
|
|
413
|
+
headers,
|
|
414
|
+
isHibernatable,
|
|
415
|
+
isRestoringHibernatable,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
return adapter;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
addRequestToActor(
|
|
422
|
+
gatewayId: GatewayId,
|
|
423
|
+
requestId: RequestId,
|
|
424
|
+
actorId: string,
|
|
425
|
+
) {
|
|
426
|
+
this.#requestToActor.push({ gatewayId, requestId, actorId });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#removeRequestToActor(gatewayId: GatewayId, requestId: RequestId) {
|
|
430
|
+
const index = this.#requestToActor.findIndex(
|
|
431
|
+
(entry) =>
|
|
432
|
+
arraysEqual(entry.gatewayId, gatewayId) &&
|
|
433
|
+
arraysEqual(entry.requestId, requestId),
|
|
434
|
+
);
|
|
435
|
+
if (index !== -1) {
|
|
436
|
+
this.#requestToActor.splice(index, 1);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
getRequestActor(
|
|
441
|
+
gatewayId: GatewayId,
|
|
442
|
+
requestId: RequestId,
|
|
443
|
+
): RunnerActor | undefined {
|
|
444
|
+
const entry = this.#requestToActor.find(
|
|
445
|
+
(entry) =>
|
|
446
|
+
arraysEqual(entry.gatewayId, gatewayId) &&
|
|
447
|
+
arraysEqual(entry.requestId, requestId),
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
if (!entry) {
|
|
451
|
+
this.log?.warn({
|
|
452
|
+
msg: "missing requestToActor entry",
|
|
453
|
+
requestId: idToStr(requestId),
|
|
454
|
+
});
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const actor = this.#runner.getActor(entry.actorId);
|
|
459
|
+
if (!actor) {
|
|
460
|
+
this.log?.warn({
|
|
461
|
+
msg: "missing actor for requestToActor lookup",
|
|
462
|
+
requestId: idToStr(requestId),
|
|
463
|
+
actorId: entry.actorId,
|
|
464
|
+
});
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return actor;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async getAndWaitForRequestActor(
|
|
472
|
+
gatewayId: GatewayId,
|
|
473
|
+
requestId: RequestId,
|
|
474
|
+
): Promise<RunnerActor | undefined> {
|
|
475
|
+
const actor = this.getRequestActor(gatewayId, requestId);
|
|
476
|
+
if (!actor) return;
|
|
477
|
+
await actor.actorStartPromise.promise;
|
|
478
|
+
return actor;
|
|
92
479
|
}
|
|
93
480
|
|
|
94
481
|
#sendMessage(
|
|
482
|
+
gatewayId: GatewayId,
|
|
95
483
|
requestId: RequestId,
|
|
96
484
|
messageKind: protocol.ToServerTunnelMessageKind,
|
|
97
485
|
) {
|
|
98
|
-
//
|
|
486
|
+
// Buffer message if not connected
|
|
99
487
|
if (!this.#runner.__webSocketReady()) {
|
|
100
|
-
this.log?.
|
|
101
|
-
msg: "
|
|
488
|
+
this.log?.debug({
|
|
489
|
+
msg: "buffering tunnel message, socket not connected to engine",
|
|
102
490
|
requestId: idToStr(requestId),
|
|
103
491
|
message: stringifyToServerTunnelMessageKind(messageKind),
|
|
104
492
|
});
|
|
493
|
+
this.#bufferedMessages.push({ gatewayId, requestId, messageKind });
|
|
105
494
|
return;
|
|
106
495
|
}
|
|
107
496
|
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
497
|
+
// Get or initialize message index for this request
|
|
498
|
+
//
|
|
499
|
+
// We don't have to wait for the actor to start since we're not calling
|
|
500
|
+
// any callbacks on the actor
|
|
501
|
+
const gatewayIdStr = idToStr(gatewayId);
|
|
111
502
|
const requestIdStr = idToStr(requestId);
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
503
|
+
const actor = this.getRequestActor(gatewayId, requestId);
|
|
504
|
+
if (!actor) {
|
|
505
|
+
this.log?.warn({
|
|
506
|
+
msg: "cannot send tunnel message, actor not found",
|
|
507
|
+
gatewayId: gatewayIdStr,
|
|
508
|
+
requestId: requestIdStr,
|
|
509
|
+
});
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Get message index from pending request
|
|
514
|
+
let clientMessageIndex: number;
|
|
515
|
+
const pending = actor.getPendingRequest(gatewayId, requestId);
|
|
516
|
+
if (pending) {
|
|
517
|
+
clientMessageIndex = pending.clientMessageIndex;
|
|
518
|
+
pending.clientMessageIndex++;
|
|
519
|
+
} else {
|
|
520
|
+
// No pending request
|
|
521
|
+
this.log?.warn({
|
|
522
|
+
msg: "missing pending request for send message, defaulting to message index 0",
|
|
523
|
+
gatewayId: gatewayIdStr,
|
|
524
|
+
requestId: requestIdStr,
|
|
525
|
+
});
|
|
526
|
+
clientMessageIndex = 0;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Build message ID from gatewayId + requestId + messageIndex
|
|
530
|
+
const messageId: protocol.MessageId = {
|
|
531
|
+
gatewayId,
|
|
532
|
+
requestId,
|
|
533
|
+
messageIndex: clientMessageIndex,
|
|
534
|
+
};
|
|
535
|
+
const messageIdStr = `${idToStr(messageId.gatewayId)}-${idToStr(messageId.requestId)}-${messageId.messageIndex}`;
|
|
117
536
|
|
|
118
537
|
this.log?.debug({
|
|
119
|
-
msg: "
|
|
120
|
-
requestId: requestIdStr,
|
|
538
|
+
msg: "sending tunnel msg",
|
|
121
539
|
messageId: messageIdStr,
|
|
540
|
+
gatewayId: gatewayIdStr,
|
|
541
|
+
requestId: requestIdStr,
|
|
542
|
+
messageIndex: clientMessageIndex,
|
|
122
543
|
message: stringifyToServerTunnelMessageKind(messageKind),
|
|
123
544
|
});
|
|
124
545
|
|
|
@@ -126,7 +547,6 @@ export class Tunnel {
|
|
|
126
547
|
const message: protocol.ToServer = {
|
|
127
548
|
tag: "ToServerTunnelMessage",
|
|
128
549
|
val: {
|
|
129
|
-
requestId,
|
|
130
550
|
messageId,
|
|
131
551
|
messageKind,
|
|
132
552
|
},
|
|
@@ -134,123 +554,33 @@ export class Tunnel {
|
|
|
134
554
|
this.#runner.__sendToServer(message);
|
|
135
555
|
}
|
|
136
556
|
|
|
137
|
-
|
|
138
|
-
if (!this.#runner.__webSocketReady()) {
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const message: protocol.ToServer = {
|
|
143
|
-
tag: "ToServerTunnelMessage",
|
|
144
|
-
val: {
|
|
145
|
-
requestId,
|
|
146
|
-
messageId,
|
|
147
|
-
messageKind: { tag: "TunnelAck", val: null },
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
this.log?.debug({
|
|
152
|
-
msg: "ack tunnel msg",
|
|
153
|
-
requestId: idToStr(requestId),
|
|
154
|
-
messageId: idToStr(messageId),
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
this.#runner.__sendToServer(message);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
#startGarbageCollector() {
|
|
161
|
-
if (this.#gcInterval) {
|
|
162
|
-
clearInterval(this.#gcInterval);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
this.#gcInterval = setInterval(() => {
|
|
166
|
-
this.#gc();
|
|
167
|
-
}, GC_INTERVAL);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
#gc() {
|
|
171
|
-
const now = Date.now();
|
|
172
|
-
const messagesToDelete: string[] = [];
|
|
173
|
-
|
|
174
|
-
for (const [messageId, pendingMessage] of this.#pendingTunnelMessages) {
|
|
175
|
-
// Check if message is older than timeout
|
|
176
|
-
if (now - pendingMessage.sentAt > MESSAGE_ACK_TIMEOUT) {
|
|
177
|
-
messagesToDelete.push(messageId);
|
|
178
|
-
|
|
179
|
-
const requestIdStr = pendingMessage.requestIdStr;
|
|
180
|
-
|
|
181
|
-
// Check if this is an HTTP request
|
|
182
|
-
const pendingRequest =
|
|
183
|
-
this.#actorPendingRequests.get(requestIdStr);
|
|
184
|
-
if (pendingRequest) {
|
|
185
|
-
// Reject the pending HTTP request
|
|
186
|
-
pendingRequest.reject(
|
|
187
|
-
new Error("Message acknowledgment timeout"),
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
// Close stream controller if it exists
|
|
191
|
-
if (pendingRequest.streamController) {
|
|
192
|
-
pendingRequest.streamController.error(
|
|
193
|
-
new Error("Message acknowledgment timeout"),
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Clean up from actorPendingRequests map
|
|
198
|
-
this.#actorPendingRequests.delete(requestIdStr);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Check if this is a WebSocket
|
|
202
|
-
const webSocket = this.#actorWebSockets.get(requestIdStr);
|
|
203
|
-
if (webSocket) {
|
|
204
|
-
// Close the WebSocket connection
|
|
205
|
-
webSocket.__closeWithRetry(
|
|
206
|
-
1000,
|
|
207
|
-
"Message acknowledgment timeout",
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
// Clean up from actorWebSockets map
|
|
211
|
-
this.#actorWebSockets.delete(requestIdStr);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Remove timed out messages
|
|
217
|
-
if (messagesToDelete.length > 0) {
|
|
218
|
-
this.log?.warn({
|
|
219
|
-
msg: "purging unacked tunnel messages, this indicates that the Rivet Engine is disconnected or not responding",
|
|
220
|
-
count: messagesToDelete.length,
|
|
221
|
-
});
|
|
222
|
-
for (const messageId of messagesToDelete) {
|
|
223
|
-
this.#pendingTunnelMessages.delete(messageId);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
closeActiveRequests(actor: ActorInstance) {
|
|
557
|
+
closeActiveRequests(actor: RunnerActor) {
|
|
229
558
|
const actorId = actor.actorId;
|
|
230
559
|
|
|
231
|
-
// Terminate all requests for this actor
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
560
|
+
// Terminate all requests for this actor. This will no send a
|
|
561
|
+
// ToServerResponse* message since the actor will no longer be loaded.
|
|
562
|
+
// The gateway is responsible for closing the request.
|
|
563
|
+
for (const entry of actor.pendingRequests) {
|
|
564
|
+
entry.request.reject(new Error(`Actor ${actorId} stopped`));
|
|
565
|
+
if (entry.gatewayId && entry.requestId) {
|
|
566
|
+
this.#removeRequestToActor(entry.gatewayId, entry.requestId);
|
|
237
567
|
}
|
|
238
568
|
}
|
|
239
|
-
actor.requests.clear();
|
|
240
569
|
|
|
241
|
-
//
|
|
242
|
-
for
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
570
|
+
// Close all WebSockets. Only send close event to non-HWS. The gateway is
|
|
571
|
+
// responsible for hibernating HWS and closing regular WS.
|
|
572
|
+
for (const entry of actor.webSockets) {
|
|
573
|
+
const isHibernatable = entry.ws[HIBERNATABLE_SYMBOL];
|
|
574
|
+
if (!isHibernatable) {
|
|
575
|
+
entry.ws._closeWithoutCallback(1000, "actor.stopped");
|
|
247
576
|
}
|
|
577
|
+
// Note: request-to-actor mapping is cleaned up in the close callback
|
|
248
578
|
}
|
|
249
|
-
actor.webSockets.clear();
|
|
250
579
|
}
|
|
251
580
|
|
|
252
581
|
async #fetch(
|
|
253
582
|
actorId: string,
|
|
583
|
+
gatewayId: protocol.GatewayId,
|
|
254
584
|
requestId: protocol.RequestId,
|
|
255
585
|
request: Request,
|
|
256
586
|
): Promise<Response> {
|
|
@@ -274,6 +604,7 @@ export class Tunnel {
|
|
|
274
604
|
const fetchHandler = this.#runner.config.fetch(
|
|
275
605
|
this.#runner,
|
|
276
606
|
actorId,
|
|
607
|
+
gatewayId,
|
|
277
608
|
requestId,
|
|
278
609
|
request,
|
|
279
610
|
);
|
|
@@ -286,94 +617,88 @@ export class Tunnel {
|
|
|
286
617
|
}
|
|
287
618
|
|
|
288
619
|
async handleTunnelMessage(message: protocol.ToClientTunnelMessage) {
|
|
289
|
-
|
|
290
|
-
const
|
|
620
|
+
// Parse the gateway ID, request ID, and message index from the messageId
|
|
621
|
+
const { gatewayId, requestId, messageIndex } = message.messageId;
|
|
622
|
+
|
|
623
|
+
const gatewayIdStr = idToStr(gatewayId);
|
|
624
|
+
const requestIdStr = idToStr(requestId);
|
|
291
625
|
this.log?.debug({
|
|
292
626
|
msg: "receive tunnel msg",
|
|
627
|
+
gatewayId: gatewayIdStr,
|
|
293
628
|
requestId: requestIdStr,
|
|
294
|
-
|
|
629
|
+
messageIndex: message.messageId.messageIndex,
|
|
295
630
|
message: stringifyToClientTunnelMessageKind(message.messageKind),
|
|
296
631
|
});
|
|
297
632
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.#sendAck(message.requestId, message.messageId);
|
|
332
|
-
|
|
333
|
-
await this.#handleRequestAbort(message.requestId);
|
|
334
|
-
break;
|
|
335
|
-
case "ToClientWebSocketOpen":
|
|
336
|
-
this.#sendAck(message.requestId, message.messageId);
|
|
337
|
-
|
|
338
|
-
await this.#handleWebSocketOpen(
|
|
339
|
-
message.requestId,
|
|
340
|
-
message.messageKind.val,
|
|
341
|
-
);
|
|
342
|
-
break;
|
|
343
|
-
case "ToClientWebSocketMessage": {
|
|
344
|
-
this.#sendAck(message.requestId, message.messageId);
|
|
345
|
-
|
|
346
|
-
const _unhandled = await this.#handleWebSocketMessage(
|
|
347
|
-
message.requestId,
|
|
348
|
-
message.messageKind.val,
|
|
349
|
-
);
|
|
350
|
-
break;
|
|
351
|
-
}
|
|
352
|
-
case "ToClientWebSocketClose":
|
|
353
|
-
this.#sendAck(message.requestId, message.messageId);
|
|
354
|
-
|
|
355
|
-
await this.#handleWebSocketClose(
|
|
356
|
-
message.requestId,
|
|
357
|
-
message.messageKind.val,
|
|
358
|
-
);
|
|
359
|
-
break;
|
|
360
|
-
default:
|
|
361
|
-
unreachable(message.messageKind);
|
|
633
|
+
switch (message.messageKind.tag) {
|
|
634
|
+
case "ToClientRequestStart":
|
|
635
|
+
await this.#handleRequestStart(
|
|
636
|
+
gatewayId,
|
|
637
|
+
requestId,
|
|
638
|
+
message.messageKind.val,
|
|
639
|
+
);
|
|
640
|
+
break;
|
|
641
|
+
case "ToClientRequestChunk":
|
|
642
|
+
await this.#handleRequestChunk(
|
|
643
|
+
gatewayId,
|
|
644
|
+
requestId,
|
|
645
|
+
message.messageKind.val,
|
|
646
|
+
);
|
|
647
|
+
break;
|
|
648
|
+
case "ToClientRequestAbort":
|
|
649
|
+
await this.#handleRequestAbort(gatewayId, requestId);
|
|
650
|
+
break;
|
|
651
|
+
case "ToClientWebSocketOpen":
|
|
652
|
+
await this.#handleWebSocketOpen(
|
|
653
|
+
gatewayId,
|
|
654
|
+
requestId,
|
|
655
|
+
message.messageKind.val,
|
|
656
|
+
);
|
|
657
|
+
break;
|
|
658
|
+
case "ToClientWebSocketMessage": {
|
|
659
|
+
await this.#handleWebSocketMessage(
|
|
660
|
+
gatewayId,
|
|
661
|
+
requestId,
|
|
662
|
+
messageIndex,
|
|
663
|
+
message.messageKind.val,
|
|
664
|
+
);
|
|
665
|
+
break;
|
|
362
666
|
}
|
|
667
|
+
case "ToClientWebSocketClose":
|
|
668
|
+
await this.#handleWebSocketClose(
|
|
669
|
+
gatewayId,
|
|
670
|
+
requestId,
|
|
671
|
+
message.messageKind.val,
|
|
672
|
+
);
|
|
673
|
+
break;
|
|
674
|
+
case "DeprecatedTunnelAck":
|
|
675
|
+
// Ignore deprecated tunnel ack messages
|
|
676
|
+
break;
|
|
677
|
+
default:
|
|
678
|
+
unreachable(message.messageKind);
|
|
363
679
|
}
|
|
364
680
|
}
|
|
365
681
|
|
|
366
682
|
async #handleRequestStart(
|
|
367
|
-
|
|
683
|
+
gatewayId: GatewayId,
|
|
684
|
+
requestId: RequestId,
|
|
368
685
|
req: protocol.ToClientRequestStart,
|
|
369
686
|
) {
|
|
370
687
|
// Track this request for the actor
|
|
371
688
|
const requestIdStr = idToStr(requestId);
|
|
372
|
-
const actor = this.#runner.
|
|
373
|
-
if (actor) {
|
|
374
|
-
|
|
689
|
+
const actor = await this.#runner.getAndWaitForActor(req.actorId);
|
|
690
|
+
if (!actor) {
|
|
691
|
+
this.log?.warn({
|
|
692
|
+
msg: "actor does not exist in handleRequestStart, request will leak",
|
|
693
|
+
actorId: req.actorId,
|
|
694
|
+
requestId: requestIdStr,
|
|
695
|
+
});
|
|
696
|
+
return;
|
|
375
697
|
}
|
|
376
698
|
|
|
699
|
+
// Add to request-to-actor mapping
|
|
700
|
+
this.addRequestToActor(gatewayId, requestId, req.actorId);
|
|
701
|
+
|
|
377
702
|
try {
|
|
378
703
|
// Convert headers map to Headers object
|
|
379
704
|
const headers = new Headers();
|
|
@@ -394,18 +719,22 @@ export class Tunnel {
|
|
|
394
719
|
const stream = new ReadableStream<Uint8Array>({
|
|
395
720
|
start: (controller) => {
|
|
396
721
|
// Store controller for chunks
|
|
397
|
-
const existing =
|
|
398
|
-
|
|
722
|
+
const existing = actor.getPendingRequest(
|
|
723
|
+
gatewayId,
|
|
724
|
+
requestId,
|
|
725
|
+
);
|
|
399
726
|
if (existing) {
|
|
400
727
|
existing.streamController = controller;
|
|
401
728
|
existing.actorId = req.actorId;
|
|
729
|
+
existing.gatewayId = gatewayId;
|
|
730
|
+
existing.requestId = requestId;
|
|
402
731
|
} else {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
732
|
+
actor.createPendingRequestWithStreamController(
|
|
733
|
+
gatewayId,
|
|
734
|
+
requestId,
|
|
735
|
+
0,
|
|
736
|
+
controller,
|
|
737
|
+
);
|
|
409
738
|
}
|
|
410
739
|
},
|
|
411
740
|
});
|
|
@@ -419,18 +748,35 @@ export class Tunnel {
|
|
|
419
748
|
// Call fetch handler with validation
|
|
420
749
|
const response = await this.#fetch(
|
|
421
750
|
req.actorId,
|
|
751
|
+
gatewayId,
|
|
422
752
|
requestId,
|
|
423
753
|
streamingRequest,
|
|
424
754
|
);
|
|
425
|
-
await this.#sendResponse(
|
|
755
|
+
await this.#sendResponse(
|
|
756
|
+
actor.actorId,
|
|
757
|
+
actor.generation,
|
|
758
|
+
gatewayId,
|
|
759
|
+
requestId,
|
|
760
|
+
response,
|
|
761
|
+
);
|
|
426
762
|
} else {
|
|
427
763
|
// Non-streaming request
|
|
764
|
+
// Create a pending request entry to track messageIndex for the response
|
|
765
|
+
actor.createPendingRequest(gatewayId, requestId, 0);
|
|
766
|
+
|
|
428
767
|
const response = await this.#fetch(
|
|
429
768
|
req.actorId,
|
|
769
|
+
gatewayId,
|
|
430
770
|
requestId,
|
|
431
771
|
request,
|
|
432
772
|
);
|
|
433
|
-
await this.#sendResponse(
|
|
773
|
+
await this.#sendResponse(
|
|
774
|
+
actor.actorId,
|
|
775
|
+
actor.generation,
|
|
776
|
+
gatewayId,
|
|
777
|
+
requestId,
|
|
778
|
+
response,
|
|
779
|
+
);
|
|
434
780
|
}
|
|
435
781
|
} catch (error) {
|
|
436
782
|
if (error instanceof RunnerShutdownError) {
|
|
@@ -438,6 +784,9 @@ export class Tunnel {
|
|
|
438
784
|
} else {
|
|
439
785
|
this.log?.error({ msg: "error handling request", error });
|
|
440
786
|
this.#sendResponseError(
|
|
787
|
+
actor.actorId,
|
|
788
|
+
actor.generation,
|
|
789
|
+
gatewayId,
|
|
441
790
|
requestId,
|
|
442
791
|
500,
|
|
443
792
|
"Internal Server Error",
|
|
@@ -445,38 +794,67 @@ export class Tunnel {
|
|
|
445
794
|
}
|
|
446
795
|
} finally {
|
|
447
796
|
// Clean up request tracking
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
797
|
+
if (this.#runner.hasActor(req.actorId, actor.generation)) {
|
|
798
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
799
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
451
800
|
}
|
|
452
801
|
}
|
|
453
802
|
}
|
|
454
803
|
|
|
455
804
|
async #handleRequestChunk(
|
|
456
|
-
|
|
805
|
+
gatewayId: GatewayId,
|
|
806
|
+
requestId: RequestId,
|
|
457
807
|
chunk: protocol.ToClientRequestChunk,
|
|
458
808
|
) {
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
809
|
+
const actor = await this.getAndWaitForRequestActor(
|
|
810
|
+
gatewayId,
|
|
811
|
+
requestId,
|
|
812
|
+
);
|
|
813
|
+
if (actor) {
|
|
814
|
+
const pending = actor.getPendingRequest(gatewayId, requestId);
|
|
815
|
+
if (pending?.streamController) {
|
|
816
|
+
pending.streamController.enqueue(new Uint8Array(chunk.body));
|
|
817
|
+
if (chunk.finish) {
|
|
818
|
+
pending.streamController.close();
|
|
819
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
820
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
821
|
+
}
|
|
466
822
|
}
|
|
467
823
|
}
|
|
468
824
|
}
|
|
469
825
|
|
|
470
|
-
async #handleRequestAbort(requestId:
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
826
|
+
async #handleRequestAbort(gatewayId: GatewayId, requestId: RequestId) {
|
|
827
|
+
const actor = await this.getAndWaitForRequestActor(
|
|
828
|
+
gatewayId,
|
|
829
|
+
requestId,
|
|
830
|
+
);
|
|
831
|
+
if (actor) {
|
|
832
|
+
const pending = actor.getPendingRequest(gatewayId, requestId);
|
|
833
|
+
if (pending?.streamController) {
|
|
834
|
+
pending.streamController.error(new Error("Request aborted"));
|
|
835
|
+
}
|
|
836
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
837
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
475
838
|
}
|
|
476
|
-
this.#actorPendingRequests.delete(requestIdStr);
|
|
477
839
|
}
|
|
478
840
|
|
|
479
|
-
async #sendResponse(
|
|
841
|
+
async #sendResponse(
|
|
842
|
+
actorId: string,
|
|
843
|
+
generation: number,
|
|
844
|
+
gatewayId: GatewayId,
|
|
845
|
+
requestId: ArrayBuffer,
|
|
846
|
+
response: Response,
|
|
847
|
+
) {
|
|
848
|
+
if (!this.#runner.hasActor(actorId, generation)) {
|
|
849
|
+
this.log?.warn({
|
|
850
|
+
msg: "actor not loaded to send response, assuming gateway has closed request",
|
|
851
|
+
actorId,
|
|
852
|
+
generation,
|
|
853
|
+
requestId,
|
|
854
|
+
});
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
|
|
480
858
|
// Always treat responses as non-streaming for now
|
|
481
859
|
// In the future, we could detect streaming responses based on:
|
|
482
860
|
// - Transfer-Encoding: chunked
|
|
@@ -497,8 +875,8 @@ export class Tunnel {
|
|
|
497
875
|
headers.set("content-length", String(body.byteLength));
|
|
498
876
|
}
|
|
499
877
|
|
|
500
|
-
// Send as non-streaming response
|
|
501
|
-
this.#sendMessage(requestId, {
|
|
878
|
+
// Send as non-streaming response if actor has not stopped
|
|
879
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
502
880
|
tag: "ToServerResponseStart",
|
|
503
881
|
val: {
|
|
504
882
|
status: response.status as protocol.u16,
|
|
@@ -510,14 +888,27 @@ export class Tunnel {
|
|
|
510
888
|
}
|
|
511
889
|
|
|
512
890
|
#sendResponseError(
|
|
891
|
+
actorId: string,
|
|
892
|
+
generation: number,
|
|
893
|
+
gatewayId: GatewayId,
|
|
513
894
|
requestId: ArrayBuffer,
|
|
514
895
|
status: number,
|
|
515
896
|
message: string,
|
|
516
897
|
) {
|
|
898
|
+
if (!this.#runner.hasActor(actorId, generation)) {
|
|
899
|
+
this.log?.warn({
|
|
900
|
+
msg: "actor not loaded to send response, assuming gateway has closed request",
|
|
901
|
+
actorId,
|
|
902
|
+
generation,
|
|
903
|
+
requestId,
|
|
904
|
+
});
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
517
908
|
const headers = new Map<string, string>();
|
|
518
909
|
headers.set("content-type", "text/plain");
|
|
519
910
|
|
|
520
|
-
this.#sendMessage(requestId, {
|
|
911
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
521
912
|
tag: "ToServerResponseStart",
|
|
522
913
|
val: {
|
|
523
914
|
status: status as protocol.u16,
|
|
@@ -529,13 +920,20 @@ export class Tunnel {
|
|
|
529
920
|
}
|
|
530
921
|
|
|
531
922
|
async #handleWebSocketOpen(
|
|
532
|
-
|
|
923
|
+
gatewayId: GatewayId,
|
|
924
|
+
requestId: RequestId,
|
|
533
925
|
open: protocol.ToClientWebSocketOpen,
|
|
534
926
|
) {
|
|
927
|
+
// NOTE: This method is safe to be async since we will not receive any
|
|
928
|
+
// further WebSocket events until we send a ToServerWebSocketOpen
|
|
929
|
+
// tunnel message. We can do any async logic we need to between thoes two events.
|
|
930
|
+
//
|
|
931
|
+
// Sending a ToServerWebSocketClose will terminate the WebSocket early.
|
|
932
|
+
|
|
535
933
|
const requestIdStr = idToStr(requestId);
|
|
536
934
|
|
|
537
935
|
// Validate actor exists
|
|
538
|
-
const actor = this.#runner.
|
|
936
|
+
const actor = await this.#runner.getAndWaitForActor(open.actorId);
|
|
539
937
|
if (!actor) {
|
|
540
938
|
this.log?.warn({
|
|
541
939
|
msg: "ignoring websocket for unknown actor",
|
|
@@ -547,43 +945,21 @@ export class Tunnel {
|
|
|
547
945
|
//
|
|
548
946
|
// See
|
|
549
947
|
// https://github.com/rivet-dev/rivet/blob/222dae87e3efccaffa2b503de40ecf8afd4e31eb/engine/packages/pegboard-gateway/src/lib.rs#L238
|
|
550
|
-
this.#sendMessage(requestId, {
|
|
948
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
551
949
|
tag: "ToServerWebSocketClose",
|
|
552
950
|
val: {
|
|
553
951
|
code: 1011,
|
|
554
952
|
reason: "Actor not found",
|
|
555
|
-
|
|
556
|
-
},
|
|
557
|
-
});
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const websocketHandler = this.#runner.config.websocket;
|
|
562
|
-
|
|
563
|
-
if (!websocketHandler) {
|
|
564
|
-
this.log?.error({
|
|
565
|
-
msg: "no websocket handler configured for tunnel",
|
|
566
|
-
});
|
|
567
|
-
// Send close immediately
|
|
568
|
-
this.#sendMessage(requestId, {
|
|
569
|
-
tag: "ToServerWebSocketClose",
|
|
570
|
-
val: {
|
|
571
|
-
code: 1011,
|
|
572
|
-
reason: "Not Implemented",
|
|
573
|
-
retry: false,
|
|
953
|
+
hibernate: false,
|
|
574
954
|
},
|
|
575
955
|
});
|
|
576
956
|
return;
|
|
577
957
|
}
|
|
578
958
|
|
|
579
959
|
// Close existing WebSocket if one already exists for this request ID.
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
// This should never occur if all is functioning correctly, but this
|
|
584
|
-
// prevents any edge case that would result in duplicate WebSockets for
|
|
585
|
-
// the same request.
|
|
586
|
-
const existingAdapter = this.#actorWebSockets.get(requestIdStr);
|
|
960
|
+
// This should never happen, but prevents any potential duplicate
|
|
961
|
+
// WebSockets from retransmits.
|
|
962
|
+
const existingAdapter = actor.getWebSocket(gatewayId, requestId);
|
|
587
963
|
if (existingAdapter) {
|
|
588
964
|
this.log?.warn({
|
|
589
965
|
msg: "closing existing websocket for duplicate open event for the same request id",
|
|
@@ -591,194 +967,212 @@ export class Tunnel {
|
|
|
591
967
|
});
|
|
592
968
|
// Close without sending a message through the tunnel since the server
|
|
593
969
|
// already knows about the new connection
|
|
594
|
-
existingAdapter.
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Track this WebSocket for the actor
|
|
598
|
-
if (actor) {
|
|
599
|
-
actor.webSockets.add(requestIdStr);
|
|
970
|
+
existingAdapter._closeWithoutCallback(1000, "ws.duplicate_open");
|
|
600
971
|
}
|
|
601
972
|
|
|
973
|
+
// Create WebSocket
|
|
602
974
|
try {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
(data: ArrayBuffer | string, isBinary: boolean) => {
|
|
607
|
-
// Send message through tunnel
|
|
608
|
-
const dataBuffer =
|
|
609
|
-
typeof data === "string"
|
|
610
|
-
? (new TextEncoder().encode(data)
|
|
611
|
-
.buffer as ArrayBuffer)
|
|
612
|
-
: data;
|
|
613
|
-
|
|
614
|
-
this.#sendMessage(requestId, {
|
|
615
|
-
tag: "ToServerWebSocketMessage",
|
|
616
|
-
val: {
|
|
617
|
-
data: dataBuffer,
|
|
618
|
-
binary: isBinary,
|
|
619
|
-
},
|
|
620
|
-
});
|
|
621
|
-
},
|
|
622
|
-
(code?: number, reason?: string, retry: boolean = false) => {
|
|
623
|
-
// Send close through tunnel
|
|
624
|
-
this.#sendMessage(requestId, {
|
|
625
|
-
tag: "ToServerWebSocketClose",
|
|
626
|
-
val: {
|
|
627
|
-
code: code || null,
|
|
628
|
-
reason: reason || null,
|
|
629
|
-
retry,
|
|
630
|
-
},
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
// Remove from map
|
|
634
|
-
this.#actorWebSockets.delete(requestIdStr);
|
|
635
|
-
|
|
636
|
-
// Clean up actor tracking
|
|
637
|
-
if (actor) {
|
|
638
|
-
actor.webSockets.delete(requestIdStr);
|
|
639
|
-
}
|
|
640
|
-
},
|
|
975
|
+
const request = buildRequestForWebSocket(
|
|
976
|
+
open.path,
|
|
977
|
+
Object.fromEntries(open.headers),
|
|
641
978
|
);
|
|
642
979
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
// Convert headers to map
|
|
647
|
-
//
|
|
648
|
-
// We need to manually ensure the original Upgrade/Connection WS
|
|
649
|
-
// headers are present
|
|
650
|
-
const headerInit: Record<string, string> = {};
|
|
651
|
-
if (open.headers) {
|
|
652
|
-
for (const [k, v] of open.headers as ReadonlyMap<
|
|
653
|
-
string,
|
|
654
|
-
string
|
|
655
|
-
>) {
|
|
656
|
-
headerInit[k] = v;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
headerInit["Upgrade"] = "websocket";
|
|
660
|
-
headerInit["Connection"] = "Upgrade";
|
|
661
|
-
|
|
662
|
-
const request = new Request(`http://localhost${open.path}`, {
|
|
663
|
-
method: "GET",
|
|
664
|
-
headers: headerInit,
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
// Send open confirmation
|
|
668
|
-
const hibernationConfig =
|
|
669
|
-
this.#runner.config.getActorHibernationConfig(
|
|
980
|
+
const canHibernate =
|
|
981
|
+
this.#runner.config.hibernatableWebSocket.canHibernate(
|
|
670
982
|
actor.actorId,
|
|
983
|
+
gatewayId,
|
|
671
984
|
requestId,
|
|
672
985
|
request,
|
|
673
986
|
);
|
|
674
|
-
adapter.canHibernate = hibernationConfig.enabled;
|
|
675
987
|
|
|
676
|
-
|
|
988
|
+
// #createWebSocket will call `runner.config.websocket` under the
|
|
989
|
+
// hood to add the event listeners for open, etc. If this handler
|
|
990
|
+
// throws, then the WebSocket will be closed before sending the
|
|
991
|
+
// open event.
|
|
992
|
+
const adapter = await this.#createWebSocket(
|
|
993
|
+
actor.actorId,
|
|
994
|
+
gatewayId,
|
|
995
|
+
requestId,
|
|
996
|
+
requestIdStr,
|
|
997
|
+
0,
|
|
998
|
+
canHibernate,
|
|
999
|
+
false,
|
|
1000
|
+
request,
|
|
1001
|
+
open.path,
|
|
1002
|
+
Object.fromEntries(open.headers),
|
|
1003
|
+
false,
|
|
1004
|
+
);
|
|
1005
|
+
|
|
1006
|
+
// Create a PendingRequest entry to track the message index
|
|
1007
|
+
actor.createPendingRequest(gatewayId, requestId, 0);
|
|
1008
|
+
|
|
1009
|
+
// Open the WebSocket after `config.socket` so (a) the event
|
|
1010
|
+
// handlers can be added and (b) any errors in `config.websocket`
|
|
1011
|
+
// will cause the WebSocket to terminate before the open event.
|
|
1012
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
677
1013
|
tag: "ToServerWebSocketOpen",
|
|
678
1014
|
val: {
|
|
679
|
-
canHibernate
|
|
680
|
-
lastMsgIndex: BigInt(hibernationConfig.lastMsgIndex ?? -1),
|
|
1015
|
+
canHibernate,
|
|
681
1016
|
},
|
|
682
1017
|
});
|
|
683
1018
|
|
|
684
|
-
//
|
|
1019
|
+
// Dispatch open event
|
|
685
1020
|
adapter._handleOpen(requestId);
|
|
686
|
-
|
|
687
|
-
// Call websocket handler
|
|
688
|
-
await websocketHandler(
|
|
689
|
-
this.#runner,
|
|
690
|
-
open.actorId,
|
|
691
|
-
adapter,
|
|
692
|
-
requestId,
|
|
693
|
-
request,
|
|
694
|
-
);
|
|
695
1021
|
} catch (error) {
|
|
696
1022
|
this.log?.error({ msg: "error handling websocket open", error });
|
|
1023
|
+
|
|
1024
|
+
// TODO: Call close event on adapter if needed
|
|
1025
|
+
|
|
697
1026
|
// Send close on error
|
|
698
|
-
this.#sendMessage(requestId, {
|
|
1027
|
+
this.#sendMessage(gatewayId, requestId, {
|
|
699
1028
|
tag: "ToServerWebSocketClose",
|
|
700
1029
|
val: {
|
|
701
1030
|
code: 1011,
|
|
702
1031
|
reason: "Server Error",
|
|
703
|
-
|
|
1032
|
+
hibernate: false,
|
|
704
1033
|
},
|
|
705
1034
|
});
|
|
706
1035
|
|
|
707
|
-
this.#actorWebSockets.delete(requestIdStr);
|
|
708
|
-
|
|
709
1036
|
// Clean up actor tracking
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1037
|
+
actor.deleteWebSocket(gatewayId, requestId);
|
|
1038
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
1039
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
713
1040
|
}
|
|
714
1041
|
}
|
|
715
1042
|
|
|
716
|
-
/// Returns false if the message was sent off
|
|
717
1043
|
async #handleWebSocketMessage(
|
|
718
|
-
|
|
1044
|
+
gatewayId: GatewayId,
|
|
1045
|
+
requestId: RequestId,
|
|
1046
|
+
serverMessageIndex: number,
|
|
719
1047
|
msg: protocol.ToClientWebSocketMessage,
|
|
720
|
-
)
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1048
|
+
) {
|
|
1049
|
+
const actor = await this.getAndWaitForRequestActor(
|
|
1050
|
+
gatewayId,
|
|
1051
|
+
requestId,
|
|
1052
|
+
);
|
|
1053
|
+
if (actor) {
|
|
1054
|
+
const adapter = actor.getWebSocket(gatewayId, requestId);
|
|
1055
|
+
if (adapter) {
|
|
1056
|
+
const data = msg.binary
|
|
1057
|
+
? new Uint8Array(msg.data)
|
|
1058
|
+
: new TextDecoder().decode(new Uint8Array(msg.data));
|
|
727
1059
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1060
|
+
adapter._handleMessage(
|
|
1061
|
+
requestId,
|
|
1062
|
+
data,
|
|
1063
|
+
serverMessageIndex,
|
|
1064
|
+
msg.binary,
|
|
1065
|
+
);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
736
1068
|
}
|
|
1069
|
+
|
|
1070
|
+
// TODO: This will never retransmit the socket and the socket will close
|
|
1071
|
+
this.log?.warn({
|
|
1072
|
+
msg: "missing websocket for incoming websocket message, this may indicate the actor stopped before processing a message",
|
|
1073
|
+
requestId,
|
|
1074
|
+
});
|
|
737
1075
|
}
|
|
738
1076
|
|
|
739
|
-
|
|
1077
|
+
sendHibernatableWebSocketMessageAck(
|
|
1078
|
+
gatewayId: ArrayBuffer,
|
|
1079
|
+
requestId: ArrayBuffer,
|
|
1080
|
+
clientMessageIndex: number,
|
|
1081
|
+
) {
|
|
1082
|
+
const requestIdStr = idToStr(requestId);
|
|
1083
|
+
|
|
740
1084
|
this.log?.debug({
|
|
741
1085
|
msg: "ack ws msg",
|
|
742
|
-
requestId:
|
|
743
|
-
index,
|
|
1086
|
+
requestId: requestIdStr,
|
|
1087
|
+
index: clientMessageIndex,
|
|
744
1088
|
});
|
|
745
1089
|
|
|
746
|
-
if (
|
|
1090
|
+
if (clientMessageIndex < 0 || clientMessageIndex > 65535)
|
|
747
1091
|
throw new Error("invalid websocket ack index");
|
|
748
1092
|
|
|
1093
|
+
// Get the actor to find the gatewayId
|
|
1094
|
+
//
|
|
1095
|
+
// We don't have to wait for the actor to start since we're not calling
|
|
1096
|
+
// any callbacks on the actor
|
|
1097
|
+
const actor = this.getRequestActor(gatewayId, requestId);
|
|
1098
|
+
if (!actor) {
|
|
1099
|
+
this.log?.warn({
|
|
1100
|
+
msg: "cannot send websocket ack, actor not found",
|
|
1101
|
+
requestId: requestIdStr,
|
|
1102
|
+
});
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Get gatewayId from the pending request
|
|
1107
|
+
const pending = actor.getPendingRequest(gatewayId, requestId);
|
|
1108
|
+
if (!pending?.gatewayId) {
|
|
1109
|
+
this.log?.warn({
|
|
1110
|
+
msg: "cannot send websocket ack, gatewayId not found in pending request",
|
|
1111
|
+
requestId: requestIdStr,
|
|
1112
|
+
});
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
749
1116
|
// Send the ack message
|
|
750
|
-
this.#sendMessage(requestId, {
|
|
1117
|
+
this.#sendMessage(pending.gatewayId, requestId, {
|
|
751
1118
|
tag: "ToServerWebSocketMessageAck",
|
|
752
1119
|
val: {
|
|
753
|
-
index,
|
|
1120
|
+
index: clientMessageIndex,
|
|
754
1121
|
},
|
|
755
1122
|
});
|
|
756
1123
|
}
|
|
757
1124
|
|
|
758
1125
|
async #handleWebSocketClose(
|
|
759
|
-
|
|
1126
|
+
gatewayId: GatewayId,
|
|
1127
|
+
requestId: RequestId,
|
|
760
1128
|
close: protocol.ToClientWebSocketClose,
|
|
761
1129
|
) {
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1130
|
+
const actor = await this.getAndWaitForRequestActor(
|
|
1131
|
+
gatewayId,
|
|
1132
|
+
requestId,
|
|
1133
|
+
);
|
|
1134
|
+
if (actor) {
|
|
1135
|
+
const adapter = actor.getWebSocket(gatewayId, requestId);
|
|
1136
|
+
if (adapter) {
|
|
1137
|
+
// We don't need to send a close response
|
|
1138
|
+
adapter._handleClose(
|
|
1139
|
+
requestId,
|
|
1140
|
+
close.code || undefined,
|
|
1141
|
+
close.reason || undefined,
|
|
1142
|
+
);
|
|
1143
|
+
actor.deleteWebSocket(gatewayId, requestId);
|
|
1144
|
+
actor.deletePendingRequest(gatewayId, requestId);
|
|
1145
|
+
this.#removeRequestToActor(gatewayId, requestId);
|
|
1146
|
+
}
|
|
771
1147
|
}
|
|
772
1148
|
}
|
|
773
1149
|
}
|
|
774
1150
|
|
|
775
|
-
/**
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1151
|
+
/**
|
|
1152
|
+
* Builds a request that represents the incoming request for a given WebSocket.
|
|
1153
|
+
*
|
|
1154
|
+
* This request is not a real request and will never be sent. It's used to be passed to the actor to behave like a real incoming request.
|
|
1155
|
+
*/
|
|
1156
|
+
function buildRequestForWebSocket(
|
|
1157
|
+
path: string,
|
|
1158
|
+
headers: Record<string, string>,
|
|
1159
|
+
): Request {
|
|
1160
|
+
// We need to manually ensure the original Upgrade/Connection WS
|
|
1161
|
+
// headers are present
|
|
1162
|
+
const fullHeaders = {
|
|
1163
|
+
...headers,
|
|
1164
|
+
Upgrade: "websocket",
|
|
1165
|
+
Connection: "Upgrade",
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
if (!path.startsWith("/")) {
|
|
1169
|
+
throw new Error("path must start with leading slash");
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const request = new Request(`http://actor${path}`, {
|
|
1173
|
+
method: "GET",
|
|
1174
|
+
headers: fullHeaders,
|
|
1175
|
+
});
|
|
781
1176
|
|
|
782
|
-
|
|
783
|
-
return uuidstringify(new Uint8Array(id));
|
|
1177
|
+
return request;
|
|
784
1178
|
}
|