@stage5/lumine 0.2.56 → 0.2.57

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 (2) hide show
  1. package/lib/sponsor.js +44 -10
  2. package/package.json +1 -1
package/lib/sponsor.js CHANGED
@@ -312,6 +312,9 @@ async function runSponsorDuty({
312
312
  process.on("SIGINT", signalHandler);
313
313
  process.on("SIGTERM", signalHandler);
314
314
 
315
+ const dutyHeartbeatEveryMs =
316
+ Math.max(10, Number(started.heartbeatEverySeconds || 30)) * 1_000;
317
+ let lastDutyHeartbeatSuccessAt = Date.now();
315
318
  const heartbeatDuty = async () => {
316
319
  if (dutyHeartbeatRunning) return;
317
320
  dutyHeartbeatRunning = true;
@@ -324,6 +327,7 @@ async function runSponsorDuty({
324
327
  body: { leaseToken },
325
328
  });
326
329
  dutyLeaseError = null;
330
+ lastDutyHeartbeatSuccessAt = Date.now();
327
331
  if (canonicalDuty.state === "stopped") stopRequested = true;
328
332
  } catch (error) {
329
333
  const status = await sponsorRequest({
@@ -340,15 +344,24 @@ async function runSponsorDuty({
340
344
  stopRequested = true;
341
345
  return;
342
346
  }
343
- dutyLeaseError = error;
344
- stopRequested = true;
347
+ // Same lease policy as the per-job heartbeat: end the duty only when the
348
+ // server rejects the lease or the lease window has fully elapsed. A
349
+ // transient network failure retries on the next tick.
350
+ const terminalDutyError = !isRetryableSponsorDutyRequestError(error);
351
+ const leaseWindowElapsed =
352
+ Date.now() - lastDutyHeartbeatSuccessAt >=
353
+ Math.max(120_000, dutyHeartbeatEveryMs * 3);
354
+ if (terminalDutyError || leaseWindowElapsed) {
355
+ dutyLeaseError = error;
356
+ stopRequested = true;
357
+ }
345
358
  } finally {
346
359
  dutyHeartbeatRunning = false;
347
360
  }
348
361
  };
349
362
  const dutyHeartbeat = setInterval(
350
363
  () => void heartbeatDuty(),
351
- Math.max(10, Number(started.heartbeatEverySeconds || 30)) * 1_000,
364
+ dutyHeartbeatEveryMs,
352
365
  );
353
366
 
354
367
  console.log(`Shared Zero/Ciel sponsor duty started with ${provider}.`);
@@ -368,13 +381,28 @@ async function runSponsorDuty({
368
381
  Number(canonicalDuty.capacity?.maxConcurrentTasks || 1),
369
382
  );
370
383
  while (!stopRequested && activeJobs.size < concurrency) {
371
- const claim = await sponsorRequest({
372
- options,
373
- auth,
374
- method: "POST",
375
- path: "/jobs/claim",
376
- body: { dutySessionId: dutyId, leaseToken },
377
- });
384
+ let claim;
385
+ try {
386
+ claim = await sponsorRequest({
387
+ options,
388
+ auth,
389
+ method: "POST",
390
+ path: "/jobs/claim",
391
+ body: { dutySessionId: dutyId, leaseToken },
392
+ });
393
+ } catch (error) {
394
+ if (!isRetryableSponsorDutyRequestError(error)) {
395
+ dutyLeaseError = error;
396
+ stopRequested = true;
397
+ break;
398
+ }
399
+ // A failed claim must not kill the duty process; the heartbeat owns
400
+ // lease validity, and the outer loop polls again after a sleep.
401
+ console.error(
402
+ `Workshop claim failed; retrying next poll: ${error?.message || error}`,
403
+ );
404
+ break;
405
+ }
378
406
  if (!claim.job) break;
379
407
  const work = processSponsorJob({
380
408
  options,
@@ -1133,6 +1161,12 @@ async function retrySponsorTransport(operation) {
1133
1161
  throw lastError;
1134
1162
  }
1135
1163
 
1164
+ function isRetryableSponsorDutyRequestError(error) {
1165
+ const status = Number(error?.status || 0);
1166
+ if (!status) return true;
1167
+ return status === 408 || status === 425 || status === 429 || status >= 500;
1168
+ }
1169
+
1136
1170
  async function sponsorRequest({
1137
1171
  options,
1138
1172
  auth,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage5/lumine",
3
- "version": "0.2.56",
3
+ "version": "0.2.57",
4
4
  "description": "Command line tools for launching Lumine builds on Twinkle.",
5
5
  "type": "module",
6
6
  "bin": {