@rivetkit/engine-runner 2.0.22-rc.2 → 2.0.23

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivetkit/engine-runner",
3
- "version": "2.0.22-rc.2",
3
+ "version": "2.0.23",
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": "2.0.22-rc.2"
19
+ "@rivetkit/engine-runner-protocol": "2.0.23"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@types/node": "^22.18.1",
package/src/mod.ts CHANGED
@@ -2,8 +2,13 @@ import * as protocol from "@rivetkit/engine-runner-protocol";
2
2
  import type { Logger } from "pino";
3
3
  import type WebSocket from "ws";
4
4
  import { logger, setLogger } from "./log.js";
5
+ import { stringifyCommandWrapper, stringifyEvent } from "./stringify";
5
6
  import { Tunnel } from "./tunnel";
6
- import { calculateBackoff, unreachable } from "./utils";
7
+ import {
8
+ calculateBackoff,
9
+ parseWebSocketCloseReason,
10
+ unreachable,
11
+ } from "./utils";
7
12
  import { importWebSocket } from "./websocket.js";
8
13
  import type { WebSocketTunnelAdapter } from "./websocket-tunnel-adapter";
9
14
 
@@ -132,6 +137,30 @@ export class Runner {
132
137
  // Tunnel for HTTP/WebSocket forwarding
133
138
  #tunnel: Tunnel | undefined;
134
139
 
140
+ // Cached child logger with runner-specific attributes
141
+ #logCached?: Logger;
142
+
143
+ get log(): Logger | undefined {
144
+ if (this.#logCached) return this.#logCached;
145
+
146
+ const l = logger();
147
+ if (l) {
148
+ // If has connected, create child logger with relevant metadata
149
+ //
150
+ // Otherwise, return default logger
151
+ if (this.runnerId) {
152
+ this.#logCached = l.child({
153
+ runnerId: this.runnerId,
154
+ });
155
+ return this.#logCached;
156
+ } else {
157
+ return l;
158
+ }
159
+ }
160
+
161
+ return undefined;
162
+ }
163
+
135
164
  constructor(config: RunnerConfig) {
136
165
  this.#config = config;
137
166
  if (this.#config.logger) setLogger(this.#config.logger);
@@ -144,6 +173,13 @@ export class Runner {
144
173
 
145
174
  // MARK: Manage actors
146
175
  sleepActor(actorId: string, generation?: number) {
176
+ if (this.#shutdown) {
177
+ this.log?.warn({
178
+ msg: "runner is shut down, cannot sleep actor",
179
+ });
180
+ return;
181
+ }
182
+
147
183
  const actor = this.getActor(actorId, generation);
148
184
  if (!actor) return;
149
185
 
@@ -186,9 +222,8 @@ export class Runner {
186
222
  }
187
223
 
188
224
  #stopAllActors() {
189
- logger()?.info({
225
+ this.log?.info({
190
226
  msg: "stopping all actors due to runner lost threshold exceeded",
191
- runnerId: this.runnerId,
192
227
  });
193
228
 
194
229
  const actorIds = Array.from(this.#actors.keys());
@@ -200,17 +235,15 @@ export class Runner {
200
235
  getActor(actorId: string, generation?: number): ActorInstance | undefined {
201
236
  const actor = this.#actors.get(actorId);
202
237
  if (!actor) {
203
- logger()?.error({
238
+ this.log?.error({
204
239
  msg: "actor not found",
205
- runnerId: this.runnerId,
206
240
  actorId,
207
241
  });
208
242
  return undefined;
209
243
  }
210
244
  if (generation !== undefined && actor.generation !== generation) {
211
- logger()?.error({
245
+ this.log?.error({
212
246
  msg: "actor generation mismatch",
213
- runnerId: this.runnerId,
214
247
  actorId,
215
248
  generation,
216
249
  });
@@ -236,17 +269,15 @@ export class Runner {
236
269
  ): ActorInstance | undefined {
237
270
  const actor = this.#actors.get(actorId);
238
271
  if (!actor) {
239
- logger()?.error({
272
+ this.log?.error({
240
273
  msg: "actor not found for removal",
241
- runnerId: this.runnerId,
242
274
  actorId,
243
275
  });
244
276
  return undefined;
245
277
  }
246
278
  if (generation !== undefined && actor.generation !== generation) {
247
- logger()?.error({
279
+ this.log?.error({
248
280
  msg: "actor generation mismatch",
249
- runnerId: this.runnerId,
250
281
  actorId,
251
282
  generation,
252
283
  });
@@ -263,7 +294,7 @@ export class Runner {
263
294
  if (this.#started) throw new Error("Cannot call runner.start twice");
264
295
  this.#started = true;
265
296
 
266
- logger()?.info({ msg: "starting runner" });
297
+ this.log?.info({ msg: "starting runner" });
267
298
 
268
299
  this.#tunnel = new Tunnel(this);
269
300
  this.#tunnel.start();
@@ -278,7 +309,7 @@ export class Runner {
278
309
  if (!this.#config.noAutoShutdown) {
279
310
  if (!SIGNAL_HANDLERS.length) {
280
311
  process.on("SIGTERM", () => {
281
- logger()?.debug("received SIGTERM");
312
+ this.log?.debug("received SIGTERM");
282
313
 
283
314
  for (const handler of SIGNAL_HANDLERS) {
284
315
  handler();
@@ -287,7 +318,7 @@ export class Runner {
287
318
  process.exit(0);
288
319
  });
289
320
  process.on("SIGINT", () => {
290
- logger()?.debug("received SIGINT");
321
+ this.log?.debug("received SIGINT");
291
322
 
292
323
  for (const handler of SIGNAL_HANDLERS) {
293
324
  handler();
@@ -296,7 +327,7 @@ export class Runner {
296
327
  process.exit(0);
297
328
  });
298
329
 
299
- logger()?.debug({
330
+ this.log?.debug({
300
331
  msg: "added SIGTERM listeners",
301
332
  });
302
333
  }
@@ -310,9 +341,8 @@ export class Runner {
310
341
 
311
342
  // MARK: Shutdown
312
343
  async shutdown(immediate: boolean, exit: boolean = false) {
313
- logger()?.info({
344
+ this.log?.info({
314
345
  msg: "starting shutdown",
315
- runnerId: this.runnerId,
316
346
  immediate,
317
347
  exit,
318
348
  });
@@ -364,13 +394,12 @@ export class Runner {
364
394
  const pegboardWebSocket = this.#pegboardWebSocket;
365
395
  if (immediate) {
366
396
  // Stop immediately
367
- pegboardWebSocket.close(1000, "Stopping");
397
+ pegboardWebSocket.close(1000, "pegboard.runner_shutdown");
368
398
  } else {
369
399
  // Wait for actors to shut down before stopping
370
400
  try {
371
- logger()?.info({
401
+ this.log?.info({
372
402
  msg: "sending stopping message",
373
- runnerId: this.runnerId,
374
403
  readyState: pegboardWebSocket.readyState,
375
404
  });
376
405
 
@@ -386,7 +415,7 @@ export class Runner {
386
415
  ) {
387
416
  this.#pegboardWebSocket.send(encoded);
388
417
  } else {
389
- logger()?.error(
418
+ this.log?.error(
390
419
  "WebSocket not available or not open for sending data",
391
420
  );
392
421
  }
@@ -396,9 +425,8 @@ export class Runner {
396
425
  throw new Error("missing pegboardWebSocket");
397
426
 
398
427
  pegboardWebSocket.addEventListener("close", (ev) => {
399
- logger()?.info({
428
+ this.log?.info({
400
429
  msg: "connection closed",
401
- runnerId: this.runnerId,
402
430
  code: ev.code,
403
431
  reason: ev.reason.toString(),
404
432
  });
@@ -408,31 +436,29 @@ export class Runner {
408
436
 
409
437
  // TODO: Wait for all actors to stop before closing ws
410
438
 
411
- logger()?.info({
439
+ this.log?.info({
412
440
  msg: "closing WebSocket",
413
- runnerId: this.runnerId,
414
441
  });
415
- pegboardWebSocket.close(1000, "Stopping");
442
+ pegboardWebSocket.close(1000, "pegboard.runner_shutdown");
416
443
 
417
444
  await closePromise;
418
445
 
419
- logger()?.info({
446
+ this.log?.info({
420
447
  msg: "websocket shutdown completed",
421
- runnerId: this.runnerId,
422
448
  });
423
449
  } catch (error) {
424
- logger()?.error({
450
+ this.log?.error({
425
451
  msg: "error during websocket shutdown:",
426
- runnerId: this.runnerId,
427
452
  error,
428
453
  });
429
454
  pegboardWebSocket.close();
430
455
  }
431
456
  }
432
457
  } else {
433
- logger()?.warn({
458
+ // This is often logged when the serverless SSE stream closes after
459
+ // the runner has already shut down
460
+ this.log?.debug({
434
461
  msg: "no runner WebSocket to shutdown or already closed",
435
- runnerId: this.runnerId,
436
462
  readyState: this.#pegboardWebSocket?.readyState,
437
463
  });
438
464
  }
@@ -474,7 +500,7 @@ export class Runner {
474
500
  const ws = new WS(this.pegboardUrl, protocols) as any as WebSocket;
475
501
  this.#pegboardWebSocket = ws;
476
502
 
477
- logger()?.info({
503
+ this.log?.info({
478
504
  msg: "connecting",
479
505
  endpoint: this.pegboardEndpoint,
480
506
  namespace: this.#config.namespace,
@@ -483,7 +509,20 @@ export class Runner {
483
509
  });
484
510
 
485
511
  ws.addEventListener("open", () => {
486
- logger()?.info({ msg: "connected" });
512
+ if (this.#reconnectAttempt > 0) {
513
+ this.log?.info({
514
+ msg: "runner reconnected",
515
+ namespace: this.#config.namespace,
516
+ runnerName: this.#config.runnerName,
517
+ reconnectAttempt: this.#reconnectAttempt,
518
+ });
519
+ } else {
520
+ this.log?.debug({
521
+ msg: "runner connected",
522
+ namespace: this.#config.namespace,
523
+ runnerName: this.#config.runnerName,
524
+ });
525
+ }
487
526
 
488
527
  // Reset reconnect attempt counter on successful connection
489
528
  this.#reconnectAttempt = 0;
@@ -539,9 +578,8 @@ export class Runner {
539
578
  });
540
579
  } else {
541
580
  clearInterval(pingLoop);
542
- logger()?.info({
581
+ this.log?.info({
543
582
  msg: "WebSocket not open, stopping ping loop",
544
- runnerId: this.runnerId,
545
583
  });
546
584
  }
547
585
  }, RUNNER_PING_INTERVAL);
@@ -554,9 +592,8 @@ export class Runner {
554
592
  this.#sendCommandAcknowledgment();
555
593
  } else {
556
594
  clearInterval(ackLoop);
557
- logger()?.info({
595
+ this.log?.info({
558
596
  msg: "WebSocket not open, stopping ack loop",
559
- runnerId: this.runnerId,
560
597
  });
561
598
  }
562
599
  }, ackInterval);
@@ -592,9 +629,8 @@ export class Runner {
592
629
  ? Number(init.metadata.runnerLostThreshold)
593
630
  : undefined;
594
631
 
595
- logger()?.info({
632
+ this.log?.info({
596
633
  msg: "received init",
597
- runnerId: init.runnerId,
598
634
  lastEventIdx: init.lastEventIdx,
599
635
  runnerLostThreshold: this.#runnerLostThreshold,
600
636
  });
@@ -622,9 +658,8 @@ export class Runner {
622
658
  });
623
659
 
624
660
  ws.addEventListener("error", (ev) => {
625
- logger()?.error({
661
+ this.log?.error({
626
662
  msg: `WebSocket error: ${ev.error}`,
627
- runnerId: this.runnerId,
628
663
  });
629
664
 
630
665
  if (!this.#shutdown) {
@@ -634,9 +669,8 @@ export class Runner {
634
669
  this.#runnerLostThreshold &&
635
670
  this.#runnerLostThreshold > 0
636
671
  ) {
637
- logger()?.info({
672
+ this.log?.info({
638
673
  msg: "starting runner lost timeout",
639
- runnerId: this.runnerId,
640
674
  seconds: this.#runnerLostThreshold / 1000,
641
675
  });
642
676
  this.#runnerLostTimeout = setTimeout(() => {
@@ -650,22 +684,32 @@ export class Runner {
650
684
  });
651
685
 
652
686
  ws.addEventListener("close", async (ev) => {
653
- logger()?.info({
654
- msg: "connection closed",
655
- runnerId: this.runnerId,
656
- code: ev.code,
657
- reason: ev.reason.toString(),
658
- });
687
+ const closeError = parseWebSocketCloseReason(ev.reason);
688
+ if (
689
+ closeError?.group === "ws" &&
690
+ closeError?.error === "eviction"
691
+ ) {
692
+ this.log?.info("runner websocket evicted");
659
693
 
660
- this.#config.onDisconnected(ev.code, ev.reason);
661
-
662
- if (ev.reason.toString().startsWith("ws.eviction")) {
663
- logger()?.info({
664
- msg: "runner evicted",
665
- runnerId: this.runnerId,
666
- });
694
+ this.#config.onDisconnected(ev.code, ev.reason);
667
695
 
668
696
  await this.shutdown(true);
697
+ } else {
698
+ if (
699
+ closeError?.group === "pegboard" &&
700
+ closeError?.error === "runner_shutdown"
701
+ ) {
702
+ this.log?.info("runner shutdown");
703
+ } else {
704
+ this.log?.warn({
705
+ msg: "runner disconnected",
706
+ code: ev.code,
707
+ reason: ev.reason.toString(),
708
+ closeError,
709
+ });
710
+ }
711
+
712
+ this.#config.onDisconnected(ev.code, ev.reason);
669
713
  }
670
714
 
671
715
  // Clear ping loop on close
@@ -687,9 +731,8 @@ export class Runner {
687
731
  this.#runnerLostThreshold &&
688
732
  this.#runnerLostThreshold > 0
689
733
  ) {
690
- logger()?.info({
734
+ this.log?.info({
691
735
  msg: "starting runner lost timeout",
692
- runnerId: this.runnerId,
693
736
  seconds: this.#runnerLostThreshold / 1000,
694
737
  });
695
738
  this.#runnerLostTimeout = setTimeout(() => {
@@ -704,17 +747,15 @@ export class Runner {
704
747
  }
705
748
 
706
749
  #handleCommands(commands: protocol.ToClientCommands) {
707
- logger()?.info({
750
+ this.log?.info({
708
751
  msg: "received commands",
709
- runnerId: this.runnerId,
710
752
  commandCount: commands.length,
711
753
  });
712
754
 
713
755
  for (const commandWrapper of commands) {
714
- logger()?.info({
756
+ this.log?.info({
715
757
  msg: "received command",
716
- runnerId: this.runnerId,
717
- commandWrapper,
758
+ command: stringifyCommandWrapper(commandWrapper),
718
759
  });
719
760
  if (commandWrapper.inner.tag === "CommandStartActor") {
720
761
  this.#handleCommandStartActor(commandWrapper);
@@ -738,9 +779,8 @@ export class Runner {
738
779
 
739
780
  const prunedCount = originalLength - this.#eventHistory.length;
740
781
  if (prunedCount > 0) {
741
- logger()?.info({
782
+ this.log?.info({
742
783
  msg: "pruned acknowledged events",
743
- runnerId: this.runnerId,
744
784
  lastAckedIdx: lastAckedIdx.toString(),
745
785
  prunedCount,
746
786
  });
@@ -760,9 +800,8 @@ export class Runner {
760
800
  !this.#eventBacklogWarned
761
801
  ) {
762
802
  this.#eventBacklogWarned = true;
763
- logger()?.warn({
803
+ this.log?.warn({
764
804
  msg: "unacknowledged event backlog exceeds threshold",
765
- runnerId: this.runnerId,
766
805
  backlogSize: this.#eventHistory.length,
767
806
  threshold: EVENT_BACKLOG_WARN_THRESHOLD,
768
807
  });
@@ -801,9 +840,8 @@ export class Runner {
801
840
  this.#config
802
841
  .onActorStart(actorId, generation, actorConfig)
803
842
  .catch((err) => {
804
- logger()?.error({
843
+ this.log?.error({
805
844
  msg: "error in onactorstart for actor",
806
- runnerId: this.runnerId,
807
845
  actorId,
808
846
  err,
809
847
  });
@@ -830,9 +868,9 @@ export class Runner {
830
868
  intentType: "sleep" | "stop",
831
869
  ) {
832
870
  if (this.#shutdown) {
833
- logger()?.warn({
871
+ console.trace("send actor intent", actorId, intentType);
872
+ this.log?.warn({
834
873
  msg: "Runner is shut down, cannot send actor intent",
835
- runnerId: this.runnerId,
836
874
  });
837
875
  return;
838
876
  }
@@ -866,12 +904,10 @@ export class Runner {
866
904
 
867
905
  this.#recordEvent(eventWrapper);
868
906
 
869
- logger()?.info({
907
+ this.log?.info({
870
908
  msg: "sending event to server",
871
- runnerId: this.runnerId,
872
- index: eventWrapper.index,
873
- tag: eventWrapper.inner.tag,
874
- val: eventWrapper.inner.val,
909
+ event: stringifyEvent(eventWrapper.inner),
910
+ index: eventWrapper.index.toString(),
875
911
  });
876
912
 
877
913
  this.__sendToServer({
@@ -886,9 +922,8 @@ export class Runner {
886
922
  stateType: "running" | "stopped",
887
923
  ) {
888
924
  if (this.#shutdown) {
889
- logger()?.warn({
925
+ this.log?.warn({
890
926
  msg: "Runner is shut down, cannot send actor state update",
891
- runnerId: this.runnerId,
892
927
  });
893
928
  return;
894
929
  }
@@ -925,12 +960,10 @@ export class Runner {
925
960
 
926
961
  this.#recordEvent(eventWrapper);
927
962
 
928
- logger()?.info({
963
+ this.log?.info({
929
964
  msg: "sending event to server",
930
- runnerId: this.runnerId,
931
- index: eventWrapper.index,
932
- tag: eventWrapper.inner.tag,
933
- val: eventWrapper.inner.val,
965
+ event: stringifyEvent(eventWrapper.inner),
966
+ index: eventWrapper.index.toString(),
934
967
  });
935
968
 
936
969
  this.__sendToServer({
@@ -941,9 +974,8 @@ export class Runner {
941
974
 
942
975
  #sendCommandAcknowledgment() {
943
976
  if (this.#shutdown) {
944
- logger()?.warn({
977
+ this.log?.warn({
945
978
  msg: "Runner is shut down, cannot send command acknowledgment",
946
- runnerId: this.runnerId,
947
979
  });
948
980
  return;
949
981
  }
@@ -953,7 +985,7 @@ export class Runner {
953
985
  return;
954
986
  }
955
987
 
956
- //logger()?.log("Sending command acknowledgment", this.#lastCommandIdx);
988
+ //this.#log?.log("Sending command acknowledgment", this.#lastCommandIdx);
957
989
 
958
990
  this.__sendToServer({
959
991
  tag: "ToServerAckCommands",
@@ -968,8 +1000,10 @@ export class Runner {
968
1000
  const request = this.#kvRequests.get(requestId);
969
1001
 
970
1002
  if (!request) {
971
- const msg = "received kv response for unknown request id";
972
- logger()?.error({ msg, runnerId: this.runnerId, requestId });
1003
+ this.log?.error({
1004
+ msg: "received kv response for unknown request id",
1005
+ requestId,
1006
+ });
973
1007
  return;
974
1008
  }
975
1009
 
@@ -1362,7 +1396,7 @@ export class Runner {
1362
1396
  }
1363
1397
 
1364
1398
  if (processedCount > 0) {
1365
- //logger()?.log(`Processed ${processedCount} queued KV requests`);
1399
+ //this.#log?.log(`Processed ${processedCount} queued KV requests`);
1366
1400
  }
1367
1401
  }
1368
1402
 
@@ -1374,9 +1408,8 @@ export class Runner {
1374
1408
 
1375
1409
  __sendToServer(message: protocol.ToServer) {
1376
1410
  if (this.#shutdown) {
1377
- logger()?.warn({
1411
+ this.log?.warn({
1378
1412
  msg: "Runner is shut down, cannot send message to server",
1379
- runnerId: this.runnerId,
1380
1413
  });
1381
1414
  return;
1382
1415
  }
@@ -1388,9 +1421,8 @@ export class Runner {
1388
1421
  ) {
1389
1422
  this.#pegboardWebSocket.send(encoded);
1390
1423
  } else {
1391
- logger()?.error({
1424
+ this.log?.error({
1392
1425
  msg: "WebSocket not available or not open for sending data",
1393
- runnerId: this.runnerId,
1394
1426
  });
1395
1427
  }
1396
1428
  }
@@ -1419,9 +1451,8 @@ export class Runner {
1419
1451
 
1420
1452
  #scheduleReconnect() {
1421
1453
  if (this.#shutdown) {
1422
- logger()?.debug({
1454
+ this.log?.debug({
1423
1455
  msg: "Runner is shut down, not attempting reconnect",
1424
- runnerId: this.runnerId,
1425
1456
  });
1426
1457
  return;
1427
1458
  }
@@ -1433,17 +1464,15 @@ export class Runner {
1433
1464
  jitter: true,
1434
1465
  });
1435
1466
 
1436
- logger()?.debug({
1467
+ this.log?.debug({
1437
1468
  msg: `Scheduling reconnect attempt ${this.#reconnectAttempt + 1} in ${delay}ms`,
1438
- runnerId: this.runnerId,
1439
1469
  });
1440
1470
 
1441
1471
  this.#reconnectTimeout = setTimeout(async () => {
1442
1472
  if (!this.#shutdown) {
1443
1473
  this.#reconnectAttempt++;
1444
- logger()?.debug({
1474
+ this.log?.debug({
1445
1475
  msg: `Attempting to reconnect (attempt ${this.#reconnectAttempt})...`,
1446
- runnerId: this.runnerId,
1447
1476
  });
1448
1477
  await this.#openPegboardWebSocket();
1449
1478
  }
@@ -1457,7 +1486,7 @@ export class Runner {
1457
1486
 
1458
1487
  if (eventsToResend.length === 0) return;
1459
1488
 
1460
- //logger()?.log(
1489
+ //this.#log?.log(
1461
1490
  // `Resending ${eventsToResend.length} unacknowledged events from index ${Number(lastEventIdx) + 1}`,
1462
1491
  //);
1463
1492
 
@@ -1488,7 +1517,7 @@ export class Runner {
1488
1517
  }
1489
1518
 
1490
1519
  if (toDelete.length > 0) {
1491
- //logger()?.log(`Cleaned up ${toDelete.length} expired KV requests`);
1520
+ //this.#log?.log(`Cleaned up ${toDelete.length} expired KV requests`);
1492
1521
  }
1493
1522
  }
1494
1523
  }