bitfab 0.36.7 → 0.36.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f18189ba0b9b7a2c8cd23ee6672f927af7f9f6400457d71d86b90c13a61698e9
4
- data.tar.gz: 61f38c9eb03166eeef759aca2914a015e92fe89247c163541c786331ed8fa26d
3
+ metadata.gz: 23d0d3e96160502ecf10da7fe952d08fec34439595e4c61e50ce9ea7c086a11c
4
+ data.tar.gz: 7bb58eab2f4749bd073593ec4c359d8460d1a1012eab60727999ad765a3e6d1e
5
5
  SHA512:
6
- metadata.gz: 8940e1f95426c3fa3343d48a340b3b4b2d2adec10c714b9e204ac7fdcfa348fba28b6bf4ed1820c1acd67536883a21701ccf401bc547cf2c3bd4c9b929169f6f
7
- data.tar.gz: 88935462030c7997ab153444e327666cc0040a0baed397c19322bfd1cedff2c9ef6624b64ce227aba02cad471d6ad05c639b1cdfbdb6a69f3a6e2f1346dca395
6
+ metadata.gz: 7360217eac4dfaa7ae66c645415598a9b794e2433434b425de54a1922243a93012270d9146309aca8d3ee65e9084da047cd6c6a5751cade83e1eb7b7d7458ab7
7
+ data.tar.gz: ee2575e01da6352effba7a1943621aa44280aabd5ead4aed76ce85561db6ccd8251c28c369de846fce1879e8ff4a08da9a6211b924d40f0a78430bec98302e76
@@ -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
- DeliveryReport = Struct.new(:span_count, :closed, :delivered, keyword_init: true)
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/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
- # Write the real server replay trace id in as it comes back; the item
349
- # held nil until now (the client correlation id is never surfaced).
350
- item[:trace_id] = mapped
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?
@@ -558,28 +570,28 @@ module Bitfab
558
570
 
559
571
  base, from_trunk = resolved
560
572
 
561
- tracked = git(root, ["diff", "--name-status", "--no-renames", "-z", base, "--", ":!.bitfab"]) || ""
573
+ tracked = git(root, ["diff", "--name-status", "--find-renames", "-z", base, "--", ":!.bitfab"]) || ""
562
574
  untracked = git(root, ["ls-files", "--others", "--exclude-standard", "-z", "--", ":!.bitfab"]) || ""
563
575
 
564
576
  entries = cc_parse_name_status_z(tracked)
565
- untracked.split("\0").reject(&:empty?).each { |p| entries << ["A", p] }
577
+ untracked.split("\0").reject(&:empty?).each { |p| entries << ["A", p, p] }
566
578
  return nil if entries.empty?
567
579
 
568
580
  files = []
569
581
  total = 0
570
- entries.each do |status, path|
582
+ entries.each do |status, before_path, path|
571
583
  break if files.length >= CC_MAX_FILES
572
584
 
573
585
  # Skip oversized files by size BEFORE reading their full contents, so a
574
586
  # huge changed file can't OOM/stall replay just to be discarded.
575
- before_bytes = (status == "A") ? 0 : cc_blob_bytes(root, base, path)
587
+ before_bytes = (status == "A") ? 0 : cc_blob_bytes(root, base, before_path)
576
588
  after_bytes = (status == "D") ? 0 : cc_working_bytes(root, path)
577
589
  next if before_bytes > CC_MAX_FILE_BYTES || after_bytes > CC_MAX_FILE_BYTES
578
590
 
579
591
  # Normalize CRLF -> LF on both sides: git's blob is already LF-normalized
580
592
  # (autocrlf clean), so a raw CRLF working file would otherwise skew every
581
593
  # line as changed. Comparing/storing LF keeps the diff meaningful.
582
- before = (status == "A") ? "" : (git(root, ["show", "#{base}:#{path}"]) || "").gsub("\r\n", "\n")
594
+ before = (status == "A") ? "" : (git(root, ["show", "#{base}:#{before_path}"]) || "").gsub("\r\n", "\n")
583
595
  after = (status == "D") ? "" : cc_read_working_file(root, path).gsub("\r\n", "\n")
584
596
  next if before == after
585
597
 
@@ -607,8 +619,15 @@ module Bitfab
607
619
  out = []
608
620
  i = 0
609
621
  while i + 1 < parts.length
610
- out << [parts[i][0], parts[i + 1]]
622
+ status = parts[i][0]
623
+ before_path = parts[i + 1]
611
624
  i += 2
625
+ if ["R", "C"].include?(status) && i < parts.length
626
+ out << [status, before_path, parts[i]]
627
+ i += 1
628
+ else
629
+ out << [status, before_path, before_path]
630
+ end
612
631
  end
613
632
  out
614
633
  end
@@ -653,11 +672,13 @@ module Bitfab
653
672
  errored = 0
654
673
  # Each event carries the single item that just finished so a progress UI
655
674
  # can render per-trace pass/fail as the run streams. The item's :trace_id
