@testdriverai/runner 7.11.78-canary → 7.11.79-test
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 +32 -4
- package/lib/ably-service.js +15 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/ably-service.js
CHANGED
|
@@ -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) {
|