bitfab 0.20.2 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/bitfab/client.rb +93 -51
- data/lib/bitfab/http_client.rb +13 -13
- data/lib/bitfab/replay.rb +36 -5
- data/lib/bitfab/traceable.rb +6 -1
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab/warn_once.rb +35 -0
- data/lib/bitfab.rb +8 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 364e0ec3b3aa00cc47a398872911e18b1a3e5253f4448214da67f9c12af51814
|
|
4
|
+
data.tar.gz: 482340c71e8d2cb0f8ed92efa7a214278529a670e836e46b6209cc18126f230b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9c10548592e02d52a74b96b268bea01d33200c3ff338b61758ec6d54dc2adcf07ca5c442212d464cb53cb86a1aeeccb7466c9ca71ba49810902ff6f5445b0b5
|
|
7
|
+
data.tar.gz: 3f12de6d0a9eeb5f7c8218d977c4d344a972f762f6c7380e5123d8976032c2215f63337698e4c7d6f18fd669786e4fded0084e0be2ef8fce1e7a696324c2f9b5
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "http_client"
|
|
|
8
8
|
require_relative "replay"
|
|
9
9
|
require_relative "span_context"
|
|
10
10
|
require_relative "serialize"
|
|
11
|
+
require_relative "warn_once"
|
|
11
12
|
|
|
12
13
|
module Bitfab
|
|
13
14
|
class Client
|
|
@@ -59,14 +60,21 @@ module Bitfab
|
|
|
59
60
|
# onto the method's current signature when its shape changed after the
|
|
60
61
|
# traces were captured. Receives (args, kwargs, ctx) where ctx is
|
|
61
62
|
# { trace_id:, source_span_id: }, and returns [new_args, new_kwargs].
|
|
63
|
+
# @param on_progress [#call, nil] optional callback invoked once per item as
|
|
64
|
+
# it finishes, with a running-totals hash { completed:, total:, succeeded:,
|
|
65
|
+
# errored: }. Use it to render replay progress (e.g. a terminal progress
|
|
66
|
+
# bar). Replay does not know pass/fail yet, so the totals only distinguish
|
|
67
|
+
# items whose method ran (:succeeded) from items that raised (:errored).
|
|
68
|
+
# A raising callback never crashes the run.
|
|
62
69
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
63
70
|
def replay(receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency: 10,
|
|
64
71
|
code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, mock: "none",
|
|
65
|
-
adapt_inputs: nil, environment: nil)
|
|
72
|
+
adapt_inputs: nil, environment: nil, on_progress: nil)
|
|
66
73
|
Replay.run(
|
|
67
74
|
self, receiver, method_name,
|
|
68
75
|
trace_function_key:, limit:, trace_ids:, max_concurrency:,
|
|
69
|
-
code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, mock:, adapt_inputs:, environment
|
|
76
|
+
code_change_description:, code_change_files:, experiment_group_id:, dataset_id:, mock:, adapt_inputs:, environment:,
|
|
77
|
+
on_progress:
|
|
70
78
|
)
|
|
71
79
|
end
|
|
72
80
|
|
|
@@ -94,59 +102,93 @@ module Bitfab
|
|
|
94
102
|
mock_on_replay: false)
|
|
95
103
|
return yield unless @enabled
|
|
96
104
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
# Span setup runs before the user's block. Tracing is a side-channel, so
|
|
106
|
+
# if anything here raises (id generation, trace-state bookkeeping, a
|
|
107
|
+
# malformed replay mock tree) the user's method must still run. On failure
|
|
108
|
+
# we clean up any partially registered trace state, warn once, and run the
|
|
109
|
+
# block untraced. `trace_id` is declared out here so the rescue can clean
|
|
110
|
+
# it up; the other locals stay visible to the real path below.
|
|
111
|
+
trace_id = nil
|
|
112
|
+
span_id = nil
|
|
113
|
+
parent_span_id = nil
|
|
114
|
+
is_root_span = nil
|
|
115
|
+
started_at = nil
|
|
116
|
+
resolved_test_run_id = nil
|
|
117
|
+
resolved_input_source_span_id = nil
|
|
118
|
+
begin
|
|
119
|
+
parent = SpanContext.current
|
|
120
|
+
replay_ctx = ReplayContext.current
|
|
121
|
+
trace_id = parent ? parent[:trace_id] : (replay_ctx&.dig(:trace_id) || SecureRandom.uuid)
|
|
122
|
+
span_id = SecureRandom.uuid
|
|
123
|
+
parent_span_id = parent&.dig(:span_id)
|
|
124
|
+
is_root_span = parent_span_id.nil?
|
|
125
|
+
started_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
126
|
+
resolved_test_run_id = replay_ctx&.dig(:test_run_id)
|
|
127
|
+
resolved_input_source_span_id = replay_ctx&.dig(:input_source_span_id)
|
|
128
|
+
resolved_input_source_trace_id = replay_ctx&.dig(:input_source_trace_id)
|
|
129
|
+
|
|
130
|
+
# Register trace state for root spans
|
|
131
|
+
if is_root_span && !TraceState.get(trace_id)
|
|
132
|
+
TraceState.create(
|
|
133
|
+
trace_id,
|
|
134
|
+
test_run_id: resolved_test_run_id,
|
|
135
|
+
input_source_trace_id: resolved_input_source_trace_id
|
|
136
|
+
)
|
|
137
|
+
end
|
|
116
138
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
139
|
+
if is_root_span
|
|
140
|
+
@pending_span_mutex.synchronize { @pending_span_threads[trace_id] = [] }
|
|
141
|
+
end
|
|
120
142
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
if mocked_output != MOCK_REPLAY_MISS
|
|
133
|
-
send_mocked_span(
|
|
134
|
-
trace_function_key:,
|
|
135
|
-
trace_id:,
|
|
136
|
-
span_id:,
|
|
137
|
-
parent_span_id:,
|
|
138
|
-
span_name:,
|
|
139
|
-
span_type:,
|
|
140
|
-
function_name:,
|
|
141
|
-
args:,
|
|
142
|
-
kwargs:,
|
|
143
|
-
mocked_output:,
|
|
144
|
-
started_at:,
|
|
145
|
-
test_run_id: resolved_test_run_id,
|
|
146
|
-
input_source_span_id: resolved_input_source_span_id
|
|
143
|
+
# Advance the per-(key, name) call counter for any non-root span under
|
|
144
|
+
# an active mock tree, even when this span won't itself be mocked.
|
|
145
|
+
# Unmarked spans must consume an index so subsequent marked siblings
|
|
146
|
+
# line up with `build_mock_tree`'s sequential numbering for the same
|
|
147
|
+
# (key, name) pair. Different (key, name) pairs have independent
|
|
148
|
+
# counters: they cannot shift each other.
|
|
149
|
+
call_index = advance_mock_counter(replay_ctx, trace_function_key, span_name, is_root_span:)
|
|
150
|
+
if call_index
|
|
151
|
+
mocked_output = check_mock_replay(
|
|
152
|
+
replay_ctx, trace_function_key, span_name, call_index, mock_on_replay:
|
|
147
153
|
)
|
|
148
|
-
|
|
154
|
+
if mocked_output != MOCK_REPLAY_MISS
|
|
155
|
+
send_mocked_span(
|
|
156
|
+
trace_function_key:,
|
|
157
|
+
trace_id:,
|
|
158
|
+
span_id:,
|
|
159
|
+
parent_span_id:,
|
|
160
|
+
span_name:,
|
|
161
|
+
span_type:,
|
|
162
|
+
function_name:,
|
|
163
|
+
args:,
|
|
164
|
+
kwargs:,
|
|
165
|
+
mocked_output:,
|
|
166
|
+
started_at:,
|
|
167
|
+
test_run_id: resolved_test_run_id,
|
|
168
|
+
input_source_span_id: resolved_input_source_span_id
|
|
169
|
+
)
|
|
170
|
+
return mocked_output
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
rescue
|
|
174
|
+
# Clean up any trace state this partial setup registered so it does not
|
|
175
|
+
# leak.
|
|
176
|
+
if trace_id
|
|
177
|
+
TraceState.delete(trace_id)
|
|
178
|
+
@pending_span_mutex.synchronize { @pending_span_threads.delete(trace_id) }
|
|
149
179
|
end
|
|
180
|
+
# During replay (a controlled eval) a setup failure must surface, not
|
|
181
|
+
# silently run the block untraced: swallowing it would execute real code
|
|
182
|
+
# with real side effects and skew the mock call counter, defeating the
|
|
183
|
+
# replay. The never-crash fallback is for production hosts only.
|
|
184
|
+
raise if ReplayContext.current
|
|
185
|
+
|
|
186
|
+
Bitfab.warn_once(
|
|
187
|
+
"span-setup:#{trace_function_key}",
|
|
188
|
+
"span setup failed for '#{trace_function_key}'; this call runs untraced. " \
|
|
189
|
+
"Your method still executes and returns normally."
|
|
190
|
+
)
|
|
191
|
+
return yield
|
|
150
192
|
end
|
|
151
193
|
|
|
152
194
|
result = nil
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "uri"
|
|
|
7
7
|
require_relative "constants"
|
|
8
8
|
require_relative "serialize"
|
|
9
9
|
require_relative "version"
|
|
10
|
+
require_relative "warn_once"
|
|
10
11
|
|
|
11
12
|
module Bitfab
|
|
12
13
|
class HttpClient
|
|
@@ -181,14 +182,13 @@ module Bitfab
|
|
|
181
182
|
def safe_generate(payload, endpoint)
|
|
182
183
|
JSON.generate(payload)
|
|
183
184
|
rescue => e
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
end
|
|
185
|
+
Bitfab.warn_once(
|
|
186
|
+
"request-body-stubbed",
|
|
187
|
+
"a request body held a non-serializable value (#{e.message}); it was " \
|
|
188
|
+
"stubbed so the span still sends, but the trace may be incomplete or " \
|
|
189
|
+
"not replayable. Capture a JSON-safe projection of this input to make " \
|
|
190
|
+
"it replayable."
|
|
191
|
+
)
|
|
192
192
|
|
|
193
193
|
begin
|
|
194
194
|
JSON.generate(sanitize_payload(payload))
|
|
@@ -242,11 +242,11 @@ module Bitfab
|
|
|
242
242
|
thread = Thread.new do
|
|
243
243
|
block.call
|
|
244
244
|
rescue => e
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
#
|
|
249
|
-
|
|
245
|
+
Bitfab.warn_once(
|
|
246
|
+
"send-request-failed",
|
|
247
|
+
"failed to send a request to the backend (further occurrences " \
|
|
248
|
+
"suppressed): #{e.message}"
|
|
249
|
+
)
|
|
250
250
|
ensure
|
|
251
251
|
@pending_threads_mutex.synchronize { @pending_threads.delete(Thread.current) }
|
|
252
252
|
end
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -100,10 +100,14 @@ module Bitfab
|
|
|
100
100
|
# { trace_id:, source_span_id: }, and returns [new_args, new_kwargs]. Runs
|
|
101
101
|
# per item inside the same rescue as the method, so a raising adapter sets
|
|
102
102
|
# that item's :error rather than crashing the run.
|
|
103
|
+
# @param on_progress [#call, nil] optional callback invoked once per item as
|
|
104
|
+
# it finishes, with a running-totals hash { completed:, total:, succeeded:,
|
|
105
|
+
# errored: }. Use it to render replay progress (e.g. a terminal progress
|
|
106
|
+
# bar). A raising callback never crashes the run.
|
|
103
107
|
# @return [Hash] with :items, :test_run_id, :test_run_url
|
|
104
108
|
def run(client, receiver, method_name, trace_function_key:, limit: nil, trace_ids: nil, max_concurrency: 10,
|
|
105
109
|
code_change_description: nil, code_change_files: nil, experiment_group_id: nil, dataset_id: nil, mock: "none",
|
|
106
|
-
adapt_inputs: nil, environment: nil)
|
|
110
|
+
adapt_inputs: nil, environment: nil, on_progress: nil)
|
|
107
111
|
unless MOCK_STRATEGIES.include?(mock.to_s)
|
|
108
112
|
raise ArgumentError, "Invalid mock strategy '#{mock}'. Must be one of: #{MOCK_STRATEGIES.join(", ")}"
|
|
109
113
|
end
|
|
@@ -157,7 +161,7 @@ module Bitfab
|
|
|
157
161
|
|
|
158
162
|
result_items = if server_items.any?
|
|
159
163
|
process_items(http_client, server_items, receiver, method_name, test_run_id, max_concurrency, mock.to_s,
|
|
160
|
-
adapt_inputs, include_db_branch_lease)
|
|
164
|
+
adapt_inputs, include_db_branch_lease, on_progress:)
|
|
161
165
|
else
|
|
162
166
|
[]
|
|
163
167
|
end
|
|
@@ -237,13 +241,39 @@ module Bitfab
|
|
|
237
241
|
|
|
238
242
|
# Process all replay items, optionally in parallel using threads.
|
|
239
243
|
def process_items(http_client, server_items, receiver, method_name, test_run_id, max_concurrency, mock_strategy,
|
|
240
|
-
adapt_inputs = nil, include_db_branch_lease = false)
|
|
244
|
+
adapt_inputs = nil, include_db_branch_lease = false, on_progress: nil)
|
|
241
245
|
concurrency = max_concurrency || server_items.length
|
|
242
246
|
|
|
247
|
+
# Reports running totals once per item as it settles. In the parallel
|
|
248
|
+
# path it runs from worker threads, so the mutex both makes the counter
|
|
249
|
+
# updates safe and serializes the user's callback (never called
|
|
250
|
+
# concurrently). A raising callback is swallowed: progress UI must never
|
|
251
|
+
# crash the run.
|
|
252
|
+
total = server_items.length
|
|
253
|
+
progress_mutex = Mutex.new
|
|
254
|
+
completed = 0
|
|
255
|
+
succeeded = 0
|
|
256
|
+
errored = 0
|
|
257
|
+
report = lambda do |result|
|
|
258
|
+
return unless on_progress
|
|
259
|
+
|
|
260
|
+
progress_mutex.synchronize do
|
|
261
|
+
completed += 1
|
|
262
|
+
result[:error].nil? ? (succeeded += 1) : (errored += 1)
|
|
263
|
+
begin
|
|
264
|
+
on_progress.call({completed:, total:, succeeded:, errored:})
|
|
265
|
+
rescue => e
|
|
266
|
+
warn "Bitfab: replay on_progress callback raised: #{e.message}"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
243
271
|
if concurrency <= 1
|
|
244
272
|
server_items.map do |item|
|
|
245
|
-
process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
|
|
246
|
-
include_db_branch_lease)
|
|
273
|
+
result = process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
|
|
274
|
+
adapt_inputs, include_db_branch_lease)
|
|
275
|
+
report.call(result)
|
|
276
|
+
result
|
|
247
277
|
end
|
|
248
278
|
else
|
|
249
279
|
results_mutex = Mutex.new
|
|
@@ -260,6 +290,7 @@ module Bitfab
|
|
|
260
290
|
result = process_single_item(http_client, item, receiver, method_name, test_run_id, mock_strategy,
|
|
261
291
|
adapt_inputs, include_db_branch_lease)
|
|
262
292
|
results_mutex.synchronize { results[idx] = result }
|
|
293
|
+
report.call(result)
|
|
263
294
|
end
|
|
264
295
|
end
|
|
265
296
|
end
|
data/lib/bitfab/traceable.rb
CHANGED
|
@@ -173,7 +173,12 @@ module Bitfab
|
|
|
173
173
|
|
|
174
174
|
wrapper = Module.new do
|
|
175
175
|
define_method(method_name) do |*args, **kwargs, &block|
|
|
176
|
-
Bitfab
|
|
176
|
+
# If Bitfab was never configured, run the real method untraced
|
|
177
|
+
# rather than raising a "not configured" error into the host app.
|
|
178
|
+
client = Bitfab.client_or_nil
|
|
179
|
+
return super(*args, **kwargs, &block) unless client
|
|
180
|
+
|
|
181
|
+
client.send(:execute_span,
|
|
177
182
|
trace_function_key:,
|
|
178
183
|
span_name:,
|
|
179
184
|
span_type: type,
|
data/lib/bitfab/version.rb
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bitfab
|
|
4
|
+
# Emit a warning at most once per distinct key for the life of the process.
|
|
5
|
+
#
|
|
6
|
+
# The SDK must never crash or spam the host app, so every failure on the
|
|
7
|
+
# user's path degrades quietly: a span is dropped, a call runs untraced, a
|
|
8
|
+
# payload is stubbed. Silent is safe but undebuggable; warning on every call
|
|
9
|
+
# from a hot path is its own problem. A one-time warning per distinct issue
|
|
10
|
+
# restores the signal without the flood. Keys should identify the specific
|
|
11
|
+
# degradation (e.g. include the trace_function_key) so each distinct issue
|
|
12
|
+
# warns once, not just the first one seen.
|
|
13
|
+
@warn_once_seen = Set.new
|
|
14
|
+
@warn_once_mutex = Mutex.new
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def warn_once(key, message)
|
|
18
|
+
@warn_once_mutex.synchronize do
|
|
19
|
+
return if @warn_once_seen.include?(key)
|
|
20
|
+
|
|
21
|
+
@warn_once_seen.add(key)
|
|
22
|
+
end
|
|
23
|
+
begin
|
|
24
|
+
warn "Bitfab: #{message}"
|
|
25
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
26
|
+
# Logging must never crash the host app (e.g. a closed $stderr).
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Test-only: clear the dedup set so a warning can fire again.
|
|
31
|
+
def _reset_warn_once
|
|
32
|
+
@warn_once_mutex.synchronize { @warn_once_seen.clear }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/bitfab.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "bitfab/version"
|
|
4
4
|
require_relative "bitfab/constants"
|
|
5
|
+
require_relative "bitfab/warn_once"
|
|
5
6
|
require_relative "bitfab/serialize"
|
|
6
7
|
require_relative "bitfab/db_snapshot"
|
|
7
8
|
require_relative "bitfab/span_context"
|
|
@@ -65,6 +66,13 @@ module Bitfab
|
|
|
65
66
|
@client or raise "Bitfab not configured. Call Bitfab.configure(api_key: '...') first."
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
# Returns the global client, or nil if not configured. Used on the traced
|
|
70
|
+
# call path so a method invoked before Bitfab.configure runs untraced
|
|
71
|
+
# rather than crashing the host app with a "not configured" error.
|
|
72
|
+
def client_or_nil
|
|
73
|
+
@client
|
|
74
|
+
end
|
|
75
|
+
|
|
68
76
|
# Reset the global client (primarily for testing).
|
|
69
77
|
def reset!
|
|
70
78
|
@client = nil
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bitfab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.21.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
@@ -127,6 +127,7 @@ files:
|
|
|
127
127
|
- lib/bitfab/span_context.rb
|
|
128
128
|
- lib/bitfab/traceable.rb
|
|
129
129
|
- lib/bitfab/version.rb
|
|
130
|
+
- lib/bitfab/warn_once.rb
|
|
130
131
|
homepage: https://bitfab.ai
|
|
131
132
|
licenses:
|
|
132
133
|
- MIT
|