bitfab 0.75.0 → 0.75.2

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: 6976d07abea8e8c456185b0f5a76bbe861148e2a10baa1c3a65ac1a694b6993d
4
- data.tar.gz: 072af5081f4c15e748478dbd058a8cc4c249804b345dcc70ed47b3e47f3c5830
3
+ metadata.gz: 5b80995a3476aa0fd9dd0eb926924081189bd6dc85efbdf9fe5ad0a07f375dae
4
+ data.tar.gz: 409f0f9b9837f142aed5233a6307fb30740f5409955dd0d74af8c9d36e27ebc6
5
5
  SHA512:
6
- metadata.gz: 5189232282c64908bada35335eac364e16bb6fcadfe9b677cdc5b3330735f59dbdebc6ff71fe4a2984ad1a5c8386b45ade841710c0b2ec1ac0d26c0d07c7d44f
7
- data.tar.gz: 90b5fd0acb0d266e6f2e7d2b6d04dc4a08c66d3e1b25ce46f51425e5020cf372167c40ba141cf05c9f7e4f2348cc9a106f1888e9a5b792bbbe9a4f08d2bb0b80
6
+ metadata.gz: fe3ae22d0d2da90db167cf4d968cf2fe71bcaba899fa3393ab822a8cc7682961c0fcb8aa9bf842e0630cca46827fbfed433813e1a1b2f0bf191f3766bee708c8
7
+ data.tar.gz: 4490e80fdac9fbb0288177e158926e46e5c17f16889d11ce1b241579c98616bf144250a59def158ce771831edfaba80c046f9d2d78604b3079d582f495eb86c1
data/lib/bitfab/client.rb CHANGED
@@ -722,7 +722,8 @@ module Bitfab
722
722
  explicit_span_receiver:,
723
723
  explicit_span_method_name: explicit_span_method_name&.to_sym,
724
724
  surface:, recording_override:, content_withheld:,
725
- sends_on_completion: sends_on_completion
725
+ sends_on_completion: sends_on_completion,
726
+ trace_function_key:
726
727
  ) do
727
728
  invoke = -> { around_body ? around_body.call { yield } : yield }
728
729
  result = if explicit_span_receiver
@@ -742,7 +743,7 @@ module Bitfab
742
743
  end
743
744
  end
744
745
  rescue => e
745
- error = e.message
746
+ error = ErrorChain.describe(e)
746
747
  complete.call(result, error)
747
748
  raise
748
749
  end
@@ -769,7 +770,8 @@ module Bitfab
769
770
  surface:,
770
771
  around_body:,
771
772
  explicit_span_receiver:,
772
- explicit_span_method_name:
773
+ explicit_span_method_name:,
774
+ trace_function_key:
773
775
  )
774
776
  end
775
777
 
@@ -812,7 +814,7 @@ module Bitfab
812
814
  end
813
815
 
814
816
  def root_trace_function_key(trace_id, trace_function_key)
815
- TraceState.get(trace_id)&.dig(:trace_function_key) || trace_function_key
817
+ TraceState.trace_function_key(trace_id) || trace_function_key
816
818
  end
817
819
 
818
820
  def wait_for_simulation_plan
@@ -902,8 +904,10 @@ module Bitfab
902
904
  # span output. The span is sent exactly once: when iteration finishes,
903
905
  # raises, or the wrapper is `.close`d.
904
906
  def wrap_enumerator(source, trace_id:, span_id:, complete:, explicit_span_receiver: nil,
905
- explicit_span_method_name: nil, surface: nil, around_body: nil)
907
+ explicit_span_method_name: nil, surface: nil, around_body: nil, trace_function_key: nil)
906
908
  span_entry = {trace_id:, span_id:, surface:}
909
+ SpanContext.record_trace_function_keys(span_entry, trace_function_key)
910
+ trace_function_keys = span_entry[:trace_function_keys]
907
911
  if explicit_span_receiver
908
912
  span_entry[:explicit_span_receiver] = explicit_span_receiver
909
913
  span_entry[:explicit_span_method_name] = explicit_span_method_name&.to_sym
@@ -916,7 +920,7 @@ module Bitfab
916
920
  # the right parent. The fiber bridge carries the same parent into
917
921
  # Enumerator.new / enum_for source fibers on this thread.
