bitfab 0.61.3 → 0.61.4

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 (32) hide show
  1. package/dist/{chunk-6IMUONDN.js → chunk-54EGXUZ3.js} +27 -2
  2. package/dist/chunk-54EGXUZ3.js.map +1 -0
  3. package/dist/{chunk-IZUZ5NBB.js → chunk-PSAEQ4OW.js} +176 -10
  4. package/dist/chunk-PSAEQ4OW.js.map +1 -0
  5. package/dist/{chunk-HVEZHLVQ.js → chunk-VWRCLZCA.js} +26 -2
  6. package/dist/{chunk-HVEZHLVQ.js.map → chunk-VWRCLZCA.js.map} +1 -1
  7. package/dist/{chunk-WOHIAKZF.js → chunk-WPYBJGTG.js} +21 -3
  8. package/dist/chunk-WPYBJGTG.js.map +1 -0
  9. package/dist/{chunk-PAUXHTNF.js → chunk-XGRQMQ2R.js} +6 -6
  10. package/dist/{chunk-PAUXHTNF.js.map → chunk-XGRQMQ2R.js.map} +1 -1
  11. package/dist/{http-BYICBOWP.js → http-C2TCBBQV.js} +2 -2
  12. package/dist/{http-3IYU5Z54.js → http-ONLJPP6W.js} +2 -2
  13. package/dist/index.cjs +214 -9
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +13 -2
  16. package/dist/index.d.ts +13 -2
  17. package/dist/index.js +3 -3
  18. package/dist/node.cjs +214 -9
  19. package/dist/node.cjs.map +1 -1
  20. package/dist/node.d.cts +1 -1
  21. package/dist/node.d.ts +1 -1
  22. package/dist/node.js +3 -3
  23. package/dist/{replay-SXHX5BBU.js → replay-RM24QKEA.js} +3 -3
  24. package/dist/replayCli.js +2 -2
  25. package/dist/seedCli.js +2 -2
  26. package/package.json +1 -1
  27. package/dist/chunk-6IMUONDN.js.map +0 -1
  28. package/dist/chunk-IZUZ5NBB.js.map +0 -1
  29. package/dist/chunk-WOHIAKZF.js.map +0 -1
  30. /package/dist/{http-3IYU5Z54.js.map → http-C2TCBBQV.js.map} +0 -0
  31. /package/dist/{http-BYICBOWP.js.map → http-ONLJPP6W.js.map} +0 -0
  32. /package/dist/{replay-SXHX5BBU.js.map → replay-RM24QKEA.js.map} +0 -0
@@ -4,8 +4,9 @@ import {
4
4
  flushTraces,
5
5
  gitRunner,
6
6
  replayContextReady,
7
- runWithReplayContext
8
- } from "./chunk-6IMUONDN.js";
7
+ runWithReplayContext,
8
+ unrefTimer
9
+ } from "./chunk-54EGXUZ3.js";
9
10
  import {
10
11
  runWithCaptureSuppressed,
11
12
  warnOnce
@@ -437,6 +438,101 @@ function fallbackUuidV4() {
437
438
  });
438
439
  }
439
440
 
