bitfab 0.59.0 → 0.59.1
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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/bitfab/client.rb +34 -21
- data/lib/bitfab/experiment_id.rb +16 -0
- data/lib/bitfab/http_client.rb +9 -9
- data/lib/bitfab/labels.rb +16 -9
- data/lib/bitfab/replay.rb +79 -49
- data/lib/bitfab/replay_cli.rb +1 -0
- data/lib/bitfab/replay_processes.rb +6 -6
- data/lib/bitfab/replay_registry.rb +4 -1
- data/lib/bitfab/span_context.rb +2 -2
- data/lib/bitfab/subtree.rb +9 -9
- data/lib/bitfab/traceable.rb +17 -13
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +5 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a45f5360238ae79c517f3514f96a7d02dbad382d4c1c3987ab3d2474d64d85d
|
|
4
|
+
data.tar.gz: 05af89e2fd6fbaed6ae530b4718234c72ad3a91001fc91da151b4a29cb4acc4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 305b1b0ea90a88ef5306eec5c0aaa622f76f0f7f9caaf6d64986c690908ceb34598a0225a4049fbf0d88d5788dc196fc0246ce29f860b000943ec08c0322568e
|
|
7
|
+
data.tar.gz: 7a022d2714b3e617dfa0a24efb7eecaec531f9bf264a693c5c05b20076a282a4ffe0b85ebb1eff2deb5a75c737951597c975230a72a7f3316c327ed9984a0bfc
|
data/README.md
CHANGED
|
@@ -256,7 +256,7 @@ def summarize(order)
|
|
|
256
256
|
end
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
-
Nodes support `name:`, `type:`, `capture:`, `
|
|
259
|
+
Nodes support `name:`, `type:`, `capture:`, `experiment_id:`, `mock_on_replay:`, and `finalize:`. `capture: false` omits the method while attaching captured descendants to its nearest visible parent. `finalize:` changes only the recorded output, never the return value. A finalizer failure is recorded on the node without crashing the caller. The before-`def`, inline, and after-`def` forms all preserve method visibility. For external code, use `Bitfab.client.node(MyClass, :method)`.
|
|
260
260
|
|
|
261
261
|
`bitfab_trace` prepares its project boundary and exclusions when the method is declared, refreshes the boundary if the root definition moves during hot reload, caches canonical source paths, and shares one `TracePoint` dispatcher across active roots on the calling thread. After a span limit is reached, it stops reading inputs and outputs immediately, then stops processing events for that session once its captured calls finish. Each active root still captures and serializes its own copy of every overlapping call, so this remains a discovery tool rather than the production default. Prefer explicit `bitfab_span` declarations on hot paths. Run `mise exec -- ruby -Ilib benchmark/subtree.rb` from this package to measure the no-network caller path on your Ruby build.
|
|
262
262
|
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -106,7 +106,7 @@ module Bitfab
|
|
|
106
106
|
tracing_enabled?
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
# Replay historical traces through a method and create
|
|
109
|
+
# Replay historical traces through a method and create an experiment.
|
|
110
110
|
#
|
|
111
111
|
# @param receiver [Object, Class] an instance for instance methods, or a Class for class methods
|
|
112
112
|
# @param method_name [Symbol] the method to replay
|
|
@@ -183,10 +183,16 @@ module Bitfab
|
|
|
183
183
|
# @param on_item_start [#call, nil] optional callback invoked when a worker
|
|
184
184
|
# begins processing each item. Pair it with on_item_finish to distinguish
|
|
185
185
|
# queued work from in-flight work. A raising callback never crashes the run.
|
|
186
|
-
# @
|
|
186
|
+
# @param on_experiment_start [#call, nil] optional callback invoked once,
|
|
187
|
+
# right after the server creates the experiment and before any item runs,
|
|
188
|
+
# with a hash { experiment_id:, experiment_url: }. A raising callback never
|
|
189
|
+
# crashes the run.
|
|
190
|
+
# @return [Hash] with :items, :experiment_id, :experiment_url, and the
|
|
191
|
+
# deprecated aliases :test_run_id and :test_run_url carrying the same values
|
|
187
192
|
def replay(receiver, method_name = nil, trace_function_key:, limit: nil, trace_ids: nil, concurrency: nil, max_concurrency: ReplayConcurrency::UNSET,
|
|
188
193
|
name: nil, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, experiment_group_id: nil, dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, judge_assertions: false, dry_run: false,
|
|
189
|
-
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil
|
|
194
|
+
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil,
|
|
195
|
+
on_experiment_start: nil)
|
|
190
196
|
Replay.run(
|
|
191
197
|
self,
|
|
192
198
|
receiver,
|
|
@@ -213,7 +219,8 @@ module Bitfab
|
|
|
213
219
|
db_branch:,
|
|
214
220
|
on_item_start:,
|
|
215
221
|
on_item_finish:,
|
|
216
|
-
on_progress
|
|
222
|
+
on_progress:,
|
|
223
|
+
on_experiment_start:
|
|
217
224
|
)
|
|
218
225
|
end
|
|
219
226
|
|
|
@@ -330,14 +337,15 @@ module Bitfab
|
|
|
330
337
|
# Configure an existing method only when it is discovered beneath a
|
|
331
338
|
# bitfab_trace root owned by this client.
|
|
332
339
|
def node(klass, method_name, name: nil, type: "custom", capture: true,
|
|
333
|
-
|
|
340
|
+
experiment_id: nil, mock_on_replay: nil, finalize: nil, test_run_id: nil)
|
|
341
|
+
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
334
342
|
Bitfab::Traceable.node(
|
|
335
343
|
klass,
|
|
336
344
|
method_name,
|
|
337
345
|
name:,
|
|
338
346
|
type:,
|
|
339
347
|
capture:,
|
|
340
|
-
|
|
348
|
+
experiment_id:,
|
|
341
349
|
mock_on_replay:,
|
|
342
350
|
finalize:,
|
|
343
351
|
client: self
|
|
@@ -376,7 +384,7 @@ module Bitfab
|
|
|
376
384
|
# Called by Traceable, not intended for direct use.
|
|
377
385
|
def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:,
|
|
378
386
|
capture_when: "always", mock_on_replay: false, independent_root: false,
|
|
379
|
-
explicit_span_receiver: nil, explicit_span_method_name: nil,
|
|
387
|
+
explicit_span_receiver: nil, explicit_span_method_name: nil, experiment_id: nil, finalize: nil,
|
|
380
388
|
surface: nil, on_start: nil, on_complete: nil, around_body: nil, span_data: {}, managed_root: false,
|
|
381
389
|
result_recorded_elsewhere: false, declared_node: false)
|
|
382
390
|
return yield unless should_record?
|
|
@@ -405,7 +413,7 @@ module Bitfab
|
|
|
405
413
|
parent_span_id = nil
|
|
406
414
|
is_root_span = nil
|
|
407
415
|
started_at = nil
|
|
408
|
-
|
|
416
|
+
resolved_experiment_id = nil
|
|
409
417
|
resolved_input_source_span_id = nil
|
|
410
418
|
begin
|
|
411
419
|
replay_ctx = ReplayContext.current
|
|
@@ -433,7 +441,7 @@ module Bitfab
|
|
|
433
441
|
)
|
|
434
442
|
end
|
|
435
443
|
started_at = Bitfab.now_iso_timestamp
|
|
436
|
-
|
|
444
|
+
resolved_experiment_id = replay_ctx ? replay_ctx[:experiment_id] : experiment_id
|
|
437
445
|
resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
|
|
438
446
|
resolved_input_source_trace_id = replay_ctx&.dig(:input_source_trace_id)
|
|
439
447
|
|
|
@@ -442,7 +450,7 @@ module Bitfab
|
|
|
442
450
|
TraceState.create(
|
|
443
451
|
trace_id,
|
|
444
452
|
trace_function_key:,
|
|
445
|
-
|
|
453
|
+
experiment_id: resolved_experiment_id,
|
|
446
454
|
input_source_trace_id: resolved_input_source_trace_id,
|
|
447
455
|
replay_attempt: replay_ctx&.dig(:replay_attempt),
|
|
448
456
|
db_snapshot: @db_snapshot
|
|
@@ -483,7 +491,7 @@ module Bitfab
|
|
|
483
491
|
kwargs:,
|
|
484
492
|
mocked_output:,
|
|
485
493
|
started_at:,
|
|
486
|
-
|
|
494
|
+
experiment_id: resolved_experiment_id,
|
|
487
495
|
input_source_span_id: resolved_input_source_span_id,
|
|
488
496
|
mock_source:,
|
|
489
497
|
instrumentation: (surface == :trace) ? "trace" : "span",
|
|
@@ -573,7 +581,7 @@ module Bitfab
|
|
|
573
581
|
error: recorded_error,
|
|
574
582
|
started_at:,
|
|
575
583
|
ended_at:,
|
|
576
|
-
|
|
584
|
+
experiment_id: resolved_experiment_id,
|
|
577
585
|
input_source_span_id: resolved_input_source_span_id
|
|
578
586
|
)
|
|
579
587
|
|
|
@@ -931,8 +939,9 @@ module Bitfab
|
|
|
931
939
|
if trace_state&.dig(:session_id)
|
|
932
940
|
payload["sessionId"] = trace_state[:session_id]
|
|
933
941
|
end
|
|
934
|
-
if trace_state&.dig(:
|
|
935
|
-
payload["
|
|
942
|
+
if trace_state&.dig(:experiment_id)
|
|
943
|
+
payload["experimentId"] = trace_state[:experiment_id]
|
|
944
|
+
payload["testRunId"] = trace_state[:experiment_id]
|
|
936
945
|
end
|
|
937
946
|
|
|
938
947
|
@http_client.send_external_trace(payload)
|
|
@@ -943,7 +952,7 @@ module Bitfab
|
|
|
943
952
|
|
|
944
953
|
def send_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
945
954
|
span_name:, span_type:, function_name:, contexts:, prompt:, args:, kwargs:, result:, error:,
|
|
946
|
-
started_at:, ended_at:,
|
|
955
|
+
started_at:, ended_at:, experiment_id: nil, input_source_span_id: nil, mocked: false,
|
|
947
956
|
mock_target: nil, mock_source: nil, span_data: {}, instrumentation: "span", declared_node: false,
|
|
948
957
|
nearest_captured_ancestor_span_id: nil)
|
|
949
958
|
extra_span_data = span_data
|
|
@@ -1016,7 +1025,10 @@ module Bitfab
|
|
|
1016
1025
|
"rootTraceFunctionKey" => TraceState.get(trace_id)&.dig(:trace_function_key) || trace_function_key,
|
|
1017
1026
|
"rawSpan" => raw_span
|
|
1018
1027
|
}
|
|
1019
|
-
|
|
1028
|
+
if experiment_id
|
|
1029
|
+
payload["experimentId"] = experiment_id
|
|
1030
|
+
payload["testRunId"] = experiment_id
|
|
1031
|
+
end
|
|
1020
1032
|
# Flag spans served from the original trace's recorded output during a
|
|
1021
1033
|
# replay so the trace view can mark them (matches the Python and
|
|
1022
1034
|
# TypeScript SDKs).
|
|
@@ -1163,12 +1175,12 @@ module Bitfab
|
|
|
1163
1175
|
nil
|
|
1164
1176
|
end
|
|
1165
1177
|
|
|
1166
|
-
# Record a span entry for a mocked invocation so the
|
|
1178
|
+
# Record a span entry for a mocked invocation so the experiment reflects the
|
|
1167
1179
|
# mocked execution. Mirrors send_span's payload shape but with the mocked
|
|
1168
1180
|
# output as the result and no error.
|
|
1169
1181
|
def send_mocked_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
1170
1182
|
span_name:, span_type:, function_name:, args:, kwargs:, mocked_output:,
|
|
1171
|
-
started_at:,
|
|
1183
|
+
started_at:, experiment_id:, input_source_span_id:, mock_source:, instrumentation: "span", declared_node: false)
|
|
1172
1184
|
ended_at = Bitfab.now_iso_timestamp
|
|
1173
1185
|
send_span(
|
|
1174
1186
|
trace_function_key:,
|
|
@@ -1186,7 +1198,7 @@ module Bitfab
|
|
|
1186
1198
|
error: nil,
|
|
1187
1199
|
started_at:,
|
|
1188
1200
|
ended_at:,
|
|
1189
|
-
|
|
1201
|
+
experiment_id:,
|
|
1190
1202
|
input_source_span_id:,
|
|
1191
1203
|
mocked: true,
|
|
1192
1204
|
instrumentation:,
|
|
@@ -1234,7 +1246,8 @@ module Bitfab
|
|
|
1234
1246
|
# values warn once and default to "always"
|
|
1235
1247
|
# @param mock_on_replay [Boolean] mark this span for the default "marked"
|
|
1236
1248
|
# mock strategy. A missing selected occurrence fails closed.
|
|
1237
|
-
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false, finalize: nil, test_run_id: nil)
|
|
1249
|
+
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false, finalize: nil, experiment_id: nil, test_run_id: nil)
|
|
1250
|
+
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
1238
1251
|
Bitfab::Traceable.wrap(
|
|
1239
1252
|
klass,
|
|
1240
1253
|
method_name,
|
|
@@ -1244,7 +1257,7 @@ module Bitfab
|
|
|
1244
1257
|
capture_when:,
|
|
1245
1258
|
mock_on_replay:,
|
|
1246
1259
|
finalize:,
|
|
1247
|
-
|
|
1260
|
+
experiment_id:,
|
|
1248
1261
|
client: @client
|
|
1249
1262
|
)
|
|
1250
1263
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bitfab
|
|
4
|
+
class << self
|
|
5
|
+
def resolve_experiment_id(experiment_id, test_run_id)
|
|
6
|
+
return experiment_id if test_run_id.nil?
|
|
7
|
+
|
|
8
|
+
if !experiment_id.nil? && experiment_id != test_run_id
|
|
9
|
+
raise ArgumentError, "experiment_id and test_run_id name the same experiment ID but were " \
|
|
10
|
+
"passed different values. Pass experiment_id only; test_run_id is deprecated."
|
|
11
|
+
end
|
|
12
|
+
warn_once("test-run-id-deprecated", "test_run_id: is deprecated; use experiment_id: instead.")
|
|
13
|
+
test_run_id
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -241,7 +241,7 @@ module Bitfab
|
|
|
241
241
|
end
|
|
242
242
|
|
|
243
243
|
# Start a replay session by fetching historical traces.
|
|
244
|
-
# Blocking call. Returns hash with
|
|
244
|
+
# Blocking call. Returns hash with experimentId, experimentUrl, and items array.
|
|
245
245
|
#
|
|
246
246
|
# @param code_change_description [String, nil] optional rationale for the
|
|
247
247
|
# code change being tested in this replay
|
|
@@ -250,7 +250,7 @@ module Bitfab
|
|
|
250
250
|
# @param experiment_group_id [String, nil] optional UUID grouping multiple
|
|
251
251
|
# replay runs into a single experiment batch
|
|
252
252
|
# @param name [String, nil] optional display name for the resulting
|
|
253
|
-
# experiment
|
|
253
|
+
# experiment
|
|
254
254
|
# @param dataset_ids [Array<String>, nil] optional UUIDs of the datasets this
|
|
255
255
|
# replay runs against. The run replays the union of their traces, graded by
|
|
256
256
|
# the union of their graders, and is attributed to every one of them. A
|
|
@@ -344,9 +344,9 @@ module Bitfab
|
|
|
344
344
|
get(endpoint, timeout: 30)
|
|
345
345
|
end
|
|
346
346
|
|
|
347
|
-
# Mark a replay
|
|
348
|
-
def complete_replay(
|
|
349
|
-
request("/api/sdk/replay/complete", {"testRunId" =>
|
|
347
|
+
# Mark a replay experiment as completed. Blocking call.
|
|
348
|
+
def complete_replay(experiment_id)
|
|
349
|
+
request("/api/sdk/replay/complete", {"experimentId" => experiment_id, "testRunId" => experiment_id}, timeout: 30)
|
|
350
350
|
end
|
|
351
351
|
|
|
352
352
|
# Release a previously-resolved DB branch by deleting its Neon branch.
|
|
@@ -356,8 +356,8 @@ module Bitfab
|
|
|
356
356
|
request("/api/sdk/replay/releaseDbBranchLease", {"neonBranchId" => neon_branch_id}, timeout: 30)
|
|
357
357
|
end
|
|
358
358
|
|
|
359
|
-
def resolve_db_branch_lease(
|
|
360
|
-
payload = {"testRunId" =>
|
|
359
|
+
def resolve_db_branch_lease(experiment_id, trace_id, db_branch_settings = nil, attempt = 0)
|
|
360
|
+
payload = {"experimentId" => experiment_id, "testRunId" => experiment_id, "traceId" => trace_id}
|
|
361
361
|
payload["attempt"] = attempt if attempt.positive?
|
|
362
362
|
payload["dbBranchSettings"] = db_branch_settings unless db_branch_settings.nil?
|
|
363
363
|
request(
|
|
@@ -398,10 +398,10 @@ module Bitfab
|
|
|
398
398
|
end
|
|
399
399
|
|
|
400
400
|
# Read the replay traces the server has fully persisted so far.
|
|
401
|
-
def get_replay_status(
|
|
401
|
+
def get_replay_status(experiment_id, expected_span_counts)
|
|
402
402
|
request(
|
|
403
403
|
"/api/sdk/replay/status",
|
|
404
|
-
{"testRunId" =>
|
|
404
|
+
{"experimentId" => experiment_id, "testRunId" => experiment_id, "expectedSpanCounts" => expected_span_counts},
|
|
405
405
|
timeout: 30
|
|
406
406
|
)
|
|
407
407
|
end
|
data/lib/bitfab/labels.rb
CHANGED
|
@@ -15,29 +15,36 @@ module Bitfab
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def save(label:, annotation:, trace_id: nil, original_trace_id: nil, attempt: nil,
|
|
18
|
-
confidence: nil,
|
|
18
|
+
confidence: nil, experiment_id: nil, assertion_id: nil, evidence: EVIDENCE_UNSET, test_run_id: nil)
|
|
19
|
+
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
19
20
|
update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
|
|
20
21
|
update["label"] = label
|
|
21
22
|
update["annotation"] = annotation
|
|
22
23
|
update["confidence"] = confidence unless confidence.nil?
|
|
23
24
|
update["evidence"] = evidence unless evidence.equal?(EVIDENCE_UNSET)
|
|
24
|
-
save_all([update],
|
|
25
|
+
save_all([update], experiment_id:).first
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
def save_all(labels, test_run_id: nil)
|
|
28
|
+
def save_all(labels, experiment_id: nil, test_run_id: nil)
|
|
29
|
+
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
28
30
|
payload = {"labels" => labels}
|
|
29
|
-
|
|
31
|
+
unless experiment_id.nil?
|
|
32
|
+
payload["experimentId"] = experiment_id
|
|
33
|
+
payload["testRunId"] = experiment_id
|
|
34
|
+
end
|
|
30
35
|
@http_client.request(LABELS_PATH, payload)["labels"]
|
|
31
36
|
end
|
|
32
37
|
|
|
33
|
-
def skip(trace_id: nil, original_trace_id: nil, attempt: nil,
|
|
38
|
+
def skip(trace_id: nil, original_trace_id: nil, attempt: nil, experiment_id: nil, assertion_id: nil, test_run_id: nil)
|
|
39
|
+
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
34
40
|
update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
|
|
35
|
-
save_all([update.merge("skip" => true)],
|
|
41
|
+
save_all([update.merge("skip" => true)], experiment_id:).first
|
|
36
42
|
end
|
|
37
43
|
|
|
38
|
-
def archive(trace_id: nil, original_trace_id: nil, attempt: nil,
|
|
44
|
+
def archive(trace_id: nil, original_trace_id: nil, attempt: nil, experiment_id: nil, assertion_id: nil, test_run_id: nil)
|
|
45
|
+
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
39
46
|
update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
|
|
40
|
-
save_all([update.merge("archive" => true)],
|
|
47
|
+
save_all([update.merge("archive" => true)], experiment_id:).first
|
|
41
48
|
end
|
|
42
49
|
|
|
43
50
|
def save_human(label:, annotation:, trace_id:, confidence: nil, assertion_id: nil,
|
|
@@ -77,7 +84,7 @@ module Bitfab
|
|
|
77
84
|
def label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
|
|
78
85
|
if trace_id.nil? == original_trace_id.nil?
|
|
79
86
|
raise ArgumentError, "Pass exactly one of trace_id or original_trace_id. " \
|
|
80
|
-
"Use original_trace_id for a replay verdict, with the
|
|
87
|
+
"Use original_trace_id for a replay verdict, with the experiment_id it ran under."
|
|
81
88
|
end
|
|
82
89
|
|
|
83
90
|
update = {}
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -27,13 +27,23 @@ module Bitfab
|
|
|
27
27
|
|
|
28
28
|
# Whole-run replay failure with every item collected before the run failed.
|
|
29
29
|
class ReplayError < StandardError
|
|
30
|
-
attr_reader :items, :
|
|
30
|
+
attr_reader :items, :experiment_id, :experiment_url
|
|
31
31
|
|
|
32
|
-
def initialize(message, items:, test_run_id
|
|
32
|
+
def initialize(message, items:, experiment_id: nil, experiment_url: nil, test_run_id: nil, test_run_url: nil)
|
|
33
33
|
super(message)
|
|
34
34
|
@items = items
|
|
35
|
-
@
|
|
36
|
-
@
|
|
35
|
+
@experiment_id = experiment_id || test_run_id
|
|
36
|
+
@experiment_url = experiment_url || test_run_url
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @deprecated Use {#experiment_id}.
|
|
40
|
+
def test_run_id
|
|
41
|
+
experiment_id
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @deprecated Use {#experiment_url}.
|
|
45
|
+
def test_run_url
|
|
46
|
+
experiment_url
|
|
37
47
|
end
|
|
38
48
|
end
|
|
39
49
|
|
|
@@ -73,13 +83,13 @@ module Bitfab
|
|
|
73
83
|
|
|
74
84
|
# Execute a block with replay context set for every fiber on the current
|
|
75
85
|
# thread. Child threads and processes intentionally do not inherit it.
|
|
76
|
-
def with_context(
|
|
86
|
+
def with_context(experiment_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
|
|
77
87
|
mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
|
|
78
88
|
db_branch_lease: nil, db_branch_timings: nil, source_bitfab_trace_id: nil, replay_attempt: nil)
|
|
79
89
|
previous = Thread.current.thread_variable_get(REPLAY_CONTEXT_KEY)
|
|
80
90
|
ctx = {
|
|
81
91
|
replay_attempt:,
|
|
82
|
-
|
|
92
|
+
experiment_id:,
|
|
83
93
|
input_source_span_id:,
|
|
84
94
|
input_source_trace_id:,
|
|
85
95
|
trace_id:,
|
|
@@ -127,7 +137,7 @@ module Bitfab
|
|
|
127
137
|
end
|
|
128
138
|
end
|
|
129
139
|
|
|
130
|
-
# Replay historical traces through a traced method and create
|
|
140
|
+
# Replay historical traces through a traced method and create an experiment.
|
|
131
141
|
module Replay
|
|
132
142
|
CODE_CHANGE_UNSET = Object.new.freeze
|
|
133
143
|
PERSISTENCE_TIMEOUT_SECONDS = 30.0
|
|
@@ -167,19 +177,19 @@ module Bitfab
|
|
|
167
177
|
items.map { |item| item.except(:_sdk_trace_id) }
|
|
168
178
|
end
|
|
169
179
|
|
|
170
|
-
def preserve_replay_failure(items,
|
|
180
|
+
def preserve_replay_failure(items, experiment_id, experiment_url)
|
|
171
181
|
yield
|
|
172
182
|
rescue => cause
|
|
173
183
|
error = ReplayError.new(
|
|
174
184
|
cause.message,
|
|
175
185
|
items: public_replay_items(items),
|
|
176
|
-
|
|
177
|
-
|
|
186
|
+
experiment_id:,
|
|
187
|
+
experiment_url:
|
|
178
188
|
)
|
|
179
189
|
raise error, cause:
|
|
180
190
|
end
|
|
181
191
|
|
|
182
|
-
# Replay historical traces through a method and create
|
|
192
|
+
# Replay historical traces through a method and create an experiment.
|
|
183
193
|
#
|
|
184
194
|
# Fetches the last N traces for the given trace function key, re-runs each
|
|
185
195
|
# through the provided receiver and method, and returns comparison data.
|
|
@@ -193,7 +203,7 @@ module Bitfab
|
|
|
193
203
|
# already determines how many traces replay. Supplying trace_ids also
|
|
194
204
|
# emits a warning.
|
|
195
205
|
# @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100)
|
|
196
|
-
# @param name [String, nil] optional display name for the resulting experiment
|
|
206
|
+
# @param name [String, nil] optional display name for the resulting experiment
|
|
197
207
|
# @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
|
|
198
208
|
# @param code_change_description [String, nil] optional rationale for the
|
|
199
209
|
# code change being tested in this replay (stored on the experiment).
|
|
@@ -255,11 +265,18 @@ module Bitfab
|
|
|
255
265
|
# begins processing each item, before replay setup and customer code run.
|
|
256
266
|
# Pair it with on_item_finish to distinguish queued work from in-flight work.
|
|
257
267
|
# A raising callback never crashes the run.
|
|
258
|
-
# @
|
|
268
|
+
# @param on_experiment_start [#call, nil] optional callback invoked once,
|
|
269
|
+
# right after the server creates the experiment and before any item runs,
|
|
270
|
+
# with a hash { experiment_id:, experiment_url: }. Use it to show the
|
|
271
|
+
# experiment link even if the run later fails. A raising callback never
|
|
272
|
+
# crashes the run.
|
|
273
|
+
# @return [Hash] with :items, :experiment_id, :experiment_url, and the
|
|
274
|
+
# deprecated aliases :test_run_id and :test_run_url carrying the same values
|
|
259
275
|
def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil,
|
|
260
276
|
concurrency: nil, max_concurrency: ReplayConcurrency::UNSET, code_change_description: CODE_CHANGE_UNSET, code_change_files: CODE_CHANGE_UNSET, experiment_group_id: nil,
|
|
261
277
|
dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, judge_assertions: false, dry_run: false,
|
|
262
|
-
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil
|
|
278
|
+
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil,
|
|
279
|
+
on_experiment_start: nil)
|
|
263
280
|
concurrency_options = ReplayConcurrency.resolve(concurrency, attempts, max_concurrency)
|
|
264
281
|
attempts = concurrency_options.attempts
|
|
265
282
|
max_concurrency = concurrency_options.max_concurrency
|
|
@@ -293,7 +310,7 @@ module Bitfab
|
|
|
293
310
|
# Reject a trace_function_key that contradicts the method's declared key.
|
|
294
311
|
# replay() fetches historical traces by trace_function_key but records the
|
|
295
312
|
# replayed spans under the method's own bitfab_span key (via send below),
|
|
296
|
-
# so a mismatch produces an incoherent
|
|
313
|
+
# so a mismatch produces an incoherent experiment (traces fetched for one
|
|
297
314
|
# function, recorded under another). Only fires when the method's key is
|
|
298
315
|
# introspectable; an untraced method falls through to the persistence
|
|
299
316
|
# check in complete_replay. Mirrors the TypeScript/Python SDKs.
|
|
@@ -358,9 +375,16 @@ module Bitfab
|
|
|
358
375
|
include_original_metadata: !adapt_inputs.nil?,
|
|
359
376
|
db_branch_settings: resolved_db_branch_settings
|
|
360
377
|
)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
378
|
+
experiment_id = replay_data["experimentId"] || replay_data["testRunId"]
|
|
379
|
+
experiment_url = replay_data["experimentUrl"] || replay_data["testRunUrl"]
|
|
380
|
+
full_experiment_url = "#{client.service_url}#{experiment_url}"
|
|
381
|
+
if on_experiment_start
|
|
382
|
+
begin
|
|
383
|
+
on_experiment_start.call({experiment_id:, experiment_url: full_experiment_url})
|
|
384
|
+
rescue => e
|
|
385
|
+
warn "Bitfab: replay on_experiment_start callback raised: #{e.message}"
|
|
386
|
+
end
|
|
387
|
+
end
|
|
364
388
|
server_items = attempts.times.flat_map do |attempt|
|
|
365
389
|
(replay_data["items"] || []).map do |source|
|
|
366
390
|
item = source.merge("attempt" => attempt)
|
|
@@ -373,7 +397,7 @@ module Bitfab
|
|
|
373
397
|
end
|
|
374
398
|
|
|
375
399
|
result_items = if server_items.any?
|
|
376
|
-
process_items(http_client, server_items, receiver, method_name,
|
|
400
|
+
process_items(http_client, server_items, receiver, method_name, experiment_id, max_concurrency, mock.to_s,
|
|
377
401
|
adapt_inputs, include_db_branch_lease, resolved_db_branch_settings, on_item_start:,
|
|
378
402
|
on_item_finish: item_finish_callback,
|
|
379
403
|
mock_overrides: resolved_overrides, dry_run:, process_executor:)
|
|
@@ -383,11 +407,14 @@ module Bitfab
|
|
|
383
407
|
|
|
384
408
|
if dry_run
|
|
385
409
|
begin
|
|
386
|
-
http_client.complete_replay(
|
|
410
|
+
http_client.complete_replay(experiment_id)
|
|
387
411
|
rescue => e
|
|
388
412
|
warn "Bitfab: could not finalize dry run: #{e.message}"
|
|
389
413
|
end
|
|
390
|
-
result = {
|
|
414
|
+
result = {
|
|
415
|
+
items: public_replay_items(result_items), experiment_id:, experiment_url: full_experiment_url,
|
|
416
|
+
test_run_id: experiment_id, test_run_url: full_experiment_url, attempts:
|
|
417
|
+
}
|
|
391
418
|
write_replay_result_file(result)
|
|
392
419
|
return result
|
|
393
420
|
end
|
|
@@ -396,8 +423,8 @@ module Bitfab
|
|
|
396
423
|
# to finalize once the server confirms every replay trace it queued: the
|
|
397
424
|
# trace-ID mapping complete_replay builds would otherwise race the
|
|
398
425
|
# in-flight batches and hand back nil for every item.
|
|
399
|
-
delivered_trace_ids = preserve_replay_failure(result_items,
|
|
400
|
-
wait_for_replay_persistence(http_client,
|
|
426
|
+
delivered_trace_ids = preserve_replay_failure(result_items, experiment_id, full_experiment_url) do
|
|
427
|
+
wait_for_replay_persistence(http_client, experiment_id, result_items.map { |item| item[:_sdk_trace_id] })
|
|
401
428
|
end
|
|
402
429
|
|
|
403
430
|
# Primary source for item[:trace_id]: the server's assigned traces.id, read
|
|
@@ -414,8 +441,8 @@ module Bitfab
|
|
|
414
441
|
# complete_replay finalizes the run and returns the token/diagnostic
|
|
415
442
|
# mapping; its failures propagate loudly because a run that never
|
|
416
443
|
# completed can't be finalized.
|
|
417
|
-
complete_response = preserve_replay_failure(result_items,
|
|
418
|
-
http_client.complete_replay(
|
|
444
|
+
complete_response = preserve_replay_failure(result_items, experiment_id, full_experiment_url) do
|
|
445
|
+
http_client.complete_replay(experiment_id)
|
|
419
446
|
end
|
|
420
447
|
trace_id_map = complete_response&.dig("traceIds")
|
|
421
448
|
# Per-replay-trace token usage keyed by server trace id: the REPLAYED
|
|
@@ -431,7 +458,7 @@ module Bitfab
|
|
|
431
458
|
# now that the row exists, attach each item's server-aggregated token usage,
|
|
432
459
|
# and detect a systemic upload failure early. Verdict persistence does NOT
|
|
433
460
|
# use this map: it is keyed by the original-trace lineage (:original_trace_id +
|
|
434
|
-
#
|
|
461
|
+
# experiment_id), which needs no client-held server id. Older servers that omit
|
|
435
462
|
# the map yield no replay tokens and leave item[:trace_id] nil.
|
|
436
463
|
unless trace_id_map.nil?
|
|
437
464
|
missing = []
|
|
@@ -461,14 +488,14 @@ module Bitfab
|
|
|
461
488
|
server_count = trace_count.nil? ? "" : " The server persisted #{trace_count} trace(s) for this run."
|
|
462
489
|
cause = RuntimeError.new("Replay completed but the server has no persisted trace for " \
|
|
463
490
|
"any of the #{completed_count} completed item(s) " \
|
|
464
|
-
"(
|
|
491
|
+
"(experiment #{experiment_id}).#{server_count} Trace uploads were " \
|
|
465
492
|
"flushed, so either the uploads failed or the replayed method is " \
|
|
466
493
|
"not traced (no root span was emitted).")
|
|
467
494
|
error = ReplayError.new(
|
|
468
495
|
cause.message,
|
|
469
496
|
items: public_replay_items(result_items),
|
|
470
|
-
|
|
471
|
-
|
|
497
|
+
experiment_id:,
|
|
498
|
+
experiment_url: full_experiment_url
|
|
472
499
|
)
|
|
473
500
|
raise error, cause:
|
|
474
501
|
end
|
|
@@ -477,7 +504,7 @@ module Bitfab
|
|
|
477
504
|
if missing.any?
|
|
478
505
|
warn "Bitfab: server has no persisted trace for #{missing.length} of " \
|
|
479
506
|
"#{completed_count} completed replay item(s) " \
|
|
480
|
-
"(
|
|
507
|
+
"(experiment #{experiment_id}). Their replay token usage is " \
|
|
481
508
|
"unavailable and they cannot be labeled."
|
|
482
509
|
end
|
|
483
510
|
end
|
|
@@ -489,8 +516,10 @@ module Bitfab
|
|
|
489
516
|
result = {
|
|
490
517
|
attempts:,
|
|
491
518
|
items: result_items,
|
|
492
|
-
|
|
493
|
-
|
|
519
|
+
experiment_id:,
|
|
520
|
+
experiment_url: full_experiment_url,
|
|
521
|
+
test_run_id: experiment_id,
|
|
522
|
+
test_run_url: full_experiment_url
|
|
494
523
|
}
|
|
495
524
|
# Persist the enriched result so the Bitfab plugin never has to parse the
|
|
496
525
|
# replay's stdout, which a dependency's logging can corrupt.
|
|
@@ -503,7 +532,8 @@ module Bitfab
|
|
|
503
532
|
begin
|
|
504
533
|
on_progress.call({
|
|
505
534
|
type: "complete",
|
|
506
|
-
|
|
535
|
+
experiment_id:,
|
|
536
|
+
test_run_id: experiment_id,
|
|
507
537
|
completed: total,
|
|
508
538
|
total:,
|
|
509
539
|
succeeded: total - errored,
|
|
@@ -744,7 +774,7 @@ module Bitfab
|
|
|
744
774
|
end
|
|
745
775
|
|
|
746
776
|
# Process all replay items, optionally in parallel using threads.
|
|
747
|
-
def process_items(http_client, server_items, receiver, method_name,
|
|
777
|
+
def process_items(http_client, server_items, receiver, method_name, experiment_id, max_concurrency, mock_strategy,
|
|
748
778
|
adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, on_item_start: nil, on_item_finish: nil,
|
|
749
779
|
mock_overrides: [], dry_run: false, process_executor: nil)
|
|
750
780
|
concurrency = max_concurrency || server_items.length
|
|
@@ -774,7 +804,7 @@ module Bitfab
|
|
|
774
804
|
|
|
775
805
|
begin
|
|
776
806
|
on_item_start.call({
|
|
777
|
-
type: "started",
|
|
807
|
+
type: "started", experiment_id:, test_run_id: experiment_id, started:, completed:, total:, succeeded:, errored:,
|
|
778
808
|
item: {
|
|
779
809
|
attempt:,
|
|
780
810
|
original_trace_id:,
|
|
@@ -804,7 +834,7 @@ module Bitfab
|
|
|
804
834
|
warn "Bitfab: replay per-item flush failed: #{e.message}"
|
|
805
835
|
end
|
|
806
836
|
|
|
807
|
-
report = lambda do |result, original_trace_id, original_span_id,
|
|
837
|
+
report = lambda do |result, original_trace_id, original_span_id, experiment_id|
|
|
808
838
|
# Deliver this item's trace now, then read its server id back off the
|
|
809
839
|
# ingest response. A flush failure never crashes the run: the id stays
|
|
810
840
|
# nil and the end-of-run barrier remains the authority on persistence.
|
|
@@ -818,7 +848,7 @@ module Bitfab
|
|
|
818
848
|
|
|
819
849
|
begin
|
|
820
850
|
on_item_finish.call({
|
|
821
|
-
|
|
851
|
+
experiment_id:, test_run_id: experiment_id, completed:, total:, succeeded:, errored:,
|
|
822
852
|
item: {
|
|
823
853
|
attempt: result[:attempt],
|
|
824
854
|
ingestion_type: result[:ingestion_type],
|
|
@@ -854,9 +884,9 @@ module Bitfab
|
|
|
854
884
|
|
|
855
885
|
execute = lambda do |item|
|
|
856
886
|
if process_executor
|
|
857
|
-
process_executor.call(item,
|
|
887
|
+
process_executor.call(item, experiment_id, mock_strategy, include_db_branch_lease, db_branch_settings, dry_run:)
|
|
858
888
|
else
|
|
859
|
-
process_single_item(http_client, item, receiver, method_name,
|
|
889
|
+
process_single_item(http_client, item, receiver, method_name, experiment_id, mock_strategy,
|
|
860
890
|
adapt_inputs, include_db_branch_lease, db_branch_settings, mock_overrides:, dry_run:)
|
|
861
891
|
end
|
|
862
892
|
end
|
|
@@ -865,7 +895,7 @@ module Bitfab
|
|
|
865
895
|
server_items.map do |item|
|
|
866
896
|
report_start.call(original_trace_id_of(item), original_span_id_of(item), item["attempt"] || 0)
|
|
867
897
|
result = execute.call(item)
|
|
868
|
-
report.call(result, original_trace_id_of(item), original_span_id_of(item),
|
|
898
|
+
report.call(result, original_trace_id_of(item), original_span_id_of(item), experiment_id)
|
|
869
899
|
result
|
|
870
900
|
end
|
|
871
901
|
else
|
|
@@ -883,7 +913,7 @@ module Bitfab
|
|
|
883
913
|
report_start.call(original_trace_id_of(item), original_span_id_of(item), item["attempt"] || 0)
|
|
884
914
|
result = execute.call(item)
|
|
885
915
|
results_mutex.synchronize { results[idx] = result }
|
|
886
|
-
report.call(result, original_trace_id_of(item), original_span_id_of(item),
|
|
916
|
+
report.call(result, original_trace_id_of(item), original_span_id_of(item), experiment_id)
|
|
887
917
|
end
|
|
888
918
|
end
|
|
889
919
|
end
|
|
@@ -899,7 +929,7 @@ module Bitfab
|
|
|
899
929
|
# deserializing inputs is captured on the returned item's :error rather
|
|
900
930
|
# than propagated, so one bad trace never aborts the whole replay run
|
|
901
931
|
# (mirrors the TypeScript and Python SDKs' per-item rescue).
|
|
902
|
-
def process_single_item(http_client, server_item, receiver, method_name,
|
|
932
|
+
def process_single_item(http_client, server_item, receiver, method_name, experiment_id, mock_strategy,
|
|
903
933
|
adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, mock_overrides: [], dry_run: false)
|
|
904
934
|
metrics = extract_server_item_metrics(server_item)
|
|
905
935
|
attempt = server_item["attempt"] || 0
|
|
@@ -925,7 +955,7 @@ module Bitfab
|
|
|
925
955
|
db_branch_timings = include_db_branch_lease ? server_item["dbBranchTimings"] : nil
|
|
926
956
|
if include_db_branch_lease && lease.nil? && lease_error.nil?
|
|
927
957
|
begin
|
|
928
|
-
resolved = http_client.resolve_db_branch_lease(
|
|
958
|
+
resolved = http_client.resolve_db_branch_lease(experiment_id, original_trace_id, db_branch_settings, attempt)
|
|
929
959
|
rescue => cause
|
|
930
960
|
error = DbBranchReplayError.new(
|
|
931
961
|
"lease_request_failed",
|
|
@@ -1001,7 +1031,7 @@ module Bitfab
|
|
|
1001
1031
|
item_data,
|
|
1002
1032
|
receiver,
|
|
1003
1033
|
method_name,
|
|
1004
|
-
|
|
1034
|
+
experiment_id,
|
|
1005
1035
|
span["id"],
|
|
1006
1036
|
metrics,
|
|
1007
1037
|
input_source_trace_id: span["externalTraceId"],
|
|
@@ -1180,7 +1210,7 @@ module Bitfab
|
|
|
1180
1210
|
# repeat itself. Acks cannot settle a request that timed out client-side
|
|
1181
1211
|
# after the server committed, so anything short of fully delivered falls
|
|
1182
1212
|
# through to the server poll, which stays the authority there.
|
|
1183
|
-
def wait_for_replay_persistence(http_client,
|
|
1213
|
+
def wait_for_replay_persistence(http_client, experiment_id, sdk_trace_ids, timeout: nil)
|
|
1184
1214
|
timeout ||= PERSISTENCE_TIMEOUT_SECONDS
|
|
1185
1215
|
trace_ids = sdk_trace_ids.compact
|
|
1186
1216
|
|
|
@@ -1212,7 +1242,7 @@ module Bitfab
|
|
|
1212
1242
|
missing = expected_span_counts.keys
|
|
1213
1243
|
|
|
1214
1244
|
while missing.any?
|
|
1215
|
-
status = http_client.get_replay_status(
|
|
1245
|
+
status = http_client.get_replay_status(experiment_id, expected_span_counts)
|
|
1216
1246
|
ready = status["traceIds"]
|
|
1217
1247
|
ready = {} unless ready.is_a?(Hash)
|
|
1218
1248
|
read_back_trace_ids.merge!(ready)
|
|
@@ -1230,7 +1260,7 @@ module Bitfab
|
|
|
1230
1260
|
"spans likely never reached the server."
|
|
1231
1261
|
end
|
|
1232
1262
|
raise "Replay traces were not fully persisted before the delivery deadline " \
|
|
1233
|
-
"(
|
|
1263
|
+
"(experiment #{experiment_id}, missing #{missing.length} of " \
|
|
1234
1264
|
"#{expected_span_counts.length} trace(s)).#{cause}"
|
|
1235
1265
|
end
|
|
1236
1266
|
|
|
@@ -1249,7 +1279,7 @@ module Bitfab
|
|
|
1249
1279
|
end
|
|
1250
1280
|
|
|
1251
1281
|
# Execute a single replay item: deserialize inputs, call method with replay context.
|
|
1252
|
-
def execute_item(item, receiver, method_name,
|
|
1282
|
+
def execute_item(item, receiver, method_name, experiment_id, input_source_span_id = nil, metrics = {},
|
|
1253
1283
|
input_source_trace_id: nil, mock_strategy: "marked", mock_tree: nil, mock_overrides: nil,
|
|
1254
1284
|
fetch_span_output: nil, adapt_inputs: nil, adapt_ctx: nil, db_branch_lease: nil, db_branch_timings: nil,
|
|
1255
1285
|
source_bitfab_trace_id: nil, db_snapshot_ref: nil, http_client: nil, attempt: 0, dry_run: false)
|
|
@@ -1273,7 +1303,7 @@ module Bitfab
|
|
|
1273
1303
|
begin
|
|
1274
1304
|
ReplayContext.with_context(
|
|
1275
1305
|
replay_attempt: attempt,
|
|
1276
|
-
|
|
1306
|
+
experiment_id:,
|
|
1277
1307
|
input_source_span_id:,
|
|
1278
1308
|
input_source_trace_id:,
|
|
1279
1309
|
trace_id: sdk_trace_id,
|
data/lib/bitfab/replay_cli.rb
CHANGED
|
@@ -46,15 +46,15 @@ module Bitfab
|
|
|
46
46
|
result = nil
|
|
47
47
|
Bitfab.collect_delivery_problems do
|
|
48
48
|
result = Replay.process_single_item(http, assignment.fetch("item"), receiver, method_name,
|
|
49
|
-
assignment.fetch("
|
|
49
|
+
assignment.fetch("experiment_id"), assignment.fetch("mock"), options[:adapt_inputs],
|
|
50
50
|
assignment.fetch("include_db_branch_lease"), assignment["db_branch_settings"],
|
|
51
51
|
mock_overrides: overrides, dry_run: assignment.fetch("dry_run"))
|
|
52
52
|
unless assignment["dry_run"]
|
|
53
53
|
begin
|
|
54
|
-
mapped = Replay.wait_for_replay_persistence(http, assignment.fetch("
|
|
54
|
+
mapped = Replay.wait_for_replay_persistence(http, assignment.fetch("experiment_id"), [result[:_sdk_trace_id]], timeout: delivery_timeout)
|
|
55
55
|
result[:trace_id] = mapped[result[:_sdk_trace_id]] || result[:trace_id]
|
|
56
56
|
if result[:_sdk_trace_id] && result[:trace_id].nil?
|
|
57
|
-
status = http.get_replay_status(assignment.fetch("
|
|
57
|
+
status = http.get_replay_status(assignment.fetch("experiment_id"), {result[:_sdk_trace_id] => 1})
|
|
58
58
|
result[:trace_id] = status.fetch("traceIds", {})[result[:_sdk_trace_id]]
|
|
59
59
|
end
|
|
60
60
|
raise "Child replay trace persistence could not be confirmed" if result[:error].nil? && result[:trace_id].nil?
|
|
@@ -67,7 +67,7 @@ module Bitfab
|
|
|
67
67
|
hook = options[:concurrency]&.on_item_finish_in_child_process
|
|
68
68
|
if hook && !assignment["dry_run"]
|
|
69
69
|
begin
|
|
70
|
-
hook.call({
|
|
70
|
+
hook.call({experiment_id: assignment.fetch("experiment_id"), test_run_id: assignment.fetch("experiment_id"), item: Replay.public_replay_items([result]).first})
|
|
71
71
|
rescue => error
|
|
72
72
|
stderr.puts "[replay] child grading hook failed for #{result[:original_trace_id]} attempt #{result[:attempt]}: #{error}"
|
|
73
73
|
end
|
|
@@ -130,14 +130,14 @@ module Bitfab
|
|
|
130
130
|
@throttle = ReplayMemoryThrottle.new(enabled: concurrency.memory_throttle)
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
def call(item,
|
|
133
|
+
def call(item, experiment_id, mock, include_db_branch_lease, db_branch_settings, dry_run: false)
|
|
134
134
|
index = @mutex.synchronize { @sequence += 1 }
|
|
135
135
|
@throttle.admit(index)
|
|
136
136
|
result_path = File.join(@directory, "#{index}.result")
|
|
137
137
|
assignment_path = File.join(@directory, "#{index}.json")
|
|
138
138
|
stdout_path, stderr_path = File.join(@directory, "#{index}.out"), File.join(@directory, "#{index}.err")
|
|
139
139
|
delivery_problems_path = File.join(@directory, "#{index}.delivery")
|
|
140
|
-
assignment = {pipeline: @args.fetch(:pipeline), params: @params, item:,
|
|
140
|
+
assignment = {pipeline: @args.fetch(:pipeline), params: @params, item:, experiment_id:, mock:, include_db_branch_lease:, db_branch_settings:, dry_run:, result_path:, delivery_problems_path:}
|
|
141
141
|
File.write(assignment_path, JSON.generate(assignment), mode: "w", perm: 0o600)
|
|
142
142
|
executable = File.expand_path("../../exe/bitfab-replay", __dir__)
|
|
143
143
|
pid = @mutex.synchronize do
|
|
@@ -240,6 +240,9 @@ module Bitfab
|
|
|
240
240
|
|
|
241
241
|
stderr.puts "[replay] Replaying #{describe_selection(options)} from \"#{registration.trace_function_key}\"..."
|
|
242
242
|
|
|
243
|
+
options[:on_experiment_start] = lambda do |experiment|
|
|
244
|
+
stderr.puts "[replay] Experiment #{experiment[:experiment_id]}: #{experiment[:experiment_url]}"
|
|
245
|
+
end
|
|
243
246
|
result = begin
|
|
244
247
|
registration.client.replay(
|
|
245
248
|
registration.receiver,
|
|
@@ -428,7 +431,7 @@ module Bitfab
|
|
|
428
431
|
stderr.puts " Missed expected: #{missed}"
|
|
429
432
|
end
|
|
430
433
|
stderr.puts " Errors: #{errors}" if errors > 0
|
|
431
|
-
stderr.puts "\n #{result[:
|
|
434
|
+
stderr.puts "\n #{result[:experiment_url]}"
|
|
432
435
|
end
|
|
433
436
|
end
|
|
434
437
|
end
|
data/lib/bitfab/span_context.rb
CHANGED
|
@@ -187,7 +187,7 @@ module Bitfab
|
|
|
187
187
|
@states_mutex.synchronize { @states[trace_id] }
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
-
def create(trace_id,
|
|
190
|
+
def create(trace_id, experiment_id: nil, input_source_trace_id: nil, replay_attempt: nil, trace_function_key: nil, db_snapshot: nil)
|
|
191
191
|
@states_mutex.synchronize do
|
|
192
192
|
@states[trace_id] ||= begin
|
|
193
193
|
started_at = Bitfab.now_iso_timestamp
|
|
@@ -195,7 +195,7 @@ module Bitfab
|
|
|
195
195
|
trace_id:,
|
|
196
196
|
trace_function_key:,
|
|
197
197
|
started_at:,
|
|
198
|
-
|
|
198
|
+
experiment_id:,
|
|
199
199
|
input_source_trace_id:,
|
|
200
200
|
replay_attempt:,
|
|
201
201
|
# Capture the wall clock now, before the wrapped function runs.
|
data/lib/bitfab/subtree.rb
CHANGED
|
@@ -50,7 +50,7 @@ module Bitfab
|
|
|
50
50
|
:started_at,
|
|
51
51
|
:error,
|
|
52
52
|
:span_type,
|
|
53
|
-
:
|
|
53
|
+
:experiment_id,
|
|
54
54
|
:span_data,
|
|
55
55
|
:declared_node,
|
|
56
56
|
keyword_init: true
|
|
@@ -83,7 +83,7 @@ module Bitfab
|
|
|
83
83
|
class ConfiguredCall
|
|
84
84
|
attr_reader :span_data, :owner
|
|
85
85
|
|
|
86
|
-
def initialize(receiver:, method_name:, name:, type:, args:, kwargs:,
|
|
86
|
+
def initialize(receiver:, method_name:, name:, type:, args:, kwargs:, experiment_id: nil,
|
|
87
87
|
nested_trace_key: nil)
|
|
88
88
|
@receiver = receiver
|
|
89
89
|
@method_name = method_name
|
|
@@ -93,7 +93,7 @@ module Bitfab
|
|
|
93
93
|
@records = Subtree.active_sessions.map do |session|
|
|
94
94
|
omitted = session.excluded?(method_name)
|
|
95
95
|
entry = unless omitted
|
|
96
|
-
session.configured_entry(name:, type:, args:, kwargs:,
|
|
96
|
+
session.configured_entry(name:, type:, args:, kwargs:, experiment_id:)
|
|
97
97
|
end
|
|
98
98
|
[session, entry, !omitted]
|
|
99
99
|
end
|
|
@@ -189,7 +189,7 @@ module Bitfab
|
|
|
189
189
|
trace_function_key:,
|
|
190
190
|
trace_id: span[:trace_id],
|
|
191
191
|
root_span_id: span[:span_id],
|
|
192
|
-
|
|
192
|
+
experiment_id: replay&.dig(:experiment_id),
|
|
193
193
|
input_source_span_id: replay&.dig(:input_source_span_id),
|
|
194
194
|
config:,
|
|
195
195
|
max_depth:,
|
|
@@ -416,7 +416,7 @@ module Bitfab
|
|
|
416
416
|
@parent_session = session.parent_session if @parent_session.equal?(session)
|
|
417
417
|
end
|
|
418
418
|
|
|
419
|
-
def initialize(client:, trace_function_key:, trace_id:, root_span_id:,
|
|
419
|
+
def initialize(client:, trace_function_key:, trace_id:, root_span_id:, experiment_id:,
|
|
420
420
|
input_source_span_id:, config:, max_depth:, include_wrappers:,
|
|
421
421
|
root_receiver:, root_method_name:, max_captured_subtree_spans: DEFAULT_MAX_CAPTURED_SUBTREE_SPANS, mock_on_replay_default: false)
|
|
422
422
|
@mock_on_replay_default = mock_on_replay_default
|
|
@@ -424,7 +424,7 @@ module Bitfab
|
|
|
424
424
|
@trace_function_key = trace_function_key
|
|
425
425
|
@trace_id = trace_id
|
|
426
426
|
@root_span_id = root_span_id
|
|
427
|
-
@
|
|
427
|
+
@experiment_id = experiment_id
|
|
428
428
|
@input_source_span_id = input_source_span_id
|
|
429
429
|
@root = config.root
|
|
430
430
|
@root_path = config.root_path
|
|
@@ -532,13 +532,13 @@ module Bitfab
|
|
|
532
532
|
entry&.span_data&.dig("content_off_by_simulation_plan") == true
|
|
533
533
|
end
|
|
534
534
|
|
|
535
|
-
def configured_entry(name:, type:, args:, kwargs:,
|
|
535
|
+
def configured_entry(name:, type:, args:, kwargs:, experiment_id:)
|
|
536
536
|
return unless reserve_node
|
|
537
537
|
|
|
538
538
|
entry = OpenSpan.new(
|
|
539
539
|
span_id: SecureRandom.uuid,
|
|
540
540
|
parent_span_id: current_parent_span_id,
|
|
541
|
-
name:, span_type: type, args:, kwargs:,
|
|
541
|
+
name:, span_type: type, args:, kwargs:, experiment_id:, started_at: now,
|
|
542
542
|
span_data: {}, declared_node: true
|
|
543
543
|
)
|
|
544
544
|
@configured_entries[entry.object_id] = entry
|
|
@@ -744,7 +744,7 @@ module Bitfab
|
|
|
744
744
|
error: entry.error,
|
|
745
745
|
started_at: entry.started_at,
|
|
746
746
|
ended_at: now,
|
|
747
|
-
|
|
747
|
+
experiment_id: entry.experiment_id || @experiment_id,
|
|
748
748
|
input_source_span_id: @input_source_span_id
|
|
749
749
|
)
|
|
750
750
|
rescue Exception # rubocop:disable Lint/RescueException
|
data/lib/bitfab/traceable.rb
CHANGED
|
@@ -53,7 +53,8 @@ module Bitfab
|
|
|
53
53
|
# preserve the bound client through the fluent wrapper, matching Python's
|
|
54
54
|
# `BitfabFunction.span()` and TypeScript's `BitfabFunction.withSpan()`.
|
|
55
55
|
def self.wrap(klass, method_name, trace_function_key:, name: nil, type: "custom",
|
|
56
|
-
capture_when: "always", mock_on_replay: false, finalize: nil,
|
|
56
|
+
capture_when: "always", mock_on_replay: false, finalize: nil, experiment_id: nil, client: nil, test_run_id: nil)
|
|
57
|
+
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
57
58
|
method_name_str = method_name.to_s
|
|
58
59
|
bound_client = client
|
|
59
60
|
wrapper = Module.new do
|
|
@@ -66,7 +67,7 @@ module Bitfab
|
|
|
66
67
|
trace_function_key:,
|
|
67
68
|
span_name: name || Subtree.qualified_name(self, method_name),
|
|
68
69
|
span_type: type, function_name: method_name_str, args:, kwargs:,
|
|
69
|
-
capture_when:, mock_on_replay:, finalize:,
|
|
70
|
+
capture_when:, mock_on_replay:, finalize:, experiment_id:, surface: :span,
|
|
70
71
|
explicit_span_receiver: self, explicit_span_method_name: method_name) do
|
|
71
72
|
super(*args, **kwargs, &block)
|
|
72
73
|
end
|
|
@@ -134,7 +135,8 @@ module Bitfab
|
|
|
134
135
|
# Configure an existing method only when it is discovered beneath a
|
|
135
136
|
# bitfab_trace root. Outside subtree capture the method runs unchanged.
|
|
136
137
|
def self.node(klass, method_name, name: nil, type: "custom", capture: true,
|
|
137
|
-
|
|
138
|
+
experiment_id: nil, mock_on_replay: nil, finalize: nil, client: nil, test_run_id: nil)
|
|
139
|
+
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
138
140
|
validate_node_options!(capture:, mock_on_replay:)
|
|
139
141
|
method_name_str = method_name.to_s
|
|
140
142
|
bound_client = client
|
|
@@ -153,7 +155,7 @@ module Bitfab
|
|
|
153
155
|
|
|
154
156
|
span_name = name || Subtree.qualified_name(self, method_name)
|
|
155
157
|
call = Subtree::ConfiguredCall.new(receiver: self, method_name:, name: span_name,
|
|
156
|
-
type:, args:, kwargs:,
|
|
158
|
+
type:, args:, kwargs:, experiment_id:)
|
|
157
159
|
unless call.owner
|
|
158
160
|
next call.run { super(*args, **kwargs, &block) }
|
|
159
161
|
end
|
|
@@ -164,7 +166,7 @@ module Bitfab
|
|
|
164
166
|
span_name:, span_type: type, function_name: method_name_str,
|
|
165
167
|
args:, kwargs:, capture_when: "nested", mock_on_replay: policy,
|
|
166
168
|
explicit_span_receiver: self, explicit_span_method_name: method_name,
|
|
167
|
-
|
|
169
|
+
experiment_id:, finalize:, surface: :trace, result_recorded_elsewhere: call.recorded_elsewhere?, declared_node: true,
|
|
168
170
|
on_start: call.method(:start), on_complete: call.method(:complete),
|
|
169
171
|
around_body: call.method(:run)) { super(*args, **kwargs, &block) }
|
|
170
172
|
end
|
|
@@ -214,7 +216,7 @@ module Bitfab
|
|
|
214
216
|
# replay() uses this to reject a trace_function_key that contradicts the
|
|
215
217
|
# method's declared key: a mismatch fetches one function's historical
|
|
216
218
|
# traces but re-records the replay under the method's own key, producing
|
|
217
|
-
# an incoherent
|
|
219
|
+
# an incoherent experiment. Mirrors the TypeScript and Python SDKs, which
|
|
218
220
|
# throw on the same key mismatch.
|
|
219
221
|
def self.trace_function_key_for(receiver, method_name)
|
|
220
222
|
name = method_name.to_s
|
|
@@ -265,7 +267,8 @@ module Bitfab
|
|
|
265
267
|
# When true, `client.replay(...)` returns this span's
|
|
266
268
|
# historical output instead of executing the wrapped method.
|
|
267
269
|
def bitfab_span(method_name, trace_function_key: nil, name: nil, type: "custom",
|
|
268
|
-
capture_when: "always", mock_on_replay: false, finalize: nil, test_run_id: nil)
|
|
270
|
+
capture_when: "always", mock_on_replay: false, finalize: nil, experiment_id: nil, test_run_id: nil)
|
|
271
|
+
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
269
272
|
trace_function_key ||= @bitfab_function_key
|
|
270
273
|
unless trace_function_key
|
|
271
274
|
raise "No trace function key provided. Pass `trace_function_key:` to `bitfab_span` " \
|
|
@@ -275,7 +278,7 @@ module Bitfab
|
|
|
275
278
|
# If the method already exists (inline or after-method style), wrap it immediately
|
|
276
279
|
if method_defined?(method_name) || private_method_defined?(method_name)
|
|
277
280
|
_bitfab_wrap_method(
|
|
278
|
-
method_name, trace_function_key:, name:, type:, capture_when:, mock_on_replay:, finalize:,
|
|
281
|
+
method_name, trace_function_key:, name:, type:, capture_when:, mock_on_replay:, finalize:, experiment_id:
|
|
279
282
|
)
|
|
280
283
|
else
|
|
281
284
|
# Method doesn't exist yet (before-method style) - register for method_added hook
|
|
@@ -287,7 +290,7 @@ module Bitfab
|
|
|
287
290
|
capture_when:,
|
|
288
291
|
mock_on_replay:,
|
|
289
292
|
finalize:,
|
|
290
|
-
|
|
293
|
+
experiment_id:
|
|
291
294
|
}
|
|
292
295
|
end
|
|
293
296
|
end
|
|
@@ -325,9 +328,10 @@ module Bitfab
|
|
|
325
328
|
# EXPERIMENTAL. Configure one method discovered beneath bitfab_trace.
|
|
326
329
|
# The node inherits the active trace key and never traces by itself.
|
|
327
330
|
def bitfab_node(method_name, name: nil, type: "custom", capture: true,
|
|
328
|
-
|
|
331
|
+
experiment_id: nil, mock_on_replay: nil, finalize: nil, test_run_id: nil)
|
|
332
|
+
experiment_id = Bitfab.resolve_experiment_id(experiment_id, test_run_id)
|
|
329
333
|
Traceable.send(:validate_node_options!, capture:, mock_on_replay:)
|
|
330
|
-
config = {name:, type:, capture:,
|
|
334
|
+
config = {name:, type:, capture:, experiment_id:, mock_on_replay:, finalize:}
|
|
331
335
|
if method_defined?(method_name) || private_method_defined?(method_name)
|
|
332
336
|
_bitfab_node_method(method_name, **config)
|
|
333
337
|
else
|
|
@@ -361,14 +365,14 @@ module Bitfab
|
|
|
361
365
|
end
|
|
362
366
|
|
|
363
367
|
def _bitfab_node_method(method_name, name: nil, type: "custom", capture: true,
|
|
364
|
-
|
|
368
|
+
experiment_id: nil, mock_on_replay: nil, finalize: nil)
|
|
365
369
|
Traceable.node(
|
|
366
370
|
self,
|
|
367
371
|
method_name,
|
|
368
372
|
name:,
|
|
369
373
|
type:,
|
|
370
374
|
capture:,
|
|
371
|
-
|
|
375
|
+
experiment_id:,
|
|
372
376
|
mock_on_replay:,
|
|
373
377
|
finalize:,
|
|
374
378
|
client: nil
|
data/lib/bitfab/version.rb
CHANGED
data/lib/bitfab.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require_relative "bitfab/version"
|
|
6
6
|
require_relative "bitfab/constants"
|
|
7
7
|
require_relative "bitfab/warn_once"
|
|
8
|
+
require_relative "bitfab/experiment_id"
|
|
8
9
|
require_relative "bitfab/payload_budget"
|
|
9
10
|
require_relative "bitfab/serialize"
|
|
10
11
|
require_relative "bitfab/db_snapshot"
|
|
@@ -175,7 +176,10 @@ module Bitfab
|
|
|
175
176
|
# succeeded, errored, and item (the object replay already passes to
|
|
176
177
|
# on_item_finish)
|
|
177
178
|
def report_replay_progress(progress)
|
|
178
|
-
|
|
179
|
+
payload = Replay.json_safe(progress)
|
|
180
|
+
experiment_id = progress[:experiment_id] if progress.is_a?(Hash)
|
|
181
|
+
payload = payload.merge(experimentId: experiment_id) unless experiment_id.nil?
|
|
182
|
+
warn("#{BITFAB_PROGRESS_PREFIX}#{payload.to_json}")
|
|
179
183
|
rescue
|
|
180
184
|
nil
|
|
181
185
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bitfab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.59.
|
|
4
|
+
version: 0.59.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
@@ -167,6 +167,7 @@ files:
|
|
|
167
167
|
- lib/bitfab/db_snapshot.rb
|
|
168
168
|
- lib/bitfab/detached_trace.rb
|
|
169
169
|
- lib/bitfab/env.rb
|
|
170
|
+
- lib/bitfab/experiment_id.rb
|
|
170
171
|
- lib/bitfab/git_command.rb
|
|
171
172
|
- lib/bitfab/git_state.rb
|
|
172
173
|
- lib/bitfab/graders.rb
|