bitfab 0.33.5 → 0.33.6
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 +68 -4
- data/lib/bitfab/client.rb +26 -51
- data/lib/bitfab/http_client.rb +62 -74
- data/lib/bitfab/otel.rb +805 -0
- data/lib/bitfab/otel_collector.rb +28 -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 +38 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d72d477d9b3533bbba990387aeaf342774ec7e3271e11e51fc297a7de9534bb
|
|
4
|
+
data.tar.gz: 624832fc2f8b1e526370a6c07b75fcb36dc5ffe62871ed888392e3f3d9eeb5ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8e17a56b93c6ab2ab59e5ba5f5bb264f68329f19f7c1801c10e750609be96b3ae2be5dca4117aeae6e41f2d62ae5be757188c6a85bc98548290f1afe6b0aef1
|
|
7
|
+
data.tar.gz: 7de5823eb5e244c4576723234d7ad6812fded976cd8f22837585a563ded544906d0f2c91114eb748d971056563fe2301a17ce937473538d8c3e507870a5e9929
|
data/README.md
CHANGED
|
@@ -19,7 +19,8 @@ gem install bitfab
|
|
|
19
19
|
## Requirements
|
|
20
20
|
|
|
21
21
|
- Ruby >= 3.4
|
|
22
|
-
-
|
|
22
|
+
- Runtime dependencies: `base64`, `opentelemetry-sdk` (`>= 1.2, < 2`, verified against 1.2 through 1.13, so an application already pinning its own OpenTelemetry version does not have to move)
|
|
23
|
+
- Optional: `opentelemetry-exporter-otlp`, only to deliver through an OpenTelemetry Collector. Use a version compatible with your `opentelemetry-sdk`; if the gem cannot load, the SDK warns once and keeps delivering directly to Bitfab
|
|
23
24
|
|
|
24
25
|
## Quick Start
|
|
25
26
|
|
|
@@ -355,17 +356,80 @@ end
|
|
|
355
356
|
|
|
356
357
|
### Automatic Flush
|
|
357
358
|
|
|
358
|
-
Spans are
|
|
359
|
+
Spans are queued on a batch worker and delivered on exit via an `at_exit` hook that flushes and shuts the worker down. Every other API call is blocking.
|
|
359
360
|
|
|
360
361
|
### Manual Flush
|
|
361
362
|
|
|
362
363
|
Wait for all pending spans to be sent:
|
|
363
364
|
|
|
364
365
|
```ruby
|
|
365
|
-
Bitfab.flush_traces
|
|
366
|
-
|
|
366
|
+
raise "Bitfab traces were not delivered" unless Bitfab.flush_traces(timeout: 30)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The return value is `false` when an export fails or the deadline expires.
|
|
370
|
+
|
|
371
|
+
### Closing a Client
|
|
372
|
+
|
|
373
|
+
A long-running process that builds transient clients should release each one when it is done:
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
client = Bitfab::Client.new(api_key: ENV.fetch("BITFAB_API_KEY"))
|
|
377
|
+
begin
|
|
378
|
+
# ... traced work ...
|
|
379
|
+
ensure
|
|
380
|
+
client.close(timeout: 30)
|
|
381
|
+
end
|
|
367
382
|
```
|
|
368
383
|
|
|
384
|
+
A shared client needs no explicit close; `at_exit` shuts remaining transports down.
|
|
385
|
+
|
|
386
|
+
## OpenTelemetry Transport
|
|
387
|
+
|
|
388
|
+
Bitfab keeps its public API (`Bitfab::Traceable`, `bitfab_span`, `Bitfab.span`), while one private
|
|
389
|
+
OpenTelemetry `TracerProvider` and `BatchSpanProcessor` per client manage the bounded queue, batch
|
|
390
|
+
worker, export scheduling, flush, and shutdown lifecycle. Pipelines are created lazily on the first
|
|
391
|
+
trace send and are never installed globally, so an unused or disabled client starts no OTel worker
|
|
392
|
+
and the SDK does not replace an application's own OpenTelemetry setup.
|
|
393
|
+
|
|
394
|
+
By default, batches are sent directly to Bitfab as OTLP/JSON. To send through a local OTel Collector
|
|
395
|
+
instead, add the `opentelemetry-exporter-otlp` gem and set `BITFAB_OTEL_EXPORTER_ENDPOINT` to the
|
|
396
|
+
Collector's OTLP/HTTP base URL (for example, `http://localhost:4318`). The SDK appends `/v1/traces`
|
|
397
|
+
and sends protobuf through OTel's official exporter. Production and replay traffic share the same
|
|
398
|
+
pipeline. Before finalizing a replay, the SDK flushes OTel and polls Bitfab's replay-status API
|
|
399
|
+
until every expected trace completion and span count is persisted, so Collector acceptance is never
|
|
400
|
+
mistaken for Bitfab persistence.
|
|
401
|
+
|
|
402
|
+
Direct requests carry at most eight carriers each and up to 32 run concurrently; set
|
|
403
|
+
`BITFAB_OTEL_EXPORT_CONCURRENCY` to an integer from `1` through `64` to tune that. Direct JSON and
|
|
404
|
+
Collector protobuf requests are partitioned at roughly 3 MB; set `BITFAB_OTEL_MAX_REQUEST_BYTES` to
|
|
405
|
+
a positive integer no greater than `3000000` for a Collector or proxy with a stricter limit.
|
|
406
|
+
Invalid values warn once and fall back to the defaults. If Bitfab accepts only part of a batch, it
|
|
407
|
+
returns the standard OTLP `partialSuccess` response and the SDK warns with the rejected-span count
|
|
408
|
+
and reason.
|
|
409
|
+
|
|
410
|
+
```yaml
|
|
411
|
+
receivers:
|
|
412
|
+
otlp:
|
|
413
|
+
protocols:
|
|
414
|
+
http:
|
|
415
|
+
|
|
416
|
+
exporters:
|
|
417
|
+
otlphttp/bitfab:
|
|
418
|
+
endpoint: https://bitfab.ai/api/sdk/otel
|
|
419
|
+
encoding: json
|
|
420
|
+
headers:
|
|
421
|
+
Authorization: Bearer ${env:BITFAB_API_KEY}
|
|
422
|
+
|
|
423
|
+
service:
|
|
424
|
+
pipelines:
|
|
425
|
+
traces:
|
|
426
|
+
receivers: [otlp]
|
|
427
|
+
exporters: [otlphttp/bitfab]
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
See the [OpenTelemetry Transport Architecture](https://docs.bitfab.ai/otel-architecture) for the
|
|
431
|
+
full component ownership, carrier format, replay barrier, batching, and lifecycle design.
|
|
432
|
+
|
|
369
433
|
### Reset Client
|
|
370
434
|
|
|
371
435
|
Clear the global client (useful for testing):
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -42,8 +42,6 @@ module Bitfab
|
|
|
42
42
|
# The key is NOT read here. HttpClient gets a proc so the key is resolved
|
|
43
43
|
# at send time, after any env loading has run.
|
|
44
44
|
@http_client = HttpClient.new(api_key: -> { resolve_api_key }, service_url: @service_url)
|
|
45
|
-
@pending_span_threads = {}
|
|
46
|
-
@pending_span_mutex = Mutex.new
|
|
47
45
|
# Mock overrides registered via register_mock_override, applied to every
|
|
48
46
|
# replay on this client (after any per-call mock_override). Instance
|
|
49
47
|
# state, no global; clear_mock_overrides resets it.
|
|
@@ -57,6 +55,25 @@ module Bitfab
|
|
|
57
55
|
@api_key_config.respond_to?(:call) ? @api_key_config.call : @api_key_config
|
|
58
56
|
end
|
|
59
57
|
|
|
58
|
+
# Flush and permanently close this client's tracing transport, releasing its
|
|
59
|
+
# batch worker. Long-running processes that build transient clients should
|
|
60
|
+
# call this; a shared client is closed at process exit.
|
|
61
|
+
#
|
|
62
|
+
# @param timeout [Numeric] maximum total seconds to wait
|
|
63
|
+
# @return [Boolean] true when everything queued was delivered in time
|
|
64
|
+
def close(timeout: 30)
|
|
65
|
+
@http_client.close(timeout:)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Wait for the spans and traces this client queued to be delivered, without
|
|
69
|
+
# closing it.
|
|
70
|
+
#
|
|
71
|
+
# @param timeout [Numeric] maximum total seconds to wait
|
|
72
|
+
# @return [Boolean] true when everything queued was delivered in time
|
|
73
|
+
def flush(timeout: 30)
|
|
74
|
+
@http_client.flush(timeout:)
|
|
75
|
+
end
|
|
76
|
+
|
|
60
77
|
# Effective tracing state, evaluated lazily: enabled only when not
|
|
61
78
|
# explicitly disabled AND a key resolves. Reading this resolves the key
|
|
62
79
|
# (and may emit the one-time empty-key warning), exactly as the first traced
|
|
@@ -294,10 +311,6 @@ module Bitfab
|
|
|
294
311
|
)
|
|
295
312
|
end
|
|
296
313
|
|
|
297
|
-
if is_root_span
|
|
298
|
-
@pending_span_mutex.synchronize { @pending_span_threads[trace_id] = [] }
|
|
299
|
-
end
|
|
300
|
-
|
|
301
314
|
# Advance the per-(key, name) call counter for any non-root span under
|
|
302
315
|
# an active mock tree, even when this span won't itself be mocked.
|
|
303
316
|
# Unmarked spans must consume an index so subsequent marked siblings
|
|
@@ -332,10 +345,7 @@ module Bitfab
|
|
|
332
345
|
rescue
|
|
333
346
|
# Clean up any trace state this partial setup registered so it does not
|
|
334
347
|
# leak.
|
|
335
|
-
if trace_id
|
|
336
|
-
TraceState.delete(trace_id)
|
|
337
|
-
@pending_span_mutex.synchronize { @pending_span_threads.delete(trace_id) }
|
|
338
|
-
end
|
|
348
|
+
TraceState.delete(trace_id) if trace_id
|
|
339
349
|
# During replay (a controlled eval) a setup failure must surface, not
|
|
340
350
|
# silently run the block untraced: swallowing it would execute real code
|
|
341
351
|
# with real side effects and skew the mock call counter, defeating the
|
|
@@ -366,7 +376,7 @@ module Bitfab
|
|
|
366
376
|
begin
|
|
367
377
|
ended_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
368
378
|
|
|
369
|
-
|
|
379
|
+
send_span(
|
|
370
380
|
trace_function_key:,
|
|
371
381
|
trace_id:,
|
|
372
382
|
span_id:,
|
|
@@ -387,10 +397,6 @@ module Bitfab
|
|
|
387
397
|
)
|
|
388
398
|
|
|
389
399
|
if is_root_span
|
|
390
|
-
pending = @pending_span_mutex.synchronize { @pending_span_threads.delete(trace_id) || [] }
|
|
391
|
-
pending << span_thread if span_thread
|
|
392
|
-
pending.each { |t| t.join(5) }
|
|
393
|
-
|
|
394
400
|
# Built AFTER the wrapped method finished (finalize runs at root
|
|
395
401
|
# span end), so :accessed reflects whether customer code obtained
|
|
396
402
|
# the branch URL during this item. nil (key omitted) when no
|
|
@@ -409,29 +415,13 @@ module Bitfab
|
|
|
409
415
|
}
|
|
410
416
|
end
|
|
411
417
|
|
|
412
|
-
|
|
418
|
+
send_trace_completion(
|
|
413
419
|
trace_function_key:,
|
|
414
420
|
trace_id:,
|
|
415
421
|
started_at:,
|
|
416
422
|
ended_at:,
|
|
417
423
|
db_snapshot_usage:
|
|
418
424
|
)
|
|
419
|
-
|
|
420
|
-
# In replay, persistence is correctness: the replay runner joins
|
|
421
|
-
# these threads before calling complete_replay, or the server's
|
|
422
|
-
# trace-ID mapping races the uploads and every item's trace_id
|
|
423
|
-
# comes back nil. The 5s join above is best-effort only; this
|
|
424
|
-
# hands the full set (span uploads + trace completion) to the
|
|
425
|
-
# runner. No-op outside replay, where sends stay fire-and-forget.
|
|
426
|
-
persistence = ReplayContext.current&.dig(:pending_persistence)
|
|
427
|
-
if persistence
|
|
428
|
-
persistence.concat(pending)
|
|
429
|
-
persistence << completion_thread if completion_thread
|
|
430
|
-
end
|
|
431
|
-
else
|
|
432
|
-
@pending_span_mutex.synchronize do
|
|
433
|
-
@pending_span_threads[trace_id] << span_thread if span_thread && @pending_span_threads.key?(trace_id)
|
|
434
|
-
end
|
|
435
425
|
end
|
|
436
426
|
rescue Exception # rubocop:disable Lint/RescueException
|
|
437
427
|
# Silently ignore: user's result/exception takes priority
|
|
@@ -633,14 +623,10 @@ module Bitfab
|
|
|
633
623
|
payload["testRunId"] = trace_state[:test_run_id]
|
|
634
624
|
end
|
|
635
625
|
|
|
636
|
-
|
|
626
|
+
@http_client.send_external_trace(payload)
|
|
637
627
|
|
|
638
628
|
# Clean up trace state
|
|
639
629
|
TraceState.delete(trace_id)
|
|
640
|
-
|
|
641
|
-
# Returned so the replay path can join it: trace completions must be
|
|
642
|
-
# persisted before complete_replay builds the trace-ID mapping.
|
|
643
|
-
completion_thread
|
|
644
630
|
end
|
|
645
631
|
|
|
646
632
|
def send_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
@@ -649,8 +635,7 @@ module Bitfab
|
|
|
649
635
|
# If drop() was called on this trace, suppress the span PAYLOAD upload for
|
|
650
636
|
# every span that completes after the flag was set. The trace completion
|
|
651
637
|
# still rides out with dropped: true, so the server scrubs any sibling
|
|
652
|
-
# spans that already raced out before the flag was set.
|
|
653
|
-
# signals "no span thread" to the caller, which already handles nil.
|
|
638
|
+
# spans that already raced out before the flag was set.
|
|
654
639
|
trace_dropped = TraceState.get(trace_id)&.dig(:dropped) == true
|
|
655
640
|
return nil if trace_dropped
|
|
656
641
|
|
|
@@ -835,16 +820,12 @@ module Bitfab
|
|
|
835
820
|
|
|
836
821
|
# Record a span entry for a mocked invocation so the test run reflects the
|
|
837
822
|
# mocked execution. Mirrors send_span's payload shape but with the mocked
|
|
838
|
-
# output as the result and no error.
|
|
839
|
-
# registered with @pending_span_threads so the root span's finalize joins
|
|
840
|
-
# it before sending trace completion; without this the trace completion
|
|
841
|
-
# can race ahead of the mocked span's HTTP send and the trace lands
|
|
842
|
-
# temporarily incomplete on the server.
|
|
823
|
+
# output as the result and no error.
|
|
843
824
|
def send_mocked_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
844
825
|
span_name:, span_type:, function_name:, args:, kwargs:, mocked_output:,
|
|
845
826
|
started_at:, test_run_id:, input_source_span_id:)
|
|
846
827
|
ended_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
847
|
-
|
|
828
|
+
send_span(
|
|
848
829
|
trace_function_key:,
|
|
849
830
|
trace_id:,
|
|
850
831
|
span_id:,
|
|
@@ -864,12 +845,6 @@ module Bitfab
|
|
|
864
845
|
input_source_span_id:,
|
|
865
846
|
mocked: true
|
|
866
847
|
)
|
|
867
|
-
# Mocked spans are always non-root (advance_mock_counter returns nil for
|
|
868
|
-
# root spans, so check_mock_replay never short-circuits them), so the
|
|
869
|
-
# thread always belongs in the parent's pending list, never standalone.
|
|
870
|
-
@pending_span_mutex.synchronize do
|
|
871
|
-
@pending_span_threads[trace_id] << span_thread if span_thread && @pending_span_threads.key?(trace_id)
|
|
872
|
-
end
|
|
873
848
|
rescue Exception # rubocop:disable Lint/RescueException
|
|
874
849
|
# Never crash the host app: mocked span recording is best-effort
|
|
875
850
|
end
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "uri"
|
|
|
6
6
|
|
|
7
7
|
require_relative "constants"
|
|
8
8
|
require_relative "serialize"
|
|
9
|
+
require_relative "transport"
|
|
9
10
|
require_relative "version"
|
|
10
11
|
require_relative "warn_once"
|
|
11
12
|
|
|
@@ -20,6 +21,31 @@ module Bitfab
|
|
|
20
21
|
@api_key = api_key
|
|
21
22
|
@service_url = (service_url || DEFAULT_SERVICE_URL).chomp("/")
|
|
22
23
|
@timeout = timeout
|
|
24
|
+
@transport_mutex = Mutex.new
|
|
25
|
+
@transport = nil
|
|
26
|
+
@closed = false
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Flush and permanently close this client's tracing transport.
|
|
30
|
+
# Returns true when everything it queued was delivered within the deadline.
|
|
31
|
+
def close(timeout: 30)
|
|
32
|
+
transport = @transport_mutex.synchronize do
|
|
33
|
+
current = @transport
|
|
34
|
+
@transport = nil
|
|
35
|
+
@closed = true
|
|
36
|
+
current
|
|
37
|
+
end
|
|
38
|
+
return true if transport.nil?
|
|
39
|
+
|
|
40
|
+
transport.shutdown(timeout)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Wait for the spans and traces this client queued to be delivered.
|
|
44
|
+
def flush(timeout: 30)
|
|
45
|
+
transport = @transport_mutex.synchronize { @transport }
|
|
46
|
+
return true if transport.nil?
|
|
47
|
+
|
|
48
|
+
transport.flush(timeout)
|
|
23
49
|
end
|
|
24
50
|
|
|
25
51
|
# Make a POST request to the Bitfab API.
|
|
@@ -37,7 +63,7 @@ module Bitfab
|
|
|
37
63
|
http.read_timeout = request_timeout
|
|
38
64
|
|
|
39
65
|
req = Net::HTTP::Post.new(uri.path, headers)
|
|
40
|
-
req.body = safe_generate(payload
|
|
66
|
+
req.body = Serialize.safe_generate(payload)
|
|
41
67
|
|
|
42
68
|
response = http.request(req)
|
|
43
69
|
|
|
@@ -62,14 +88,9 @@ module Bitfab
|
|
|
62
88
|
raise last_error
|
|
63
89
|
end
|
|
64
90
|
|
|
65
|
-
#
|
|
66
|
-
# Returns the thread for callers that need to await completion.
|
|
91
|
+
# Queue an external span on this client's trace transport (fire-and-forget).
|
|
67
92
|
def send_external_span(payload)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Bitfab._run_in_background do
|
|
71
|
-
request("/api/sdk/externalSpans", merged, timeout: 30)
|
|
72
|
-
end
|
|
93
|
+
trace_transport&.submit("external_span", payload.merge("sdkVersion" => VERSION))
|
|
73
94
|
end
|
|
74
95
|
|
|
75
96
|
# Make a GET request to the Bitfab API.
|
|
@@ -208,54 +229,45 @@ module Bitfab
|
|
|
208
229
|
)
|
|
209
230
|
end
|
|
210
231
|
|
|
211
|
-
#
|
|
232
|
+
# Queue an external trace on this client's trace transport (fire-and-forget).
|
|
212
233
|
def send_external_trace(payload)
|
|
213
|
-
|
|
234
|
+
trace_transport&.submit("external_trace", payload.merge("sdkVersion" => VERSION))
|
|
235
|
+
end
|
|
214
236
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
237
|
+
# Read the replay traces the server has fully persisted so far.
|
|
238
|
+
def get_replay_status(test_run_id, expected_span_counts)
|
|
239
|
+
request(
|
|
240
|
+
"/api/sdk/replay/status",
|
|
241
|
+
{"testRunId" => test_run_id, "expectedSpanCounts" => expected_span_counts},
|
|
242
|
+
timeout: 30
|
|
243
|
+
)
|
|
218
244
|
end
|
|
219
245
|
|
|
220
246
|
private
|
|
221
247
|
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
"a request body held a non-serializable value (#{e.message}); it was " \
|
|
236
|
-
"stubbed so the span still sends, but the trace may be incomplete or " \
|
|
237
|
-
"not replayable. Capture a JSON-safe projection of this input to make " \
|
|
238
|
-
"it replayable."
|
|
239
|
-
)
|
|
248
|
+
# Returns nil once the client is closed. A stray traced call after close
|
|
249
|
+
# must not crash the host app, and raising here would be swallowed by the
|
|
250
|
+
# span finalize path, dropping the span with no explanation.
|
|
251
|
+
def trace_transport
|
|
252
|
+
@transport_mutex.synchronize do
|
|
253
|
+
if @closed
|
|
254
|
+
Bitfab.warn_once(
|
|
255
|
+
"client-closed",
|
|
256
|
+
"client is closed; dropping spans. Close it after the traced work " \
|
|
257
|
+
"finishes, or keep one client for the life of the process."
|
|
258
|
+
)
|
|
259
|
+
next nil
|
|
260
|
+
end
|
|
240
261
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
JSON.generate({"error" => "payload_serialize_failed"})
|
|
262
|
+
@transport ||= Transport.create_trace_transport(
|
|
263
|
+
api_key: @api_key,
|
|
264
|
+
direct_sender: method(:send_transport_request)
|
|
265
|
+
)
|
|
246
266
|
end
|
|
247
267
|
end
|
|
248
268
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
# the whole payload could collapse the entire body to a single stub string
|
|
252
|
-
# (oversize/cyclic), sending a JSON string instead of a span object.
|
|
253
|
-
def sanitize_payload(payload)
|
|
254
|
-
return {"error" => "payload_serialize_failed"} unless payload.is_a?(Hash)
|
|
255
|
-
|
|
256
|
-
payload.each_with_object({}) do |(k, v), acc|
|
|
257
|
-
acc[k.to_s] = Serialize.serialize_value(v)
|
|
258
|
-
end
|
|
269
|
+
def send_transport_request(endpoint, payload, timeout)
|
|
270
|
+
request(endpoint, payload, timeout:, max_retries: 1)
|
|
259
271
|
end
|
|
260
272
|
|
|
261
273
|
# Normalize each entry to a hash with stable string keys, accepting either
|
|
@@ -281,39 +293,15 @@ module Bitfab
|
|
|
281
293
|
end
|
|
282
294
|
end
|
|
283
295
|
|
|
284
|
-
# --- Background thread management ---
|
|
285
|
-
|
|
286
|
-
@pending_threads_mutex = Mutex.new
|
|
287
|
-
@pending_threads = []
|
|
288
|
-
|
|
289
296
|
class << self
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
def _run_in_background(&block)
|
|
293
|
-
thread = Thread.new do
|
|
294
|
-
block.call
|
|
295
|
-
rescue => e
|
|
296
|
-
Bitfab.warn_once(
|
|
297
|
-
"send-request-failed",
|
|
298
|
-
"failed to send a request to the backend (further occurrences " \
|
|
299
|
-
"suppressed): #{e.message}"
|
|
300
|
-
)
|
|
301
|
-
ensure
|
|
302
|
-
@pending_threads_mutex.synchronize { @pending_threads.delete(Thread.current) }
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
@pending_threads_mutex.synchronize { @pending_threads << thread }
|
|
306
|
-
thread
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
# Wait for all pending background threads to complete.
|
|
297
|
+
# Wait for queued spans and trace completions to reach the server, within
|
|
298
|
+
# one total deadline. Returns true when everything landed in time.
|
|
310
299
|
def flush_traces(timeout: 30)
|
|
311
|
-
|
|
312
|
-
threads.each { |t| t.join(timeout) }
|
|
300
|
+
Transport.flush_trace_transports([timeout, 0].max)
|
|
313
301
|
end
|
|
314
302
|
end
|
|
315
303
|
|
|
316
304
|
at_exit do
|
|
317
|
-
|
|
305
|
+
Transport.shutdown_trace_transports(4)
|
|
318
306
|
end
|
|
319
307
|
end
|