656
- # is nil at this stage: the server replay trace id isn't known until run()
657
- # writes it in after complete_replay, and the client correlation id is
658
- # never surfaced. original_trace_id (the historical trace being replayed,
659
- # taken from the server item) is what a UI keys on to identify what just
660
- # finished. source_trace_id/source_span_id are kept as deprecated aliases.
675
+ # is the server's assigned traces.id, read back off the ingest response and
676
+ # surfaced as this item finishes (its trace is flushed on finish); nil only
677
+ # if that flush could not confirm delivery in time (the end-of-run barrier
678
+ # then fills the returned item), and the client correlation id is never
679
+ # surfaced. original_trace_id (the historical trace being replayed, taken
680
+ # from the server item) is what a UI keys on to identify what just finished.
681
+ # source_trace_id/source_span_id are kept as deprecated aliases.
661
682
  report_start = lambda do |original_trace_id, original_span_id|
662
683
  progress_mutex.synchronize do
663
684
  started += 1
@@ -679,7 +700,27 @@ module Bitfab
679
700
  end
680
701
  end
681
702
 
703
+ # Flush an item's trace the moment it finishes, so its server traces.id is
704
+ # read back off the ingest response and surfaced on the item as it settles,
705
+ # instead of waiting for the end-of-run barrier. Waiting until the end is
706
+ # worst at low concurrency: each trace is closed and ready when its item
707
+ # finishes, but nothing fills a batch to force an export, so ready ids sit
708
+ # in the queue for the whole run. Serializing on a mutex keeps concurrent
709
+ # finishers from storming the server: a flush drains the whole queue, so
710
+ # the first finisher exports the batch and the rest find nothing to send.
711
+ flush_mutex = Mutex.new
712
+ flush_finished_item_trace = lambda do
713
+ flush_mutex.synchronize { Bitfab.flush_traces(timeout: PERSISTENCE_TIMEOUT_SECONDS) }
714
+ rescue => e
715
+ warn "Bitfab: replay per-item flush failed: #{e.message}"
716
+ end
717
+
682
718
  report = lambda do |result, original_trace_id, original_span_id, test_run_id|
719
+ # Deliver this item's trace now, then read its server id back off the
720
+ # ingest response. A flush failure never crashes the run: the id stays
721
+ # nil and the end-of-run barrier remains the authority on persistence.
722
+ flush_finished_item_trace.call
723
+ result[:trace_id] = http_client.peek_server_trace_id(result[:_sdk_trace_id]) || result[:trace_id]
683
724
  progress_mutex.synchronize do
684
725
  completed += 1
685
726
  error = result[:error]
@@ -1023,7 +1064,7 @@ module Bitfab
1023
1064
  # nothing to wait on, and must not emit an export request to discover it.
1024
1065
  unless http_client.closed_deliveries?(trace_ids)
1025
1066
  http_client.take_trace_deliveries(trace_ids)
1026
- return
1067
+ return {}
1027
1068
  end
1028
1069
 
1029
1070
  # A failed flush is a hint, not a verdict. It means delivery was not
@@ -1033,9 +1074,15 @@ module Bitfab
1033
1074
  # fully landed.
1034
1075
  flushed = Bitfab.flush_traces(timeout: PERSISTENCE_TIMEOUT_SECONDS)
1035
1076
 
1036
- deliveries = http_client.take_trace_deliveries(trace_ids).select { |_id, report| report.closed }
1077
+ raw_deliveries = http_client.take_trace_deliveries(trace_ids)
1078
+ # Server-assigned traces.id per replay trace, read back off the ingest
1079
+ # response during the flush and keyed by the client-side replay id.
1080
+ read_back_trace_ids = raw_deliveries.each_with_object({}) do |(id, report), acc|
1081
+ acc[id] = report.server_trace_id unless report.server_trace_id.nil?
1082
+ end
1083
+ deliveries = raw_deliveries.select { |_id, report| report.closed }
1037
1084
  expected_span_counts = deliveries.transform_values(&:span_count)
1038
- return if expected_span_counts.empty? || deliveries.each_value.all?(&:delivered)
1085
+ return read_back_trace_ids if expected_span_counts.empty? || deliveries.each_value.all?(&:delivered)
1039
1086
 
1040
1087
  deadline = Otel.monotonic_now + PERSISTENCE_TIMEOUT_SECONDS
1041
1088
  missing = expected_span_counts.keys
@@ -1045,7 +1092,7 @@ module Bitfab
1045
1092
  ready = status["traceIds"]
1046
1093
  ready = {} unless ready.is_a?(Hash)
1047
1094
  missing = expected_span_counts.keys - ready.keys
1048
- return if missing.empty?
1095
+ return read_back_trace_ids if missing.empty?
1049
1096
  break if Otel.monotonic_now >= deadline
1050
1097
 
1051
1098
  sleep((deadline - Otel.monotonic_now).clamp(0, PERSISTENCE_POLL_SECONDS))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.36.7"
4
+ VERSION = "0.36.9"
5
5
  end
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.36.7
4
+ version: 0.36.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team