@rivetkit/engine-runner 25.7.3 → 25.8.1
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/package.json +2 -2
- package/src/mod.ts +70 -67
- package/src/tunnel.ts +21 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rivetkit/engine-runner",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"import": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"uuid": "^12.0.0",
|
|
17
17
|
"pino": "^9.9.5",
|
|
18
18
|
"ws": "^8.18.3",
|
|
19
|
-
"@rivetkit/engine-runner-protocol": "25.
|
|
19
|
+
"@rivetkit/engine-runner-protocol": "25.8.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^22.18.1",
|
package/src/mod.ts
CHANGED
|
@@ -10,6 +10,9 @@ import { setLogger, logger } from "./log.js";
|
|
|
10
10
|
const KV_EXPIRE: number = 30_000;
|
|
11
11
|
const PROTOCOL_VERSION: number = 1;
|
|
12
12
|
|
|
13
|
+
/** Warn once the backlog significantly exceeds the server's ack batch size. */
|
|
14
|
+
const EVENT_BACKLOG_WARN_THRESHOLD = 10_000;
|
|
15
|
+
|
|
13
16
|
export interface ActorInstance {
|
|
14
17
|
actorId: string;
|
|
15
18
|
generation: number;
|
|
@@ -92,8 +95,8 @@ export class Runner {
|
|
|
92
95
|
#runnerLostTimeout?: NodeJS.Timeout;
|
|
93
96
|
|
|
94
97
|
// Event storage for resending
|
|
95
|
-
#eventHistory:
|
|
96
|
-
#
|
|
98
|
+
#eventHistory: protocol.EventWrapper[] = [];
|
|
99
|
+
#eventBacklogWarned: boolean = false;
|
|
97
100
|
|
|
98
101
|
// Command acknowledgment
|
|
99
102
|
#ackInterval?: NodeJS.Timeout;
|
|
@@ -112,12 +115,6 @@ export class Runner {
|
|
|
112
115
|
|
|
113
116
|
this.#tunnel = new Tunnel(this);
|
|
114
117
|
|
|
115
|
-
// TODO(RVT-4986): Prune when server acks events
|
|
116
|
-
// Start pruning old events every minute
|
|
117
|
-
this.#eventPruneInterval = setInterval(() => {
|
|
118
|
-
this.#pruneOldEvents();
|
|
119
|
-
}, 60000); // Run every minute
|
|
120
|
-
|
|
121
118
|
// Start cleaning up old unsent KV requests every 15 seconds
|
|
122
119
|
this.#kvCleanupInterval = setInterval(() => {
|
|
123
120
|
this.#cleanupOldKvRequests();
|
|
@@ -205,7 +202,7 @@ export class Runner {
|
|
|
205
202
|
): ActorInstance | undefined {
|
|
206
203
|
const actor = this.#actors.get(actorId);
|
|
207
204
|
if (!actor) {
|
|
208
|
-
logger()?.error({ msg: "actor not found", actorId });
|
|
205
|
+
logger()?.error({ msg: "actor not found for removal", actorId });
|
|
209
206
|
return undefined;
|
|
210
207
|
}
|
|
211
208
|
if (generation !== undefined && actor.generation !== generation) {
|
|
@@ -290,12 +287,6 @@ export class Runner {
|
|
|
290
287
|
this.#ackInterval = undefined;
|
|
291
288
|
}
|
|
292
289
|
|
|
293
|
-
// Clear event prune interval
|
|
294
|
-
if (this.#eventPruneInterval) {
|
|
295
|
-
clearInterval(this.#eventPruneInterval);
|
|
296
|
-
this.#eventPruneInterval = undefined;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
290
|
// Clear KV cleanup interval
|
|
300
291
|
if (this.#kvCleanupInterval) {
|
|
301
292
|
clearInterval(this.#kvCleanupInterval);
|
|
@@ -398,21 +389,11 @@ export class Runner {
|
|
|
398
389
|
return `${wsEndpoint}?protocol_version=${PROTOCOL_VERSION}&namespace=${encodeURIComponent(this.#config.namespace)}&runner_key=${encodeURIComponent(this.#config.runnerKey)}`;
|
|
399
390
|
}
|
|
400
391
|
|
|
401
|
-
get pegboardTunnelUrl() {
|
|
402
|
-
const endpoint =
|
|
403
|
-
this.#config.pegboardRelayEndpoint ||
|
|
404
|
-
this.#config.pegboardEndpoint ||
|
|
405
|
-
this.#config.endpoint;
|
|
406
|
-
const wsEndpoint = endpoint
|
|
407
|
-
.replace("http://", "ws://")
|
|
408
|
-
.replace("https://", "wss://");
|
|
409
|
-
return `${wsEndpoint}?protocol_version=${PROTOCOL_VERSION}&namespace=${encodeURIComponent(this.#config.namespace)}&runner_name=${encodeURIComponent(this.#config.runnerName)}&runner_key=${encodeURIComponent(this.#config.runnerKey)}`;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
392
|
// MARK: Runner protocol
|
|
413
393
|
async #openPegboardWebSocket() {
|
|
414
394
|
const protocols = ["rivet", `rivet_target.runner`];
|
|
415
|
-
if (this.config.token)
|
|
395
|
+
if (this.config.token)
|
|
396
|
+
protocols.push(`rivet_token.${this.config.token}`);
|
|
416
397
|
|
|
417
398
|
const WS = await importWebSocket();
|
|
418
399
|
const ws = new WS(this.pegboardUrl, protocols) as any as WebSocket;
|
|
@@ -510,7 +491,13 @@ export class Runner {
|
|
|
510
491
|
// Handle message
|
|
511
492
|
if (message.tag === "ToClientInit") {
|
|
512
493
|
const init = message.val;
|
|
513
|
-
|
|
494
|
+
|
|
495
|
+
if (this.runnerId != init.runnerId) {
|
|
496
|
+
this.runnerId = init.runnerId;
|
|
497
|
+
|
|
498
|
+
// Clear history if runner id changed
|
|
499
|
+
this.#eventHistory.length = 0;
|
|
500
|
+
}
|
|
514
501
|
|
|
515
502
|
// Store the runner lost threshold from metadata
|
|
516
503
|
this.#runnerLostThreshold = init.metadata?.runnerLostThreshold
|
|
@@ -532,7 +519,7 @@ export class Runner {
|
|
|
532
519
|
const commands = message.val;
|
|
533
520
|
this.#handleCommands(commands);
|
|
534
521
|
} else if (message.tag === "ToClientAckEvents") {
|
|
535
|
-
|
|
522
|
+
this.#handleAckEvents(message.val);
|
|
536
523
|
} else if (message.tag === "ToClientKvResponse") {
|
|
537
524
|
const kvResponse = message.val;
|
|
538
525
|
this.#handleKvResponse(kvResponse);
|
|
@@ -570,7 +557,7 @@ export class Runner {
|
|
|
570
557
|
}
|
|
571
558
|
});
|
|
572
559
|
|
|
573
|
-
ws.addEventListener("close", (ev) => {
|
|
560
|
+
ws.addEventListener("close", async (ev) => {
|
|
574
561
|
logger()?.info({
|
|
575
562
|
msg: "connection closed",
|
|
576
563
|
code: ev.code,
|
|
@@ -579,6 +566,12 @@ export class Runner {
|
|
|
579
566
|
|
|
580
567
|
this.#config.onDisconnected();
|
|
581
568
|
|
|
569
|
+
if (ev.reason.toString() == "ws.eviction") {
|
|
570
|
+
logger()?.info("runner evicted");
|
|
571
|
+
|
|
572
|
+
await this.shutdown(true);
|
|
573
|
+
}
|
|
574
|
+
|
|
582
575
|
// Clear ping loop on close
|
|
583
576
|
if (this.#pingLoop) {
|
|
584
577
|
clearInterval(this.#pingLoop);
|
|
@@ -633,6 +626,45 @@ export class Runner {
|
|
|
633
626
|
}
|
|
634
627
|
}
|
|
635
628
|
|
|
629
|
+
#handleAckEvents(ack: protocol.ToClientAckEvents) {
|
|
630
|
+
const lastAckedIdx = ack.lastEventIdx;
|
|
631
|
+
|
|
632
|
+
const originalLength = this.#eventHistory.length;
|
|
633
|
+
this.#eventHistory = this.#eventHistory.filter(
|
|
634
|
+
(event) => event.index > lastAckedIdx,
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
const prunedCount = originalLength - this.#eventHistory.length;
|
|
638
|
+
if (prunedCount > 0) {
|
|
639
|
+
logger()?.info({
|
|
640
|
+
msg: "pruned acknowledged events",
|
|
641
|
+
lastAckedIdx: lastAckedIdx.toString(),
|
|
642
|
+
prunedCount,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (this.#eventHistory.length <= EVENT_BACKLOG_WARN_THRESHOLD) {
|
|
647
|
+
this.#eventBacklogWarned = false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Track events to send to the server in case we need to resend it on disconnect. */
|
|
652
|
+
#recordEvent(eventWrapper: protocol.EventWrapper) {
|
|
653
|
+
this.#eventHistory.push(eventWrapper);
|
|
654
|
+
|
|
655
|
+
if (
|
|
656
|
+
this.#eventHistory.length > EVENT_BACKLOG_WARN_THRESHOLD &&
|
|
657
|
+
!this.#eventBacklogWarned
|
|
658
|
+
) {
|
|
659
|
+
this.#eventBacklogWarned = true;
|
|
660
|
+
logger()?.warn({
|
|
661
|
+
msg: "unacknowledged event backlog exceeds threshold",
|
|
662
|
+
backlogSize: this.#eventHistory.length,
|
|
663
|
+
threshold: EVENT_BACKLOG_WARN_THRESHOLD,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
636
668
|
#handleCommandStartActor(commandWrapper: protocol.CommandWrapper) {
|
|
637
669
|
const startCommand = commandWrapper.inner
|
|
638
670
|
.val as protocol.CommandStartActor;
|
|
@@ -724,11 +756,7 @@ export class Runner {
|
|
|
724
756
|
},
|
|
725
757
|
};
|
|
726
758
|
|
|
727
|
-
|
|
728
|
-
this.#eventHistory.push({
|
|
729
|
-
event: eventWrapper,
|
|
730
|
-
timestamp: Date.now(),
|
|
731
|
-
});
|
|
759
|
+
this.#recordEvent(eventWrapper);
|
|
732
760
|
|
|
733
761
|
logger()?.info({
|
|
734
762
|
msg: "sending event to server",
|
|
@@ -763,7 +791,7 @@ export class Runner {
|
|
|
763
791
|
tag: "ActorStateStopped",
|
|
764
792
|
val: {
|
|
765
793
|
code: protocol.StopCode.Ok,
|
|
766
|
-
message:
|
|
794
|
+
message: null,
|
|
767
795
|
},
|
|
768
796
|
};
|
|
769
797
|
} else {
|
|
@@ -785,11 +813,7 @@ export class Runner {
|
|
|
785
813
|
},
|
|
786
814
|
};
|
|
787
815
|
|
|
788
|
-
|
|
789
|
-
this.#eventHistory.push({
|
|
790
|
-
event: eventWrapper,
|
|
791
|
-
timestamp: Date.now(),
|
|
792
|
-
});
|
|
816
|
+
this.#recordEvent(eventWrapper);
|
|
793
817
|
|
|
794
818
|
logger()?.info({
|
|
795
819
|
msg: "sending event to server",
|
|
@@ -1142,11 +1166,7 @@ export class Runner {
|
|
|
1142
1166
|
},
|
|
1143
1167
|
};
|
|
1144
1168
|
|
|
1145
|
-
|
|
1146
|
-
this.#eventHistory.push({
|
|
1147
|
-
event: eventWrapper,
|
|
1148
|
-
timestamp: Date.now(),
|
|
1149
|
-
});
|
|
1169
|
+
this.#recordEvent(eventWrapper);
|
|
1150
1170
|
|
|
1151
1171
|
this.__sendToServer({
|
|
1152
1172
|
tag: "ToServerEvents",
|
|
@@ -1272,7 +1292,7 @@ export class Runner {
|
|
|
1272
1292
|
tag: "ToServerlessServerInit",
|
|
1273
1293
|
val: {
|
|
1274
1294
|
runnerId: this.runnerId,
|
|
1275
|
-
}
|
|
1295
|
+
},
|
|
1276
1296
|
});
|
|
1277
1297
|
|
|
1278
1298
|
// Embed version
|
|
@@ -1280,7 +1300,7 @@ export class Runner {
|
|
|
1280
1300
|
buffer.writeUInt16LE(PROTOCOL_VERSION, 0);
|
|
1281
1301
|
Buffer.from(data).copy(buffer, 2);
|
|
1282
1302
|
|
|
1283
|
-
return buffer.toString(
|
|
1303
|
+
return buffer.toString("base64");
|
|
1284
1304
|
}
|
|
1285
1305
|
|
|
1286
1306
|
#scheduleReconnect() {
|
|
@@ -1313,7 +1333,7 @@ export class Runner {
|
|
|
1313
1333
|
|
|
1314
1334
|
#resendUnacknowledgedEvents(lastEventIdx: bigint) {
|
|
1315
1335
|
const eventsToResend = this.#eventHistory.filter(
|
|
1316
|
-
(
|
|
1336
|
+
(event) => event.index > lastEventIdx,
|
|
1317
1337
|
);
|
|
1318
1338
|
|
|
1319
1339
|
if (eventsToResend.length === 0) return;
|
|
@@ -1323,29 +1343,12 @@ export class Runner {
|
|
|
1323
1343
|
//);
|
|
1324
1344
|
|
|
1325
1345
|
// Resend events in batches
|
|
1326
|
-
const events = eventsToResend.map((item) => item.event);
|
|
1327
1346
|
this.__sendToServer({
|
|
1328
1347
|
tag: "ToServerEvents",
|
|
1329
|
-
val:
|
|
1348
|
+
val: eventsToResend,
|
|
1330
1349
|
});
|
|
1331
1350
|
}
|
|
1332
1351
|
|
|
1333
|
-
// TODO(RVT-4986): Prune when server acks events instead of based on old events
|
|
1334
|
-
#pruneOldEvents() {
|
|
1335
|
-
const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
|
|
1336
|
-
const originalLength = this.#eventHistory.length;
|
|
1337
|
-
|
|
1338
|
-
// Remove events older than 5 minutes
|
|
1339
|
-
this.#eventHistory = this.#eventHistory.filter(
|
|
1340
|
-
(item) => item.timestamp > fiveMinutesAgo,
|
|
1341
|
-
);
|
|
1342
|
-
|
|
1343
|
-
const prunedCount = originalLength - this.#eventHistory.length;
|
|
1344
|
-
if (prunedCount > 0) {
|
|
1345
|
-
//logger()?.log(`Pruned ${prunedCount} old events from history`);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
1352
|
#cleanupOldKvRequests() {
|
|
1350
1353
|
const thirtySecondsAgo = Date.now() - KV_EXPIRE;
|
|
1351
1354
|
const toDelete: number[] = [];
|
package/src/tunnel.ts
CHANGED
|
@@ -16,7 +16,7 @@ interface PendingRequest {
|
|
|
16
16
|
actorId?: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
interface
|
|
19
|
+
interface PendingTunnelMessage {
|
|
20
20
|
sentAt: number;
|
|
21
21
|
requestIdStr: string;
|
|
22
22
|
}
|
|
@@ -24,10 +24,14 @@ interface PendingMessage {
|
|
|
24
24
|
export class Tunnel {
|
|
25
25
|
#runner: Runner;
|
|
26
26
|
|
|
27
|
+
/** Requests over the tunnel to the actor that are in flight. */
|
|
27
28
|
#actorPendingRequests: Map<string, PendingRequest> = new Map();
|
|
29
|
+
/** WebSockets over the tunnel to the actor that are in flight. */
|
|
28
30
|
#actorWebSockets: Map<string, WebSocketTunnelAdapter> = new Map();
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
/** Messages sent from the actor over the tunnel that have not been acked by the gateway. */
|
|
33
|
+
#pendingTunnelMessages: Map<string, PendingTunnelMessage> = new Map();
|
|
34
|
+
|
|
31
35
|
#gcInterval?: NodeJS.Timeout;
|
|
32
36
|
|
|
33
37
|
constructor(runner: Runner) {
|
|
@@ -65,7 +69,9 @@ export class Tunnel {
|
|
|
65
69
|
) {
|
|
66
70
|
// TODO: Switch this with runner WS
|
|
67
71
|
if (!this.#runner.__webSocketReady()) {
|
|
68
|
-
|
|
72
|
+
logger()?.warn(
|
|
73
|
+
"cannot send tunnel message, socket not connected to engine",
|
|
74
|
+
);
|
|
69
75
|
return;
|
|
70
76
|
}
|
|
71
77
|
|
|
@@ -73,7 +79,7 @@ export class Tunnel {
|
|
|
73
79
|
const messageId = generateUuidBuffer();
|
|
74
80
|
|
|
75
81
|
const requestIdStr = bufferToString(requestId);
|
|
76
|
-
this.#
|
|
82
|
+
this.#pendingTunnelMessages.set(bufferToString(messageId), {
|
|
77
83
|
sentAt: Date.now(),
|
|
78
84
|
requestIdStr,
|
|
79
85
|
});
|
|
@@ -121,7 +127,7 @@ export class Tunnel {
|
|
|
121
127
|
const now = Date.now();
|
|
122
128
|
const messagesToDelete: string[] = [];
|
|
123
129
|
|
|
124
|
-
for (const [messageId, pendingMessage] of this.#
|
|
130
|
+
for (const [messageId, pendingMessage] of this.#pendingTunnelMessages) {
|
|
125
131
|
// Check if message is older than timeout
|
|
126
132
|
if (now - pendingMessage.sentAt > MESSAGE_ACK_TIMEOUT) {
|
|
127
133
|
messagesToDelete.push(messageId);
|
|
@@ -161,9 +167,14 @@ export class Tunnel {
|
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
// Remove timed out messages
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
170
|
+
if (messagesToDelete.length > 0) {
|
|
171
|
+
logger()?.warn({
|
|
172
|
+
msg: "purging unacked tunnel messages, this indicates that the Rivet Engine is disconnected or not responding",
|
|
173
|
+
count: messagesToDelete.length,
|
|
174
|
+
});
|
|
175
|
+
for (const messageId of messagesToDelete) {
|
|
176
|
+
this.#pendingTunnelMessages.delete(messageId);
|
|
177
|
+
}
|
|
167
178
|
}
|
|
168
179
|
}
|
|
169
180
|
|
|
@@ -214,9 +225,9 @@ export class Tunnel {
|
|
|
214
225
|
if (message.messageKind.tag === "TunnelAck") {
|
|
215
226
|
// Mark pending message as acknowledged and remove it
|
|
216
227
|
const msgIdStr = bufferToString(message.messageId);
|
|
217
|
-
const pending = this.#
|
|
228
|
+
const pending = this.#pendingTunnelMessages.get(msgIdStr);
|
|
218
229
|
if (pending) {
|
|
219
|
-
this.#
|
|
230
|
+
this.#pendingTunnelMessages.delete(msgIdStr);
|
|
220
231
|
}
|
|
221
232
|
} else {
|
|
222
233
|
this.#sendAck(message.requestId, message.messageId);
|
|
@@ -438,7 +449,6 @@ export class Tunnel {
|
|
|
438
449
|
const websocketHandler = this.#runner.config.websocket;
|
|
439
450
|
|
|
440
451
|
if (!websocketHandler) {
|
|
441
|
-
console.error("No websocket handler configured for tunnel");
|
|
442
452
|
logger()?.error({
|
|
443
453
|
msg: "no websocket handler configured for tunnel",
|
|
444
454
|
});
|