bitfab 0.36.6 → 0.36.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/lib/bitfab/http_client.rb +34 -3
- data/lib/bitfab/otel.rb +5 -30
- data/lib/bitfab/replay.rb +53 -13
- data/lib/bitfab/version.rb +1 -1
- 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: 87c160ba40f51aca6fded283a601f2e917b948403436879e7dcca1793673a4aa
|
|
4
|
+
data.tar.gz: 730c4a41a9f26ee51e2e25490d8dd89aa1205a0acb091f0ce9e2560b40583622
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58ca8e468386578b6792e8aef86a74ab7f601b432e7333391931a028b8dd0a7e1006a317be2aeccc58fcdef5a89ab550fe950c89527ecf0babe888ed5cb32d1e
|
|
7
|
+
data.tar.gz: 56aa1281a7583d0c106b64044def3494221e33e9e1b45f25b2ee0113c43a72f6fc76c55ca2c0230b0430f92c2e55e7a0a7174ca980ac53c972b72a632d912155
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -13,10 +13,12 @@ require_relative "warn_once"
|
|
|
13
13
|
|
|
14
14
|
module Bitfab
|
|
15
15
|
# What a caller learns about one tracked trace once it takes it back.
|
|
16
|
-
|
|
16
|
+
# server_trace_id is the server's assigned traces.id, read back off the OTLP
|
|
17
|
+
# ingest response; nil when the server predates the field or nothing was acked.
|
|
18
|
+
DeliveryReport = Struct.new(:span_count, :closed, :delivered, :server_trace_id, keyword_init: true)
|
|
17
19
|
|
|
18
20
|
TraceDelivery = Struct.new(:submitted_span_ids, :acked_span_ids, :closed, :closing_acked,
|
|
19
|
-
keyword_init: true)
|
|
21
|
+
:server_trace_id, keyword_init: true)
|
|
20
22
|
|
|
21
23
|
class HttpClient
|
|
22
24
|
OTLP_TRACES_ENDPOINT = "/api/sdk/otel/v1/traces"
|
|
@@ -413,6 +415,9 @@ module Bitfab
|
|
|
413
415
|
)
|
|
414
416
|
end
|
|
415
417
|
|
|
418
|
+
server_trace_ids = response.is_a?(Hash) ? response["traceIds"] : nil
|
|
419
|
+
record_server_trace_ids(server_trace_ids) if server_trace_ids.is_a?(Hash)
|
|
420
|
+
|
|
416
421
|
partial_success = response.is_a?(Hash) ? response["partialSuccess"] : nil
|
|
417
422
|
return unless partial_success.is_a?(Hash)
|
|
418
423
|
|
|
@@ -492,14 +497,40 @@ module Bitfab
|
|
|
492
497
|
span_count: delivery.submitted_span_ids.size,
|
|
493
498
|
closed: delivery.closed,
|
|
494
499
|
delivered: delivery.closing_acked &&
|
|
495
|
-
delivery.submitted_span_ids.subset?(delivery.acked_span_ids)
|
|
500
|
+
delivery.submitted_span_ids.subset?(delivery.acked_span_ids),
|
|
501
|
+
server_trace_id: delivery.server_trace_id
|
|
496
502
|
)
|
|
497
503
|
end
|
|
498
504
|
end
|
|
499
505
|
end
|
|
500
506
|
|
|
507
|
+
# The server's assigned traces.id for a tracked trace if it has already been
|
|
508
|
+
# read back off an ingest response, without stopping tracking. Lets a replay
|
|
509
|
+
# surface the id mid-run for items whose spans already landed.
|
|
510
|
+
def peek_server_trace_id(trace_id)
|
|
511
|
+
@delivery_mutex.synchronize { trace_deliveries[trace_id]&.server_trace_id }
|
|
512
|
+
end
|
|
513
|
+
|
|
501
514
|
private
|
|
502
515
|
|
|
516
|
+
# Record the server's assigned traces.id for each tracked source trace, read
|
|
517
|
+
# back off the OTLP ingest response. Keyed by source trace id, the same key
|
|
518
|
+
# the delivery ledger uses. Untracked ids are ignored.
|
|
519
|
+
#
|
|
520
|
+
# Called on exporter threads, hence the mutex.
|
|
521
|
+
def record_server_trace_ids(mapping)
|
|
522
|
+
@delivery_mutex.synchronize do
|
|
523
|
+
mapping.each do |source_trace_id, server_trace_id|
|
|
524
|
+
next unless server_trace_id.is_a?(String)
|
|
525
|
+
|
|
526
|
+
delivery = trace_deliveries[source_trace_id]
|
|
527
|
+
next if delivery.nil?
|
|
528
|
+
|
|
529
|
+
delivery.server_trace_id = server_trace_id
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
|
|
503
534
|
def record_submitted_carrier(ref)
|
|
504
535
|
return if ref.nil?
|
|
505
536
|
|
data/lib/bitfab/otel.rb
CHANGED
|
@@ -19,6 +19,7 @@ module Bitfab
|
|
|
19
19
|
module Otel
|
|
20
20
|
OPERATION_ATTRIBUTE = "bitfab.operation"
|
|
21
21
|
PAYLOAD_ATTRIBUTE = "bitfab.payload"
|
|
22
|
+
CARRIER_REF_IVAR = :@bitfab_carrier_ref
|
|
22
23
|
MAX_REQUEST_BYTES_ENV = "BITFAB_OTEL_MAX_REQUEST_BYTES"
|
|
23
24
|
EXPORT_CONCURRENCY_ENV = "BITFAB_OTEL_EXPORT_CONCURRENCY"
|
|
24
25
|
MAX_EXPORT_REQUEST_BYTES = 3_000_000
|
|
@@ -164,24 +165,6 @@ module Bitfab
|
|
|
164
165
|
end
|
|
165
166
|
end
|
|
166
167
|
|
|
167
|
-
# Bounded, because the only thing that removes an entry is the encode
|
|
168
|
-
# that consumes it: a span the processor drops under backpressure never
|
|
169
|
-
# reaches encode, and its ref would otherwise sit here for the life of the
|
|
170
|
-
# process. The queue is the ceiling on how many spans can be waiting at
|
|
171
|
-
# once, so anything older than that is already unreachable. Evicting a
|
|
172
|
-
# live one costs an ack, and the barrier falls back to asking the server.
|
|
173
|
-
def record_carrier_ref(span_id, ref)
|
|
174
|
-
ensure_process_state
|
|
175
|
-
submission_mutex.synchronize do
|
|
176
|
-
carrier_refs[span_id] = ref
|
|
177
|
-
carrier_refs.shift while carrier_refs.size > MAX_QUEUE_SIZE
|
|
178
|
-
end
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def take_carrier_ref(span_id)
|
|
182
|
-
submission_mutex.synchronize { carrier_refs.delete(span_id) }
|
|
183
|
-
end
|
|
184
|
-
|
|
185
168
|
def monotonic_now
|
|
186
169
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
187
170
|
end
|
|
@@ -198,10 +181,8 @@ module Bitfab
|
|
|
198
181
|
reset_state_after_fork
|
|
199
182
|
end
|
|
200
183
|
|
|
201
|
-
# Carrier refs belong to the parent's run, never the child's.
|
|
202
184
|
def reset_state_after_fork
|
|
203
185
|
@state_pid = Process.pid
|
|
204
|
-
@carrier_refs = {}
|
|
205
186
|
end
|
|
206
187
|
|
|
207
188
|
private
|
|
@@ -217,14 +198,6 @@ module Bitfab
|
|
|
217
198
|
def live_transports_mutex
|
|
218
199
|
@live_transports_mutex ||= Mutex.new
|
|
219
200
|
end
|
|
220
|
-
|
|
221
|
-
def submission_mutex
|
|
222
|
-
@submission_mutex ||= Mutex.new
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def carrier_refs
|
|
226
|
-
@carrier_refs ||= {}
|
|
227
|
-
end
|
|
228
201
|
end
|
|
229
202
|
|
|
230
203
|
# Encodes carrier spans as OTLP/JSON and posts them straight to Bitfab.
|
|
@@ -551,6 +524,7 @@ module Bitfab
|
|
|
551
524
|
payload,
|
|
552
525
|
max_carrier_bytes: PayloadBudget::MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES
|
|
553
526
|
)
|
|
527
|
+
encoded_payload.instance_variable_set(CARRIER_REF_IVAR, meta.ref) if meta.ref
|
|
554
528
|
started_at = meta.started_at || Time.now
|
|
555
529
|
ended_at = meta.ended_at || Time.now
|
|
556
530
|
errored = meta.errored
|
|
@@ -573,7 +547,6 @@ module Bitfab
|
|
|
573
547
|
},
|
|
574
548
|
start_timestamp: started_at
|
|
575
549
|
)
|
|
576
|
-
Otel.record_carrier_ref(span.context.hex_span_id, meta.ref) if meta.ref
|
|
577
550
|
span.status = OpenTelemetry::Trace::Status.error if errored
|
|
578
551
|
span.finish(end_timestamp: ended_at)
|
|
579
552
|
end
|
|
@@ -709,7 +682,9 @@ module Bitfab
|
|
|
709
682
|
class << self
|
|
710
683
|
def encode_span(span)
|
|
711
684
|
encoded = JSON.generate(span_to_otlp(span))
|
|
712
|
-
|
|
685
|
+
payload = span.attributes&.fetch(PAYLOAD_ATTRIBUTE, nil)
|
|
686
|
+
ref = payload&.instance_variable_get(CARRIER_REF_IVAR)
|
|
687
|
+
EncodedSpan.new(encoded, encoded.bytesize, ref)
|
|
713
688
|
end
|
|
714
689
|
|
|
715
690
|
def trim_span(span)
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -315,10 +315,21 @@ module Bitfab
|
|
|
315
315
|
# to finalize once the server confirms every replay trace it queued: the
|
|
316
316
|
# trace-ID mapping complete_replay builds would otherwise race the
|
|
317
317
|
# in-flight batches and hand back nil for every item.
|
|
318
|
-
preserve_replay_failure(result_items, test_run_id, full_test_run_url) do
|
|
318
|
+
delivered_trace_ids = preserve_replay_failure(result_items, test_run_id, full_test_run_url) do
|
|
319
319
|
wait_for_replay_persistence(http_client, test_run_id, result_items.map { |item| item[:_sdk_trace_id] })
|
|
320
320
|
end
|
|
321
321
|
|
|
322
|
+
# Primary source for item[:trace_id]: the server's assigned traces.id, read
|
|
323
|
+
# back per trace off the ingest response during the flush above (server-
|
|
324
|
+
# sourced, no polling, already in hand before complete_replay). The
|
|
325
|
+
# complete_replay map below remains the fallback for servers that don't
|
|
326
|
+
# return ids on ingest.
|
|
327
|
+
result_items.each do |item|
|
|
328
|
+
local_id = item[:_sdk_trace_id]
|
|
329
|
+
read_back = local_id && delivered_trace_ids[local_id]
|
|
330
|
+
item[:trace_id] = read_back if read_back
|
|
331
|
+
end
|
|
332
|
+
|
|
322
333
|
# complete_replay finalizes the run and returns the token/diagnostic
|
|
323
334
|
# mapping; its failures propagate loudly because a run that never
|
|
324
335
|
# completed can't be finalized.
|
|
@@ -345,9 +356,10 @@ module Bitfab
|
|
|
345
356
|
result_items.each do |item|
|
|
346
357
|
local_id = item[:_sdk_trace_id]
|
|
347
358
|
mapped = local_id && trace_id_map[local_id]
|
|
348
|
-
#
|
|
349
|
-
#
|
|
350
|
-
|
|
359
|
+
# Fallback fill for servers that did not return ids on the ingest
|
|
360
|
+
# response; the read-back loop above already set it when they did, so
|
|
361
|
+
# never overwrite a resolved id with nil here.
|
|
362
|
+
item[:trace_id] ||= mapped
|
|
351
363
|
if item[:error].nil?
|
|
352
364
|
completed_count += 1
|
|
353
365
|
missing << local_id if mapped.nil?
|
|
@@ -653,11 +665,13 @@ module Bitfab
|
|
|
653
665
|
errored = 0
|
|
654
666
|
# Each event carries the single item that just finished so a progress UI
|
|
655
667
|
# can render per-trace pass/fail as the run streams. The item's :trace_id
|
|
656
|
-
# is
|
|
657
|
-
#
|
|
658
|
-
#
|
|
659
|
-
#
|
|
660
|
-
#
|
|
668
|
+
# is the server's assigned traces.id, read back off the ingest response and
|
|
669
|
+
# surfaced as this item finishes (its trace is flushed on finish); nil only
|
|
670
|
+
# if that flush could not confirm delivery in time (the end-of-run barrier
|
|
671
|
+
# then fills the returned item), and the client correlation id is never
|
|
672
|
+
# surfaced. original_trace_id (the historical trace being replayed, taken
|
|
673
|
+
# from the server item) is what a UI keys on to identify what just finished.
|
|
674
|
+
# source_trace_id/source_span_id are kept as deprecated aliases.
|
|
661
675
|
report_start = lambda do |original_trace_id, original_span_id|
|
|
662
676
|
progress_mutex.synchronize do
|
|
663
677
|
started += 1
|
|
@@ -679,7 +693,27 @@ module Bitfab
|
|
|
679
693
|
end
|
|
680
694
|
end
|
|
681
695
|
|
|
696
|
+
# Flush an item's trace the moment it finishes, so its server traces.id is
|
|
697
|
+
# read back off the ingest response and surfaced on the item as it settles,
|
|
698
|
+
# instead of waiting for the end-of-run barrier. Waiting until the end is
|
|
699
|
+
# worst at low concurrency: each trace is closed and ready when its item
|
|
700
|
+
# finishes, but nothing fills a batch to force an export, so ready ids sit
|
|
701
|
+
# in the queue for the whole run. Serializing on a mutex keeps concurrent
|
|
702
|
+
# finishers from storming the server: a flush drains the whole queue, so
|
|
703
|
+
# the first finisher exports the batch and the rest find nothing to send.
|
|
704
|
+
flush_mutex = Mutex.new
|
|
705
|
+
flush_finished_item_trace = lambda do
|
|
706
|
+
flush_mutex.synchronize { Bitfab.flush_traces(timeout: PERSISTENCE_TIMEOUT_SECONDS) }
|
|
707
|
+
rescue => e
|
|
708
|
+
warn "Bitfab: replay per-item flush failed: #{e.message}"
|
|
709
|
+
end
|
|
710
|
+
|
|
682
711
|
report = lambda do |result, original_trace_id, original_span_id, test_run_id|
|
|
712
|
+
# Deliver this item's trace now, then read its server id back off the
|
|
713
|
+
# ingest response. A flush failure never crashes the run: the id stays
|
|
714
|
+
# nil and the end-of-run barrier remains the authority on persistence.
|
|
715
|
+
flush_finished_item_trace.call
|
|
716
|
+
result[:trace_id] = http_client.peek_server_trace_id(result[:_sdk_trace_id]) || result[:trace_id]
|
|
683
717
|
progress_mutex.synchronize do
|
|
684
718
|
completed += 1
|
|
685
719
|
error = result[:error]
|
|
@@ -1023,7 +1057,7 @@ module Bitfab
|
|
|
1023
1057
|
# nothing to wait on, and must not emit an export request to discover it.
|
|
1024
1058
|
unless http_client.closed_deliveries?(trace_ids)
|
|
1025
1059
|
http_client.take_trace_deliveries(trace_ids)
|
|
1026
|
-
return
|
|
1060
|
+
return {}
|
|
1027
1061
|
end
|
|
1028
1062
|
|
|
1029
1063
|
# A failed flush is a hint, not a verdict. It means delivery was not
|
|
@@ -1033,9 +1067,15 @@ module Bitfab
|
|
|
1033
1067
|
# fully landed.
|
|
1034
1068
|
flushed = Bitfab.flush_traces(timeout: PERSISTENCE_TIMEOUT_SECONDS)
|
|
1035
1069
|
|
|
1036
|
-
|
|
1070
|
+
raw_deliveries = http_client.take_trace_deliveries(trace_ids)
|
|
1071
|
+
# Server-assigned traces.id per replay trace, read back off the ingest
|
|
1072
|
+
# response during the flush and keyed by the client-side replay id.
|
|
1073
|
+
read_back_trace_ids = raw_deliveries.each_with_object({}) do |(id, report), acc|
|
|
1074
|
+
acc[id] = report.server_trace_id unless report.server_trace_id.nil?
|
|
1075
|
+
end
|
|
1076
|
+
deliveries = raw_deliveries.select { |_id, report| report.closed }
|
|
1037
1077
|
expected_span_counts = deliveries.transform_values(&:span_count)
|
|
1038
|
-
return if expected_span_counts.empty? || deliveries.each_value.all?(&:delivered)
|
|
1078
|
+
return read_back_trace_ids if expected_span_counts.empty? || deliveries.each_value.all?(&:delivered)
|
|
1039
1079
|
|
|
1040
1080
|
deadline = Otel.monotonic_now + PERSISTENCE_TIMEOUT_SECONDS
|
|
1041
1081
|
missing = expected_span_counts.keys
|
|
@@ -1045,7 +1085,7 @@ module Bitfab
|
|
|
1045
1085
|
ready = status["traceIds"]
|
|
1046
1086
|
ready = {} unless ready.is_a?(Hash)
|
|
1047
1087
|
missing = expected_span_counts.keys - ready.keys
|
|
1048
|
-
return if missing.empty?
|
|
1088
|
+
return read_back_trace_ids if missing.empty?
|
|
1049
1089
|
break if Otel.monotonic_now >= deadline
|
|
1050
1090
|
|
|
1051
1091
|
sleep((deadline - Otel.monotonic_now).clamp(0, PERSISTENCE_POLL_SECONDS))
|
data/lib/bitfab/version.rb
CHANGED