bitfab 0.36.2 → 0.36.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.
- checksums.yaml +4 -4
- data/lib/bitfab/client.rb +14 -8
- data/lib/bitfab/replay.rb +202 -66
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +13 -10
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5578e8fdcdd81610b4e65fa94a5e9be88339e816742f6ba7fea81b53867eedf5
|
|
4
|
+
data.tar.gz: 308d35969ac643d1ddcfb76c169a6108906c958275b392f7d159ea647b0bf63c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c0511909223595a7792e780d0c77b8c0f5ef2d239e4b2a5dda7946a63fbe4801893ae02ba3392b74520ad9d82844e519d3319a900f22c30bef4857e616c0420
|
|
7
|
+
data.tar.gz: 0ae83e2f731bc2f6fa9f564b6fe8754b7fff82c5ae03a914067cca6fb75f4ddb10e935bba8976de6f0dabe7025812e49d9392e2ace61b5202c02d80b21fe00aa
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -132,21 +132,27 @@ module Bitfab
|
|
|
132
132
|
# +:min_cu+/+:max_cu+ size the branch compute in Neon Compute Units and
|
|
133
133
|
# +:warmup_sql+ warms its cache. Read the resolved branch inside the
|
|
134
134
|
# replayed method with +Bitfab.current_replay_branch+.
|
|
135
|
-
# @param
|
|
136
|
-
# it finishes, with a running-totals hash
|
|
137
|
-
# errored: }.
|
|
138
|
-
#
|
|
139
|
-
# items whose method ran (:succeeded) from items
|
|
140
|
-
# A raising callback never crashes the run.
|
|
135
|
+
# @param on_item_finish [#call, nil] optional callback invoked exactly once
|
|
136
|
+
# per item as it finishes, always with a running-totals hash containing
|
|
137
|
+
# { completed:, total:, succeeded:, errored:, item: }. It never receives a
|
|
138
|
+
# whole-run completion event. Replay does not know pass/fail yet, so the
|
|
139
|
+
# totals only distinguish items whose method ran (:succeeded) from items
|
|
140
|
+
# that raised (:errored). A raising callback never crashes the run.
|
|
141
|
+
# @param on_progress [#call, nil] deprecated compatibility callback. It
|
|
142
|
+
# receives the same per-item events plus the legacy item-less +"complete"+
|
|
143
|
+
# event. Ignored when +on_item_finish+ is also provided.
|
|
144
|
+
# @param on_item_start [#call, nil] optional callback invoked when a worker
|
|
145
|
+
# begins processing each item. Pair it with on_item_finish to distinguish
|
|
146
|
+
# queued work from in-flight work. A raising callback never crashes the run.
|
|
141
147
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
142
148
|
def replay(receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency: 10,
|
|
143
149
|
name: nil, code_change_description: Replay::CODE_CHANGE_UNSET, code_change_files: Replay::CODE_CHANGE_UNSET, experiment_group_id: nil, dataset_id: nil, grader_ids: nil, mock: "marked",
|
|
144
|
-
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_progress: nil)
|
|
150
|
+
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)
|
|
145
151
|
Replay.run(
|
|
146
152
|
self, receiver, method_name,
|
|
147
153
|
trace_function_key:, limit:, trace_ids:, name:, max_concurrency:,
|
|
148
154
|
code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, grader_ids:, mock:, adapt_inputs:,
|
|
149
|
-
mock_override:, db_branch:,
|
|
155
|
+
mock_override:, db_branch:, on_item_start:, on_item_finish:,
|
|
150
156
|
on_progress:
|
|
151
157
|
)
|
|
152
158
|
end
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -12,6 +12,30 @@ require_relative "traceable"
|
|
|
12
12
|
require_relative "transport"
|
|
13
13
|
|
|
14
14
|
module Bitfab
|
|
15
|
+
# A requested replay database branch could not be resolved. The resolver code
|
|
16
|
+
# and original message remain structured for callers to inspect.
|
|
17
|
+
class DbBranchReplayError < StandardError
|
|
18
|
+
attr_reader :code, :original_trace_id
|
|
19
|
+
|
|
20
|
+
def initialize(code, message, original_trace_id)
|
|
21
|
+
super(message)
|
|
22
|
+
@code = code
|
|
23
|
+
@original_trace_id = original_trace_id
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Whole-run replay failure with every item collected before the run failed.
|
|
28
|
+
class ReplayError < StandardError
|
|
29
|
+
attr_reader :items, :test_run_id, :test_run_url
|
|
30
|
+
|
|
31
|
+
def initialize(message, items:, test_run_id:, test_run_url:)
|
|
32
|
+
super(message)
|
|
33
|
+
@items = items
|
|
34
|
+
@test_run_id = test_run_id
|
|
35
|
+
@test_run_url = test_run_url
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
15
39
|
# Replay mock strategies. Mirrors the Python and TypeScript SDKs.
|
|
16
40
|
#
|
|
17
41
|
# - "marked" : only spans declared with mock_on_replay: true return historical
|
|
@@ -85,6 +109,50 @@ module Bitfab
|
|
|
85
109
|
|
|
86
110
|
module_function
|
|
87
111
|
|
|
112
|
+
def json_safe(value)
|
|
113
|
+
case value
|
|
114
|
+
when Exception
|
|
115
|
+
serialized = {type: value.class.name, message: value.message, backtrace: value.backtrace}
|
|
116
|
+
if value.is_a?(DbBranchReplayError)
|
|
117
|
+
serialized[:code] = value.code
|
|
118
|
+
serialized[:original_trace_id] = value.original_trace_id
|
|
119
|
+
serialized[:cause] = json_safe(value.cause) if value.cause
|
|
120
|
+
end
|
|
121
|
+
serialized
|
|
122
|
+
when Hash
|
|
123
|
+
value.to_h { |key, child| [key, json_safe(child)] }
|
|
124
|
+
when Array
|
|
125
|
+
value.map { |child| json_safe(child) }
|
|
126
|
+
else
|
|
127
|
+
value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def replay_item_error_message(error)
|
|
132
|
+
return error.message unless error.is_a?(DbBranchReplayError)
|
|
133
|
+
|
|
134
|
+
"Replay requested a database branch for trace #{error.original_trace_id} but it " \
|
|
135
|
+
"could not be resolved (#{error.code}): #{error.message}. The method was not run, " \
|
|
136
|
+
"because replaying it against the live database would produce a result that looks " \
|
|
137
|
+
"valid but did not use the historical data you asked for."
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def public_replay_items(items)
|
|
141
|
+
items.map { |item| item.except(:_sdk_trace_id) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def preserve_replay_failure(items, test_run_id, test_run_url)
|
|
145
|
+
yield
|
|
146
|
+
rescue => cause
|
|
147
|
+
error = ReplayError.new(
|
|
148
|
+
cause.message,
|
|
149
|
+
items: public_replay_items(items),
|
|
150
|
+
test_run_id:,
|
|
151
|
+
test_run_url:
|
|
152
|
+
)
|
|
153
|
+
raise error, cause:
|
|
154
|
+
end
|
|
155
|
+
|
|
88
156
|
# Replay historical traces through a method and create a test run.
|
|
89
157
|
#
|
|
90
158
|
# Fetches the last N traces for the given trace function key, re-runs each
|
|
@@ -133,11 +201,11 @@ module Bitfab
|
|
|
133
201
|
# and ceiling in Neon Compute Units, and +:warmup_sql+ warms the branch's
|
|
134
202
|
# cache before the replayed method sees it. Read the resolved branch
|
|
135
203
|
# inside the method with +Bitfab.current_replay_branch+.
|
|
136
|
-
# @param
|
|
204
|
+
# @param on_item_finish [#call, nil] optional callback invoked once per item as
|
|
137
205
|
# it finishes, with a running-totals hash { completed:, total:, succeeded:,
|
|
138
206
|
# errored:, item: } where item is { trace_id:, original_trace_id:,
|
|
139
207
|
# original_span_id:, error:, duration_ms: } for the single item that just
|
|
140
|
-
#
|
|
208
|
+
# finished (source_trace_id/source_span_id remain as deprecated aliases).
|
|
141
209
|
# trace_id is the new server replay trace id, written in after the run
|
|
142
210
|
# completes (nil during progress callbacks); original_trace_id is the
|
|
143
211
|
# ORIGINAL (historical) trace that was replayed, error is that item's replay
|
|
@@ -145,14 +213,22 @@ module Bitfab
|
|
|
145
213
|
# duration_ms is how long that one trace took to replay. Use it to
|
|
146
214
|
# render replay progress (e.g. a per-trace log). A raising callback never
|
|
147
215
|
# crashes the run.
|
|
216
|
+
# @param on_progress [#call, nil] deprecated compatibility callback. It
|
|
217
|
+
# receives the same per-item finish events plus the legacy whole-run
|
|
218
|
+
# +"complete"+ event. Ignored when +on_item_finish+ is also provided.
|
|
219
|
+
# @param on_item_start [#call, nil] optional callback invoked when a worker
|
|
220
|
+
# begins processing each item, before replay setup and customer code run.
|
|
221
|
+
# Pair it with on_item_finish to distinguish queued work from in-flight work.
|
|
222
|
+
# A raising callback never crashes the run.
|
|
148
223
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
149
224
|
def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, name: nil,
|
|
150
225
|
max_concurrency: 10, code_change_description: CODE_CHANGE_UNSET, code_change_files: CODE_CHANGE_UNSET, experiment_group_id: nil,
|
|
151
226
|
dataset_id: nil, grader_ids: nil, mock: "marked",
|
|
152
|
-
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_progress: nil)
|
|
227
|
+
adapt_inputs: nil, mock_override: nil, db_branch: nil, on_item_start: nil, on_item_finish: nil, on_progress: nil)
|
|
153
228
|
unless MOCK_STRATEGIES.include?(mock.to_s)
|
|
154
229
|
raise ArgumentError, "Invalid mock strategy '#{mock}'. Must be one of: #{MOCK_STRATEGIES.join(", ")}"
|
|
155
230
|
end
|
|
231
|
+
item_finish_callback = on_item_finish || on_progress
|
|
156
232
|
if trace_ids
|
|
157
233
|
raise ArgumentError, "trace_ids must contain at least one trace ID." if trace_ids.empty?
|
|
158
234
|
if trace_ids.length > 100
|
|
@@ -223,11 +299,13 @@ module Bitfab
|
|
|
223
299
|
)
|
|
224
300
|
test_run_id = replay_data["testRunId"]
|
|
225
301
|
test_run_url = replay_data["testRunUrl"]
|
|
302
|
+
full_test_run_url = "#{client.service_url}#{test_run_url}"
|
|
226
303
|
server_items = replay_data["items"] || []
|
|
227
304
|
|
|
228
305
|
result_items = if server_items.any?
|
|
229
306
|
process_items(http_client, server_items, receiver, method_name, test_run_id, max_concurrency, mock.to_s,
|
|
230
|
-
adapt_inputs, include_db_branch_lease, resolved_db_branch_settings,
|
|
307
|
+
adapt_inputs, include_db_branch_lease, resolved_db_branch_settings, on_item_start:,
|
|
308
|
+
on_item_finish: item_finish_callback,
|
|
231
309
|
mock_overrides: resolved_overrides)
|
|
232
310
|
else
|
|
233
311
|
[]
|
|
@@ -237,12 +315,16 @@ module Bitfab
|
|
|
237
315
|
# to finalize once the server confirms every replay trace it queued: the
|
|
238
316
|
# trace-ID mapping complete_replay builds would otherwise race the
|
|
239
317
|
# in-flight batches and hand back nil for every item.
|
|
240
|
-
|
|
318
|
+
preserve_replay_failure(result_items, test_run_id, full_test_run_url) do
|
|
319
|
+
wait_for_replay_persistence(http_client, test_run_id, result_items.map { |item| item[:_sdk_trace_id] })
|
|
320
|
+
end
|
|
241
321
|
|
|
242
322
|
# complete_replay finalizes the run and returns the token/diagnostic
|
|
243
323
|
# mapping; its failures propagate loudly because a run that never
|
|
244
324
|
# completed can't be finalized.
|
|
245
|
-
complete_response =
|
|
325
|
+
complete_response = preserve_replay_failure(result_items, test_run_id, full_test_run_url) do
|
|
326
|
+
http_client.complete_replay(test_run_id)
|
|
327
|
+
end
|
|
246
328
|
trace_id_map = complete_response&.dig("traceIds")
|
|
247
329
|
# Per-replay-trace token usage keyed by server trace id: the REPLAYED
|
|
248
330
|
# run's tokens (span-aggregated server-side), used below to fill each
|
|
@@ -278,11 +360,18 @@ module Bitfab
|
|
|
278
360
|
if completed_count.positive? && missing.length == completed_count
|
|
279
361
|
trace_count = complete_response["traceCount"]
|
|
280
362
|
server_count = trace_count.nil? ? "" : " The server persisted #{trace_count} trace(s) for this run."
|
|
281
|
-
|
|
363
|
+
cause = RuntimeError.new("Replay completed but the server has no persisted trace for " \
|
|
282
364
|
"any of the #{completed_count} completed item(s) " \
|
|
283
365
|
"(test_run_id #{test_run_id}).#{server_count} Trace uploads were " \
|
|
284
366
|
"flushed, so either the uploads failed or the replayed method is " \
|
|
285
|
-
"not traced (no root span was emitted)."
|
|
367
|
+
"not traced (no root span was emitted).")
|
|
368
|
+
error = ReplayError.new(
|
|
369
|
+
cause.message,
|
|
370
|
+
items: public_replay_items(result_items),
|
|
371
|
+
test_run_id:,
|
|
372
|
+
test_run_url: full_test_run_url
|
|
373
|
+
)
|
|
374
|
+
raise error, cause:
|
|
286
375
|
end
|
|
287
376
|
# SOME completed items missing: per-item upload failure. Warn, but
|
|
288
377
|
# return the run; the items that landed can still be labeled.
|
|
@@ -301,16 +390,14 @@ module Bitfab
|
|
|
301
390
|
result = {
|
|
302
391
|
items: result_items,
|
|
303
392
|
test_run_id:,
|
|
304
|
-
test_run_url:
|
|
393
|
+
test_run_url: full_test_run_url
|
|
305
394
|
}
|
|
306
|
-
# Persist the enriched result
|
|
307
|
-
#
|
|
308
|
-
# write it to BITFAB_REPLAY_RESULT_PATH when the plugin set that env var,
|
|
309
|
-
# and stream a terminal "complete" progress event. The plugin prefers the
|
|
310
|
-
# streamed event and falls back to the file. The event routes through
|
|
311
|
-
# on_progress so only progress-reporting runs emit it.
|
|
395
|
+
# Persist the enriched result so the Bitfab plugin never has to parse the
|
|
396
|
+
# replay's stdout, which a dependency's logging can corrupt.
|
|
312
397
|
write_replay_result_file(result)
|
|
313
|
-
|
|
398
|
+
# Preserve the legacy terminal event only for on_progress. on_item_finish
|
|
399
|
+
# is strictly item-scoped, so every invocation always includes an item.
|
|
400
|
+
if on_item_finish.nil? && on_progress
|
|
314
401
|
errored = result_items.count { |item| !item[:error].nil? }
|
|
315
402
|
total = result_items.length
|
|
316
403
|
begin
|
|
@@ -543,7 +630,7 @@ module Bitfab
|
|
|
543
630
|
begin
|
|
544
631
|
dir = File.dirname(result_path)
|
|
545
632
|
FileUtils.mkdir_p(dir) unless dir.nil? || dir.empty? || dir == "."
|
|
546
|
-
File.write(result_path, "#{
|
|
633
|
+
File.write(result_path, "#{Bitfab.serialize_replay_result(result)}\n")
|
|
547
634
|
rescue => e
|
|
548
635
|
warn "Bitfab: failed to write replay result to BITFAB_REPLAY_RESULT_PATH (#{result_path}): #{e.message}"
|
|
549
636
|
end
|
|
@@ -551,36 +638,56 @@ module Bitfab
|
|
|
551
638
|
|
|
552
639
|
# Process all replay items, optionally in parallel using threads.
|
|
553
640
|
def process_items(http_client, server_items, receiver, method_name, test_run_id, max_concurrency, mock_strategy,
|
|
554
|
-
adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil,
|
|
641
|
+
adapt_inputs = nil, include_db_branch_lease = false, db_branch_settings = nil, on_item_start: nil, on_item_finish: nil,
|
|
555
642
|
mock_overrides: [])
|
|
556
643
|
concurrency = max_concurrency || server_items.length
|
|
557
644
|
|
|
558
|
-
#
|
|
559
|
-
#
|
|
560
|
-
#
|
|
561
|
-
# concurrently). A raising callback is swallowed: progress UI must never
|
|
562
|
-
# crash the run.
|
|
645
|
+
# Lifecycle callbacks run from worker threads in the parallel path, so the
|
|
646
|
+
# mutex makes counter updates safe and serializes each callback. A raising
|
|
647
|
+
# callback is swallowed because progress UI must never crash the run.
|
|
563
648
|
total = server_items.length
|
|
564
649
|
progress_mutex = Mutex.new
|
|
650
|
+
started = 0
|
|
565
651
|
completed = 0
|
|
566
652
|
succeeded = 0
|
|
567
653
|
errored = 0
|
|
568
|
-
# Each event carries the single item that just
|
|
654
|
+
# Each event carries the single item that just finished so a progress UI
|
|
569
655
|
# can render per-trace pass/fail as the run streams. The item's :trace_id
|
|
570
656
|
# is nil at this stage: the server replay trace id isn't known until run()
|
|
571
657
|
# writes it in after complete_replay, and the client correlation id is
|
|
572
658
|
# never surfaced. original_trace_id (the historical trace being replayed,
|
|
573
659
|
# taken from the server item) is what a UI keys on to identify what just
|
|
574
|
-
#
|
|
575
|
-
|
|
576
|
-
|
|
660
|
+
# finished. source_trace_id/source_span_id are kept as deprecated aliases.
|
|
661
|
+
report_start = lambda do |original_trace_id, original_span_id|
|
|
662
|
+
progress_mutex.synchronize do
|
|
663
|
+
started += 1
|
|
664
|
+
next unless on_item_start
|
|
577
665
|
|
|
666
|
+
begin
|
|
667
|
+
on_item_start.call({
|
|
668
|
+
type: "started", test_run_id:, started:, completed:, total:, succeeded:, errored:,
|
|
669
|
+
item: {
|
|
670
|
+
original_trace_id:,
|
|
671
|
+
original_span_id:,
|
|
672
|
+
source_trace_id: original_trace_id,
|
|
673
|
+
source_span_id: original_span_id
|
|
674
|
+
}
|
|
675
|
+
})
|
|
676
|
+
rescue => e
|
|
677
|
+
warn "Bitfab: replay on_item_start callback raised: #{e.message}"
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
report = lambda do |result, original_trace_id, original_span_id, test_run_id|
|
|
578
683
|
progress_mutex.synchronize do
|
|
579
684
|
completed += 1
|
|
580
685
|
error = result[:error]
|
|
581
686
|
error.nil? ? (succeeded += 1) : (errored += 1)
|
|
687
|
+
next unless on_item_finish
|
|
688
|
+
|
|
582
689
|
begin
|
|
583
|
-
|
|
690
|
+
on_item_finish.call({
|
|
584
691
|
test_run_id:, completed:, total:, succeeded:, errored:,
|
|
585
692
|
item: {
|
|
586
693
|
trace_id: result[:trace_id],
|
|
@@ -593,6 +700,8 @@ module Bitfab
|
|
|
593
700
|
result: result[:result],
|
|
594
701
|
original_output: result[:original_output],
|
|
595
702
|
error:,
|
|
703
|
+
trace_error: result[:trace_error],
|
|
704
|
+
replay_error: result[:replay_error],
|
|
596
705
|
duration_ms: result[:duration_ms],
|
|
597
706
|
tokens: result[:tokens],
|
|
598
707
|
model: result[:model],
|
|
@@ -600,13 +709,14 @@ module Bitfab
|
|
|
600
709
|
}
|
|
601
710
|
})
|
|
602
711
|
rescue => e
|
|
603
|
-
warn "Bitfab: replay
|
|
712
|
+
warn "Bitfab: replay on_item_finish callback raised: #{e.message}"
|
|
604
713
|
end
|
|
605
714
|
end
|
|
606
715
|
end
|
|
607
716
|
|
|
608
717
|
if concurrency <= 1
|
|
609
718
|
server_items.map do |item|
|
|
719
|
+
report_start.call(original_trace_id_of(item), original_span_id_of(item))
|
|
610
720
|
result = process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
|
|
611
721
|
adapt_inputs, include_db_branch_lease, db_branch_settings, mock_overrides:)
|
|
612
722
|
report.call(result, original_trace_id_of(item), original_span_id_of(item), test_run_id)
|
|
@@ -624,6 +734,7 @@ module Bitfab
|
|
|
624
734
|
item, idx = work_mutex.synchronize { work_queue.shift }
|
|
625
735
|
break unless item
|
|
626
736
|
|
|
737
|
+
report_start.call(original_trace_id_of(item), original_span_id_of(item))
|
|
627
738
|
result = process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
|
|
628
739
|
adapt_inputs, include_db_branch_lease, db_branch_settings, mock_overrides:)
|
|
629
740
|
results_mutex.synchronize { results[idx] = result }
|
|
@@ -664,16 +775,26 @@ module Bitfab
|
|
|
664
775
|
lease_error = include_db_branch_lease ? server_item["dbBranchLeaseError"] : nil
|
|
665
776
|
db_snapshot_ref = server_item["dbSnapshotRef"]
|
|
666
777
|
if include_db_branch_lease && lease.nil? && lease_error.nil?
|
|
667
|
-
|
|
778
|
+
begin
|
|
779
|
+
resolved = http_client.resolve_db_branch_lease(test_run_id, original_trace_id, db_branch_settings)
|
|
780
|
+
rescue => cause
|
|
781
|
+
error = DbBranchReplayError.new(
|
|
782
|
+
"lease_request_failed",
|
|
783
|
+
"Bitfab could not request the database branch: #{cause.message}",
|
|
784
|
+
original_trace_id
|
|
785
|
+
)
|
|
786
|
+
raise error, cause:
|
|
787
|
+
end
|
|
668
788
|
lease = resolved["lease"]
|
|
669
789
|
lease_error = resolved["leaseError"]
|
|
670
790
|
db_snapshot_ref = resolved["dbSnapshotRef"] || db_snapshot_ref
|
|
671
791
|
end
|
|
672
792
|
if lease_error
|
|
673
|
-
raise
|
|
674
|
-
|
|
675
|
-
"
|
|
676
|
-
|
|
793
|
+
raise DbBranchReplayError.new(
|
|
794
|
+
lease_error["code"].to_s,
|
|
795
|
+
lease_error["message"].to_s,
|
|
796
|
+
original_trace_id
|
|
797
|
+
)
|
|
677
798
|
end
|
|
678
799
|
|
|
679
800
|
span = http_client.get_external_span(original_span_id, replay_view: true)
|
|
@@ -752,7 +873,9 @@ module Bitfab
|
|
|
752
873
|
input: [],
|
|
753
874
|
result: nil,
|
|
754
875
|
original_output: nil,
|
|
755
|
-
error: e
|
|
876
|
+
error: replay_item_error_message(e),
|
|
877
|
+
trace_error: nil,
|
|
878
|
+
replay_error: e,
|
|
756
879
|
duration_ms: metrics&.dig(:duration_ms),
|
|
757
880
|
tokens: metrics&.dig(:tokens),
|
|
758
881
|
model: metrics&.dig(:model),
|
|
@@ -941,51 +1064,64 @@ module Bitfab
|
|
|
941
1064
|
|
|
942
1065
|
fn_result = nil
|
|
943
1066
|
fn_error = nil
|
|
1067
|
+
replay_error = nil
|
|
944
1068
|
# Client-side correlation id that tags this item's replay spans so the
|
|
945
1069
|
# server can echo back the row id it minted (resolved in run()'s
|
|
946
1070
|
# complete-replay loop). Carried on the item under :_sdk_trace_id, never
|
|
947
1071
|
# surfaced as the public :trace_id.
|
|
948
1072
|
sdk_trace_id = SecureRandom.uuid
|
|
949
1073
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1074
|
+
begin
|
|
1075
|
+
ReplayContext.with_context(
|
|
1076
|
+
test_run_id:,
|
|
1077
|
+
input_source_span_id:,
|
|
1078
|
+
input_source_trace_id:,
|
|
1079
|
+
trace_id: sdk_trace_id,
|
|
1080
|
+
mock_tree:,
|
|
1081
|
+
mock_strategy:,
|
|
1082
|
+
mock_overrides:,
|
|
1083
|
+
fetch_span_output:,
|
|
1084
|
+
db_branch_lease:,
|
|
1085
|
+
source_bitfab_trace_id:
|
|
1086
|
+
) do
|
|
1087
|
+
begin
|
|
1088
|
+
if adapt_inputs
|
|
1089
|
+
ctx = adapt_ctx || {
|
|
1090
|
+
original_trace_id: nil,
|
|
1091
|
+
original_span_id: input_source_span_id,
|
|
1092
|
+
# Deprecated aliases for original_trace_id/original_span_id.
|
|
1093
|
+
source_trace_id: nil,
|
|
1094
|
+
source_span_id: input_source_span_id
|
|
1095
|
+
}
|
|
1096
|
+
args, kwargs = adapt_inputs.call(args, kwargs, ctx)
|
|
1097
|
+
end
|
|
1098
|
+
rescue => e
|
|
1099
|
+
replay_error = e
|
|
1100
|
+
end
|
|
1101
|
+
if replay_error.nil?
|
|
1102
|
+
begin
|
|
1103
|
+
fn_result = if kwargs.empty?
|
|
1104
|
+
receiver.send(method_name, *args)
|
|
1105
|
+
else
|
|
1106
|
+
receiver.send(method_name, *args, **kwargs)
|
|
1107
|
+
end
|
|
1108
|
+
rescue => e
|
|
1109
|
+
fn_error = e
|
|
1110
|
+
end
|
|
1111
|
+
end
|
|
979
1112
|
end
|
|
980
1113
|
rescue => e
|
|
981
|
-
|
|
1114
|
+
replay_error = e
|
|
982
1115
|
end
|
|
983
1116
|
|
|
1117
|
+
item_error = fn_error || replay_error
|
|
984
1118
|
{
|
|
985
1119
|
input: args,
|
|
986
1120
|
result: fn_result,
|
|
987
1121
|
original_output: item["output"],
|
|
988
|
-
error:
|
|
1122
|
+
error: item_error&.message,
|
|
1123
|
+
trace_error: fn_error,
|
|
1124
|
+
replay_error:,
|
|
989
1125
|
duration_ms: metrics[:duration_ms],
|
|
990
1126
|
tokens: metrics[:tokens],
|
|
991
1127
|
model: metrics[:model],
|
data/lib/bitfab/version.rb
CHANGED
data/lib/bitfab.rb
CHANGED
|
@@ -144,24 +144,27 @@ module Bitfab
|
|
|
144
144
|
CurrentTrace.new(entry[:trace_id])
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
-
# A ready-made
|
|
148
|
-
#
|
|
149
|
-
# progress
|
|
150
|
-
#
|
|
151
|
-
# (progress must not crash a run). The progress hash's item is forwarded
|
|
152
|
-
# verbatim through to_json, so the plugin sees the source trace id, local
|
|
153
|
-
# replay trace id, outputs, error, and duration of the item that just settled.
|
|
147
|
+
# A ready-made replay lifecycle reporter. Pass it as both on_item_start and
|
|
148
|
+
# on_item_finish so the Bitfab plugin sees in-flight and finished items. It writes
|
|
149
|
+
# @@bitfab:progress lines to stderr, leaving stdout for ReplayResult JSON, and
|
|
150
|
+
# never raises because progress reporting must not crash a run.
|
|
154
151
|
#
|
|
155
152
|
# @example
|
|
156
|
-
#
|
|
153
|
+
# reporter = Bitfab.method(:report_replay_progress)
|
|
154
|
+
# Bitfab.client.replay(..., on_item_start: reporter, on_item_finish: reporter)
|
|
157
155
|
#
|
|
158
156
|
# @param progress [Hash] running totals with keys completed, total,
|
|
159
157
|
# succeeded, errored, and item (the object replay already passes to
|
|
160
|
-
#
|
|
158
|
+
# on_item_finish)
|
|
161
159
|
def report_replay_progress(progress)
|
|
162
|
-
warn "#{BITFAB_PROGRESS_PREFIX}#{progress.to_json}"
|
|
160
|
+
warn "#{BITFAB_PROGRESS_PREFIX}#{Replay.json_safe(progress).to_json}"
|
|
163
161
|
rescue
|
|
164
162
|
nil
|
|
165
163
|
end
|
|
164
|
+
|
|
165
|
+
# Serialize a replay result while retaining structured exception fields.
|
|
166
|
+
def serialize_replay_result(result)
|
|
167
|
+
JSON.pretty_generate(Replay.json_safe(result))
|
|
168
|
+
end
|
|
166
169
|
end
|
|
167
170
|
end
|