441
+ // src/replayHeartbeat.ts
442
+ var REPLAY_HEARTBEAT_INTERVAL_MS = 6e4;
443
+ function startReplayHeartbeat(send) {
444
+ let stopped = false;
445
+ let inFlight;
446
+ const timer = setInterval(() => {
447
+ if (stopped || inFlight !== void 0) {
448
+ return;
449
+ }
450
+ inFlight = send().catch(() => void 0).finally(() => {
451
+ inFlight = void 0;
452
+ });
453
+ }, REPLAY_HEARTBEAT_INTERVAL_MS);
454
+ unrefTimer(timer);
455
+ return {
456
+ stop: async () => {
457
+ stopped = true;
458
+ clearInterval(timer);
459
+ await inFlight;
460
+ }
461
+ };
462
+ }
463
+
464
+ // src/replayInterrupt.ts
465
+ var REPLAY_INTERRUPT_SIGNALS = [
466
+ "SIGINT",
467
+ "SIGTERM"
468
+ ];
469
+ function signalCapableProcess() {
470
+ if (typeof process === "undefined" || typeof process.on !== "function") {
471
+ return void 0;
472
+ }
473
+ return process;
474
+ }
475
+ function describeReplayInterrupt(experimentId, markedInterrupted) {
476
+ if (markedInterrupted) {
477
+ return `[replay] Experiment ${experimentId} interrupted.`;
478
+ }
479
+ return `[replay] Experiment ${experimentId} stopped, but Bitfab could not mark it interrupted. It will be marked interrupted within 15 minutes. A resume in the next two minutes may be refused as still running.`;
480
+ }
481
+ function reportInterrupted(experimentId, markedInterrupted) {
482
+ try {
483
+ console.error(describeReplayInterrupt(experimentId, markedInterrupted));
484
+ } catch {
485
+ }
486
+ }
487
+ function installReplayInterruptHandlers(target) {
488
+ const host = signalCapableProcess();
489
+ if (host === void 0) {
490
+ return { remove: () => {
491
+ }, interrupted: () => false };
492
+ }
493
+ const handlers = /* @__PURE__ */ new Map();
494
+ const removeHandlers = () => {
495
+ for (const [signal, handler] of handlers) {
496
+ host.removeListener(signal, handler);
497
+ }
498
+ handlers.clear();
499
+ };
500
+ let interrupting = false;
501
+ const handleSignal = async (signal) => {
502
+ const hostHandlesSignal = host.listenerCount(signal) > 1;
503
+ const markedInterrupted = await target.interrupt();
504
+ removeHandlers();
505
+ if (target.onInterrupt !== void 0) {
506
+ try {
507
+ target.onInterrupt({
508
+ experimentId: target.experimentId,
509
+ experimentUrl: target.experimentUrl,
510
+ signal,
511
+ markedInterrupted
512
+ });
513
+ } catch {
514
+ }
515
+ return;
516
+ }
517
+ reportInterrupted(target.experimentId, markedInterrupted);
518
+ if (!hostHandlesSignal) {
519
+ host.kill(host.pid, signal);
520
+ }
521
+ };
522
+ for (const signal of REPLAY_INTERRUPT_SIGNALS) {
523
+ const handler = () => {
524
+ if (interrupting) {
525
+ return;
526
+ }
527
+ interrupting = true;
528
+ void handleSignal(signal);
529
+ };
530
+ handlers.set(signal, handler);
531
+ host.on(signal, handler);
532
+ }
533
+ return { remove: removeHandlers, interrupted: () => interrupting };
534
+ }
535
+
440
536
  // src/selectiveReplay.ts