918
922
  ReplayContext.with_span(trace_id:, span_id:) do
919
- SpanContext.with_fiber_bridge(trace_id:, span_id:, surface:) do
923
+ SpanContext.with_fiber_bridge(trace_id:, span_id:, surface:, trace_function_key:, trace_function_keys:) do
920
924
  SpanContext.stack.push(span_entry)
921
925
  begin
922
926
  iterate = lambda do
@@ -940,7 +944,7 @@ module Bitfab
940
944
  end
941
945
  complete.call(yielded, nil)
942
946
  rescue => e
943
- complete.call(yielded, e.message)
947
+ complete.call(yielded, ErrorChain.describe(e))
944
948
  raise
945
949
  ensure
946
950
  SpanContext.stack.pop
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitfab
4
+ # Reads the chain of causes below an exception. A database driver error
5
+ # usually reaches a traced method wrapped one or two levels down, so
6
+ # recording only the top message makes a connection timeout read as a broken
7
+ # query. Spans and replay results carry the deepest cause too.
8
+ module ErrorChain
9
+ # How many causes below an exception are followed.
10
+ MAX_DEPTH = 10
11
+ CODE_METHODS = %i[code sqlstate errno].freeze
12
+
13
+ module_function
14
+
15
+ # The causes below `error`, nearest first, stopping at the depth limit or
16
+ # at a cause already seen.
17
+ def causes(error)
18
+ found = []
19
+ seen = {error.object_id => true}
20
+ current = error
21
+ while found.length < MAX_DEPTH
22
+ cause = current.cause
23
+ break if cause.nil? || seen[cause.object_id]
24
+
25
+ found << cause
26
+ seen[cause.object_id] = true
27
+ current = cause
28
+ end
29
+ found
30
+ end
31
+
32
+ # The exception's message, followed by the deepest cause's message when it
33
+ # wraps another exception, e.g. `Failed query: ... (caused by: connection
34
+ # timeout)`. The cause is left off when the message already contains it.
35
+ def describe(error)
36
+ message = error.message
37
+ deepest = causes(error).last
38
+ return message if deepest.nil?
39
+
40
+ deepest_message = deepest.message.to_s.strip
41
+ return message if deepest_message.empty? || message.include?(deepest_message)
42
+
43
+ "#{message} (caused by: #{deepest_message})"
44
+ end
45
+
46
+ # An exception as a JSON record: type, message, backtrace, the driver's
47
+ # code when it has one, and its causes nested under `cause` down to the
48
+ # depth limit. The block adds fields a specific exception type carries.
49
+ def serialize(error, &extra)
50
+ serialize_at(error, 1, {}, &extra)
51
+ end
52
+
53
+ # `seen` is created fresh by `serialize` for each top-level call, so one
54
+ # call never skips a cause another call already recorded.
55
+ def serialize_at(error, depth, seen, &extra)
56
+ seen[error.object_id] = true
57
+ serialized = {type: error.class.name, message: error.message, backtrace: error.backtrace}
58
+ code = error_code(error)
59
+ serialized[:code] = code unless code.nil?
60
+ serialized.merge!(extra.call(error)) if extra
61
+ cause = error.cause
62
+ if cause && !seen[cause.object_id] && depth < MAX_DEPTH
63
+ serialized[:cause] = serialize_at(cause, depth + 1, seen, &extra)
64
+ end
65
+ serialized
66
+ end
67
+ private_class_method :serialize_at
68
+
69
+ def error_code(error)
70
+ CODE_METHODS.each do |name|
71
+ next unless error.respond_to?(name)
72
+
73
+ value = begin
74
+ error.public_send(name)
75
+ rescue
76
+ nil
77
+ end
78
+ return value if value.is_a?(String) || value.is_a?(Integer)
79
+ end
80
+ nil
81
+ end
82
+ end
83
+ end
data/lib/bitfab/replay.rb CHANGED
@@ -160,13 +160,9 @@ module Bitfab
160
160
  def json_safe(value)
161
161
  case value
162
162
  when Exception
