bitfab 0.33.5 → 0.34.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 +43 -4
- data/lib/bitfab/client.rb +26 -51
- data/lib/bitfab/http_client.rb +67 -74
- data/lib/bitfab/otel.rb +717 -0
- data/lib/bitfab/replay.rb +48 -24
- data/lib/bitfab/serialize.rb +40 -0
- data/lib/bitfab/transport.rb +27 -0
- data/lib/bitfab/version.rb +1 -1
- metadata +23 -1
data/lib/bitfab/replay.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "constants"
|
|
|
9
9
|
require_relative "mock_override"
|
|
10
10
|
require_relative "serialize"
|
|
11
11
|
require_relative "traceable"
|
|
12
|
+
require_relative "transport"
|
|
12
13
|
|
|
13
14
|
module Bitfab
|
|
14
15
|
# Replay mock strategies. Mirrors the Python and TypeScript SDKs.
|
|
@@ -29,12 +30,8 @@ module Bitfab
|
|
|
29
30
|
|
|
30
31
|
# Execute a block with replay context set on the current thread.
|
|
31
32
|
# The context is automatically cleared when the block completes.
|
|
32
|
-
#
|
|
33
|
-
# pending_persistence, when given, collects the root span's persistence
|
|
34
|
-
# threads (span uploads + trace completion) so the replay runner can join
|
|
35
|
-
# them before complete_replay builds the trace-ID mapping.
|
|
36
33
|
def with_context(test_run_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
|
|
37
|
-
mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
|
|
34
|
+
mock_tree: nil, mock_strategy: nil, mock_overrides: nil, fetch_span_output: nil,
|
|
38
35
|
db_branch_lease: nil, source_bitfab_trace_id: nil)
|
|
39
36
|
previous = Thread.current[REPLAY_CONTEXT_KEY]
|
|
40
37
|
ctx = {
|
|
@@ -43,7 +40,6 @@ module Bitfab
|
|
|
43
40
|
input_source_trace_id:,
|
|
44
41
|
trace_id:
|
|
45
42
|
}
|
|
46
|
-
ctx[:pending_persistence] = pending_persistence if pending_persistence
|
|
47
43
|
if mock_tree
|
|
48
44
|
ctx[:mock_tree] = mock_tree
|
|
49
45
|
ctx[:mock_strategy] = mock_strategy || "marked"
|
|
@@ -82,6 +78,8 @@ module Bitfab
|
|
|
82
78
|
# Replay historical traces through a traced method and create a test run.
|
|
83
79
|
module Replay
|
|
84
80
|
CODE_CHANGE_UNSET = Object.new.freeze
|
|
81
|
+
PERSISTENCE_TIMEOUT_SECONDS = 30.0
|
|
82
|
+
PERSISTENCE_POLL_SECONDS = 0.1
|
|
85
83
|
|
|
86
84
|
module_function
|
|
87
85
|
|
|
@@ -233,11 +231,15 @@ module Bitfab
|
|
|
233
231
|
[]
|
|
234
232
|
end
|
|
235
233
|
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
239
|
-
#
|
|
240
|
-
|
|
234
|
+
# Spans and completions ride a batched transport, so the run is only safe
|
|
235
|
+
# to finalize once the server confirms every replay trace it queued: the
|
|
236
|
+
# trace-ID mapping complete_replay builds would otherwise race the
|
|
237
|
+
# in-flight batches and hand back nil for every item.
|
|
238
|
+
wait_for_replay_persistence(http_client, test_run_id, result_items.map { |item| item[:_sdk_trace_id] })
|
|
239
|
+
|
|
240
|
+
# complete_replay finalizes the run and returns the token/diagnostic
|
|
241
|
+
# mapping; its failures propagate loudly because a run that never
|
|
242
|
+
# completed can't be finalized.
|
|
241
243
|
complete_response = http_client.complete_replay(test_run_id)
|
|
242
244
|
trace_id_map = complete_response&.dig("traceIds")
|
|
243
245
|
# Per-replay-trace token usage keyed by server trace id: the REPLAYED
|
|
@@ -277,7 +279,7 @@ module Bitfab
|
|
|
277
279
|
raise "Replay completed but the server has no persisted trace for " \
|
|
278
280
|
"any of the #{completed_count} completed item(s) " \
|
|
279
281
|
"(test_run_id #{test_run_id}).#{server_count} Trace uploads were " \
|
|
280
|
-
"
|
|
282
|
+
"flushed, so either the uploads failed or the replayed method is " \
|
|
281
283
|
"not traced (no root span was emitted)."
|
|
282
284
|
end
|
|
283
285
|
# SOME completed items missing: per-item upload failure. Warn, but
|
|
@@ -876,6 +878,40 @@ module Bitfab
|
|
|
876
878
|
}
|
|
877
879
|
end
|
|
878
880
|
|
|
881
|
+
# Block until the server has persisted every replay trace this run queued.
|
|
882
|
+
#
|
|
883
|
+
# Flushing only proves the batches left the process. The server writes a
|
|
884
|
+
# trace's spans and completion independently, so the barrier also polls
|
|
885
|
+
# replay status with the span count each trace owes: a trace is ready only
|
|
886
|
+
# once its completion and all of its spans have landed.
|
|
887
|
+
def wait_for_replay_persistence(http_client, test_run_id, sdk_trace_ids)
|
|
888
|
+
expected_span_counts = Transport.take_replay_span_counts(sdk_trace_ids.compact)
|
|
889
|
+
return if expected_span_counts.empty?
|
|
890
|
+
|
|
891
|
+
unless Bitfab.flush_traces(timeout: PERSISTENCE_TIMEOUT_SECONDS)
|
|
892
|
+
raise "Replay traces could not be flushed before the delivery deadline " \
|
|
893
|
+
"(test_run_id #{test_run_id})."
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
deadline = Otel.monotonic_now + PERSISTENCE_TIMEOUT_SECONDS
|
|
897
|
+
missing = expected_span_counts.keys
|
|
898
|
+
|
|
899
|
+
while missing.any?
|
|
900
|
+
status = http_client.get_replay_status(test_run_id, expected_span_counts)
|
|
901
|
+
ready = status["traceIds"]
|
|
902
|
+
ready = {} unless ready.is_a?(Hash)
|
|
903
|
+
missing = expected_span_counts.keys - ready.keys
|
|
904
|
+
return if missing.empty?
|
|
905
|
+
break if Otel.monotonic_now >= deadline
|
|
906
|
+
|
|
907
|
+
sleep((deadline - Otel.monotonic_now).clamp(0, PERSISTENCE_POLL_SECONDS))
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
raise "Replay traces were not fully persisted before the delivery deadline " \
|
|
911
|
+
"(test_run_id #{test_run_id}, missing #{missing.length} of " \
|
|
912
|
+
"#{expected_span_counts.length} trace(s))."
|
|
913
|
+
end
|
|
914
|
+
|
|
879
915
|
# Normalize a complete-replay tokens hash (string-keyed JSON) into the
|
|
880
916
|
# symbol-keyed shape the replay item exposes. Nil when the server reported
|
|
881
917
|
# no token data for this trace.
|
|
@@ -904,11 +940,6 @@ module Bitfab
|
|
|
904
940
|
# complete-replay loop). Carried on the item under :_sdk_trace_id, never
|
|
905
941
|
# surfaced as the public :trace_id.
|
|
906
942
|
sdk_trace_id = SecureRandom.uuid
|
|
907
|
-
# Collects the root span's persistence threads (span uploads + trace
|
|
908
|
-
# completion). Joined below so this item's trace is on the server
|
|
909
|
-
# before run() calls complete_replay: otherwise the server's trace-ID
|
|
910
|
-
# mapping races the uploads and the item's trace_id comes back nil.
|
|
911
|
-
pending_persistence = []
|
|
912
943
|
|
|
913
944
|
ReplayContext.with_context(
|
|
914
945
|
test_run_id:,
|
|
@@ -919,7 +950,6 @@ module Bitfab
|
|
|
919
950
|
mock_strategy:,
|
|
920
951
|
mock_overrides:,
|
|
921
952
|
fetch_span_output:,
|
|
922
|
-
pending_persistence:,
|
|
923
953
|
db_branch_lease:,
|
|
924
954
|
source_bitfab_trace_id:
|
|
925
955
|
) do
|
|
@@ -945,12 +975,6 @@ module Bitfab
|
|
|
945
975
|
fn_error = e.message
|
|
946
976
|
end
|
|
947
977
|
|
|
948
|
-
# Wait for this item's trace (spans + completion) to be fully persisted
|
|
949
|
-
# before the item resolves. Runs on the error path too: a raising
|
|
950
|
-
# method still emits a root span whose trace must land before
|
|
951
|
-
# complete_replay. Joins are bounded by the HTTP layer's own timeouts.
|
|
952
|
-
pending_persistence.each(&:join)
|
|
953
|
-
|
|
954
978
|
{
|
|
955
979
|
input: args,
|
|
956
980
|
result: fn_result,
|
data/lib/bitfab/serialize.rb
CHANGED
|
@@ -236,5 +236,45 @@ module Bitfab
|
|
|
236
236
|
[[raw_input], {}]
|
|
237
237
|
end
|
|
238
238
|
end
|
|
239
|
+
|
|
240
|
+
# JSON-encode a request or carrier body without ever raising on a stray
|
|
241
|
+
# value.
|
|
242
|
+
#
|
|
243
|
+
# Upstream serialization (serialize_value) should already have flattened
|
|
244
|
+
# user data. This is the boundary backstop: if anything non-serializable
|
|
245
|
+
# still slips through, it is run through serialize_value (which never
|
|
246
|
+
# raises and stubs strays) instead of letting JSON.generate raise and drop
|
|
247
|
+
# the whole span/trace silently. A degraded payload warns loudly so the
|
|
248
|
+
# trace isn't quietly left incomplete or not replayable.
|
|
249
|
+
def safe_generate(payload)
|
|
250
|
+
JSON.generate(payload)
|
|
251
|
+
rescue => e
|
|
252
|
+
Bitfab.warn_once(
|
|
253
|
+
"request-body-stubbed",
|
|
254
|
+
"a request body held a non-serializable value (#{e.message}); it was " \
|
|
255
|
+
"stubbed so the span still sends, but the trace may be incomplete or " \
|
|
256
|
+
"not replayable. Capture a JSON-safe projection of this input to make " \
|
|
257
|
+
"it replayable."
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
begin
|
|
261
|
+
JSON.generate(sanitize_payload(payload))
|
|
262
|
+
rescue
|
|
263
|
+
# Truly pathological. Still never drop silently: send a marker body.
|
|
264
|
+
JSON.generate({"error" => "payload_serialize_failed"})
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Serialize each top-level value so a bad or oversize value is stubbed in
|
|
269
|
+
# place while the payload keeps its object shape. Running serialize_value on
|
|
270
|
+
# the whole payload could collapse the entire body to a single stub string
|
|
271
|
+
# (oversize/cyclic), sending a JSON string instead of a span object.
|
|
272
|
+
def sanitize_payload(payload)
|
|
273
|
+
return {"error" => "payload_serialize_failed"} unless payload.is_a?(Hash)
|
|
274
|
+
|
|
275
|
+
payload.each_with_object({}) do |(k, v), acc|
|
|
276
|
+
acc[k.to_s] = serialize_value(v)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
239
279
|
end
|
|
240
280
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "otel"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
# Framework-neutral seam between span/trace sending and the delivery
|
|
7
|
+
# mechanism, so callers never depend on OpenTelemetry directly.
|
|
8
|
+
module Transport
|
|
9
|
+
class << self
|
|
10
|
+
def create_trace_transport(direct_sender:)
|
|
11
|
+
Otel.create_transport(direct_sender:)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def flush_trace_transports(timeout = 30.0)
|
|
15
|
+
Otel.flush_transports(timeout)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def shutdown_trace_transports(timeout = 30.0)
|
|
19
|
+
Otel.shutdown_transports(timeout)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def take_replay_span_counts(trace_ids)
|
|
23
|
+
Otel.take_replay_span_counts(trace_ids)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/bitfab/version.rb
CHANGED
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.34.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
@@ -23,6 +23,26 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: opentelemetry-sdk
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '1.2'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2'
|
|
26
46
|
- !ruby/object:Gem::Dependency
|
|
27
47
|
name: rake
|
|
28
48
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -122,11 +142,13 @@ files:
|
|
|
122
142
|
- lib/bitfab/db_snapshot.rb
|
|
123
143
|
- lib/bitfab/http_client.rb
|
|
124
144
|
- lib/bitfab/mock_override.rb
|
|
145
|
+
- lib/bitfab/otel.rb
|
|
125
146
|
- lib/bitfab/replay.rb
|
|
126
147
|
- lib/bitfab/replay_branch.rb
|
|
127
148
|
- lib/bitfab/serialize.rb
|
|
128
149
|
- lib/bitfab/span_context.rb
|
|
129
150
|
- lib/bitfab/traceable.rb
|
|
151
|
+
- lib/bitfab/transport.rb
|
|
130
152
|
- lib/bitfab/version.rb
|
|
131
153
|
- lib/bitfab/warn_once.rb
|
|
132
154
|
homepage: https://bitfab.ai
|