@testdriverai/runner 7.11.78-canary → 7.11.79-canary

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/index.js CHANGED
@@ -333,9 +333,23 @@ class PresenceRunner {
333
333
  }
334
334
 
335
335
  startHeartbeat() {
336
- // Update presence every 30 seconds with lastSeen timestamp
336
+ // Update presence every ~30 seconds with lastSeen timestamp
337
337
  // This allows detection of zombie runners (present but not responding)
338
- this.heartbeatInterval = setInterval(async () => {
338
+ //
339
+ // All runners in a fleet tend to boot around the same time (autoscaling
340
+ // group launches, deploys), which would otherwise make every runner's
341
+ // heartbeat fire in lockstep on the shared per-team "runners" presence
342
+ // channel. That thundering-herd burst can push the channel over Ably's
343
+ // per-channel publish rate limit as the fleet grows. Use a
344
+ // self-rescheduling setTimeout with random jitter (instead of a fixed
345
+ // setInterval) so heartbeats spread out over time rather than
346
+ // clustering at the same instant.
347
+ const HEARTBEAT_BASE_MS = 30000;
348
+ const HEARTBEAT_JITTER_MS = 5000; // +/- up to 5s
349
+ // Integer in the inclusive range [-HEARTBEAT_JITTER_MS, HEARTBEAT_JITTER_MS]
350
+ const nextDelay = () => HEARTBEAT_BASE_MS + Math.floor(Math.random() * (2 * HEARTBEAT_JITTER_MS + 1)) - HEARTBEAT_JITTER_MS;
351
+
352
+ const beat = async () => {
339
353
  if (this.shuttingDown) return;
340
354
  try {
341
355
  // Free space rides along on the heartbeat so every runner reports its
@@ -355,12 +369,26 @@ class PresenceRunner {
355
369
  } catch (err) {
356
370
  log(`Warning: Heartbeat update failed: ${err.message}`);
357
371
  }
358
- }, 30000);
372
+ // Reschedule regardless of success/failure (so a transient publish
373
+ // error doesn't permanently kill the heartbeat), but only if the
374
+ // runner isn't shutting down. There is no `await` between this check
375
+ // and the `setTimeout` call, so `stopHeartbeat()` cannot race with it
376
+ // and clear a handle we're about to overwrite — Node's single-threaded
377
+ // event loop guarantees this block runs to completion uninterrupted.
378
+ if (!this.shuttingDown) {
379
+ this.heartbeatInterval = setTimeout(beat, nextDelay());
380
+ }
381
+ };
382
+
383
+ this.heartbeatInterval = setTimeout(beat, nextDelay());
359
384
  }
360
385
 
361
386
  stopHeartbeat() {
362
387
  if (this.heartbeatInterval) {
363
- clearInterval(this.heartbeatInterval);
388
+ // heartbeatInterval now holds a setTimeout handle (self-rescheduling
389
+ // heartbeat with jitter — see startHeartbeat), but clearTimeout also
390
+ // safely clears a setInterval handle, so this stays defensive either way.
391
+ clearTimeout(this.heartbeatInterval);
364
392
  this.heartbeatInterval = null;
365
393
  }
366
394
  }
@@ -311,6 +311,7 @@ class AblyService extends EventEmitter {
311
311
  this._execOutputDraining = false;
312
312
 
313
313
  this._automation.on('exec.output', ({ requestId, chunk }) => {
314
+ if (this._closed) return; // don't enqueue once we're tearing down
314
315
  this._execOutputQueue.push({ requestId, chunk });
315
316
  this._drainExecOutputQueue();
316
317
  });
@@ -593,6 +594,15 @@ class AblyService extends EventEmitter {
593
594
 
594
595
  try {
595
596
  while (this._execOutputQueue.length > 0) {
597
+ // Stop draining once close() has started tearing down the channel —
598
+ // a publish landing after detach() can make Ably silently
599
+ // re-attach the channel, doubling channel-open/close lifecycle
600
+ // events for what should be one clean session teardown.
601
+ if (this._closed) {
602
+ this._execOutputQueue.length = 0;
603
+ break;
604
+ }
605
+
596
606
  // Rate limit: wait if needed
597
607
  const now = Date.now();
598
608
  const elapsed = now - this._execOutputLastTime;
@@ -600,6 +610,11 @@ class AblyService extends EventEmitter {
600
610
  await new Promise((resolve) => setTimeout(resolve, this._execOutputMinIntervalMs - elapsed));
601
611
  }
602
612
 
613
+ if (this._closed) {
614
+ this._execOutputQueue.length = 0;
615
+ break;
616
+ }
617
+
603
618
  // Coalesce all queued chunks for the same requestId
604
619
  const batch = {};
605
620
  while (this._execOutputQueue.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testdriverai/runner",
3
- "version": "7.11.78-canary",
3
+ "version": "7.11.79-canary",
4
4
  "description": "TestDriver Runner - Ably-based remote automation agent with Node.js automation",
5
5
  "main": "index.js",
6
6
  "bin": {