163
- serialized = {type: value.class.name, message: value.message, backtrace: value.backtrace}
164
- if value.is_a?(DbBranchReplayError)
165
- serialized[:code] = value.code
166
- serialized[:original_trace_id] = value.original_trace_id
167
- serialized[:cause] = json_safe(value.cause) if value.cause
163
+ ErrorChain.serialize(value) do |error|
164
+ error.is_a?(DbBranchReplayError) ? {original_trace_id: error.original_trace_id} : {}
168
165
  end
169
- serialized
170
166
  when Hash
171
167
  value.to_h { |key, child| [key, json_safe(child)] }
172
168
  when Array
@@ -177,7 +173,7 @@ module Bitfab
177
173
  end
178
174
 
179
175
  def replay_item_error_message(error)
180
- return error.message unless error.is_a?(DbBranchReplayError)
176
+ return ErrorChain.describe(error) unless error.is_a?(DbBranchReplayError)
181
177
 
182
178
  "Replay requested a database branch for trace #{error.original_trace_id} but it " \
183
179
  "could not be resolved (#{error.code}): #{error.message}. The method was not run, " \
@@ -1528,7 +1524,7 @@ module Bitfab
1528
1524
  input: kwargs.empty? ? args : [*args, kwargs],
1529
1525
  result: fn_result,
1530
1526
  original_output: item["output"],
1531
- error: item_error&.message,
1527
+ error: item_error && ErrorChain.describe(item_error),
1532
1528
  trace_error: fn_error,
1533
1529
  replay_error:,
1534
1530
  duration_ms: replay_duration_ms,
@@ -19,6 +19,16 @@ module Bitfab
19
19
  @span_state[:trace_id]
20
20
  end
21
21
 
22
+ # The keys of every trace function running at this point, outermost first,
23
+ # each listed once. A trace function called inside another one comes after
24
+ # it, so a cost recorded here can be counted toward each of them. Empty
25
+ # outside a trace.
26
+ def trace_function_keys
27
+ span_keys = @span_state[:trace_function_keys] || []
28
+ root_keys = Subtree.active_sessions.reverse.map(&:trace_function_key).uniq
29
+ (root_keys - span_keys) + span_keys
30
+ end
31
+
22
32
  # Add a context entry to this span.
23
33
  # The entire hash is pushed as a single entry in the contexts array.
24
34
  # Context entries are accumulated - multiple calls add to the list.
@@ -53,6 +63,15 @@ module Bitfab
53
63
  @trace_id = trace_id
54
64
  end
55
65
 
66
+ # The key of the trace function the current trace is stored under (the one
67
+ # that started it). A bitfab_span or get_function call inside another trace
68
+ # function joins that trace, so it reports the outer key. A bitfab_trace
69
+ # root inside another root starts its own trace, so it reports its own key.
70
+ # Empty string outside a trace.
71
+ def trace_function_key
72
+ TraceState.trace_function_key(@trace_id) || ""
73
+ end
74
+
56
75
  # Set the session ID for this trace.
57
76
  # Session ID is used to group traces from the same user session.
58
77
  # This is stored as a database column.
@@ -121,7 +140,8 @@ module Bitfab
121
140
  end
122
141
 
123
142
  # Thread-local span stack for tracking nested spans.
124
- # Each entry is a Hash with :trace_id and :span_id keys.
143
+ # Each entry is a Hash with :trace_id and :span_id keys, plus the span's own
144
+ # :trace_function_key and the :trace_function_keys running at it.
125
145
  module SpanContext
126
146
  STACK_KEY = :__bitfab_span_stack
127
147
  FIBER_BRIDGE_STACK_KEY = :__bitfab_span_fiber_bridge_stack
@@ -139,8 +159,9 @@ module Bitfab
139
159
  # Enumerator.new and enum_for run their source body in another Fiber on
140
160
  # the same thread. Bridge only the active Enumerator parent across that
141
161
  # boundary; ordinary fiber-local span stacks remain isolated.
142
- def with_fiber_bridge(trace_id:, span_id:, surface: nil)
162
+ def with_fiber_bridge(trace_id:, span_id:, surface: nil, trace_function_key: nil, trace_function_keys: nil)
143
163
  entry = {trace_id:, span_id:}
164
+ record_trace_function_keys(entry, trace_function_key, trace_function_keys)
144
165
  entry[:surface] = surface if surface
145
166
  fiber_bridge_stack.push(entry)
146
167
  yield
