bitfab 0.51.9 → 0.51.10
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 +3 -3
- data/exe/bitfab-seed +13 -0
- data/lib/bitfab/assertion_categories.rb +42 -0
- data/lib/bitfab/assertions.rb +35 -0
- data/lib/bitfab/baml.rb +208 -0
- data/lib/bitfab/client.rb +95 -31
- data/lib/bitfab/db_snapshot.rb +18 -5
- data/lib/bitfab/detached_trace.rb +46 -0
- data/lib/bitfab/graders.rb +23 -0
- data/lib/bitfab/http_client.rb +106 -35
- data/lib/bitfab/labels.rb +80 -0
- data/lib/bitfab/replay.rb +92 -23
- data/lib/bitfab/replay_cli.rb +7 -1
- data/lib/bitfab/replay_concurrency.rb +44 -0
- data/lib/bitfab/replay_memory.rb +111 -0
- data/lib/bitfab/replay_processes.rb +218 -0
- data/lib/bitfab/replay_registry.rb +100 -15
- data/lib/bitfab/seed.rb +128 -0
- data/lib/bitfab/seed_cli.rb +67 -0
- data/lib/bitfab/seed_context.rb +21 -0
- data/lib/bitfab/simulation_plan.rb +248 -0
- data/lib/bitfab/span_context.rb +9 -4
- data/lib/bitfab/span_origin.rb +13 -0
- data/lib/bitfab/subtree.rb +144 -5
- data/lib/bitfab/thread_propagation.rb +111 -0
- data/lib/bitfab/traceable.rb +70 -155
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +9 -2
- metadata +32 -1
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -12,6 +12,9 @@ require_relative "transport"
|
|
|
12
12
|
require_relative "trace_completion"
|
|
13
13
|
require_relative "version"
|
|
14
14
|
require_relative "warn_once"
|
|
15
|
+
require_relative "simulation_plan"
|
|
16
|
+
require_relative "span_origin"
|
|
17
|
+
require_relative "thread_propagation"
|
|
15
18
|
|
|
16
19
|
module Bitfab
|
|
17
20
|
# What a caller learns about one tracked trace once it takes it back.
|
|
@@ -45,49 +48,91 @@ module Bitfab
|
|
|
45
48
|
@carrier_seq = 0
|
|
46
49
|
@transport = nil
|
|
47
50
|
@closed = false
|
|
51
|
+
@close_started = false
|
|
52
|
+
@close_result = nil
|
|
53
|
+
@close_condition = ConditionVariable.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def configure_simulation_plan(enabled: true)
|
|
57
|
+
@simulation_plan = SimulationPlan.new(self, enabled:)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def refresh_simulation_plan
|
|
61
|
+
@simulation_plan&.refresh
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def get_simulation_plan
|
|
65
|
+
get("/api/sdk/sim-plan", timeout: SimulationPlan::READ_TIMEOUT, connection_close: true)
|
|
48
66
|
end
|
|
49
67
|
|
|
50
68
|
# Flush and permanently close this client's tracing transport.
|
|
51
69
|
# Returns true when everything it queued was delivered within the deadline.
|
|
52
70
|
def close(timeout: 30)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + [timeout, 0].max
|
|
72
|
+
@transport_mutex.synchronize do
|
|
73
|
+
while @close_started && @close_result.nil?
|
|
74
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
75
|
+
return false if remaining <= 0
|
|
76
|
+
@close_condition.wait(@transport_mutex, remaining)
|
|
77
|
+
end
|
|
78
|
+
return @close_result unless @close_result.nil?
|
|
79
|
+
@close_started = true
|
|
58
80
|
end
|
|
59
|
-
return true if transport.nil?
|
|
60
81
|
|
|
61
|
-
|
|
82
|
+
result = false
|
|
83
|
+
begin
|
|
84
|
+
remaining = [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
85
|
+
released = @simulation_plan.nil? || @simulation_plan.release([remaining / 2.0, SimulationPlan::READ_TIMEOUT].min)
|
|
86
|
+
@simulation_plan&.stop
|
|
87
|
+
remaining = [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
88
|
+
transport = @transport_mutex.synchronize do
|
|
89
|
+
current = @transport
|
|
90
|
+
@transport = nil
|
|
91
|
+
@closed = true
|
|
92
|
+
current
|
|
93
|
+
end
|
|
94
|
+
shutdown = transport.nil? || transport.shutdown(remaining)
|
|
95
|
+
result = released && shutdown
|
|
96
|
+
ensure
|
|
97
|
+
@transport_mutex.synchronize do
|
|
98
|
+
@close_result = result
|
|
99
|
+
@close_condition.broadcast
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
result
|
|
62
103
|
end
|
|
63
104
|
|
|
64
105
|
# Wait for the spans and traces this client queued to be delivered.
|
|
65
106
|
def flush(timeout: 30)
|
|
107
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + [timeout, 0].max
|
|
108
|
+
released = @simulation_plan.nil? || @simulation_plan.release([timeout / 2.0, SimulationPlan::READ_TIMEOUT].min)
|
|
109
|
+
timeout = [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
|
|
66
110
|
transport = @transport_mutex.synchronize { @transport }
|
|
67
|
-
return
|
|
111
|
+
return released if transport.nil?
|
|
68
112
|
|
|
69
|
-
transport.flush(timeout)
|
|
113
|
+
transport.flush(timeout) && released
|
|
70
114
|
end
|
|
71
115
|
|
|
72
116
|
# Make a POST request to the Bitfab API.
|
|
73
117
|
# Returns parsed JSON response hash.
|
|
74
|
-
def request(endpoint, payload, timeout: nil, max_retries: 1, retry_delay: 0.1)
|
|
75
|
-
send_encoded(endpoint, Serialize.safe_generate(payload), timeout:, max_retries:, retry_delay:)
|
|
118
|
+
def request(endpoint, payload, timeout: nil, max_retries: 1, retry_delay: 0.1, method: "POST")
|
|
119
|
+
send_encoded(endpoint, Serialize.safe_generate(payload), timeout:, max_retries:, retry_delay:, method:)
|
|
76
120
|
end
|
|
77
121
|
|
|
78
122
|
# POST an already-encoded body. The span transport encodes its own batches,
|
|
79
123
|
# so routing them back through #request would encode the same data twice.
|
|
80
|
-
def send_encoded(endpoint, body, timeout: nil, max_retries: 1, retry_delay: 0.1)
|
|
124
|
+
def send_encoded(endpoint, body, timeout: nil, max_retries: 1, retry_delay: 0.1, method: "POST")
|
|
81
125
|
send_prepared(
|
|
82
126
|
endpoint,
|
|
83
127
|
Compress.prepare_request_body(body),
|
|
84
128
|
timeout:,
|
|
85
129
|
max_retries:,
|
|
86
|
-
retry_delay
|
|
130
|
+
retry_delay:,
|
|
131
|
+
method:
|
|
87
132
|
)
|
|
88
133
|
end
|
|
89
134
|
|
|
90
|
-
def send_prepared(endpoint, prepared, timeout: nil, max_retries: 1, retry_delay: 0.1)
|
|
135
|
+
def send_prepared(endpoint, prepared, timeout: nil, max_retries: 1, retry_delay: 0.1, method: "POST")
|
|
91
136
|
uri = URI("#{@service_url}#{endpoint}")
|
|
92
137
|
request_timeout = timeout || @timeout
|
|
93
138
|
|
|
@@ -99,7 +144,7 @@ module Bitfab
|
|
|
99
144
|
http.open_timeout = request_timeout
|
|
100
145
|
http.read_timeout = request_timeout
|
|
101
146
|
|
|
102
|
-
req = Net::
|
|
147
|
+
req = Net::HTTPGenericRequest.new(method, true, true, uri.request_uri, headers)
|
|
103
148
|
req["Content-Encoding"] = prepared.content_encoding if prepared.content_encoding
|
|
104
149
|
req.body = prepared.body
|
|
105
150
|
|
|
@@ -128,18 +173,32 @@ module Bitfab
|
|
|
128
173
|
|
|
129
174
|
# Queue an external span on this client's trace transport (fire-and-forget).
|
|
130
175
|
def send_external_span(payload)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
payload.merge("
|
|
136
|
-
|
|
137
|
-
)
|
|
176
|
+
if (data = payload.dig("rawSpan", "span_data")).is_a?(Hash) && !data.key?("runtime")
|
|
177
|
+
payload = payload.merge("rawSpan" => payload["rawSpan"].merge("span_data" => data.merge("runtime" => ThreadPropagation.runtime)))
|
|
178
|
+
end
|
|
179
|
+
if payload["rawSpan"].is_a?(Hash) && !payload["rawSpan"].key?("span_origin")
|
|
180
|
+
payload = payload.merge("rawSpan" => payload["rawSpan"].merge("span_origin" => Bitfab.make_span_origin("span")))
|
|
181
|
+
end
|
|
182
|
+
submit = ->(record) {
|
|
183
|
+
ref = carrier_ref(record)
|
|
184
|
+
@trace_completion.record(ref.trace_id, ref.span_id) if ref&.span_id
|
|
185
|
+
trace_transport&.submit(
|
|
186
|
+
"external_span",
|
|
187
|
+
record.merge("sdkVersion" => VERSION),
|
|
188
|
+
recorded_meta("external_span", record, ref)
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
@simulation_plan ? @simulation_plan.send_span(payload, &submit) : submit.call(payload)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def send_internal_trace(function_id, payload)
|
|
195
|
+
record = payload.merge("functionId" => function_id, "sdkVersion" => VERSION)
|
|
196
|
+
trace_transport&.submit("internal_trace", record, recorded_meta("internal_trace", record, nil))
|
|
138
197
|
end
|
|
139
198
|
|
|
140
199
|
# Make a GET request to the Bitfab API.
|
|
141
200
|
# Returns parsed JSON response hash.
|
|
142
|
-
def get(endpoint, timeout: nil)
|
|
201
|
+
def get(endpoint, timeout: nil, connection_close: false)
|
|
143
202
|
uri = URI("#{@service_url}#{endpoint}")
|
|
144
203
|
request_timeout = timeout || @timeout
|
|
145
204
|
|
|
@@ -150,6 +209,7 @@ module Bitfab
|
|
|
150
209
|
|
|
151
210
|
# request_uri (not path) so any query string on the endpoint survives.
|
|
152
211
|
req = Net::HTTP::Get.new(uri.request_uri, headers)
|
|
212
|
+
req["Connection"] = "close" if connection_close
|
|
153
213
|
response = http.request(req)
|
|
154
214
|
|
|
155
215
|
unless response.is_a?(Net::HTTPSuccess)
|
|
@@ -189,7 +249,7 @@ module Bitfab
|
|
|
189
249
|
# same org and trace function or the server rejects the replay
|
|
190
250
|
def start_replay(trace_function_key, limit, trace_ids: nil, code_change_description: nil,
|
|
191
251
|
code_change_files: nil, experiment_group_id: nil, name: nil, include_db_branch_lease: false, dataset_ids: nil,
|
|
192
|
-
grader_ids: nil, db_branch_settings: nil)
|
|
252
|
+
grader_ids: nil, db_branch_settings: nil, attempts: 1, only_with_assertions: false, include_original_metadata: false)
|
|
193
253
|
payload = {
|
|
194
254
|
"traceFunctionKey" => trace_function_key
|
|
195
255
|
}
|
|
@@ -211,6 +271,9 @@ module Bitfab
|
|
|
211
271
|
end
|
|
212
272
|
end
|
|
213
273
|
payload["graderIds"] = grader_ids unless grader_ids.nil?
|
|
274
|
+
payload["attempts"] = attempts if attempts > 1
|
|
275
|
+
payload["onlyWithAssertions"] = true if only_with_assertions
|
|
276
|
+
payload["includeOriginalMetadata"] = true if include_original_metadata
|
|
214
277
|
payload["dbBranchSettings"] = db_branch_settings unless db_branch_settings.nil?
|
|
215
278
|
git_state = GitState.resolved
|
|
216
279
|
payload["git"] = git_state if git_state
|
|
@@ -279,8 +342,9 @@ module Bitfab
|
|
|
279
342
|
request("/api/sdk/replay/releaseDbBranchLease", {"neonBranchId" => neon_branch_id}, timeout: 30)
|
|
280
343
|
end
|
|
281
344
|
|
|
282
|
-
def resolve_db_branch_lease(test_run_id, trace_id, db_branch_settings = nil)
|
|
345
|
+
def resolve_db_branch_lease(test_run_id, trace_id, db_branch_settings = nil, attempt = 0)
|
|
283
346
|
payload = {"testRunId" => test_run_id, "traceId" => trace_id}
|
|
347
|
+
payload["attempt"] = attempt if attempt.positive?
|
|
284
348
|
payload["dbBranchSettings"] = db_branch_settings unless db_branch_settings.nil?
|
|
285
349
|
request(
|
|
286
350
|
"/api/sdk/replay/resolveDbBranchLease",
|
|
@@ -291,15 +355,18 @@ module Bitfab
|
|
|
291
355
|
|
|
292
356
|
# Queue an external trace on this client's trace transport (fire-and-forget).
|
|
293
357
|
def send_external_trace(payload)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
358
|
+
submit = ->(record) {
|
|
359
|
+
ref = carrier_ref(record)
|
|
360
|
+
if record["completed"] == true && ref
|
|
361
|
+
@trace_completion.close(ref.trace_id, dropped: record["dropped"] == true) do |count|
|
|
362
|
+
submit_external_trace(record.merge("expectedSpanCount" => count))
|
|
363
|
+
end
|
|
364
|
+
else
|
|
365
|
+
@trace_completion.open(ref.trace_id) if ref
|
|
366
|
+
submit_external_trace(record)
|
|
298
367
|
end
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
submit_external_trace(payload)
|
|
302
|
-
end
|
|
368
|
+
}
|
|
369
|
+
@simulation_plan ? @simulation_plan.send_trace(payload, &submit) : submit.call(payload)
|
|
303
370
|
end
|
|
304
371
|
|
|
305
372
|
def submit_external_trace(payload)
|
|
@@ -647,11 +714,15 @@ module Bitfab
|
|
|
647
714
|
# Wait for queued spans and trace completions to reach the server, within
|
|
648
715
|
# one total deadline. Returns true when everything landed in time.
|
|
649
716
|
def flush_traces(timeout: 30)
|
|
650
|
-
|
|
717
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + [timeout, 0].max
|
|
718
|
+
released = SimulationPlan.release_all([timeout / 2.0, SimulationPlan::READ_TIMEOUT].min)
|
|
719
|
+
flushed = Transport.flush_trace_transports([deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max)
|
|
720
|
+
released && flushed
|
|
651
721
|
end
|
|
652
722
|
end
|
|
653
723
|
|
|
654
724
|
at_exit do
|
|
655
|
-
|
|
725
|
+
SimulationPlan.release_all(2, stop: true)
|
|
726
|
+
Transport.shutdown_trace_transports(2)
|
|
656
727
|
end
|
|
657
728
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
class Labels
|
|
7
|
+
LABELS_PATH = "/api/sdk/traces/labels"
|
|
8
|
+
HUMAN_LABELS_PATH = "#{LABELS_PATH}/human"
|
|
9
|
+
|
|
10
|
+
def initialize(http_client)
|
|
11
|
+
@http_client = http_client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def save(label:, annotation:, trace_id: nil, original_trace_id: nil, attempt: nil,
|
|
15
|
+
confidence: nil, test_run_id: nil, assertion_id: nil)
|
|
16
|
+
update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
|
|
17
|
+
update["label"] = label
|
|
18
|
+
update["annotation"] = annotation
|
|
19
|
+
update["confidence"] = confidence unless confidence.nil?
|
|
20
|
+
save_all([update], test_run_id:).first
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def save_all(labels, test_run_id: nil)
|
|
24
|
+
payload = {"labels" => labels}
|
|
25
|
+
payload["testRunId"] = test_run_id unless test_run_id.nil?
|
|
26
|
+
@http_client.request(LABELS_PATH, payload)["labels"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def skip(trace_id: nil, original_trace_id: nil, attempt: nil, test_run_id: nil, assertion_id: nil)
|
|
30
|
+
update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
|
|
31
|
+
save_all([update.merge("skip" => true)], test_run_id:).first
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def archive(trace_id: nil, original_trace_id: nil, attempt: nil, test_run_id: nil, assertion_id: nil)
|
|
35
|
+
update = label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
|
|
36
|
+
save_all([update.merge("archive" => true)], test_run_id:).first
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def save_human(label:, annotation:, trace_id:, confidence: nil, assertion_id: nil)
|
|
40
|
+
update = {"traceId" => trace_id, "label" => label, "annotation" => annotation}
|
|
41
|
+
update["assertionId"] = assertion_id unless assertion_id.nil?
|
|
42
|
+
update["confidence"] = confidence unless confidence.nil?
|
|
43
|
+
save_human_all([update]).first
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def save_human_all(labels)
|
|
47
|
+
@http_client.request(HUMAN_LABELS_PATH, {"labels" => labels})["labels"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def get(trace_id)
|
|
51
|
+
get_all([trace_id]).first
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def get_all(trace_ids)
|
|
55
|
+
return [] if trace_ids.empty?
|
|
56
|
+
|
|
57
|
+
query = URI.encode_www_form("traceIds" => trace_ids.join(","))
|
|
58
|
+
@http_client.get("#{LABELS_PATH}?#{query}")["labels"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def label_target(trace_id:, original_trace_id:, attempt:, assertion_id:)
|
|
64
|
+
if trace_id.nil? == original_trace_id.nil?
|
|
65
|
+
raise ArgumentError, "Pass exactly one of trace_id or original_trace_id. " \
|
|
66
|
+
"Use original_trace_id for a replay verdict, with the test_run_id it ran under."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
update = {}
|
|
70
|
+
update["assertionId"] = assertion_id unless assertion_id.nil?
|
|
71
|
+
if trace_id.nil?
|
|
72
|
+
update["originalTraceId"] = original_trace_id
|
|
73
|
+
update["attempt"] = attempt unless attempt.nil?
|
|
74
|
+
else
|
|
75
|
+
update["traceId"] = trace_id
|
|
76
|
+
end
|
|
77
|
+
update
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -75,9 +75,10 @@ module Bitfab
|
|
|
75
75
|
# thread. Child threads and processes intentionally do not inherit it.
|
|
76
76
|
def with_context(test_run_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
|
|
77
77
|
mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
|
|
78
|
-
db_branch_lease: nil, db_branch_timings: nil, source_bitfab_trace_id: nil)
|
|
78
|
+
db_branch_lease: nil, db_branch_timings: nil, source_bitfab_trace_id: nil, replay_attempt: nil)
|
|
79
79
|
previous = Thread.current.thread_variable_get(REPLAY_CONTEXT_KEY)
|
|
80
80
|
ctx = {
|
|
81
|
+
replay_attempt:,
|
|
81
82
|
test_run_id:,
|
|
82
83
|
input_source_span_id:,
|
|
83
84
|
input_source_trace_id:,
|
|
@@ -256,12 +257,27 @@ module Bitfab
|
|
|
256
257
|
# A raising callback never crashes the run.
|
|
257
258
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
258
259
|
def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil,
|
|
259
|
-
max_concurrency:
|
|
260
|
-
dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked",
|
|
260
|
+
concurrency: nil, max_concurrency: ReplayConcurrency::UNSET, code_change_description: CODE_CHANGE_UNSET, code_change_files: CODE_CHANGE_UNSET, experiment_group_id: nil,
|
|
261
|
+
dataset_id: nil, dataset_ids: nil, grader_ids: nil, mock: "marked", attempts: ReplayConcurrency::UNSET, only_with_assertions: false, dry_run: false,
|
|
261
262
|
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)
|
|
263
|
+
concurrency_options = ReplayConcurrency.resolve(concurrency, attempts, max_concurrency)
|
|
264
|
+
attempts = concurrency_options.attempts
|
|
265
|
+
max_concurrency = concurrency_options.max_concurrency
|
|
266
|
+
process_executor = (concurrency_options.primitive == "process") ? ReplayProcesses.current : nil
|
|
267
|
+
if concurrency_options.primitive == "process" && process_executor.nil?
|
|
268
|
+
raise ArgumentError, "Process replay requires bitfab-replay --registry so each child can load the application"
|
|
269
|
+
end
|
|
262
270
|
unless MOCK_STRATEGIES.include?(mock.to_s)
|
|
263
271
|
raise ArgumentError, "Invalid mock strategy '#{mock}'. Must be one of: #{MOCK_STRATEGIES.join(", ")}"
|
|
264
272
|
end
|
|
273
|
+
unless max_concurrency.nil?
|
|
274
|
+
unless max_concurrency.is_a?(Integer) && max_concurrency.positive?
|
|
275
|
+
raise ArgumentError, "max_concurrency must be a positive integer or nil for unlimited concurrency."
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
unless attempts.is_a?(Integer) && attempts.between?(1, 100)
|
|
279
|
+
raise ArgumentError, "attempts must be an integer from 1 to 100."
|
|
280
|
+
end
|
|
265
281
|
item_finish_callback = on_item_finish || on_progress
|
|
266
282
|
if trace_ids
|
|
267
283
|
raise ArgumentError, "trace_ids must contain at least one trace ID." if trace_ids.empty?
|
|
@@ -281,7 +297,7 @@ module Bitfab
|
|
|
281
297
|
# function, recorded under another). Only fires when the method's key is
|
|
282
298
|
# introspectable; an untraced method falls through to the persistence
|
|
283
299
|
# check in complete_replay. Mirrors the TypeScript/Python SDKs.
|
|
284
|
-
declared_key = Traceable.trace_function_key_for(receiver, method_name)
|
|
300
|
+
declared_key = method_name && Traceable.trace_function_key_for(receiver, method_name)
|
|
285
301
|
if declared_key && declared_key != trace_function_key
|
|
286
302
|
raise ArgumentError,
|
|
287
303
|
"Method #{method_name} is traced under trace function key '#{declared_key}' but replay was " \
|
|
@@ -289,6 +305,13 @@ module Bitfab
|
|
|
289
305
|
"or point at the method traced under '#{trace_function_key}'."
|
|
290
306
|
end
|
|
291
307
|
|
|
308
|
+
raise ArgumentError, "Replay requires an API key." unless client.send(:resolve_api_key)
|
|
309
|
+
unless declared_key
|
|
310
|
+
callable = method_name ? receiver.method(method_name) : receiver
|
|
311
|
+
receiver = ManagedCallable.new(client, trace_function_key, callable)
|
|
312
|
+
method_name = :call
|
|
313
|
+
end
|
|
314
|
+
|
|
292
315
|
http_client = client.instance_variable_get(:@http_client)
|
|
293
316
|
|
|
294
317
|
# Resolved override list: per-call overrides FIRST (they win), then the
|
|
@@ -302,7 +325,7 @@ module Bitfab
|
|
|
302
325
|
# the count), so it's omitted from the request entirely.
|
|
303
326
|
effective_limit = trace_ids ? nil : (limit || 5)
|
|
304
327
|
|
|
305
|
-
include_db_branch_lease = db_branch_enabled?(db_branch)
|
|
328
|
+
include_db_branch_lease = db_branch_enabled?(db_branch) && !dry_run
|
|
306
329
|
|
|
307
330
|
# code_change_files controls capture: omitted auto-captures, an array
|
|
308
331
|
# wins, and explicit nil opts out. Preserve a caller-supplied description
|
|
@@ -329,22 +352,45 @@ module Bitfab
|
|
|
329
352
|
include_db_branch_lease:,
|
|
330
353
|
dataset_ids: resolved_dataset_ids,
|
|
331
354
|
grader_ids:,
|
|
355
|
+
attempts:,
|
|
356
|
+
only_with_assertions:,
|
|
357
|
+
include_original_metadata: !adapt_inputs.nil?,
|
|
332
358
|
db_branch_settings: resolved_db_branch_settings
|
|
333
359
|
)
|
|
334
360
|
test_run_id = replay_data["testRunId"]
|
|
335
361
|
test_run_url = replay_data["testRunUrl"]
|
|
336
362
|
full_test_run_url = "#{client.service_url}#{test_run_url}"
|
|
337
|
-
server_items =
|
|
363
|
+
server_items = attempts.times.flat_map do |attempt|
|
|
364
|
+
(replay_data["items"] || []).map do |source|
|
|
365
|
+
item = source.merge("attempt" => attempt)
|
|
366
|
+
if attempt.positive?
|
|
367
|
+
item.delete("dbBranchLease")
|
|
368
|
+
item.delete("dbBranchLeaseError")
|
|
369
|
+
end
|
|
370
|
+
item
|
|
371
|
+
end
|
|
372
|
+
end
|
|
338
373
|
|
|
339
374
|
result_items = if server_items.any?
|
|
340
375
|
process_items(http_client, server_items, receiver, method_name, test_run_id, max_concurrency, mock.to_s,
|
|
341
376
|
adapt_inputs, include_db_branch_lease, resolved_db_branch_settings, on_item_start:,
|
|
342
377
|
on_item_finish: item_finish_callback,
|
|
343
|
-
mock_overrides: resolved_overrides)
|
|
378
|
+
mock_overrides: resolved_overrides, dry_run:, process_executor:)
|
|
344
379
|
else
|
|
345
380
|
[]
|
|
346
381
|
end
|
|
347
382
|
|
|
383
|
+
if dry_run
|
|
384
|
+
begin
|
|
385
|
+
http_client.complete_replay(test_run_id)
|
|
386
|
+
rescue => e
|
|
387
|
+
warn "Bitfab: could not finalize dry run: #{e.message}"
|
|
388
|
+
end
|
|
389
|
+
result = {items: public_replay_items(result_items), test_run_id:, test_run_url: full_test_run_url, attempts:}
|
|
390
|
+
write_replay_result_file(result)
|
|
391
|
+
return result
|
|
392
|
+
end
|
|
393
|
+
|
|
348
394
|
# Spans and completions ride a batched transport, so the run is only safe
|
|
349
395
|
# to finalize once the server confirms every replay trace it queued: the
|
|
350
396
|
# trace-ID mapping complete_replay builds would otherwise race the
|
|
@@ -440,6 +486,7 @@ module Bitfab
|
|
|
440
486
|
result_items.each { |item| item.delete(:_sdk_trace_id) }
|
|
441
487
|
|
|
442
488
|
result = {
|
|
489
|
+
attempts:,
|
|
443
490
|
items: result_items,
|
|
444
491
|
test_run_id:,
|
|
445
492
|
test_run_url: full_test_run_url
|
|
@@ -698,7 +745,7 @@ module Bitfab
|
|
|
698
745
|
# Process all replay items, optionally in parallel using threads.
|
|
699
746
|
def process_items(http_client, server_items, receiver, method_name, test_run_id, max_concurrency, mock_strategy,
|
|
700
747
|
adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, on_item_start: nil, on_item_finish: nil,
|
|
701
|
-
mock_overrides: [])
|
|
748
|
+
mock_overrides: [], dry_run: false, process_executor: nil)
|
|
702
749
|
concurrency = max_concurrency || server_items.length
|
|
703
750
|
|
|
704
751
|
# Lifecycle callbacks run from worker threads in the parallel path, so the
|
|
@@ -719,7 +766,7 @@ module Bitfab
|
|
|
719
766
|
# surfaced. original_trace_id (the historical trace being replayed, taken
|
|
720
767
|
# from the server item) is what a UI keys on to identify what just finished.
|
|
721
768
|
# source_trace_id/source_span_id are kept as deprecated aliases.
|
|
722
|
-
report_start = lambda do |original_trace_id, original_span_id|
|
|
769
|
+
report_start = lambda do |original_trace_id, original_span_id, attempt|
|
|
723
770
|
progress_mutex.synchronize do
|
|
724
771
|
started += 1
|
|
725
772
|
next unless on_item_start
|
|
@@ -728,6 +775,7 @@ module Bitfab
|
|
|
728
775
|
on_item_start.call({
|
|
729
776
|
type: "started", test_run_id:, started:, completed:, total:, succeeded:, errored:,
|
|
730
777
|
item: {
|
|
778
|
+
attempt:,
|
|
731
779
|
original_trace_id:,
|
|
732
780
|
original_span_id:,
|
|
733
781
|
source_trace_id: original_trace_id,
|
|
@@ -759,7 +807,7 @@ module Bitfab
|
|
|
759
807
|
# Deliver this item's trace now, then read its server id back off the
|
|
760
808
|
# ingest response. A flush failure never crashes the run: the id stays
|
|
761
809
|
# nil and the end-of-run barrier remains the authority on persistence.
|
|
762
|
-
flush_finished_item_trace.call
|
|
810
|
+
flush_finished_item_trace.call unless dry_run
|
|
763
811
|
result[:trace_id] = http_client.peek_server_trace_id(result[:_sdk_trace_id]) || result[:trace_id]
|
|
764
812
|
progress_mutex.synchronize do
|
|
765
813
|
completed += 1
|
|
@@ -771,6 +819,8 @@ module Bitfab
|
|
|
771
819
|
on_item_finish.call({
|
|
772
820
|
test_run_id:, completed:, total:, succeeded:, errored:,
|
|
773
821
|
item: {
|
|
822
|
+
attempt: result[:attempt],
|
|
823
|
+
ingestion_type: result[:ingestion_type],
|
|
774
824
|
trace_id: result[:trace_id],
|
|
775
825
|
original_trace_id:,
|
|
776
826
|
original_span_id:,
|
|
@@ -801,11 +851,19 @@ module Bitfab
|
|
|
801
851
|
end
|
|
802
852
|
end
|
|
803
853
|
|
|
854
|
+
execute = lambda do |item|
|
|
855
|
+
if process_executor
|
|
856
|
+
process_executor.call(item, test_run_id, mock_strategy, include_db_branch_lease, db_branch_settings, dry_run:)
|
|
857
|
+
else
|
|
858
|
+
process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
|
|
859
|
+
adapt_inputs, include_db_branch_lease, db_branch_settings, mock_overrides:, dry_run:)
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
|
|
804
863
|
if concurrency <= 1
|
|
805
864
|
server_items.map do |item|
|
|
806
|
-
report_start.call(original_trace_id_of(item), original_span_id_of(item))
|
|
807
|
-
result =
|
|
808
|
-
adapt_inputs, include_db_branch_lease, db_branch_settings, mock_overrides:)
|
|
865
|
+
report_start.call(original_trace_id_of(item), original_span_id_of(item), item["attempt"] || 0)
|
|
866
|
+
result = execute.call(item)
|
|
809
867
|
report.call(result, original_trace_id_of(item), original_span_id_of(item), test_run_id)
|
|
810
868
|
result
|
|
811
869
|
end
|
|
@@ -821,9 +879,8 @@ module Bitfab
|
|
|
821
879
|
item, idx = work_mutex.synchronize { work_queue.shift }
|
|
822
880
|
break unless item
|
|
823
881
|
|
|
824
|
-
report_start.call(original_trace_id_of(item), original_span_id_of(item))
|
|
825
|
-
result =
|
|
826
|
-
adapt_inputs, include_db_branch_lease, db_branch_settings, mock_overrides:)
|
|
882
|
+
report_start.call(original_trace_id_of(item), original_span_id_of(item), item["attempt"] || 0)
|
|
883
|
+
result = execute.call(item)
|
|
827
884
|
results_mutex.synchronize { results[idx] = result }
|
|
828
885
|
report.call(result, original_trace_id_of(item), original_span_id_of(item), test_run_id)
|
|
829
886
|
end
|
|
@@ -842,8 +899,9 @@ module Bitfab
|
|
|
842
899
|
# than propagated, so one bad trace never aborts the whole replay run
|
|
843
900
|
# (mirrors the TypeScript and Python SDKs' per-item rescue).
|
|
844
901
|
def process_single_item(http_client, server_item, receiver, method_name, test_run_id, mock_strategy,
|
|
845
|
-
adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, mock_overrides: [])
|
|
902
|
+
adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, mock_overrides: [], dry_run: false)
|
|
846
903
|
metrics = extract_server_item_metrics(server_item)
|
|
904
|
+
attempt = server_item["attempt"] || 0
|
|
847
905
|
# The ORIGINAL (historical) trace/span this item replays. Canonical
|
|
848
906
|
# keys are originalTraceId/originalSpanId; older servers send them under
|
|
849
907
|
# the deprecated sourceTraceId/sourceSpanId aliases (see *_of helpers).
|
|
@@ -866,7 +924,7 @@ module Bitfab
|
|
|
866
924
|
db_branch_timings = include_db_branch_lease ? server_item["dbBranchTimings"] : nil
|
|
867
925
|
if include_db_branch_lease && lease.nil? && lease_error.nil?
|
|
868
926
|
begin
|
|
869
|
-
resolved = http_client.resolve_db_branch_lease(test_run_id, original_trace_id, db_branch_settings)
|
|
927
|
+
resolved = http_client.resolve_db_branch_lease(test_run_id, original_trace_id, db_branch_settings, attempt)
|
|
870
928
|
rescue => cause
|
|
871
929
|
error = DbBranchReplayError.new(
|
|
872
930
|
"lease_request_failed",
|
|
@@ -905,7 +963,7 @@ module Bitfab
|
|
|
905
963
|
overrides_present = !mock_overrides.nil? && !mock_overrides.empty?
|
|
906
964
|
include_outputs = mock_strategy == "all"
|
|
907
965
|
mock_tree = nil
|
|
908
|
-
if mock_strategy == "all" || mock_strategy == "marked" || overrides_present
|
|
966
|
+
if !dry_run && (mock_strategy == "all" || mock_strategy == "marked" || overrides_present)
|
|
909
967
|
begin
|
|
910
968
|
tree = http_client.get_span_tree(
|
|
911
969
|
original_span_id,
|
|
@@ -930,6 +988,7 @@ module Bitfab
|
|
|
930
988
|
fetch_span_output = (mock_tree && !include_outputs) ? build_span_output_fetcher(http_client) : nil
|
|
931
989
|
|
|
932
990
|
adapt_ctx = {
|
|
991
|
+
metadata: server_item["originalMetadata"].is_a?(Hash) ? server_item["originalMetadata"].dup : {},
|
|
933
992
|
original_trace_id:,
|
|
934
993
|
original_span_id:,
|
|
935
994
|
# Deprecated aliases for original_trace_id/original_span_id.
|
|
@@ -951,6 +1010,8 @@ module Bitfab
|
|
|
951
1010
|
fetch_span_output:,
|
|
952
1011
|
adapt_inputs:,
|
|
953
1012
|
adapt_ctx:,
|
|
1013
|
+
attempt:,
|
|
1014
|
+
dry_run:,
|
|
954
1015
|
db_branch_lease: lease,
|
|
955
1016
|
db_branch_timings:,
|
|
956
1017
|
source_bitfab_trace_id: original_trace_id,
|
|
@@ -960,6 +1021,8 @@ module Bitfab
|
|
|
960
1021
|
rescue => e
|
|
961
1022
|
warn "Bitfab: replay item for span #{original_span_id} failed before execution: #{e.message}"
|
|
962
1023
|
{
|
|
1024
|
+
attempt:,
|
|
1025
|
+
ingestion_type: server_item["ingestionType"],
|
|
963
1026
|
input: [],
|
|
964
1027
|
result: nil,
|
|
965
1028
|
original_output: nil,
|
|
@@ -1103,6 +1166,7 @@ module Bitfab
|
|
|
1103
1166
|
original_duration_ms: server_item["originalDurationMs"] || server_item["durationMs"],
|
|
1104
1167
|
original_tokens: server_item["originalTokens"] || server_item["tokens"],
|
|
1105
1168
|
original_model: server_item["originalModel"] || server_item["model"],
|
|
1169
|
+
ingestion_type: server_item["ingestionType"],
|
|
1106
1170
|
tokens: nil
|
|
1107
1171
|
}
|
|
1108
1172
|
end
|
|
@@ -1149,6 +1213,7 @@ module Bitfab
|
|
|
1149
1213
|
status = http_client.get_replay_status(test_run_id, expected_span_counts)
|
|
1150
1214
|
ready = status["traceIds"]
|
|
1151
1215
|
ready = {} unless ready.is_a?(Hash)
|
|
1216
|
+
read_back_trace_ids.merge!(ready)
|
|
1152
1217
|
missing = expected_span_counts.keys - ready.keys
|
|
1153
1218
|
return read_back_trace_ids if missing.empty?
|
|
1154
1219
|
break if Otel.monotonic_now >= deadline
|
|
@@ -1185,7 +1250,7 @@ module Bitfab
|
|
|
1185
1250
|
def execute_item(item, receiver, method_name, test_run_id, input_source_span_id = nil, metrics = {},
|
|
1186
1251
|
input_source_trace_id: nil, mock_strategy: "marked", mock_tree: nil, mock_overrides: nil,
|
|
1187
1252
|
fetch_span_output: nil, adapt_inputs: nil, adapt_ctx: nil, db_branch_lease: nil, db_branch_timings: nil,
|
|
1188
|
-
source_bitfab_trace_id: nil, db_snapshot_ref: nil, http_client: nil)
|
|
1253
|
+
source_bitfab_trace_id: nil, db_snapshot_ref: nil, http_client: nil, attempt: 0, dry_run: false)
|
|
1189
1254
|
args, kwargs = Serialize.deserialize_inputs(item)
|
|
1190
1255
|
|
|
1191
1256
|
fn_result = nil
|
|
@@ -1201,10 +1266,11 @@ module Bitfab
|
|
|
1201
1266
|
# Declared before this item's first span is submitted: the transport
|
|
1202
1267
|
# records delivery only for traces someone asked about, so anything
|
|
1203
1268
|
# submitted before this would go untracked.
|
|
1204
|
-
http_client&.track_trace_deliveries([sdk_trace_id])
|
|
1269
|
+
http_client&.track_trace_deliveries([sdk_trace_id]) unless dry_run
|
|
1205
1270
|
|
|
1206
1271
|
begin
|
|
1207
1272
|
ReplayContext.with_context(
|
|
1273
|
+
replay_attempt: attempt,
|
|
1208
1274
|
test_run_id:,
|
|
1209
1275
|
input_source_span_id:,
|
|
1210
1276
|
input_source_trace_id:,
|
|
@@ -1231,7 +1297,7 @@ module Bitfab
|
|
|
1231
1297
|
rescue => e
|
|
1232
1298
|
replay_error = e
|
|
1233
1299
|
end
|
|
1234
|
-
if replay_error.nil?
|
|
1300
|
+
if replay_error.nil? && !dry_run
|
|
1235
1301
|
begin
|
|
1236
1302
|
replay_started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1237
1303
|
fn_result = if kwargs.empty?
|
|
@@ -1239,6 +1305,7 @@ module Bitfab
|
|
|
1239
1305
|
else
|
|
1240
1306
|
receiver.send(method_name, *args, **kwargs)
|
|
1241
1307
|
end
|
|
1308
|
+
fn_result = fn_result.to_a if fn_result.is_a?(Enumerator)
|
|
1242
1309
|
replay_duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - replay_started) * 1000).round
|
|
1243
1310
|
rescue => e
|
|
1244
1311
|
# The method ran and raised, so it still has a duration.
|
|
@@ -1258,7 +1325,9 @@ module Bitfab
|
|
|
1258
1325
|
|
|
1259
1326
|
item_error = fn_error || replay_error
|
|
1260
1327
|
{
|
|
1261
|
-
|
|
1328
|
+
attempt:,
|
|
1329
|
+
ingestion_type: metrics[:ingestion_type],
|
|
1330
|
+
input: kwargs.empty? ? args : [*args, kwargs],
|
|
1262
1331
|
result: fn_result,
|
|
1263
1332
|
original_output: item["output"],
|
|
1264
1333
|
error: item_error&.message,
|
data/lib/bitfab/replay_cli.rb
CHANGED
|
@@ -14,7 +14,13 @@ module Bitfab
|
|
|
14
14
|
registry_path, replay_argv = parse(argv)
|
|
15
15
|
loader = registry_loader || method(:load_registry)
|
|
16
16
|
registry = loader.call(registry_path)
|
|
17
|
-
|
|
17
|
+
if replay_argv.first == "--execute-item"
|
|
18
|
+
raise ArgumentError, "Expected one process assignment path" unless replay_argv.length == 2
|
|
19
|
+
return ReplayProcesses.execute_assignment(registry, replay_argv[1], stderr:)
|
|
20
|
+
end
|
|
21
|
+
ReplayProcesses.with_registry(registry_path, replay_argv, stderr:) do
|
|
22
|
+
ReplayCli.run(registry, argv: replay_argv, stdout:, stderr:)
|
|
23
|
+
end
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def main(argv: ARGV, stdout: $stdout, stderr: $stderr)
|