bitfab 0.14.0 → 0.15.0

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: d6264d16b3fdf093418d97c43141db5a83e890d0b5e8993f255498c01548c31b
4
- data.tar.gz: 158be7664defce737b4b6e3a1db39f6bf13fee37aee9f9948fb63f7b2dd2a254
3
+ metadata.gz: b0e39c364993d34e99e1d7e3c3a878e0fc6fbfe685fcb0335128bfbf7252161b
4
+ data.tar.gz: 54bcd62faffceac5c0f67f26f5dedbed0057061019d24d1d789f9988fbf0a44a
5
5
  SHA512:
6
- metadata.gz: cb6a19c8fcfd500c58e0bbf7a1332fb27b6701282cc859bf2590477bae8fd4aaf871e37e15fc9eb0f70cbe7c121b5550f1d7ce536d771992282e0484eb545d1e
7
- data.tar.gz: b6ab4f9aa78b82b8a14d993e1357ae9db9fd09447a16ea8e5ff301998504548f7d54339923f238e4065661fcc61e8ae3db2171a8b4c00f30b1695d20f87d7546
6
+ metadata.gz: 04cef22ee4135b8c43e1e35f5ead74f5650d9f49e16b818de05980d01a054b145bf4adbad1c9fc3ec01b0e17130e108df666f75bc8bc9bf4666eb23eb9ba7acc
7
+ data.tar.gz: bb4724aff3d7fc6a9f04e1a923fd5aaabb2e808ef2310ef2c1d51e6a246c296653c1ea85509a92e1a1b927c131074e0907c07623ade5affaa6bab8fd20b2464c
data/lib/bitfab/client.rb CHANGED
@@ -183,12 +183,24 @@ module Bitfab
183
183
  pending << span_thread if span_thread
184
184
  pending.each { |t| t.join(5) }
185
185
 
186
- send_trace_completion(
186
+ completion_thread = send_trace_completion(
187
187
  trace_function_key:,
188
188
  trace_id:,
189
189
  started_at:,
190
190
  ended_at:
191
191
  )
192
+
193
+ # In replay, persistence is correctness: the replay runner joins
194
+ # these threads before calling complete_replay, or the server's
195
+ # trace-ID mapping races the uploads and every item's trace_id
196
+ # comes back nil. The 5s join above is best-effort only; this
197
+ # hands the full set (span uploads + trace completion) to the
198
+ # runner. No-op outside replay, where sends stay fire-and-forget.
199
+ persistence = ReplayContext.current&.dig(:pending_persistence)
200
+ if persistence
201
+ persistence.concat(pending)
202
+ persistence << completion_thread if completion_thread
203
+ end
192
204
  else
193
205
  @pending_span_mutex.synchronize do
194
206
  @pending_span_threads[trace_id] << span_thread if span_thread && @pending_span_threads.key?(trace_id)
@@ -314,10 +326,14 @@ module Bitfab
314
326
  payload["testRunId"] = trace_state[:test_run_id]
315
327
  end
316
328
 
317
- @http_client.send_external_trace(payload)
329
+ completion_thread = @http_client.send_external_trace(payload)
318
330
 
319
331
  # Clean up trace state
320
332
  TraceState.delete(trace_id)
333
+
334
+ # Returned so the replay path can join it — trace completions must be
335
+ # persisted before complete_replay builds the trace-ID mapping.
336
+ completion_thread
321
337
  end
322
338
 
323
339
  def send_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
data/lib/bitfab/replay.rb CHANGED
@@ -22,8 +22,12 @@ module Bitfab
22
22
 
23
23
  # Execute a block with replay context set on the current thread.
24
24
  # The context is automatically cleared when the block completes.
25
+ #
26
+ # pending_persistence, when given, collects the root span's persistence
27
+ # threads (span uploads + trace completion) so the replay runner can join
28
+ # them before complete_replay builds the trace-ID mapping.
25
29
  def with_context(test_run_id:, input_source_span_id: nil, input_source_trace_id: nil, trace_id: nil,
26
- mock_tree: nil, mock_strategy: nil)
30
+ mock_tree: nil, mock_strategy: nil, pending_persistence: nil)
27
31
  previous = Thread.current[REPLAY_CONTEXT_KEY]
28
32
  ctx = {
29
33
  test_run_id:,
@@ -31,6 +35,7 @@ module Bitfab
31
35
  input_source_trace_id:,
32
36
  trace_id:
33
37
  }
38
+ ctx[:pending_persistence] = pending_persistence if pending_persistence
34
39
  if mock_tree
35
40
  ctx[:mock_tree] = mock_tree
36
41
  ctx[:mock_strategy] = mock_strategy || "none"
@@ -111,17 +116,63 @@ module Bitfab
111
116
  []
112
117
  end
113
118
 