441
537
  function replayNodeKey(node) {
442
538
  return JSON.stringify([node.traceFunctionKey, node.spanName]);
@@ -1357,7 +1453,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1357
1453
  const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
1358
1454
  let session;
1359
1455
  if (options?.resume === void 0) {
1360
- const started2 = await httpClient.startReplay(
1456
+ const started = await httpClient.startReplay(
1361
1457
  traceFunctionKey,
1362
1458
  // limit is meaningless with explicit traceIds (the ID list determines
1363
1459
  // the count), so it's omitted from the request entirely.
@@ -1380,7 +1476,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1380
1476
  options?.judgeAssertions,
1381
1477
  options?.experimentalSelectiveReplay
1382
1478
  );
1383
- if (started2.items.length === 0) {
1479
+ if (started.items.length === 0) {
1384
1480
  try {
1385
1481
  console.warn(
1386
1482
  `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
@@ -1389,10 +1485,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1389
1485
  }
1390
1486
  }
1391
1487
  session = {
1392
- experimentId: started2.experimentId,
1393
- experimentUrl: started2.experimentUrl,
1488
+ experimentId: started.experimentId,
1489
+ experimentUrl: started.experimentUrl,
1394
1490
  attempts: requestedAttempts,
1395
- work: expandReplayWork(started2.items, requestedAttempts),
1491
+ work: expandReplayWork(started.items, requestedAttempts),
1396
1492
  finished: [],
1397
1493
  includeDbBranchLease,
1398
1494
  dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
@@ -1407,9 +1503,68 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1407
1503
  options.dbBranch
1408
1504
  );
1409
1505
  }
1410
- const { experimentId, experimentUrl, attempts } = session;
1506
+ const { experimentId } = session;
1507
+ const fullExperimentUrl = `${serviceUrl}${session.experimentUrl}`;
1508
+ const heartbeat = startReplayHeartbeat(
1509
+ () => httpClient.heartbeatReplay(experimentId)
1510
+ );
1511
+ const interruptReplay = async () => {
1512
+ await heartbeat.stop();
1513
+ return httpClient.interruptReplay(experimentId);
1514
+ };
1515
+ const interruptHandlers = installReplayInterruptHandlers({
1516
+ experimentId,
1517
+ experimentUrl: fullExperimentUrl,
1518
+ interrupt: interruptReplay,
1519
+ onInterrupt: options?.onInterrupt
1520
+ });
1521
+ let completeReturned = false;
1522
+ const markCompleteReturned = () => {
1523
+ completeReturned = true;
1524
+ void heartbeat.stop();
1525
+ };
1526
+ try {
1527
+ return await runReplaySession(
1528
+ httpClient,
1529
+ fn,
1530
+ session,
1531
+ fullExperimentUrl,
1532
+ maxConcurrency,
1533
+ interruptHandlers.remove,
1534
+ markCompleteReturned,
1535
+ interruptHandlers.interrupted,
1536
+ options,
1537
+ registeredOverrides
1538
+ );
1539
+ } catch (error) {
1540
+ if (!completeReturned && !interruptHandlers.interrupted()) {
1541
+ await interruptReplay();
1542
+ }
1543
+ throw error;
1544
+ } finally {
1545
+ interruptHandlers.remove();
1546
+ await heartbeat.stop();
1547
+ }
1548
+ }
1549
+ function throwIfInterrupted(interrupted, items, experimentId, experimentUrl) {
1550
+ if (!interrupted()) {
1551
+ return;
1552
+ }
1553
+ const cause = new BitfabError(
1554
+ `Experiment ${experimentId} was interrupted, so it was not completed or graded. Resume it by passing { resume: "${experimentId}" } to replay, or with bitfab-replay --resume ${experimentId}.`,
1555
+ experimentUrl
1556
+ );
1557
+ throw new ReplayError(
1558
+ cause.message,
1559
+ items,
1560
+ experimentId,
1561
+ experimentUrl,
1562
+ cause
1563
+ );
1564
+ }
1565
+ async function runReplaySession(httpClient, fn, session, fullExperimentUrl, maxConcurrency, stopInterruptingOnSignal, markCompleteReturned, interrupted, options, registeredOverrides) {
1566
+ const { experimentId, attempts } = session;
1411
1567
  const mockStrategy = options?.mock ?? "marked";
1412
- const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
1413
1568
  try {
1414
1569
  options?.onExperimentStart?.({
1415
1570
  experimentId,
@@ -1559,7 +1714,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1559
1714
  );
1560
1715
  const resultItems = [...carriedOverItems, ...ranItems];
1561
1716
  if (options?.dryRun === true) {
1717
+ throwIfInterrupted(
1718
+ interrupted,
1719
+ resultItems,
1720
+ experimentId,
1721
+ fullExperimentUrl
1722
+ );
1723
+ stopInterruptingOnSignal();
1562
1724
  await httpClient.completeReplay(experimentId).catch(() => void 0);
1725
+ markCompleteReturned();
1563
1726
  const dryResult = {
1564
1727
  items: resultItems,
1565
1728
  experimentId,
@@ -1584,12 +1747,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
1584
1747
  ranItems[index].traceId = readBack;
1585
1748
  }
1586
1749
  }
1750
+ throwIfInterrupted(interrupted, resultItems, experimentId, fullExperimentUrl);
1751
+ stopInterruptingOnSignal();
1587
1752
  const completeResult = await preserveReplayFailure(
1588
1753
  () => httpClient.completeReplay(experimentId),
1589
1754
  resultItems,
1590
1755
  experimentId,
1591
1756
  fullExperimentUrl
1592
1757
  );
1758
+ markCompleteReturned();
1593
1759
  const serverTraceIds = completeResult.traceIds;
1594
1760
  const replayTokens = completeResult.tokens;
1595
1761
  if (serverTraceIds !== void 0) {
@@ -1702,4 +1868,4 @@ export {
1702
1868
  sleepForReplayPersistence,
1703
1869
  replay
1704
1870
  };
1705
- //# sourceMappingURL=chunk-IZUZ5NBB.js.map
1871
+ //# sourceMappingURL=chunk-PSAEQ4OW.js.map