bitfab 0.36.10 → 0.38.0
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 +2 -2
- data/lib/bitfab/client.rb +59 -41
- data/lib/bitfab/mock_override.rb +4 -4
- data/lib/bitfab/replay.rb +101 -46
- data/lib/bitfab/span_context.rb +22 -1
- data/lib/bitfab/version.rb +1 -1
- 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: e4bb59de5ab0ec989d85e8be14ee08ad935ada5145e01826b34657fc8c9e6623
|
|
4
|
+
data.tar.gz: 78b0255c5a3062470f4149baf632c94a5b18b430567485a1ba58f5b9cf9b9534
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e127f198a39372bebaff5a0cf395b4b6b00f2ede4bf9768b8e3a4556ee00271ddf44fe4a40f16fcffac72d3def4d3c83c3f756801ba156edbe58ecbfd189f2e5
|
|
7
|
+
data.tar.gz: ca803b3df3783f6bb01d5eb5a518300334d0ccd4f31d43f3870c981779cd69710d076a1ded3f058c3b9e8aa65f643b3872dd60f99ac8e4394cd5f97855ec1970
|
data/README.md
CHANGED
|
@@ -301,7 +301,7 @@ client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "marked")
|
|
|
301
301
|
# "none": everything runs real code
|
|
302
302
|
client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "none")
|
|
303
303
|
|
|
304
|
-
# "all": every child
|
|
304
|
+
# "all": every matched recorded child returns history; missing occurrences fail closed
|
|
305
305
|
client.replay(pipeline, :process, trace_function_key: "my-fn", mock: "all")
|
|
306
306
|
```
|
|
307
307
|
|
|
@@ -331,7 +331,7 @@ class Pipeline
|
|
|
331
331
|
end
|
|
332
332
|
```
|
|
333
333
|
|
|
334
|
-
Use the default `mock: "marked"` behavior when you want to iterate on `process`'s logic without paying for the LLM call each run. Use `mock: "all"` for the cheapest possible replay (every child span returns its recorded output).
|
|
334
|
+
Use the default `mock: "marked"` behavior when you want to iterate on `process`'s logic without paying for the LLM call each run. Use `mock: "all"` for the cheapest possible replay (every matched recorded child span returns its recorded output; a missing occurrence fails the item closed).
|
|
335
335
|
|
|
336
336
|
### Error Handling
|
|
337
337
|
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Bitfab
|
|
|
16
16
|
UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i
|
|
17
17
|
|
|
18
18
|
# Sentinel returned by check_mock_replay when this span should run real
|
|
19
|
-
# code (no mock active
|
|
19
|
+
# code (no mock active or the base strategy did not select it).
|
|
20
20
|
# Using a sentinel rather than nil/false avoids confusing legitimate mocked
|
|
21
21
|
# outputs (which may themselves be nil or false).
|
|
22
22
|
MOCK_REPLAY_MISS = Object.new.freeze
|
|
@@ -87,9 +87,10 @@ module Bitfab
|
|
|
87
87
|
# @param receiver [Object, Class] an instance for instance methods, or a Class for class methods
|
|
88
88
|
# @param method_name [Symbol] the method to replay
|
|
89
89
|
# @param trace_function_key [String] the trace function key for this method
|
|
90
|
-
# @param limit [Integer, nil] maximum number of traces to replay (default: 5
|
|
91
|
-
#
|
|
92
|
-
# determines how many traces replay
|
|
90
|
+
# @param limit [Integer, nil] maximum number of traces to replay (default: 5;
|
|
91
|
+
# maximum: 5,000). Ignored when trace_ids or dataset_id is passed because
|
|
92
|
+
# either source already determines how many traces replay. Supplying
|
|
93
|
+
# trace_ids also emits a warning.
|
|
93
94
|
# @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100)
|
|
94
95
|
# @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
|
|
95
96
|
# @param code_change_description [String, nil] optional rationale for the
|
|
@@ -111,7 +112,9 @@ module Bitfab
|
|
|
111
112
|
# same org and trace function or the server rejects the replay
|
|
112
113
|
# @param mock [String] mock strategy for child spans: "marked" (default),
|
|
113
114
|
# "none", or "all". "marked" only mocks spans declared with
|
|
114
|
-
# mock_on_replay: true; "all" mocks every child span.
|
|
115
|
+
# mock_on_replay: true; "all" mocks every matched recorded child span. A selected
|
|
116
|
+
# occurrence that is unavailable errors the item without executing the
|
|
117
|
+
# real child.
|
|
115
118
|
# @param adapt_inputs [#call, nil] optional hook to reshape recorded inputs
|
|
116
119
|
# onto the method's current signature when its shape changed after the
|
|
117
120
|
# traces were captured. Receives (args, kwargs, ctx) where ctx is
|
|
@@ -122,16 +125,17 @@ module Bitfab
|
|
|
122
125
|
# match.call(node) selects spans to substitute (node is
|
|
123
126
|
# { trace_function_key:, span_name:, type:, original_span_id: }). value is
|
|
124
127
|
# EITHER a flat value injected directly OR a callable invoked with
|
|
125
|
-
# { node:, inputs:, get_original_output: }; its result becomes
|
|
126
|
-
# output (full replacement), so downstream real code runs against it. The
|
|
128
|
+
# { node:, inputs:, kwargs:, get_original_output: }; its result becomes
|
|
129
|
+
# the span's output (full replacement), so downstream real code runs against it. The
|
|
127
130
|
# first matching override wins. Per-call overrides take precedence over
|
|
128
131
|
# those registered via register_mock_override, and both take precedence
|
|
129
132
|
# over the base mock strategy. The root span is never overridden.
|
|
130
|
-
# @param db_branch [Hash, nil]
|
|
131
|
-
#
|
|
132
|
-
# +:min_cu+/+:max_cu+ size the branch compute
|
|
133
|
-
# +:warmup_sql+ warms its cache. Read the
|
|
134
|
-
# replayed method with
|
|
133
|
+
# @param db_branch [Boolean, Hash, nil] +true+ or a Hash enables database
|
|
134
|
+
# branching; +false+/+nil+ leave it off. An empty Hash uses the mirror
|
|
135
|
+
# project's own sizing. Keys +:min_cu+/+:max_cu+ size the branch compute
|
|
136
|
+
# in Neon Compute Units and +:warmup_sql+ warms its cache. Read the
|
|
137
|
+
# resolved branch inside the replayed method with
|
|
138
|
+
# +Bitfab.current_replay_branch+.
|
|
135
139
|
# @param on_item_finish [#call, nil] optional callback invoked exactly once
|
|
136
140
|
# per item as it finishes, always with a running-totals hash containing
|
|
137
141
|
# { completed:, total:, succeeded:, errored:, item: }. It never receives a
|
|
@@ -281,7 +285,7 @@ module Bitfab
|
|
|
281
285
|
)
|
|
282
286
|
resolved_capture_when = "always"
|
|
283
287
|
end
|
|
284
|
-
return yield if resolved_capture_when == "nested" && SpanContext.current.nil?
|
|
288
|
+
return yield if resolved_capture_when == "nested" && SpanContext.current.nil? && ReplayContext.current_span.nil?
|
|
285
289
|
|
|
286
290
|
# Span setup runs before the user's block. Tracing is a side-channel, so
|
|
287
291
|
# if anything here raises (id generation, trace-state bookkeeping, a
|
|
@@ -297,8 +301,8 @@ module Bitfab
|
|
|
297
301
|
resolved_test_run_id = nil
|
|
298
302
|
resolved_input_source_span_id = nil
|
|
299
303
|
begin
|
|
300
|
-
parent = SpanContext.current
|
|
301
304
|
replay_ctx = ReplayContext.current
|
|
305
|
+
parent = SpanContext.current || ReplayContext.current_span
|
|
302
306
|
trace_id = parent ? parent[:trace_id] : (replay_ctx&.dig(:trace_id) || SecureRandom.uuid)
|
|
303
307
|
span_id = SecureRandom.uuid
|
|
304
308
|
parent_span_id = parent&.dig(:span_id)
|
|
@@ -420,7 +424,12 @@ module Bitfab
|
|
|
420
424
|
# Deprecated wire alias, kept so this SDK still reports usage
|
|
421
425
|
# against servers that predate the rename.
|
|
422
426
|
source_trace_id: replay_ctx[:source_bitfab_trace_id],
|
|
423
|
-
accessed: replay_ctx[:db_snapshot_accessed] == true
|
|
427
|
+
accessed: replay_ctx[:db_snapshot_accessed] == true,
|
|
428
|
+
# Echoed verbatim (camelCase inside) rather than re-cased into
|
|
429
|
+
# this record's snake_case: it is the server's own object
|
|
430
|
+
# coming back, and a translation layer here is one more thing
|
|
431
|
+
# to drift.
|
|
432
|
+
timings: replay_ctx[:db_branch_timings]
|
|
424
433
|
}
|
|
425
434
|
end
|
|
426
435
|
|
|
@@ -440,12 +449,14 @@ module Bitfab
|
|
|
440
449
|
end
|
|
441
450
|
|
|
442
451
|
begin
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
452
|
+
ReplayContext.with_span(trace_id:, span_id:) do
|
|
453
|
+
SpanContext.with_span(trace_id:, span_id:) do
|
|
454
|
+
result = yield
|
|
455
|
+
ensure
|
|
456
|
+
# Capture contexts before the span context is popped
|
|
457
|
+
span_contexts = SpanContext.current&.dig(:contexts)
|
|
458
|
+
span_prompt = SpanContext.current&.dig(:prompt)
|
|
459
|
+
end
|
|
449
460
|
end
|
|
450
461
|
rescue => e
|
|
451
462
|
error = e.message
|
|
@@ -466,12 +477,6 @@ module Bitfab
|
|
|
466
477
|
# collects yielded values as the span output, and finalizes the span
|
|
467
478
|
# once iteration completes (or errors).
|
|
468
479
|
#
|
|
469
|
-
# Limitation: when the source enumerator itself runs its body in a
|
|
470
|
-
# separate fiber (e.g. `Enumerator.new { |y| ... }` or `enum_for(...)`
|
|
471
|
-
# without a block), nested `bitfab_span` calls inside that body fiber
|
|
472
|
-
# still see an empty stack because `Thread.current[STACK_KEY]` is
|
|
473
|
-
# fiber-local. Lazy chains over collections (`.lazy.map`) and ordinary
|
|
474
|
-
# `each` callbacks DO run in the iterating fiber and nest correctly.
|
|
475
480
|
if result.is_a?(Enumerator)
|
|
476
481
|
return wrap_enumerator(result, trace_id:, span_id:, finalize:)
|
|
477
482
|
end
|
|
@@ -544,19 +549,24 @@ module Bitfab
|
|
|
544
549
|
Enumerator.new do |yielder|
|
|
545
550
|
# Push our span onto this fiber's stack so anything that runs in this
|
|
546
551
|
# fiber (including the user's lazy block and `each` callbacks) sees
|
|
547
|
-
# the right parent.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
552
|
+
# the right parent. The fiber bridge carries the same parent into
|
|
553
|
+
# Enumerator.new / enum_for source fibers on this thread.
|
|
554
|
+
ReplayContext.with_span(trace_id:, span_id:) do
|
|
555
|
+
SpanContext.with_fiber_bridge(trace_id:, span_id:) do
|
|
556
|
+
SpanContext.stack.push(span_entry)
|
|
557
|
+
begin
|
|
558
|
+
source.each do |value|
|
|
559
|
+
yielded << value
|
|
560
|
+
yielder << value
|
|
561
|
+
end
|
|
562
|
+
finalize.call(yielded, nil)
|
|
563
|
+
rescue => e
|
|
564
|
+
finalize.call(yielded, e.message)
|
|
565
|
+
raise
|
|
566
|
+
ensure
|
|
567
|
+
SpanContext.stack.pop
|
|
568
|
+
end
|
|
553
569
|
end
|
|
554
|
-
finalize.call(yielded, nil)
|
|
555
|
-
rescue => e
|
|
556
|
-
finalize.call(yielded, e.message)
|
|
557
|
-
raise
|
|
558
|
-
ensure
|
|
559
|
-
SpanContext.stack.pop
|
|
560
570
|
end
|
|
561
571
|
end
|
|
562
572
|
end
|
|
@@ -612,6 +622,9 @@ module Bitfab
|
|
|
612
622
|
usage["source_trace_id"] = db_snapshot_usage[:original_trace_id]
|
|
613
623
|
end
|
|
614
624
|
usage["accessed"] = db_snapshot_usage[:accessed]
|
|
625
|
+
if db_snapshot_usage[:timings]
|
|
626
|
+
usage["timings"] = db_snapshot_usage[:timings]
|
|
627
|
+
end
|
|
615
628
|
raw_trace["db_snapshot_usage"] = usage
|
|
616
629
|
end
|
|
617
630
|
|
|
@@ -799,7 +812,11 @@ module Bitfab
|
|
|
799
812
|
return MOCK_REPLAY_MISS
|
|
800
813
|
end
|
|
801
814
|
|
|
802
|
-
|
|
815
|
+
unless mock_entry
|
|
816
|
+
raise StandardError,
|
|
817
|
+
"Replay selected span '#{trace_function_key}:#{span_name}' for mocking, " \
|
|
818
|
+
"but recorded occurrence #{call_index + 1} is unavailable. The real span was not executed."
|
|
819
|
+
end
|
|
803
820
|
|
|
804
821
|
[resolve_recorded_output(mock_entry, replay_ctx), "recorded"]
|
|
805
822
|
end
|
|
@@ -902,7 +919,8 @@ module Bitfab
|
|
|
902
919
|
# @param capture_when [String, Symbol] "always" to allow a root trace, or
|
|
903
920
|
# "nested" to capture only when another Bitfab span is active. Unknown
|
|
904
921
|
# values warn once and default to "always"
|
|
905
|
-
# @param mock_on_replay [Boolean] mark this span for the default "marked"
|
|
922
|
+
# @param mock_on_replay [Boolean] mark this span for the default "marked"
|
|
923
|
+
# mock strategy. A missing selected occurrence fails closed.
|
|
906
924
|
def wrap(klass, method_name, name: nil, type: "custom", capture_when: "always", mock_on_replay: false)
|
|
907
925
|
Bitfab::Traceable.wrap(
|
|
908
926
|
klass, method_name,
|
data/lib/bitfab/mock_override.rb
CHANGED
|
@@ -31,10 +31,10 @@ module Bitfab
|
|
|
31
31
|
# nil is a legitimate injected value. To inject a proc as the literal
|
|
32
32
|
# output, wrap it in a callable value: value: ->(ctx) { the_proc }.
|
|
33
33
|
#
|
|
34
|
-
# Unlike the TypeScript SDK,
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
34
|
+
# Unlike the TypeScript SDK, get_original_output is synchronous. Under
|
|
35
|
+
# non-"all" replay the first access may fetch the recorded output lazily;
|
|
36
|
+
# that fetch is memoized for the replay item. Under mock: "all" the output is
|
|
37
|
+
# inline even when overrides are present. Ruby has no async-span limitation.
|
|
38
38
|
module MockOverride
|
|
39
39
|
module_function
|
|
40
40
|
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -41,28 +41,47 @@ module Bitfab
|
|
|
41
41
|
# - "marked" : only spans declared with mock_on_replay: true return historical
|
|
42
42
|
# output; everything else runs real code (default)
|
|
43
43
|
# - "none" : every child span runs real code
|
|
44
|
-
# - "all" : every child span returns its historical
|
|
44
|
+
# - "all" : every matched recorded child span returns its historical
|
|
45
|
+
# output; a missing occurrence fails the item closed
|
|
45
46
|
MOCK_STRATEGIES = %w[none all marked].freeze
|
|
46
47
|
|
|
47
|
-
#
|
|
48
|
+
# Replay context shared by fibers on the current thread. Ruby's
|
|
49
|
+
# Thread#[] storage is fiber-local, so use true thread variables here: lazy
|
|
50
|
+
# Enumerator bodies run in another fiber but must retain replay mocking.
|
|
48
51
|
module ReplayContext
|
|
49
52
|
module_function
|
|
50
53
|
|
|
51
54
|
def current
|
|
52
|
-
Thread.current
|
|
55
|
+
Thread.current.thread_variable_get(REPLAY_CONTEXT_KEY)
|
|
53
56
|
end
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
def current_span
|
|
59
|
+
current&.dig(:span_stack)&.last
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def with_span(trace_id:, span_id:)
|
|
63
|
+
ctx = current
|
|
64
|
+
return yield unless ctx
|
|
65
|
+
|
|
66
|
+
entry = {trace_id:, span_id:}
|
|
67
|
+
ctx[:span_stack].push(entry)
|
|
68
|
+
yield
|
|
69
|
+
ensure
|
|
70
|
+
ctx[:span_stack].pop if ctx
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Execute a block with replay context set for every fiber on the current
|
|
74
|
+
# thread. Child threads and processes intentionally do not inherit it.
|
|
57
75
|
def with_context(test_run_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
|
|
58
76
|
mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
|
|
59
|
-
db_branch_lease: nil, source_bitfab_trace_id: nil)
|
|
60
|
-
previous = Thread.current
|
|
77
|
+
db_branch_lease: nil, db_branch_timings: nil, source_bitfab_trace_id: nil)
|
|
78
|
+
previous = Thread.current.thread_variable_get(REPLAY_CONTEXT_KEY)
|
|
61
79
|
ctx = {
|
|
62
80
|
test_run_id:,
|
|
63
81
|
input_source_span_id:,
|
|
64
82
|
input_source_trace_id:,
|
|
65
|
-
trace_id
|
|
83
|
+
trace_id:,
|
|
84
|
+
span_stack: []
|
|
66
85
|
}
|
|
67
86
|
if mock_tree
|
|
68
87
|
ctx[:mock_tree] = mock_tree
|
|
@@ -73,8 +92,9 @@ module Bitfab
|
|
|
73
92
|
# is present. process_single_item fetches the tree whenever overrides
|
|
74
93
|
# exist, even under mock: "none".
|
|
75
94
|
ctx[:mock_overrides] = mock_overrides if mock_overrides && !mock_overrides.empty?
|
|
76
|
-
# Lazy per-span output fetcher for
|
|
77
|
-
#
|
|
95
|
+
# Lazy per-span output fetcher for a payload-free non-"all" tree path.
|
|
96
|
+
# Absent on the eager "all" path, whose outputs are inline even when
|
|
97
|
+
# overrides are present.
|
|
78
98
|
ctx[:fetch_span_output] = fetch_span_output if fetch_span_output
|
|
79
99
|
end
|
|
80
100
|
# The per-trace DB branch (resolved server-side) and the Bitfab trace ID
|
|
@@ -93,11 +113,15 @@ module Bitfab
|
|
|
93
113
|
# accessed for free, and the flag would stop separating "branch was
|
|
94
114
|
# used" from "branch was offered".
|
|
95
115
|
ctx[:db_branch_lease] = db_branch_lease if db_branch_lease
|
|
116
|
+
# Kept off ReplayBranch: customer code reads that mid-replay to reach the
|
|
117
|
+
# branch, and provisioning latency is a property of the run, not of the
|
|
118
|
+
# connection. It rides the context only to be echoed on the completion.
|
|
119
|
+
ctx[:db_branch_timings] = db_branch_timings if db_branch_timings
|
|
96
120
|
ctx[:source_bitfab_trace_id] = source_bitfab_trace_id if source_bitfab_trace_id
|
|
97
|
-
Thread.current
|
|
121
|
+
Thread.current.thread_variable_set(REPLAY_CONTEXT_KEY, ctx)
|
|
98
122
|
yield
|
|
99
123
|
ensure
|
|
100
|
-
Thread.current
|
|
124
|
+
Thread.current.thread_variable_set(REPLAY_CONTEXT_KEY, previous)
|
|
101
125
|
end
|
|
102
126
|
end
|
|
103
127
|
|
|
@@ -163,8 +187,9 @@ module Bitfab
|
|
|
163
187
|
# @param method_name [Symbol] the method to replay
|
|
164
188
|
# @param trace_function_key [String] the trace function key for this method
|
|
165
189
|
# @param limit [Integer, nil] maximum number of traces to replay (default: 5).
|
|
166
|
-
# Ignored when trace_ids is passed
|
|
167
|
-
# already determines how many traces replay.
|
|
190
|
+
# Ignored when trace_ids or dataset_id is passed because either source
|
|
191
|
+
# already determines how many traces replay. Supplying trace_ids also
|
|
192
|
+
# emits a warning.
|
|
168
193
|
# @param trace_ids [Array<String>, nil] optional list of trace IDs to replay (max 100)
|
|
169
194
|
# @param name [String, nil] optional display name for the resulting experiment/test run
|
|
170
195
|
# @param max_concurrency [Integer, nil] max threads for parallel replay (default: 10)
|
|
@@ -187,7 +212,7 @@ module Bitfab
|
|
|
187
212
|
# same org and trace function or the server rejects the replay
|
|
188
213
|
# @param mock [String] mock strategy for child spans: "marked" (default),
|
|
189
214
|
# "none", or "all". "marked" only mocks spans declared with
|
|
190
|
-
# mock_on_replay: true; "all" mocks every child span.
|
|
215
|
+
# mock_on_replay: true; "all" mocks every matched recorded child span.
|
|
191
216
|
# @param adapt_inputs [#call, nil] optional hook to reshape recorded inputs
|
|
192
217
|
# onto the method's current signature when its shape changed after the
|
|
193
218
|
# traces were captured. Receives (args, kwargs, ctx) where ctx is
|
|
@@ -206,11 +231,11 @@ module Bitfab
|
|
|
206
231
|
# errored:, item: } where item is { trace_id:, original_trace_id:,
|
|
207
232
|
# original_span_id:, error:, duration_ms: } for the single item that just
|
|
208
233
|
# finished (source_trace_id/source_span_id remain as deprecated aliases).
|
|
209
|
-
# trace_id is the new server replay trace id,
|
|
210
|
-
#
|
|
211
|
-
# ORIGINAL (historical) trace that was replayed,
|
|
212
|
-
# error or nil, and
|
|
213
|
-
#
|
|
234
|
+
# trace_id is the new server replay trace id, available after that item's
|
|
235
|
+
# trace is flushed (nil only when delivery could not be confirmed);
|
|
236
|
+
# original_trace_id is the ORIGINAL (historical) trace that was replayed,
|
|
237
|
+
# error is that item's replay error or nil, and duration_ms is the source
|
|
238
|
+
# trace's recorded duration. Use it to
|
|
214
239
|
# render replay progress (e.g. a per-trace log). A raising callback never
|
|
215
240
|
# crashes the run.
|
|
216
241
|
# @param on_progress [#call, nil] deprecated compatibility callback. It
|
|
@@ -744,9 +769,13 @@ module Bitfab
|
|
|
744
769
|
trace_error: result[:trace_error],
|
|
745
770
|
replay_error: result[:replay_error],
|
|
746
771
|
duration_ms: result[:duration_ms],
|
|
772
|
+
original_duration_ms: result[:original_duration_ms],
|
|
773
|
+
original_tokens: result[:original_tokens],
|
|
774
|
+
original_model: result[:original_model],
|
|
747
775
|
tokens: result[:tokens],
|
|
748
776
|
model: result[:model],
|
|
749
|
-
db_snapshot_ref: result[:db_snapshot_ref]
|
|
777
|
+
db_snapshot_ref: result[:db_snapshot_ref],
|
|
778
|
+
db_branch_timings: result[:db_branch_timings]
|
|
750
779
|
}
|
|
751
780
|
})
|
|
752
781
|
rescue => e
|
|
@@ -807,14 +836,17 @@ module Bitfab
|
|
|
807
836
|
# (only when include_db_branch_lease was sent). Release it in the +ensure+
|
|
808
837
|
# below so any raise (span fetch, mock-tree build, or the replayed
|
|
809
838
|
# method) frees the Neon resource. Items whose source trace had no
|
|
810
|
-
# snapshot ref arrive without a lease
|
|
811
|
-
#
|
|
839
|
+
# snapshot ref arrive without a lease, so the app uses its normal DB
|
|
840
|
+
# path. Unsafe calls on that path still require replay mocking.
|
|
812
841
|
lease = include_db_branch_lease ? server_item["dbBranchLease"] : nil
|
|
813
842
|
# A resolve that was ATTEMPTED and failed is different: the caller asked
|
|
814
843
|
# for a branch, so running their method against live data would produce a
|
|
815
844
|
# result that looks valid and is not. Fail the item instead, loudly.
|
|
816
845
|
lease_error = include_db_branch_lease ? server_item["dbBranchLeaseError"] : nil
|
|
817
846
|
db_snapshot_ref = server_item["dbSnapshotRef"]
|
|
847
|
+
# Reported whichever way the resolve went, so it is tracked separately
|
|
848
|
+
# from both the lease and the error rather than hanging off either.
|
|
849
|
+
db_branch_timings = include_db_branch_lease ? server_item["dbBranchTimings"] : nil
|
|
818
850
|
if include_db_branch_lease && lease.nil? && lease_error.nil?
|
|
819
851
|
begin
|
|
820
852
|
resolved = http_client.resolve_db_branch_lease(test_run_id, original_trace_id, db_branch_settings)
|
|
@@ -829,6 +861,7 @@ module Bitfab
|
|
|
829
861
|
lease = resolved["lease"]
|
|
830
862
|
lease_error = resolved["leaseError"]
|
|
831
863
|
db_snapshot_ref = resolved["dbSnapshotRef"] || db_snapshot_ref
|
|
864
|
+
db_branch_timings = resolved["timings"] || db_branch_timings
|
|
832
865
|
end
|
|
833
866
|
if lease_error
|
|
834
867
|
raise DbBranchReplayError.new(
|
|
@@ -847,10 +880,11 @@ module Bitfab
|
|
|
847
880
|
# the tree must be fetched for them to fire even under mock: "none".
|
|
848
881
|
#
|
|
849
882
|
# Only mock: "all" needs every span's recorded output inline (it mocks
|
|
850
|
-
# every child), so it fetches an eager tree
|
|
851
|
-
#
|
|
852
|
-
#
|
|
853
|
-
#
|
|
883
|
+
# every matched recorded child), so it fetches an eager tree even when
|
|
884
|
+
# overrides exist.
|
|
885
|
+
# Non-"all" runs that need a tree ("marked", or "none" with overrides)
|
|
886
|
+
# fetch it payload-free (includeOutputs=false) and pull each mocked span's
|
|
887
|
+
# output lazily by externalSpanId, never dragging down unused outputs.
|
|
854
888
|
overrides_present = !mock_overrides.nil? && !mock_overrides.empty?
|
|
855
889
|
include_outputs = mock_strategy == "all"
|
|
856
890
|
mock_tree = nil
|
|
@@ -864,14 +898,10 @@ module Bitfab
|
|
|
864
898
|
mock_tree = build_mock_tree(tree["root"] || {})
|
|
865
899
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
866
900
|
raise if e.is_a?(SystemExit) || e.is_a?(SignalException)
|
|
867
|
-
|
|
868
|
-
#
|
|
869
|
-
#
|
|
870
|
-
|
|
871
|
-
# "marked" can fall back to real execution (its marked spans just
|
|
872
|
-
# re-run). Mirrors the Python SDK's has_overrides re-raise.
|
|
873
|
-
raise if mock_strategy == "all" || overrides_present
|
|
874
|
-
mock_tree = nil
|
|
901
|
+
raise unless mock_strategy == "marked" && !overrides_present
|
|
902
|
+
# Keep an active empty tree so the root can still run, while any
|
|
903
|
+
# marked child fails closed at its call site.
|
|
904
|
+
mock_tree = {}
|
|
875
905
|
end
|
|
876
906
|
end
|
|
877
907
|
|
|
@@ -905,6 +935,7 @@ module Bitfab
|
|
|
905
935
|
adapt_inputs:,
|
|
906
936
|
adapt_ctx:,
|
|
907
937
|
db_branch_lease: lease,
|
|
938
|
+
db_branch_timings:,
|
|
908
939
|
source_bitfab_trace_id: original_trace_id,
|
|
909
940
|
db_snapshot_ref:,
|
|
910
941
|
http_client:
|
|
@@ -918,16 +949,20 @@ module Bitfab
|
|
|
918
949
|
error: replay_item_error_message(e),
|
|
919
950
|
trace_error: nil,
|
|
920
951
|
replay_error: e,
|
|
921
|
-
duration_ms:
|
|
952
|
+
duration_ms: nil,
|
|
953
|
+
original_duration_ms: metrics&.dig(:original_duration_ms),
|
|
954
|
+
original_tokens: metrics&.dig(:original_tokens),
|
|
955
|
+
original_model: metrics&.dig(:original_model),
|
|
922
956
|
tokens: metrics&.dig(:tokens),
|
|
923
|
-
model: metrics&.dig(:
|
|
957
|
+
model: metrics&.dig(:original_model),
|
|
924
958
|
trace_id: nil,
|
|
925
959
|
original_trace_id:,
|
|
926
960
|
original_span_id:,
|
|
927
961
|
# Deprecated aliases for original_trace_id/original_span_id.
|
|
928
962
|
source_trace_id: original_trace_id,
|
|
929
963
|
source_span_id: original_span_id,
|
|
930
|
-
db_snapshot_ref
|
|
964
|
+
db_snapshot_ref:,
|
|
965
|
+
db_branch_timings:
|
|
931
966
|
}
|
|
932
967
|
ensure
|
|
933
968
|
release_db_branch_lease(http_client, lease) if lease
|
|
@@ -1041,11 +1076,15 @@ module Bitfab
|
|
|
1041
1076
|
# the ORIGINAL trace's tokens); the replayed run's tokens are filled in by
|
|
1042
1077
|
# run() from the complete-replay response once spans are aggregated
|
|
1043
1078
|
# server-side, and stay nil here and on older servers.
|
|
1079
|
+
# The original trace's measurements, falling back to the unprefixed keys a
|
|
1080
|
+
# server that predates the rename sends. :tokens stays nil here: it is the
|
|
1081
|
+
# REPLAYED run's, filled in from the complete-replay response.
|
|
1044
1082
|
def extract_server_item_metrics(server_item)
|
|
1045
1083
|
{
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1084
|
+
original_duration_ms: server_item["originalDurationMs"] || server_item["durationMs"],
|
|
1085
|
+
original_tokens: server_item["originalTokens"] || server_item["tokens"],
|
|
1086
|
+
original_model: server_item["originalModel"] || server_item["model"],
|
|
1087
|
+
tokens: nil
|
|
1049
1088
|
}
|
|
1050
1089
|
end
|
|
1051
1090
|
|
|
@@ -1126,13 +1165,15 @@ module Bitfab
|
|
|
1126
1165
|
# Execute a single replay item: deserialize inputs, call method with replay context.
|
|
1127
1166
|
def execute_item(item, receiver, method_name, test_run_id, input_source_span_id = nil, metrics = {},
|
|
1128
1167
|
input_source_trace_id: nil, mock_strategy: "marked", mock_tree: nil, mock_overrides: nil,
|
|
1129
|
-
fetch_span_output: nil, adapt_inputs: nil, adapt_ctx: nil, db_branch_lease: nil,
|
|
1130
|
-
db_snapshot_ref: nil, http_client: nil)
|
|
1168
|
+
fetch_span_output: nil, adapt_inputs: nil, adapt_ctx: nil, db_branch_lease: nil, db_branch_timings: nil,
|
|
1169
|
+
source_bitfab_trace_id: nil, db_snapshot_ref: nil, http_client: nil)
|
|
1131
1170
|
args, kwargs = Serialize.deserialize_inputs(item)
|
|
1132
1171
|
|
|
1133
1172
|
fn_result = nil
|
|
1134
1173
|
fn_error = nil
|
|
1135
1174
|
replay_error = nil
|
|
1175
|
+
replay_duration_ms = nil
|
|
1176
|
+
replay_started = nil
|
|
1136
1177
|
# Client-side correlation id that tags this item's replay spans so the
|
|
1137
1178
|
# server can echo back the row id it minted (resolved in run()'s
|
|
1138
1179
|
# complete-replay loop). Carried on the item under :_sdk_trace_id, never
|
|
@@ -1154,6 +1195,7 @@ module Bitfab
|
|
|
1154
1195
|
mock_overrides:,
|
|
1155
1196
|
fetch_span_output:,
|
|
1156
1197
|
db_branch_lease:,
|
|
1198
|
+
db_branch_timings:,
|
|
1157
1199
|
source_bitfab_trace_id:
|
|
1158
1200
|
) do
|
|
1159
1201
|
begin
|
|
@@ -1172,17 +1214,26 @@ module Bitfab
|
|
|
1172
1214
|
end
|
|
1173
1215
|
if replay_error.nil?
|
|
1174
1216
|
begin
|
|
1217
|
+
replay_started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1175
1218
|
fn_result = if kwargs.empty?
|
|
1176
1219
|
receiver.send(method_name, *args)
|
|
1177
1220
|
else
|
|
1178
1221
|
receiver.send(method_name, *args, **kwargs)
|
|
1179
1222
|
end
|
|
1223
|
+
replay_duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - replay_started) * 1000).round
|
|
1180
1224
|
rescue => e
|
|
1225
|
+
# The method ran and raised, so it still has a duration.
|
|
1226
|
+
if replay_started
|
|
1227
|
+
replay_duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - replay_started) * 1000).round
|
|
1228
|
+
end
|
|
1181
1229
|
fn_error = e
|
|
1182
1230
|
end
|
|
1183
1231
|
end
|
|
1184
1232
|
end
|
|
1185
1233
|
rescue => e
|
|
1234
|
+
if replay_started
|
|
1235
|
+
replay_duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - replay_started) * 1000).round
|
|
1236
|
+
end
|
|
1186
1237
|
replay_error = e
|
|
1187
1238
|
end
|
|
1188
1239
|
|
|
@@ -1194,9 +1245,12 @@ module Bitfab
|
|
|
1194
1245
|
error: item_error&.message,
|
|
1195
1246
|
trace_error: fn_error,
|
|
1196
1247
|
replay_error:,
|
|
1197
|
-
duration_ms:
|
|
1248
|
+
duration_ms: replay_duration_ms,
|
|
1249
|
+
original_duration_ms: metrics[:original_duration_ms],
|
|
1250
|
+
original_tokens: metrics[:original_tokens],
|
|
1251
|
+
original_model: metrics[:original_model],
|
|
1198
1252
|
tokens: metrics[:tokens],
|
|
1199
|
-
model: metrics[:
|
|
1253
|
+
model: metrics[:original_model],
|
|
1200
1254
|
# Written in by run() from the complete-replay response once the server
|
|
1201
1255
|
# has minted this replay trace's row. Nil until then: the client-side
|
|
1202
1256
|
# correlation id (below) is never surfaced as the public :trace_id.
|
|
@@ -1207,7 +1261,8 @@ module Bitfab
|
|
|
1207
1261
|
# Deprecated aliases for original_trace_id/original_span_id.
|
|
1208
1262
|
source_trace_id: source_bitfab_trace_id,
|
|
1209
1263
|
source_span_id: input_source_span_id,
|
|
1210
|
-
db_snapshot_ref
|
|
1264
|
+
db_snapshot_ref:,
|
|
1265
|
+
db_branch_timings:
|
|
1211
1266
|
}
|
|
1212
1267
|
end
|
|
1213
1268
|
end
|
data/lib/bitfab/span_context.rb
CHANGED
|
@@ -114,6 +114,7 @@ module Bitfab
|
|
|
114
114
|
# Each entry is a Hash with :trace_id and :span_id keys.
|
|
115
115
|
module SpanContext
|
|
116
116
|
STACK_KEY = :__bitfab_span_stack
|
|
117
|
+
FIBER_BRIDGE_STACK_KEY = :__bitfab_span_fiber_bridge_stack
|
|
117
118
|
|
|
118
119
|
module_function
|
|
119
120
|
|
|
@@ -122,7 +123,18 @@ module Bitfab
|
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
def current
|
|
125
|
-
stack.last
|
|
126
|
+
stack.last || fiber_bridge_stack.last
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Enumerator.new and enum_for run their source body in another Fiber on
|
|
130
|
+
# the same thread. Bridge only the active Enumerator parent across that
|
|
131
|
+
# boundary; ordinary fiber-local span stacks remain isolated.
|
|
132
|
+
def with_fiber_bridge(trace_id:, span_id:)
|
|
133
|
+
entry = {trace_id:, span_id:}
|
|
134
|
+
fiber_bridge_stack.push(entry)
|
|
135
|
+
yield
|
|
136
|
+
ensure
|
|
137
|
+
fiber_bridge_stack.pop
|
|
126
138
|
end
|
|
127
139
|
|
|
128
140
|
# Execute a block with a new span pushed onto the stack.
|
|
@@ -134,6 +146,15 @@ module Bitfab
|
|
|
134
146
|
ensure
|
|
135
147
|
stack.pop
|
|
136
148
|
end
|
|
149
|
+
|
|
150
|
+
def fiber_bridge_stack
|
|
151
|
+
current_thread = Thread.current
|
|
152
|
+
current_thread.thread_variable_get(FIBER_BRIDGE_STACK_KEY) || begin
|
|
153
|
+
value = []
|
|
154
|
+
current_thread.thread_variable_set(FIBER_BRIDGE_STACK_KEY, value)
|
|
155
|
+
value
|
|
156
|
+
end
|
|
157
|
+
end
|
|
137
158
|
end
|
|
138
159
|
|
|
139
160
|
# Global storage for trace states (trace_id -> state hash)
|
data/lib/bitfab/version.rb
CHANGED