114
- Bitfab.flush_traces
115
-
116
- begin
117
- complete_response = http_client.complete_replay(test_run_id)
118
- trace_id_map = complete_response&.dig("traceIds") || {}
119
+ # Every item joined its own trace-persistence threads (span uploads +
120
+ # completion) in execute_item, so all replay traces are on the server
121
+ # by now — no flush needed, and complete_replay's trace-ID mapping is
122
+ # deterministic. complete_replay failures propagate: a missing mapping
123
+ # means verdicts can't be persisted, which callers must hear about
124
+ # loudly.
125
+ complete_response = http_client.complete_replay(test_run_id)
126
+ trace_id_map = complete_response&.dig("traceIds")
127
+
128
+ if trace_id_map.nil?
129
+ # Older servers don't return the mapping. Preserve the legacy
130
+ # nil-trace_id behavior but say why.
131
+ warn "Bitfab: server did not return replay trace IDs; item trace_id " \
132
+ "will be nil (server upgrade required for verdict persistence)"
133
+ result_items.each { |item| item[:trace_id] = nil }
134
+ else
135
+ # Map each item's locally-generated trace ID to the server's trace
136
+ # row ID. A completed item with no mapping means its trace was sent
137
+ # but the server has no record — a nil trace_id blocks verdict
138
+ # persistence and the Studio experiments view downstream, so this
139
+ # must never be silent.
140
+ #
141
+ # Severity splits on scope:
142
+ # - ALL completed items missing: systemic (the replayed method is
143
+ # not traced, or uploads are wholesale broken). Raise; the run's
144
+ # results are unusable for persistence.
145
+ # - SOME completed items missing: per-item upload failure (transient
146
+ # network blip, one oversized payload). Nil those items and warn
147
+ # loudly, but return the run so callers can persist verdicts for
148
+ # the items that landed.
149
+ missing = []
150
+ completed_count = 0
119
151
  result_items.each do |item|
120
- item[:trace_id] = trace_id_map[item[:trace_id]]
152
+ next unless item[:trace_id]
153
+
154
+ mapped = trace_id_map[item[:trace_id]]
155
+ if item[:error].nil?
156
+ completed_count += 1
157
+ missing << item[:trace_id] if mapped.nil?
158
+ end
159
+ item[:trace_id] = mapped
160
+ end
161
+ if missing.any?
162
+ trace_count = complete_response["traceCount"]
163
+ server_count = trace_count.nil? ? "" : " The server persisted #{trace_count} trace(s) for this run."
164
+ if missing.length == completed_count
165
+ raise "Replay completed but the server has no persisted trace for " \
166
+ "any of the #{completed_count} completed item(s) " \
167
+ "(test_run_id #{test_run_id}).#{server_count} Trace uploads were " \
168
+ "joined, so either the uploads failed or the replayed method is " \
169
+ "not traced (no root span was emitted)."
170
+ end
171
+ warn "Bitfab: server has no persisted trace for #{missing.length} of " \
172
+ "#{completed_count} completed replay item(s) " \
173
+ "(test_run_id #{test_run_id}).#{server_count} Their trace_id is nil " \
174
+ "and verdicts cannot be persisted for them. Missing: #{missing.join(", ")}"
121
175
  end
122
- rescue => e
123
- warn "Bitfab: Failed to complete replay: #{e.message}"
124
- result_items.each { |item| item[:trace_id] = nil }
125
176
  end
126
177
 
127
178
  {
@@ -286,6 +337,11 @@ module Bitfab
286
337
  fn_result = nil
287
338
  fn_error = nil
288
339
  sdk_trace_id = SecureRandom.uuid
340
+ # Collects the root span's persistence threads (span uploads + trace
341
+ # completion). Joined below so this item's trace is on the server
342
+ # before run() calls complete_replay — otherwise the server's trace-ID
343
+ # mapping races the uploads and the item's trace_id comes back nil.
344
+ pending_persistence = []
289
345
 
290
346
  ReplayContext.with_context(
291
347
  test_run_id:,
@@ -293,7 +349,8 @@ module Bitfab
293
349
  input_source_trace_id:,
294
350
  trace_id: sdk_trace_id,
295
351
  mock_tree:,
296
- mock_strategy:
352
+ mock_strategy:,
353
+ pending_persistence:
297
354
  ) do
298
355
  fn_result = if kwargs.empty?
299
356
  receiver.send(method_name, *args)
@@ -304,6 +361,12 @@ module Bitfab
304
361
  fn_error = e.message
305
362
  end
306
363
 
364
+ # Wait for this item's trace (spans + completion) to be fully persisted
365
+ # before the item resolves. Runs on the error path too — a raising
366
+ # method still emits a root span whose trace must land before
367
+ # complete_replay. Joins are bounded by the HTTP layer's own timeouts.
368
+ pending_persistence.each(&:join)
369
+
307
370
  {
308
371
  input: args,
309
372
  result: fn_result,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.14.0"
4
+ VERSION = "0.15.0"
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.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team