bitfab 0.57.6 → 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/baml.rb +3 -2
- data/lib/bitfab/client.rb +24 -13
- data/lib/bitfab/connection_pool.rb +136 -0
- data/lib/bitfab/http_client.rb +10 -18
- 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/simulation_plan.rb +8 -6
- data/lib/bitfab/span_context.rb +2 -1
- data/lib/bitfab/subtree.rb +46 -11
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab/warn_once.rb +23 -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: 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/baml.rb
CHANGED
|
@@ -170,7 +170,6 @@ module Bitfab
|
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
class Client
|
|
173
|
-
# Execute a stored BAML prompt locally and return its parsed output.
|
|
174
173
|
def call(method_name, **inputs)
|
|
175
174
|
version = @http_client.request("/api/sdk/functions/lookup", {"name" => method_name})
|
|
176
175
|
raise ArgumentError, "Function #{method_name.inspect} not found. Create it at #{@service_url}/functions" unless version["id"]
|
|
@@ -182,7 +181,9 @@ module Bitfab
|
|
|
182
181
|
rescue
|
|
183
182
|
result.to_s
|
|
184
183
|
end
|
|
185
|
-
|
|
184
|
+
if should_record? && !active_span_content_withheld?
|
|
185
|
+
@http_client.send_internal_trace(version["id"], {"result" => output, "inputs" => inputs, "rawCollector" => collector, "source" => "ruby-sdk"})
|
|
186
|
+
end
|
|
186
187
|
result
|
|
187
188
|
end
|
|
188
189
|
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -49,14 +49,10 @@ module Bitfab
|
|
|
49
49
|
@enabled_in_code = capture_enabled.nil? ? enabled : capture_enabled
|
|
50
50
|
@capture_configured_from_env = nil
|
|
51
51
|
@strict = strict
|
|
52
|
-
# Cached only once a non-empty key is found, so an early resolve (before
|
|
53
|
-
# env loaded) can't poison a later one.
|
|
54
52
|
@resolved_api_key = nil
|
|
55
53
|
@api_key_warned = false
|
|
56
|
-
# The key is NOT read here. HttpClient gets a proc so the key is resolved
|
|
57
|
-
# at send time, after any env loading has run.
|
|
58
54
|
@http_client = HttpClient.new(api_key: -> { resolve_api_key }, service_url: @service_url, timeout:)
|
|
59
|
-
@http_client.configure_simulation_plan(enabled: simulation_plan)
|
|
55
|
+
@http_client.configure_simulation_plan(enabled: simulation_plan, start: !api_key_resolved_lazily?)
|
|
60
56
|
@datasets = Datasets.new(@http_client)
|
|
61
57
|
@assertion_categories = AssertionCategories.new(@http_client)
|
|
62
58
|
@traces = Traces.new(@http_client)
|
|
@@ -381,11 +377,10 @@ module Bitfab
|
|
|
381
377
|
explicit_span_receiver: nil, explicit_span_method_name: nil, test_run_id: nil, finalize: nil,
|
|
382
378
|
surface: nil, on_start: nil, on_complete: nil, around_body: nil, span_data: {}, managed_root: false,
|
|
383
379
|
result_recorded_elsewhere: false)
|
|
384
|
-
# Decide at CALL time, not construction. The key may be set after the
|
|
385
|
-
# client is built (env loaded later), so re-checking per call lets a
|
|
386
|
-
# late-resolved key take effect.
|
|
387
380
|
return yield unless should_record?
|
|
388
381
|
|
|
382
|
+
@http_client.refresh_simulation_plan
|
|
383
|
+
|
|
389
384
|
resolved_capture_when = capture_when.to_s
|
|
390
385
|
unless ["always", "nested"].include?(resolved_capture_when)
|
|
391
386
|
Bitfab.warn_once(
|
|
@@ -513,6 +508,12 @@ module Bitfab
|
|
|
513
508
|
completed = false
|
|
514
509
|
@http_client.trace_completion.start(trace_id, span_id)
|
|
515
510
|
recording_override = managed_root ? {} : nil
|
|
511
|
+
content_withheld = lambda do
|
|
512
|
+
instrumentation = recording_override&.dig(:instrumentation) || ((surface == :trace) ? "trace" : "span")
|
|
513
|
+
content_off_by_simulation_plan?(
|
|
514
|
+
trace_id:, trace_function_key:, span_name: recording_override&.dig(:name) || span_name, instrumentation:
|
|
515
|
+
) || (!parent_span_id.nil? && simulation_plan_unavailable?(instrumentation:))
|
|
516
|
+
end
|
|
516
517
|
|
|
517
518
|
complete = lambda do |final_result, final_error|
|
|
518
519
|
# Never crash the host app due to span building/sending. Idempotent:
|
|
@@ -528,9 +529,7 @@ module Bitfab
|
|
|
528
529
|
recorded_error = final_error
|
|
529
530
|
recorded_name = recording_override&.dig(:name) || span_name
|
|
530
531
|
recorded_instrumentation = recording_override&.dig(:instrumentation) || ((surface == :trace) ? "trace" : "span")
|
|
531
|
-
content_off =
|
|
532
|
-
trace_id:, trace_function_key:, span_name: recorded_name, instrumentation: recorded_instrumentation
|
|
533
|
-
) || (!parent_span_id.nil? && simulation_plan_unavailable?(instrumentation: recorded_instrumentation))
|
|
532
|
+
content_off = content_withheld.call
|
|
534
533
|
if finalize && final_error.nil? && !(content_off && !result_recorded_elsewhere)
|
|
535
534
|
begin
|
|
536
535
|
recorded_result = finalize.call(final_result)
|
|
@@ -613,7 +612,7 @@ module Bitfab
|
|
|
613
612
|
span_id:,
|
|
614
613
|
explicit_span_receiver:,
|
|
615
614
|
explicit_span_method_name: explicit_span_method_name&.to_sym,
|
|
616
|
-
surface:, recording_override:
|
|
615
|
+
surface:, recording_override:, content_withheld:
|
|
617
616
|
) do
|
|
618
617
|
invoke = -> { around_body ? around_body.call { yield } : yield }
|
|
619
618
|
result = if explicit_span_receiver
|
|
@@ -704,6 +703,18 @@ module Bitfab
|
|
|
704
703
|
nil
|
|
705
704
|
end
|
|
706
705
|
|
|
706
|
+
def active_span_content_withheld?
|
|
707
|
+
return true if Subtree.current_session&.current_span_content_withheld?
|
|
708
|
+
|
|
709
|
+
SpanContext.current&.dig(:content_withheld)&.call == true
|
|
710
|
+
rescue
|
|
711
|
+
true
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def api_key_resolved_lazily?
|
|
715
|
+
@api_key_config.respond_to?(:call) || @api_key_config.to_s.strip.empty?
|
|
716
|
+
end
|
|
717
|
+
|
|
707
718
|
# Resolve the API key lazily, the first time a span actually needs it.
|
|
708
719
|
#
|
|
709
720
|
# The key is intentionally NOT read at construction: a client built when a
|
|
@@ -955,7 +966,7 @@ module Bitfab
|
|
|
955
966
|
span_data["error_source"] = "code"
|
|
956
967
|
end
|
|
957
968
|
span_data["contexts"] = contexts if contexts&.any?
|
|
958
|
-
span_data["prompt"] = prompt if prompt
|
|
969
|
+
span_data["prompt"] = prompt if prompt && !content_off
|
|
959
970
|
|
|
960
971
|
raw_span = {
|
|
961
972
|
"id" => span_id,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
|
|
5
|
+
module Bitfab
|
|
6
|
+
class ConnectionPool
|
|
7
|
+
KEEP_ALIVE_TIMEOUT_SECONDS = 30
|
|
8
|
+
MAX_IDLE_CONNECTIONS = 64
|
|
9
|
+
|
|
10
|
+
IdleConnection = Struct.new(:http, :released_at)
|
|
11
|
+
private_constant :IdleConnection
|
|
12
|
+
|
|
13
|
+
@registry = ObjectSpace::WeakMap.new
|
|
14
|
+
@registry_mutex = Mutex.new
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def register(pool)
|
|
18
|
+
@registry_mutex.synchronize { @registry[pool] = true }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def close_all
|
|
22
|
+
pools = @registry_mutex.synchronize { @registry.keys }
|
|
23
|
+
pools.each(&:close)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize
|
|
28
|
+
@pid = Process.pid
|
|
29
|
+
@mutex = Mutex.new
|
|
30
|
+
@idle = {}
|
|
31
|
+
self.class.register(self)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def with_connection(uri, timeout)
|
|
35
|
+
key = [uri.scheme, uri.host, uri.port]
|
|
36
|
+
owner_pid = Process.pid
|
|
37
|
+
reusable = false
|
|
38
|
+
http = checkout(key, uri, timeout)
|
|
39
|
+
response = yield http
|
|
40
|
+
reusable = true
|
|
41
|
+
response
|
|
42
|
+
ensure
|
|
43
|
+
release(key, http, owner_pid, reusable) if http
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def close
|
|
47
|
+
drain.each { |http| finish(http) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def idle_count
|
|
51
|
+
reset_after_fork
|
|
52
|
+
@mutex.synchronize { @idle.sum { |_key, connections| connections.size } }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def checkout(key, uri, timeout)
|
|
58
|
+
http = take_idle(key) || connect(uri, timeout)
|
|
59
|
+
http.read_timeout = timeout
|
|
60
|
+
http.write_timeout = timeout
|
|
61
|
+
http
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def connect(uri, timeout)
|
|
65
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
66
|
+
http.use_ssl = uri.scheme == "https"
|
|
67
|
+
http.open_timeout = timeout
|
|
68
|
+
http.read_timeout = timeout
|
|
69
|
+
http.write_timeout = timeout
|
|
70
|
+
http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT_SECONDS
|
|
71
|
+
http.start
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def take_idle(key)
|
|
75
|
+
reset_after_fork
|
|
76
|
+
now = monotonic_now
|
|
77
|
+
expired = []
|
|
78
|
+
http = @mutex.synchronize do
|
|
79
|
+
connections = @idle[key] || []
|
|
80
|
+
stale, fresh = connections.partition { |idle| now - idle.released_at >= KEEP_ALIVE_TIMEOUT_SECONDS }
|
|
81
|
+
expired.concat(stale.map(&:http))
|
|
82
|
+
@idle[key] = fresh
|
|
83
|
+
fresh.pop&.http
|
|
84
|
+
end
|
|
85
|
+
expired.each { |stale_http| finish(stale_http) }
|
|
86
|
+
http
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def release(key, http, owner_pid, reusable)
|
|
90
|
+
return unless owner_pid == Process.pid
|
|
91
|
+
|
|
92
|
+
unless reusable && http.started?
|
|
93
|
+
finish(http)
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
reset_after_fork
|
|
98
|
+
pooled = @mutex.synchronize do
|
|
99
|
+
next false if @idle.sum { |_key, connections| connections.size } >= MAX_IDLE_CONNECTIONS
|
|
100
|
+
|
|
101
|
+
(@idle[key] ||= []) << IdleConnection.new(http, monotonic_now)
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
finish(http) unless pooled
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def drain
|
|
108
|
+
return [] if reset_after_fork
|
|
109
|
+
|
|
110
|
+
@mutex.synchronize do
|
|
111
|
+
connections = @idle.values.flatten.map(&:http)
|
|
112
|
+
@idle = {}
|
|
113
|
+
connections
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def reset_after_fork
|
|
118
|
+
return false if @pid == Process.pid
|
|
119
|
+
|
|
120
|
+
@pid = Process.pid
|
|
121
|
+
@mutex = Mutex.new
|
|
122
|
+
@idle = {}
|
|
123
|
+
true
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def finish(http)
|
|
127
|
+
http.finish if http.started?
|
|
128
|
+
rescue
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def monotonic_now
|
|
133
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "uri"
|
|
|
6
6
|
|
|
7
7
|
require_relative "git_state"
|
|
8
8
|
require_relative "compress"
|
|
9
|
+
require_relative "connection_pool"
|
|
9
10
|
require_relative "constants"
|
|
10
11
|
require_relative "serialize"
|
|
11
12
|
require_relative "transport"
|
|
@@ -37,6 +38,7 @@ module Bitfab
|
|
|
37
38
|
@api_key = api_key
|
|
38
39
|
@service_url = (service_url || DEFAULT_SERVICE_URL).chomp("/")
|
|
39
40
|
@timeout = timeout
|
|
41
|
+
@connections = ConnectionPool.new
|
|
40
42
|
@transport_mutex = Mutex.new
|
|
41
43
|
# Eager for the same reason the transport's mutex is: `||=` racing between
|
|
42
44
|
# the export threads recording acks would hand each a different mutex.
|
|
@@ -53,9 +55,9 @@ module Bitfab
|
|
|
53
55
|
@close_condition = ConditionVariable.new
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
def configure_simulation_plan(enabled: true)
|
|
58
|
+
def configure_simulation_plan(enabled: true, start: true)
|
|
57
59
|
@simulation_plan = SimulationPlan.new(self, enabled:)
|
|
58
|
-
@simulation_plan.refresh
|
|
60
|
+
@simulation_plan.refresh if start
|
|
59
61
|
@simulation_plan
|
|
60
62
|
end
|
|
61
63
|
|
|
@@ -80,7 +82,7 @@ module Bitfab
|
|
|
80
82
|
end
|
|
81
83
|
|
|
82
84
|
def get_simulation_plan
|
|
83
|
-
get("/api/sdk/sim-plan", timeout: SimulationPlan::READ_TIMEOUT
|
|
85
|
+
get("/api/sdk/sim-plan", timeout: SimulationPlan::READ_TIMEOUT)
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
# Flush and permanently close this client's tracing transport.
|
|
@@ -112,6 +114,7 @@ module Bitfab
|
|
|
112
114
|
shutdown = transport.nil? || transport.shutdown(remaining)
|
|
113
115
|
result = released && shutdown
|
|
114
116
|
ensure
|
|
117
|
+
@connections.close
|
|
115
118
|
@transport_mutex.synchronize do
|
|
116
119
|
@close_result = result
|
|
117
120
|
@close_condition.broadcast
|
|
@@ -157,16 +160,11 @@ module Bitfab
|
|
|
157
160
|
last_error = nil
|
|
158
161
|
|
|
159
162
|
max_retries.times do |attempt|
|
|
160
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
161
|
-
http.use_ssl = uri.scheme == "https"
|
|
162
|
-
http.open_timeout = request_timeout
|
|
163
|
-
http.read_timeout = request_timeout
|
|
164
|
-
|
|
165
163
|
req = Net::HTTPGenericRequest.new(method, true, true, uri.request_uri, headers)
|
|
166
164
|
req["Content-Encoding"] = prepared.content_encoding if prepared.content_encoding
|
|
167
165
|
req.body = prepared.body
|
|
168
166
|
|
|
169
|
-
response = http.request(req)
|
|
167
|
+
response = @connections.with_connection(uri, request_timeout) { |http| http.request(req) }
|
|
170
168
|
|
|
171
169
|
unless response.is_a?(Net::HTTPSuccess)
|
|
172
170
|
raise Net::HTTPError.new("HTTP #{response.code}: #{response.body}", response)
|
|
@@ -216,19 +214,12 @@ module Bitfab
|
|
|
216
214
|
|
|
217
215
|
# Make a GET request to the Bitfab API.
|
|
218
216
|
# Returns parsed JSON response hash.
|
|
219
|
-
def get(endpoint, timeout: nil
|
|
217
|
+
def get(endpoint, timeout: nil)
|
|
220
218
|
uri = URI("#{@service_url}#{endpoint}")
|
|
221
219
|
request_timeout = timeout || @timeout
|
|
222
220
|
|
|
223
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
224
|
-
http.use_ssl = uri.scheme == "https"
|
|
225
|
-
http.open_timeout = request_timeout
|
|
226
|
-
http.read_timeout = request_timeout
|
|
227
|
-
|
|
228
|
-
# request_uri (not path) so any query string on the endpoint survives.
|
|
229
221
|
req = Net::HTTP::Get.new(uri.request_uri, headers)
|
|
230
|
-
|
|
231
|
-
response = http.request(req)
|
|
222
|
+
response = @connections.with_connection(uri, request_timeout) { |http| http.request(req) }
|
|
232
223
|
|
|
233
224
|
unless response.is_a?(Net::HTTPSuccess)
|
|
234
225
|
raise Net::HTTPError.new("HTTP #{response.code}: #{response.body}", response)
|
|
@@ -742,5 +733,6 @@ module Bitfab
|
|
|
742
733
|
at_exit do
|
|
743
734
|
SimulationPlan.release_all(2, stop: true)
|
|
744
735
|
Transport.shutdown_trace_transports(2)
|
|
736
|
+
ConnectionPool.close_all
|
|
745
737
|
end
|
|
746
738
|
end
|
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
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Bitfab
|
|
4
4
|
class SimulationPlan
|
|
5
|
-
CONTENT_KEYS = %w[input input_meta output output_meta input_serialized output_serialized].freeze
|
|
5
|
+
CONTENT_KEYS = %w[input input_meta output output_meta input_serialized output_serialized prompt].freeze
|
|
6
6
|
FRAMEWORKS = %w[openai-agents langgraph claude-agent-sdk vercel-ai].freeze
|
|
7
7
|
READ_TIMEOUT = 5.0
|
|
8
8
|
REFRESH_SECONDS = 60.0
|
|
@@ -54,6 +54,7 @@ module Bitfab
|
|
|
54
54
|
@draining = Set.new
|
|
55
55
|
@stopped = false
|
|
56
56
|
@first_read_finished = false
|
|
57
|
+
@first_read_deadline = nil
|
|
57
58
|
self.class.register(self)
|
|
58
59
|
end
|
|
59
60
|
|
|
@@ -144,14 +145,13 @@ module Bitfab
|
|
|
144
145
|
@mutex.synchronize { @content_off.nil? && !reading? }
|
|
145
146
|
end
|
|
146
147
|
|
|
147
|
-
def wait_for_first_read
|
|
148
|
-
|
|
148
|
+
def wait_for_first_read
|
|
149
|
+
refresh
|
|
149
150
|
return if disabled?
|
|
150
151
|
|
|
151
|
-
deadline = now + [timeout, 0].max
|
|
152
152
|
@mutex.synchronize do
|
|
153
|
-
while reading? && @thread
|
|
154
|
-
remaining =
|
|
153
|
+
while reading? && @thread && @first_read_deadline
|
|
154
|
+
remaining = @first_read_deadline - now
|
|
155
155
|
break unless remaining.positive?
|
|
156
156
|
@condition.wait(@mutex, remaining)
|
|
157
157
|
end
|
|
@@ -229,6 +229,7 @@ module Bitfab
|
|
|
229
229
|
end
|
|
230
230
|
|
|
231
231
|
def start_reader(delay)
|
|
232
|
+
@first_read_deadline ||= now + delay + READ_TIMEOUT
|
|
232
233
|
@thread = Thread.new { read_until_loaded(delay) }
|
|
233
234
|
@thread.name = "bitfab-sim-plan"
|
|
234
235
|
rescue ThreadError
|
|
@@ -302,6 +303,7 @@ module Bitfab
|
|
|
302
303
|
@thread = nil
|
|
303
304
|
@held = []
|
|
304
305
|
@draining = Set.new
|
|
306
|
+
@first_read_deadline = nil unless @first_read_finished
|
|
305
307
|
end
|
|
306
308
|
end
|
|
307
309
|
end
|
data/lib/bitfab/span_context.rb
CHANGED
|
@@ -150,10 +150,11 @@ module Bitfab
|
|
|
150
150
|
|
|
151
151
|
# Execute a block with a new span pushed onto the stack.
|
|
152
152
|
# The span is automatically popped when the block completes.
|
|
153
|
-
def with_span(trace_id:, span_id:, explicit_span_receiver: nil, explicit_span_method_name: nil, surface: nil, recording_override: nil)
|
|
153
|
+
def with_span(trace_id:, span_id:, explicit_span_receiver: nil, explicit_span_method_name: nil, surface: nil, recording_override: nil, content_withheld: nil)
|
|
154
154
|
entry = {trace_id:, span_id:}
|
|
155
155
|
entry[:surface] = surface if surface
|
|
156
156
|
entry[:recording_override] = recording_override if recording_override
|
|
157
|
+
entry[:content_withheld] = content_withheld if content_withheld
|
|
157
158
|
if explicit_span_receiver
|
|
158
159
|
entry[:explicit_span_receiver] = explicit_span_receiver
|
|
159
160
|
entry[:explicit_span_method_name] = explicit_span_method_name
|
data/lib/bitfab/subtree.rb
CHANGED
|
@@ -10,8 +10,9 @@ 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
|
+
LIMIT_ORDER = %i[max_depth max_spans max_captured_subtree_spans].freeze
|
|
15
16
|
SESSION_KEY = :bitfab_subtree_session
|
|
16
17
|
DISPATCHER_KEY = :bitfab_subtree_dispatcher
|
|
17
18
|
FIBER_CONTEXT_KEY = :bitfab_subtree_fiber_context
|
|
@@ -444,7 +445,8 @@ module Bitfab
|
|
|
444
445
|
@method_metadata = Hash.new { |owners, owner| owners[owner] = {} }
|
|
445
446
|
@spans_used = 1
|
|
446
447
|
@captured_subtree_count = 0
|
|
447
|
-
@
|
|
448
|
+
@limits_fired = Set.new
|
|
449
|
+
@dropped_spans = 0
|
|
448
450
|
@exhausted = false
|
|
449
451
|
@drained = false
|
|
450
452
|
@deferred_frames = []
|
|
@@ -478,7 +480,7 @@ module Bitfab
|
|
|
478
480
|
unless @deferred
|
|
479
481
|
@fiber_parents.clear
|
|
480
482
|
finish_open_spans
|
|
481
|
-
|
|
483
|
+
record_truncation
|
|
482
484
|
end
|
|
483
485
|
end
|
|
484
486
|
|
|
@@ -491,7 +493,7 @@ module Bitfab
|
|
|
491
493
|
end
|
|
492
494
|
|
|
493
495
|
def handle(event)
|
|
494
|
-
return if @drained
|
|
496
|
+
return count_drained_call(event) if @drained
|
|
495
497
|
|
|
496
498
|
if event.event == :raise
|
|
497
499
|
record_error(event)
|
|
@@ -523,16 +525,21 @@ module Bitfab
|
|
|
523
525
|
def reserve_node
|
|
524
526
|
stack = current_stack(Fiber.current.object_id)
|
|
525
527
|
if captured_depth(stack) >= @max_depth
|
|
526
|
-
|
|
528
|
+
drop(:max_depth)
|
|
527
529
|
return false
|
|
528
530
|
end
|
|
529
531
|
return true if take_slot(discovered: false)
|
|
530
532
|
|
|
531
|
-
|
|
533
|
+
drop(:max_spans)
|
|
532
534
|
@exhausted = true
|
|
533
535
|
false
|
|
534
536
|
end
|
|
535
537
|
|
|
538
|
+
def current_span_content_withheld?
|
|
539
|
+
entry = @stacks[Fiber.current.object_id]&.reverse_each&.find { |candidate| candidate.is_a?(OpenSpan) }
|
|
540
|
+
entry&.span_data&.dig("content_off_by_simulation_plan") == true
|
|
541
|
+
end
|
|
542
|
+
|
|
536
543
|
def configured_entry(name:, type:, args:, kwargs:, test_run_id:)
|
|
537
544
|
return unless reserve_node
|
|
538
545
|
|
|
@@ -624,11 +631,13 @@ module Bitfab
|
|
|
624
631
|
fiber_id = Fiber.current.object_id
|
|
625
632
|
if @suppressed_depths[fiber_id].to_i.positive?
|
|
626
633
|
@suppressed_depths[fiber_id] += 1
|
|
634
|
+
count_dropped_call(event, resolved_path)
|
|
627
635
|
return
|
|
628
636
|
end
|
|
629
637
|
|
|
630
638
|
if @exhausted
|
|
631
639
|
suppress(fiber_id)
|
|
640
|
+
count_dropped_call(event, resolved_path)
|
|
632
641
|
return
|
|
633
642
|
end
|
|
634
643
|
|
|
@@ -638,13 +647,13 @@ module Bitfab
|
|
|
638
647
|
|
|
639
648
|
stack = current_stack(fiber_id)
|
|
640
649
|
if captured_depth(stack) >= @max_depth
|
|
641
|
-
|
|
650
|
+
drop(:max_depth)
|
|
642
651
|
suppress(fiber_id)
|
|
643
652
|
return
|
|
644
653
|
end
|
|
645
654
|
content_off = content_off?(metadata.name)
|
|
646
655
|
if (limit = limit_reached(discovered: true, content_off:))
|
|
647
|
-
|
|
656
|
+
drop(limit)
|
|
648
657
|
stop_discovery(fiber_id) unless limit == :max_captured_subtree_spans && content_off_names?
|
|
649
658
|
return
|
|
650
659
|
end
|
|
@@ -927,9 +936,35 @@ module Bitfab
|
|
|
927
936
|
false
|
|
928
937
|
end
|
|
929
938
|
|
|
930
|
-
def
|
|
931
|
-
|
|
939
|
+
def drop(limit)
|
|
940
|
+
@limits_fired << limit
|
|
941
|
+
@dropped_spans += 1
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
def count_dropped_call(event, resolved_path)
|
|
945
|
+
metadata = metadata_for(event, resolved_path)
|
|
946
|
+
return if metadata.excluded || (!@include_wrappers && metadata.passthrough)
|
|
947
|
+
|
|
948
|
+
@dropped_spans += 1
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def count_drained_call(event)
|
|
952
|
+
return unless event.event == :call
|
|
953
|
+
return if skip_boundary_call?(event)
|
|
932
954
|
|
|
955
|
+
resolved_path = owned_path(event.path)
|
|
956
|
+
count_dropped_call(event, resolved_path) if resolved_path
|
|
957
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
958
|
+
nil
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
def record_truncation
|
|
962
|
+
return if @limits_fired.empty?
|
|
963
|
+
|
|
964
|
+
CurrentTrace.new(@trace_id).set_metadata(
|
|
965
|
+
"bitfab.truncated_by" => LIMIT_ORDER.select { |limit| @limits_fired.include?(limit) }.join(","),
|
|
966
|
+
"bitfab.dropped_spans" => @dropped_spans
|
|
967
|
+
)
|
|
933
968
|
Bitfab.warn_once(
|
|
934
969
|
"subtree-truncated:#{@trace_function_key}",
|
|
935
970
|
"'#{@trace_function_key}' hit a subtree capture limit (max_depth=#{@max_depth}, " \
|
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 }
|
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.57.
|
|
4
|
+
version: 0.57.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
@@ -161,6 +161,7 @@ files:
|
|
|
161
161
|
- lib/bitfab/client.rb
|
|
162
162
|
- lib/bitfab/commit_ref.rb
|
|
163
163
|
- lib/bitfab/compress.rb
|
|
164
|
+
- lib/bitfab/connection_pool.rb
|
|
164
165
|
- lib/bitfab/constants.rb
|
|
165
166
|
- lib/bitfab/datasets.rb
|
|
166
167
|
- lib/bitfab/db_snapshot.rb
|