@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.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/src/mod.ts +70 -67
  3. 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.7.3",
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.7.3"
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: { event: protocol.EventWrapper; timestamp: number }[] = [];
96
- #eventPruneInterval?: NodeJS.Timeout;
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) protocols.push(`rivet_token.${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
- this.runnerId = init.runnerId;
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
- throw new Error("TODO");
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
- // Store event in history for potential resending
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: "hello",
794
+ message: null,
767
795
  },
768
796
  };
769
797
  } else {
@@ -785,11 +813,7 @@ export class Runner {
785
813
  },
786
814
  };
787
815
 
788
- // Store event in history for potential resending
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
- // Store event in history for potential resending
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('base64');
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
- (item) => item.event.index > lastEventIdx,
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: events,
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 PendingMessage {
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
- #pendingMessages: Map<string, PendingMessage> = new Map();
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
- console.warn("Cannot send tunnel message, WebSocket not connected");
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.#pendingMessages.set(bufferToString(messageId), {
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.#pendingMessages) {
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
- for (const messageId of messagesToDelete) {
165
- this.#pendingMessages.delete(messageId);
166
- console.warn(`Purged unacked message: ${messageId}`);
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.#pendingMessages.get(msgIdStr);
228
+ const pending = this.#pendingTunnelMessages.get(msgIdStr);
218
229
  if (pending) {
219
- this.#pendingMessages.delete(msgIdStr);
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
  });