@@ -150,8 +171,9 @@ module Bitfab
150
171
 
151
172
  # Execute a block with a new span pushed onto the stack.
152
173
  # 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, content_withheld: nil, sends_on_completion: nil)
174
+ def with_span(trace_id:, span_id:, explicit_span_receiver: nil, explicit_span_method_name: nil, surface: nil, recording_override: nil, content_withheld: nil, sends_on_completion: nil, trace_function_key: nil)
154
175
  entry = {trace_id:, span_id:}
176
+ record_trace_function_keys(entry, trace_function_key)
155
177
  entry[:sends_on_completion] = sends_on_completion if sends_on_completion
156
178
  entry[:surface] = surface if surface
157
179
  entry[:recording_override] = recording_override if recording_override
@@ -166,6 +188,22 @@ module Bitfab
166
188
  stack.pop
167
189
  end
168
190
 
191
+ # The trace function keys running at this point with the given key added
192
+ # at the end, each listed once. A nil key returns the running keys as is.
193
+ def trace_function_keys_with(trace_function_key)
194
+ running = current&.dig(:trace_function_keys) || []
195
+ trace_function_key ? running | [trace_function_key] : running
196
+ end
197
+
198
+ # Store a span's own trace function key and the keys running at that span
199
+ # on its stack entry, computing the running keys from the current span
200
+ # when they are not given. Nothing is stored when no key is running.
201
+ def record_trace_function_keys(entry, trace_function_key, trace_function_keys = nil)
202
+ entry[:trace_function_key] = trace_function_key if trace_function_key
203
+ keys = trace_function_keys || trace_function_keys_with(trace_function_key)
204
+ entry[:trace_function_keys] = keys unless keys.empty?
205
+ end
206
+
169
207
  def fiber_bridge_stack
170
208
  current_thread = Thread.current
171
209
  current_thread.thread_variable_get(FIBER_BRIDGE_STACK_KEY) || begin
@@ -208,6 +246,10 @@ module Bitfab
208
246
  end
209
247
  end
210
248
 
249
+ def trace_function_key(trace_id)
250
+ get(trace_id)&.dig(:trace_function_key)
251
+ end
252
+
211
253
  def delete(trace_id)
212
254
  @states_mutex.synchronize { @states.delete(trace_id) }
213
255
  end
@@ -740,7 +740,7 @@ module Bitfab
740
740
  return unless entry.is_a?(OpenSpan)
741
741
 
742
742
  exception = event.raised_exception
743
- entry.error = "#{exception.class}: #{exception.message}"
743
+ entry.error = "#{exception.class}: #{ErrorChain.describe(exception)}"
744
744
  end
745
745
 
746
746
  def clear_rescued_error(event)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.75.0"
4
+ VERSION = "0.75.2"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -5,6 +5,7 @@ require "json"
5
5
  require_relative "bitfab/version"
6
6
  require_relative "bitfab/constants"
7
7
  require_relative "bitfab/warn_once"
8
+ require_relative "bitfab/error_chain"
8
9
  require_relative "bitfab/experiment_id"
9
10
  require_relative "bitfab/payload_budget"
10
11
  require_relative "bitfab/serialize"
@@ -49,6 +50,10 @@ module Bitfab
49
50
  ""
50
51
  end
51
52
 
53
+ def trace_function_keys
54
+ []
55
+ end
56
+
52
57
  def add_context(_context)
53
58
  # No-op
54
59
  end
@@ -61,6 +66,10 @@ module Bitfab
61
66
  # No-op trace handle returned when outside a span context.
62
67
  # All methods do nothing, preventing crashes when called outside traced code.
63
68
  class NoOpCurrentTrace
69
+ def trace_function_key
70
+ ""
71
+ end
72
+
64
73
  def set_session_id(_session_id)
65
74
  # No-op
66
75
  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.75.0
4
+ version: 0.75.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -183,6 +183,7 @@ files:
183
183
  - lib/bitfab/db_snapshot.rb
184
184
  - lib/bitfab/detached_trace.rb
185
185
  - lib/bitfab/env.rb
186
+ - lib/bitfab/error_chain.rb
186
187
  - lib/bitfab/experiment_id.rb
187
188
  - lib/bitfab/experiments.rb
188
189
  - lib/bitfab/git_command.rb