bitfab 0.57.7 → 0.57.8
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 +1 -1
- data/lib/bitfab/otel.rb +4 -4
- data/lib/bitfab/replay.rb +4 -3
- data/lib/bitfab/replay_concurrency.rb +22 -2
- data/lib/bitfab/replay_processes.rb +48 -25
- data/lib/bitfab/replay_registry.rb +2 -1
- data/lib/bitfab/subtree.rb +2 -2
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab/warn_once.rb +23 -0
- 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: 187464e848086662f6d74efe91dfe95476bdae7284d7ba5668f49b2a4af4c606
|
|
4
|
+
data.tar.gz: 16fce40beaf7dc062e5d6d31624d6393730847b1eaee578b60459b5f0d27897a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a5f7f9cac1900dca07228819c1c40965cf020deb5adba9f9827c36c76265911e79c5ce3aca16db5e486c354a436f63e9ac487896af9c2cf522581886424887d
|
|
7
|
+
data.tar.gz: 560d27de6a0766f9625b82bcf33053ebfffd3792dbaf2a21cd59ddd7a129742d265965d086d68138566dee51a5ba02893fdcf0bf67a95ebc8d24bc4d37ecf6ff
|
data/README.md
CHANGED
|
@@ -235,7 +235,7 @@ class OrderPipeline
|
|
|
235
235
|
end
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
-
Capture uses `TracePoint` only during the root call. It skips Ruby/runtime code, dependency gems, Bitfab SDK internals, blocks, lambdas, and forwarding wrappers. When the root belongs to an installed application gem, that gem is the first-party boundary; neighboring dependency gems remain excluded. It follows recursion, modules, inheritance, `define_method`, and same-thread child Fibers; aliases are recorded and excluded by the name that was invoked. Child Fibers created during capture inherit node policies, trace identity, parenting, and depth limits while retaining separate call stacks. Pre-existing Fibers, `Fiber.new(storage: nil)`, and child threads do not inherit subtree context. When an inner capture ends, child Fibers retain any still-active outer capture; inherited context expires once all enclosing captures end. While a root remains active, hitting a span limit stops new capture but allows already-open spans in every Fiber to finish. `max_depth:` (default `30`), `max_captured_subtree_spans:` (default `
|
|
238
|
+
Capture uses `TracePoint` only during the root call. It skips Ruby/runtime code, dependency gems, Bitfab SDK internals, blocks, lambdas, and forwarding wrappers. When the root belongs to an installed application gem, that gem is the first-party boundary; neighboring dependency gems remain excluded. It follows recursion, modules, inheritance, `define_method`, and same-thread child Fibers; aliases are recorded and excluded by the name that was invoked. Child Fibers created during capture inherit node policies, trace identity, parenting, and depth limits while retaining separate call stacks. Pre-existing Fibers, `Fiber.new(storage: nil)`, and child threads do not inherit subtree context. When an inner capture ends, child Fibers retain any still-active outer capture; inherited context expires once all enclosing captures end. While a root remains active, hitting a span limit stops new capture but allows already-open spans in every Fiber to finish. `max_depth:` (default `30`), `max_captured_subtree_spans:` (default `500`), `max_spans:` (default `10000`), `exclude:`, and `include_wrappers:` control the subtree. `max_spans:` is a strict cap on the total number of spans one trace root sends. It counts the root span itself, discovered calls, `bitfab_node` calls, nested trace roots, and spans whose capture is on or off, including spans sent without inputs and outputs because the sim plan could not be read. Nothing past it is sent. It must be at least 1, and a lower value raises `ArgumentError`. `max_captured_subtree_spans:` caps discovered calls sent with their inputs and outputs. It does not count `bitfab_node` calls, nested trace roots, or spans the loaded sim plan turned capture off for, so those keep recording past it up to `max_spans:`. Spans sent without inputs and outputs only because the sim plan could not be read still count toward it. Past it, further discovered calls with capture on record nothing. It is never higher than `max_spans:`. Once the plan has loaded, the SDK never builds the inputs or outputs of a span whose capture it turned off. The client starts reading the sim plan when it is built. A `bitfab_trace` root that starts while that first read is still running waits for it, for at most five seconds. Once the first read has finished, whether it succeeded or failed, no later root waits. Nested roots and the calls beneath a root never wait, and nothing waits when the plan is disabled. If that first read fails, spans other than each trace's root span are sent without inputs and outputs until a read succeeds, and they still count toward both limits. Automatically captured calls are not replay mock targets. Declare a call with `bitfab_node` when replay must substitute it. The configured node records once in each enclosing trace. Mixing explicit spans with subtree tracing raises `Bitfab::MixedTracingError`.
|
|
239
239
|
|
|
240
240
|
When capture ends, already-open automatic calls in suspended Fibers are emitted once with a nil output and `Subtree capture ended before the call returned` as the error, unless a real error was already observed. Their inputs and parenting are retained. The SDK does not resume Fibers, and later resumption cannot add spans to the ended capture. If hot reload moves the root to an unresolved source, the last valid first-party boundary and its exclusions remain in use.
|
|
241
241
|
|
data/lib/bitfab/otel.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Bitfab
|
|
|
24
24
|
EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY"
|
|
25
25
|
MAX_EXPORT_REQUEST_BYTES = 3_000_000
|
|
26
26
|
MAX_DECOMPRESSED_REQUEST_BYTES = 8_000_000
|
|
27
|
-
MAX_QUEUE_SIZE =
|
|
27
|
+
MAX_QUEUE_SIZE = 16_384
|
|
28
28
|
DIRECT_MAX_EXPORT_BATCH_SIZE = 512
|
|
29
29
|
DIRECT_MAX_REQUEST_BATCH_SIZE = 128
|
|
30
30
|
DEFAULT_EXPORT_CONCURRENCY = 32
|
|
@@ -331,17 +331,17 @@ module Bitfab
|
|
|
331
331
|
if e.oversized
|
|
332
332
|
warn_oversized(batch.spans.size)
|
|
333
333
|
else
|
|
334
|
-
Bitfab.
|
|
334
|
+
Bitfab.warn_delivery_problem("failed to export an OpenTelemetry span batch: #{e.message}")
|
|
335
335
|
end
|
|
336
336
|
false
|
|
337
337
|
rescue => e
|
|
338
|
-
Bitfab.
|
|
338
|
+
Bitfab.warn_delivery_problem("failed to export an OpenTelemetry span batch: #{e.message}")
|
|
339
339
|
false
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
def warn_oversized(span_count)
|
|
343
343
|
subject = (span_count == 1) ? "a single OpenTelemetry span" : "an OpenTelemetry span batch"
|
|
344
|
-
Bitfab.
|
|
344
|
+
Bitfab.warn_delivery_problem("#{subject} exceeded the ingestion request limit and could not be exported")
|
|
345
345
|
end
|
|
346
346
|
|
|
347
347
|
def send_with_retries(request)
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -1179,7 +1179,8 @@ module Bitfab
|
|
|
1179
1179
|
# repeat itself. Acks cannot settle a request that timed out client-side
|
|
1180
1180
|
# after the server committed, so anything short of fully delivered falls
|
|
1181
1181
|
# through to the server poll, which stays the authority there.
|
|
1182
|
-
def wait_for_replay_persistence(http_client, test_run_id, sdk_trace_ids)
|
|
1182
|
+
def wait_for_replay_persistence(http_client, test_run_id, sdk_trace_ids, timeout: nil)
|
|
1183
|
+
timeout ||= PERSISTENCE_TIMEOUT_SECONDS
|
|
1183
1184
|
trace_ids = sdk_trace_ids.compact
|
|
1184
1185
|
|
|
1185
1186
|
# Checked before flushing: a run whose traces never closed submitted
|
|
@@ -1194,7 +1195,7 @@ module Bitfab
|
|
|
1194
1195
|
# timeout can fire while requests are still in flight and the server goes
|
|
1195
1196
|
# on to persist every one of them. Failing here failed runs whose data had
|
|
1196
1197
|
# fully landed.
|
|
1197
|
-
flushed = Bitfab.flush_traces(timeout:
|
|
1198
|
+
flushed = Bitfab.flush_traces(timeout:)
|
|
1198
1199
|
|
|
1199
1200
|
raw_deliveries = http_client.take_trace_deliveries(trace_ids)
|
|
1200
1201
|
# Server-assigned traces.id per replay trace, read back off the ingest
|
|
@@ -1206,7 +1207,7 @@ module Bitfab
|
|
|
1206
1207
|
expected_span_counts = deliveries.transform_values(&:span_count)
|
|
1207
1208
|
return read_back_trace_ids if expected_span_counts.empty? || deliveries.each_value.all?(&:delivered)
|
|
1208
1209
|
|
|
1209
|
-
deadline = Otel.monotonic_now +
|
|
1210
|
+
deadline = Otel.monotonic_now + timeout
|
|
1210
1211
|
missing = expected_span_counts.keys
|
|
1211
1212
|
|
|
1212
1213
|
while missing.any?
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Bitfab
|
|
4
4
|
class ReplayConcurrency
|
|
5
5
|
UNSET = Object.new.freeze
|
|
6
|
-
attr_reader :attempts, :max_concurrency, :primitive, :memory_throttle, :on_item_finish_in_child_process, :child_timeout
|
|
6
|
+
attr_reader :attempts, :max_concurrency, :primitive, :memory_throttle, :on_item_finish_in_child_process, :child_timeout, :child_delivery_timeout
|
|
7
7
|
|
|
8
|
-
def initialize(attempts: 1, max_concurrency: UNSET, primitive: "async", memory_throttle: UNSET, on_item_finish_in_child_process: nil, child_timeout: 2400)
|
|
8
|
+
def initialize(attempts: 1, max_concurrency: UNSET, primitive: "async", memory_throttle: UNSET, on_item_finish_in_child_process: nil, child_timeout: 2400, child_delivery_timeout: UNSET)
|
|
9
9
|
raise ArgumentError, "primitive must be async or process" unless %w[async process].include?(primitive)
|
|
10
10
|
raise ArgumentError, "attempts must be an integer from 1 to 100" unless attempts.is_a?(Integer) && attempts.between?(1, 100)
|
|
11
11
|
max_concurrency = (primitive == "process") ? 4 : 10 if max_concurrency.equal?(UNSET)
|
|
@@ -24,11 +24,31 @@ module Bitfab
|
|
|
24
24
|
end
|
|
25
25
|
raise ArgumentError, "child_timeout must be a positive finite number" unless child_timeout.is_a?(Numeric) && child_timeout.finite? && child_timeout.positive?
|
|
26
26
|
@child_timeout = child_timeout
|
|
27
|
+
@child_delivery_timeout = resolve_child_delivery_timeout(child_delivery_timeout, primitive)
|
|
27
28
|
@attempts, @max_concurrency, @primitive = attempts, max_concurrency, primitive
|
|
28
29
|
@memory_throttle, @on_item_finish_in_child_process = memory_throttle, on_item_finish_in_child_process
|
|
29
30
|
freeze
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def resolve_child_delivery_timeout(timeout, primitive)
|
|
36
|
+
if timeout.equal?(UNSET)
|
|
37
|
+
return (primitive == "process") ? Replay::PERSISTENCE_TIMEOUT_SECONDS : nil
|
|
38
|
+
end
|
|
39
|
+
return nil if timeout.nil? && primitive != "process"
|
|
40
|
+
unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
|
|
41
|
+
raise ArgumentError, "child_delivery_timeout must be a positive number of seconds (got #{timeout.inspect})"
|
|
42
|
+
end
|
|
43
|
+
if primitive != "process"
|
|
44
|
+
raise ArgumentError, "child_delivery_timeout bounds how long a child process waits for its spans to reach Bitfab, " \
|
|
45
|
+
"and only primitive: process creates one. Under primitive: async the run's own delivery wait applies."
|
|
46
|
+
end
|
|
47
|
+
timeout
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
public
|
|
51
|
+
|
|
32
52
|
def self.resolve(concurrency, attempts, max_concurrency)
|
|
33
53
|
if concurrency
|
|
34
54
|
raise ArgumentError, "concurrency must be a ReplayConcurrency" unless concurrency.is_a?(self)
|
|
@@ -42,37 +42,57 @@ module Bitfab
|
|
|
42
42
|
end
|
|
43
43
|
http = client.instance_variable_get(:@http_client)
|
|
44
44
|
overrides = MockOverride.normalize(options[:mock_override]) + (client.instance_variable_get(:@mock_overrides) || [])
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
result[:trace_id] =
|
|
45
|
+
delivery_timeout = options[:concurrency]&.child_delivery_timeout || Replay::PERSISTENCE_TIMEOUT_SECONDS
|
|
46
|
+
result = nil
|
|
47
|
+
Bitfab.collect_delivery_problems do
|
|
48
|
+
result = Replay.process_single_item(http, assignment.fetch("item"), receiver, method_name,
|
|
49
|
+
assignment.fetch("test_run_id"), assignment.fetch("mock"), options[:adapt_inputs],
|
|
50
|
+
assignment.fetch("include_db_branch_lease"), assignment["db_branch_settings"],
|
|
51
|
+
mock_overrides: overrides, dry_run: assignment.fetch("dry_run"))
|
|
52
|
+
unless assignment["dry_run"]
|
|
53
|
+
begin
|
|
54
|
+
mapped = Replay.wait_for_replay_persistence(http, assignment.fetch("test_run_id"), [result[:_sdk_trace_id]], timeout: delivery_timeout)
|
|
55
|
+
result[:trace_id] = mapped[result[:_sdk_trace_id]] || result[:trace_id]
|
|
56
|
+
if result[:_sdk_trace_id] && result[:trace_id].nil?
|
|
57
|
+
status = http.get_replay_status(assignment.fetch("test_run_id"), {result[:_sdk_trace_id] => 1})
|
|
58
|
+
result[:trace_id] = status.fetch("traceIds", {})[result[:_sdk_trace_id]]
|
|
59
|
+
end
|
|
60
|
+
raise "Child replay trace persistence could not be confirmed" if result[:error].nil? && result[:trace_id].nil?
|
|
61
|
+
rescue => error
|
|
62
|
+
result[:replay_error] = error
|
|
63
|
+
result[:error] = error.message
|
|
56
64
|
end
|
|
57
|
-
raise "Child replay trace persistence could not be confirmed" if result[:error].nil? && result[:trace_id].nil?
|
|
58
|
-
rescue => error
|
|
59
|
-
result[:replay_error] = error
|
|
60
|
-
result[:error] = error.message
|
|
61
65
|
end
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
write_result(assignment.fetch("result_path"), result)
|
|
67
|
+
hook = options[:concurrency]&.on_item_finish_in_child_process
|
|
68
|
+
if hook && !assignment["dry_run"]
|
|
69
|
+
begin
|
|
70
|
+
hook.call({test_run_id: assignment.fetch("test_run_id"), item: Replay.public_replay_items([result]).first})
|
|
71
|
+
rescue => error
|
|
72
|
+
stderr.puts "[replay] child grading hook failed for #{result[:original_trace_id]} attempt #{result[:attempt]}: #{error}"
|
|
73
|
+
end
|
|
70
74
|
end
|
|
75
|
+
unless client.close(timeout: delivery_timeout) || assignment["dry_run"]
|
|
76
|
+
Bitfab.count_delivery_problem("delivery was not confirmed within child_delivery_timeout (#{delivery_timeout}s). " \
|
|
77
|
+
"Spans that failed or were still sending when the child exited may never reach Bitfab.")
|
|
78
|
+
end
|
|
79
|
+
write_delivery_problems(assignment["delivery_problems_path"], result, Bitfab.delivery_problem_counts, stderr)
|
|
71
80
|
end
|
|
72
|
-
client.close(timeout: 5)
|
|
73
81
|
result
|
|
74
82
|
end
|
|
75
83
|
|
|
84
|
+
def write_delivery_problems(path, result, problems, stderr)
|
|
85
|
+
return if problems.empty?
|
|
86
|
+
label = "#{result[:original_trace_id]}##{result[:attempt]}"
|
|
87
|
+
report = problems.map do |message, count|
|
|
88
|
+
"[replay] span delivery problem for #{label}: #{message}#{" (x#{count})" if count > 1}\n"
|
|
89
|
+
end.join
|
|
90
|
+
stderr.write(report)
|
|
91
|
+
File.write(path, report, mode: "w", perm: 0o600) if path
|
|
92
|
+
rescue SystemCallError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
76
96
|
def write_result(path, result)
|
|
77
97
|
encoded = begin
|
|
78
98
|
Marshal.dump(result)
|
|
@@ -116,7 +136,8 @@ module Bitfab
|
|
|
116
136
|
result_path = File.join(@directory, "#{index}.result")
|
|
117
137
|
assignment_path = File.join(@directory, "#{index}.json")
|
|
118
138
|
stdout_path, stderr_path = File.join(@directory, "#{index}.out"), File.join(@directory, "#{index}.err")
|
|
119
|
-
|
|
139
|
+
delivery_problems_path = File.join(@directory, "#{index}.delivery")
|
|
140
|
+
assignment = {pipeline: @args.fetch(:pipeline), params: @params, item:, test_run_id:, mock:, include_db_branch_lease:, db_branch_settings:, dry_run:, result_path:, delivery_problems_path:}
|
|
120
141
|
File.write(assignment_path, JSON.generate(assignment), mode: "w", perm: 0o600)
|
|
121
142
|
executable = File.expand_path("../../exe/bitfab-replay", __dir__)
|
|
122
143
|
pid = @mutex.synchronize do
|
|
@@ -154,6 +175,8 @@ module Bitfab
|
|
|
154
175
|
end
|
|
155
176
|
child_errors = read_tail(stderr_path)
|
|
156
177
|
@mutex.synchronize { @stderr.write(child_errors) } unless child_errors.empty?
|
|
178
|
+
delivery_problems = File.file?(delivery_problems_path) ? File.read(delivery_problems_path) : ""
|
|
179
|
+
@mutex.synchronize { @stderr.write(delivery_problems) } unless delivery_problems.empty? || child_errors.include?(delivery_problems)
|
|
157
180
|
unless File.file?(result_path)
|
|
158
181
|
output_tail = read_tail(stdout_path)
|
|
159
182
|
@mutex.synchronize { @stderr.write(output_tail) } unless output_tail.empty?
|
|
@@ -220,7 +220,8 @@ module Bitfab
|
|
|
220
220
|
end,
|
|
221
221
|
memory_throttle: args.fetch(:memory_throttle, configured ? configured.memory_throttle : ReplayConcurrency::UNSET),
|
|
222
222
|
on_item_finish_in_child_process: configured&.on_item_finish_in_child_process,
|
|
223
|
-
child_timeout: configured&.child_timeout || 2400
|
|
223
|
+
child_timeout: configured&.child_timeout || 2400,
|
|
224
|
+
child_delivery_timeout: configured&.child_delivery_timeout || ReplayConcurrency::UNSET
|
|
224
225
|
)
|
|
225
226
|
ReplayProcesses.current&.configure(registry, options[:concurrency]) if options[:concurrency].primitive == "process"
|
|
226
227
|
end
|
data/lib/bitfab/subtree.rb
CHANGED
|
@@ -10,8 +10,8 @@ module Bitfab
|
|
|
10
10
|
|
|
11
11
|
module Subtree
|
|
12
12
|
DEFAULT_MAX_DEPTH = 30
|
|
13
|
-
DEFAULT_MAX_SPANS =
|
|
14
|
-
DEFAULT_MAX_CAPTURED_SUBTREE_SPANS =
|
|
13
|
+
DEFAULT_MAX_SPANS = 10_000
|
|
14
|
+
DEFAULT_MAX_CAPTURED_SUBTREE_SPANS = 500
|
|
15
15
|
LIMIT_ORDER = %i[max_depth max_spans max_captured_subtree_spans].freeze
|
|
16
16
|
SESSION_KEY = :bitfab_subtree_session
|
|
17
17
|
DISPATCHER_KEY = :bitfab_subtree_dispatcher
|
data/lib/bitfab/version.rb
CHANGED
data/lib/bitfab/warn_once.rb
CHANGED
|
@@ -12,6 +12,8 @@ module Bitfab
|
|
|
12
12
|
# warns once, not just the first one seen.
|
|
13
13
|
@warn_once_seen = Set.new
|
|
14
14
|
@warn_once_mutex = Mutex.new
|
|
15
|
+
@delivery_problems = nil
|
|
16
|
+
@delivery_problems_mutex = Mutex.new
|
|
15
17
|
|
|
16
18
|
class << self
|
|
17
19
|
def warn_once(key, message)
|
|
@@ -37,6 +39,27 @@ module Bitfab
|
|
|
37
39
|
# Logging must never crash the host app (e.g. a closed $stderr).
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
def warn_delivery_problem(message)
|
|
43
|
+
count_delivery_problem(message)
|
|
44
|
+
warn_always(message)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def count_delivery_problem(message)
|
|
48
|
+
@delivery_problems_mutex.synchronize { @delivery_problems[message] += 1 if @delivery_problems }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def delivery_problem_counts
|
|
52
|
+
@delivery_problems_mutex.synchronize { @delivery_problems ? @delivery_problems.dup : {} }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def collect_delivery_problems
|
|
56
|
+
problems = Hash.new(0)
|
|
57
|
+
@delivery_problems_mutex.synchronize { @delivery_problems = problems }
|
|
58
|
+
yield problems
|
|
59
|
+
ensure
|
|
60
|
+
@delivery_problems_mutex.synchronize { @delivery_problems = nil }
|
|
61
|
+
end
|
|
62
|
+
|
|
40
63
|
# Test-only: clear the dedup set so a warning can fire again.
|
|
41
64
|
def _reset_warn_once
|
|
42
65
|
@warn_once_mutex.synchronize { @warn_once_seen.clear }
|