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
@@ -7,7 +7,7 @@ import {
7
7
  flushTraces,
8
8
  parseRetryAfterMs,
9
9
  serializePayloadBody
10
- } from "./chunk-HVEZHLVQ.js";
10
+ } from "./chunk-VWRCLZCA.js";
11
11
  export {
12
12
  BitfabError,
13
13
  HttpClient,
@@ -18,4 +18,4 @@ export {
18
18
  parseRetryAfterMs,
19
19
  serializePayloadBody
20
20
  };
21
- //# sourceMappingURL=http-BYICBOWP.js.map
21
+ //# sourceMappingURL=http-C2TCBBQV.js.map
@@ -7,7 +7,7 @@ import {
7
7
  flushTraces,
8
8
  parseRetryAfterMs,
9
9
  serializePayloadBody
10
- } from "./chunk-6IMUONDN.js";
10
+ } from "./chunk-54EGXUZ3.js";
11
11
  import "./chunk-BAOIRE5T.js";
12
12
  export {
13
13
  BitfabError,
@@ -19,4 +19,4 @@ export {
19
19
  parseRetryAfterMs,
20
20
  serializePayloadBody
21
21
  };
22
- //# sourceMappingURL=http-3IYU5Z54.js.map
22
+ //# sourceMappingURL=http-ONLJPP6W.js.map
package/dist/index.cjs CHANGED
@@ -51,7 +51,7 @@ var __version__, __packageName__;
51
51
  var init_version_generated = __esm({
52
52
  "src/version.generated.ts"() {
53
53
  "use strict";
54
- __version__ = "0.61.3";
54
+ __version__ = "0.61.4";
55
55
  __packageName__ = "bitfab";
56
56
  }
57
57
  });
@@ -2286,7 +2286,7 @@ function warnDroppedExternalTrace(error) {
2286
2286
  function experimentIdWireFields(experimentId) {
2287
2287
  return { experimentId, testRunId: experimentId };
2288
2288
  }
2289
- var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, REPLAY_COMPLETE_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, liveHttpClients, carrierSeq, HttpClient;
2289
+ var REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS, REPLAY_COMPLETE_REQUEST_TIMEOUT_MS, REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS, REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS, OTLP_TRACES_ENDPOINT, RETRYABLE_STATUSES, EXIT_FLUSH_TIMEOUT_MS, DEFAULT_LIFECYCLE_TIMEOUT_MS2, pendingTracePromises, liveHttpClients, carrierSeq, HttpClient;
2290
2290
  var init_http = __esm({
2291
2291
  "src/http.ts"() {
2292
2292
  "use strict";
@@ -2305,6 +2305,8 @@ var init_http = __esm({
2305
2305
  init_warnOnce();
2306
2306
  REPLAY_DB_BRANCH_REQUEST_TIMEOUT_MS = 3e5;
2307
2307
  REPLAY_COMPLETE_REQUEST_TIMEOUT_MS = 12e4;
2308
+ REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS = 5e3;
2309
+ REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS = 5e3;
2308
2310
  OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces";
2309
2311
  RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
2310
2312
  EXIT_FLUSH_TIMEOUT_MS = 5e3;
@@ -3117,6 +3119,28 @@ var init_http = __esm({
3117
3119
  { timeout: REPLAY_COMPLETE_REQUEST_TIMEOUT_MS }
3118
3120
  );
3119
3121
  }
3122
+ async interruptReplay(experimentId) {
3123
+ try {
3124
+ await this.request(
3125
+ "/api/sdk/replay/interrupt",
3126
+ experimentIdWireFields(experimentId),
3127
+ { timeout: REPLAY_INTERRUPT_REQUEST_TIMEOUT_MS }
3128
+ );
3129
+ return true;
3130
+ } catch {
3131
+ return false;
3132
+ }
3133
+ }
3134
+ async heartbeatReplay(experimentId) {
3135
+ try {
3136
+ await this.request(
3137
+ "/api/sdk/replay/heartbeat",
3138
+ experimentIdWireFields(experimentId),
3139
+ { timeout: REPLAY_HEARTBEAT_REQUEST_TIMEOUT_MS }
3140
+ );
3141
+ } catch {
3142
+ }
3143
+ }
3120
3144
  async getReseedSource(traceId) {
3121
3145
  return this.get(
3122
3146
  `/api/sdk/traces/${encodeURIComponent(traceId)}/reseedSource`
@@ -3620,6 +3644,114 @@ var init_integrationFixture = __esm({
3620
3644
  }
3621
3645
  });
3622
3646
 
3647
+ // src/replayHeartbeat.ts
3648
+ function startReplayHeartbeat(send) {
3649
+ let stopped = false;
3650
+ let inFlight;
3651
+ const timer = setInterval(() => {
3652
+ if (stopped || inFlight !== void 0) {
3653
+ return;
3654
+ }
3655
+ inFlight = send().catch(() => void 0).finally(() => {
3656
+ inFlight = void 0;
3657
+ });
3658
+ }, REPLAY_HEARTBEAT_INTERVAL_MS);
3659
+ unrefTimer(timer);
3660
+ return {
3661
+ stop: async () => {
3662
+ stopped = true;
3663
+ clearInterval(timer);
3664
+ await inFlight;
3665
+ }
3666
+ };
3667
+ }
3668
+ var REPLAY_HEARTBEAT_INTERVAL_MS;
3669
+ var init_replayHeartbeat = __esm({
3670
+ "src/replayHeartbeat.ts"() {
3671
+ "use strict";
3672
+ init_unrefTimer();
3673
+ REPLAY_HEARTBEAT_INTERVAL_MS = 6e4;
3674
+ }
3675
+ });
3676
+
3677
+ // src/replayInterrupt.ts
3678
+ function signalCapableProcess() {
3679
+ if (typeof process === "undefined" || typeof process.on !== "function") {
3680
+ return void 0;
3681
+ }
3682
+ return process;
3683
+ }
3684
+ function describeReplayInterrupt(experimentId, markedInterrupted) {
3685
+ if (markedInterrupted) {
3686
+ return `[replay] Experiment ${experimentId} interrupted.`;
3687
+ }
3688
+ 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.`;
3689
+ }
3690
+ function reportInterrupted(experimentId, markedInterrupted) {
3691
+ try {
3692
+ console.error(describeReplayInterrupt(experimentId, markedInterrupted));
3693
+ } catch {
3694
+ }
3695
+ }
3696
+ function installReplayInterruptHandlers(target) {
3697
+ const host = signalCapableProcess();
3698
+ if (host === void 0) {
3699
+ return { remove: () => {
3700
+ }, interrupted: () => false };
3701
+ }
3702
+ const handlers = /* @__PURE__ */ new Map();
3703
+ const removeHandlers = () => {
3704
+ for (const [signal, handler] of handlers) {
3705
+ host.removeListener(signal, handler);
3706
+ }
3707
+ handlers.clear();
3708
+ };
3709
+ let interrupting = false;
3710
+ const handleSignal = async (signal) => {
3711
+ const hostHandlesSignal = host.listenerCount(signal) > 1;
3712
+ const markedInterrupted = await target.interrupt();
3713
+ removeHandlers();
3714
+ if (target.onInterrupt !== void 0) {
3715
+ try {
3716
+ target.onInterrupt({
3717
+ experimentId: target.experimentId,
3718
+ experimentUrl: target.experimentUrl,
3719
+ signal,
3720
+ markedInterrupted
3721
+ });
3722
+ } catch {
3723
+ }
3724
+ return;
3725
+ }
3726
+ reportInterrupted(target.experimentId, markedInterrupted);
3727
+ if (!hostHandlesSignal) {
3728
+ host.kill(host.pid, signal);
3729
+ }
3730
+ };
3731
+ for (const signal of REPLAY_INTERRUPT_SIGNALS) {
3732
+ const handler = () => {
3733
+ if (interrupting) {
3734
+ return;
3735
+ }
3736
+ interrupting = true;
3737
+ void handleSignal(signal);
3738
+ };
3739
+ handlers.set(signal, handler);
3740
+ host.on(signal, handler);
3741
+ }
3742
+ return { remove: removeHandlers, interrupted: () => interrupting };
3743
+ }
3744
+ var REPLAY_INTERRUPT_SIGNALS;
3745
+ var init_replayInterrupt = __esm({
3746
+ "src/replayInterrupt.ts"() {
3747
+ "use strict";
3748
+ REPLAY_INTERRUPT_SIGNALS = [
3749
+ "SIGINT",
3750
+ "SIGTERM"
3751
+ ];
3752
+ }
3753
+ });
3754
+
3623
3755
  // src/selectiveReplay.ts
3624
3756
  function replayNodeKey(node) {
3625
3757
  return JSON.stringify([node.traceFunctionKey, node.spanName]);
@@ -4529,7 +4661,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4529
4661
  const includeDbBranchLease = dbBranchEnabled(options?.dbBranch) && options?.dryRun !== true;
4530
4662
  let session;
4531
4663
  if (options?.resume === void 0) {
4532
- const started2 = await httpClient.startReplay(
4664
+ const started = await httpClient.startReplay(
4533
4665
  traceFunctionKey,
4534
4666
  // limit is meaningless with explicit traceIds (the ID list determines
4535
4667
  // the count), so it's omitted from the request entirely.
@@ -4552,7 +4684,7 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4552
4684
  options?.judgeAssertions,
4553
4685
  options?.experimentalSelectiveReplay
4554
4686
  );
4555
- if (started2.items.length === 0) {
4687
+ if (started.items.length === 0) {
4556
4688
  try {
4557
4689
  console.warn(
4558
4690
  `Bitfab: no traces matched "${traceFunctionKey}", so this replay ran nothing. Capture a trace, or seed one with seedTrace, before replaying.`
@@ -4561,10 +4693,10 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4561
4693
  }
4562
4694
  }
4563
4695
  session = {
4564
- experimentId: started2.experimentId,
4565
- experimentUrl: started2.experimentUrl,
4696
+ experimentId: started.experimentId,
4697
+ experimentUrl: started.experimentUrl,
4566
4698
  attempts: requestedAttempts,
4567
- work: expandReplayWork(started2.items, requestedAttempts),
4699
+ work: expandReplayWork(started.items, requestedAttempts),
4568
4700
  finished: [],
4569
4701
  includeDbBranchLease,
4570
4702
  dbBranchSettings: resolveDbBranchSettings(options?.dbBranch)
@@ -4579,9 +4711,68 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4579
4711
  options.dbBranch
4580
4712
  );
4581
4713
  }
4582
- const { experimentId, experimentUrl, attempts } = session;
4714
+ const { experimentId } = session;
4715
+ const fullExperimentUrl = `${serviceUrl}${session.experimentUrl}`;
4716
+ const heartbeat = startReplayHeartbeat(
4717
+ () => httpClient.heartbeatReplay(experimentId)
4718
+ );
4719
+ const interruptReplay = async () => {
4720
+ await heartbeat.stop();
4721
+ return httpClient.interruptReplay(experimentId);
4722
+ };
4723
+ const interruptHandlers = installReplayInterruptHandlers({
4724
+ experimentId,
4725
+ experimentUrl: fullExperimentUrl,
4726
+ interrupt: interruptReplay,
4727
+ onInterrupt: options?.onInterrupt
4728
+ });
4729
+ let completeReturned = false;
4730
+ const markCompleteReturned = () => {
4731
+ completeReturned = true;
4732
+ void heartbeat.stop();
4733
+ };
4734
+ try {
4735
+ return await runReplaySession(
4736
+ httpClient,
4737
+ fn,
4738
+ session,
4739
+ fullExperimentUrl,
4740
+ maxConcurrency,
4741
+ interruptHandlers.remove,
4742
+ markCompleteReturned,
4743
+ interruptHandlers.interrupted,
4744
+ options,
4745
+ registeredOverrides
4746
+ );
4747
+ } catch (error) {
4748
+ if (!completeReturned && !interruptHandlers.interrupted()) {
4749
+ await interruptReplay();
4750
+ }
4751
+ throw error;
4752
+ } finally {
4753
+ interruptHandlers.remove();
4754
+ await heartbeat.stop();
4755
+ }
4756
+ }
4757
+ function throwIfInterrupted(interrupted, items, experimentId, experimentUrl) {
4758
+ if (!interrupted()) {
4759
+ return;
4760
+ }
4761
+ const cause = new BitfabError(
4762
+ `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}.`,
4763
+ experimentUrl
4764
+ );
4765
+ throw new ReplayError(
4766
+ cause.message,
4767
+ items,
4768
+ experimentId,
4769
+ experimentUrl,
4770
+ cause
4771
+ );
4772
+ }
4773
+ async function runReplaySession(httpClient, fn, session, fullExperimentUrl, maxConcurrency, stopInterruptingOnSignal, markCompleteReturned, interrupted, options, registeredOverrides) {
4774
+ const { experimentId, attempts } = session;
4583
4775
  const mockStrategy = options?.mock ?? "marked";
4584
- const fullExperimentUrl = `${serviceUrl}${experimentUrl}`;
4585
4776
  try {
4586
4777
  options?.onExperimentStart?.({
4587
4778
  experimentId,
@@ -4731,7 +4922,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4731
4922
  );
4732
4923
  const resultItems = [...carriedOverItems, ...ranItems];
4733
4924
  if (options?.dryRun === true) {
4925
+ throwIfInterrupted(
4926
+ interrupted,
4927
+ resultItems,
4928
+ experimentId,
4929
+ fullExperimentUrl
4930
+ );
4931
+ stopInterruptingOnSignal();
4734
4932
  await httpClient.completeReplay(experimentId).catch(() => void 0);
4933
+ markCompleteReturned();
4735
4934
  const dryResult = {
4736
4935
  items: resultItems,
4737
4936
  experimentId,
@@ -4756,12 +4955,15 @@ async function replay(httpClient, serviceUrl, traceFunctionKey, fn, options, reg
4756
4955
  ranItems[index].traceId = readBack;
4757
4956
  }
4758
4957
  }
4958
+ throwIfInterrupted(interrupted, resultItems, experimentId, fullExperimentUrl);
4959
+ stopInterruptingOnSignal();
4759
4960
  const completeResult = await preserveReplayFailure(
4760
4961
  () => httpClient.completeReplay(experimentId),
4761
4962
  resultItems,
4762
4963
  experimentId,
4763
4964
  fullExperimentUrl
4764
4965
  );
4966
+ markCompleteReturned();
4765
4967
  const serverTraceIds = completeResult.traceIds;
4766
4968
  const replayTokens = completeResult.tokens;
4767
4969
  if (serverTraceIds !== void 0) {
@@ -4865,6 +5067,8 @@ var init_replay = __esm({
4865
5067
  init_mockOverride();
4866
5068
  init_randomUuid();
4867
5069
  init_replayContext();
5070
+ init_replayHeartbeat();
5071
+ init_replayInterrupt();
4868
5072
  init_selectiveReplayRuntime();
4869
5073
  init_serialize();
4870
5074
  REPLAY_PERSISTENCE_TIMEOUT_MS = 3e4;
@@ -11641,6 +11845,7 @@ init_replay();
11641
11845
  // src/replayRegistry.ts
11642
11846
  init_errors();
11643
11847
  init_replay();
11848
+ init_replayInterrupt();
11644
11849
  function defineReplayRegistry(registry) {
11645
11850
  return registry;
11646
11851
  }