airbrake-ruby 4.1.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +5 -5
  2. data/lib/airbrake-ruby/async_sender.rb +22 -96
  3. data/lib/airbrake-ruby/backtrace.rb +8 -7
  4. data/lib/airbrake-ruby/benchmark.rb +39 -0
  5. data/lib/airbrake-ruby/code_hunk.rb +1 -1
  6. data/lib/airbrake-ruby/config/processor.rb +84 -0
  7. data/lib/airbrake-ruby/config/validator.rb +9 -3
  8. data/lib/airbrake-ruby/config.rb +76 -20
  9. data/lib/airbrake-ruby/deploy_notifier.rb +1 -1
  10. data/lib/airbrake-ruby/file_cache.rb +6 -0
  11. data/lib/airbrake-ruby/filter_chain.rb +16 -1
  12. data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
  13. data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +2 -2
  14. data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
  15. data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +5 -5
  16. data/lib/airbrake-ruby/filters/git_repository_filter.rb +3 -0
  17. data/lib/airbrake-ruby/filters/git_revision_filter.rb +2 -0
  18. data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
  19. data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
  20. data/lib/airbrake-ruby/filters/keys_filter.rb +39 -20
  21. data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
  22. data/lib/airbrake-ruby/filters/sql_filter.rb +30 -6
  23. data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
  24. data/lib/airbrake-ruby/filters/thread_filter.rb +4 -2
  25. data/lib/airbrake-ruby/grouppable.rb +12 -0
  26. data/lib/airbrake-ruby/ignorable.rb +1 -0
  27. data/lib/airbrake-ruby/inspectable.rb +2 -2
  28. data/lib/airbrake-ruby/loggable.rb +2 -2
  29. data/lib/airbrake-ruby/mergeable.rb +12 -0
  30. data/lib/airbrake-ruby/monotonic_time.rb +48 -0
  31. data/lib/airbrake-ruby/notice.rb +10 -20
  32. data/lib/airbrake-ruby/notice_notifier.rb +23 -42
  33. data/lib/airbrake-ruby/performance_breakdown.rb +52 -0
  34. data/lib/airbrake-ruby/performance_notifier.rb +126 -49
  35. data/lib/airbrake-ruby/promise.rb +1 -0
  36. data/lib/airbrake-ruby/query.rb +26 -11
  37. data/lib/airbrake-ruby/queue.rb +65 -0
  38. data/lib/airbrake-ruby/remote_settings/settings_data.rb +120 -0
  39. data/lib/airbrake-ruby/remote_settings.rb +145 -0
  40. data/lib/airbrake-ruby/request.rb +20 -6
  41. data/lib/airbrake-ruby/stashable.rb +15 -0
  42. data/lib/airbrake-ruby/stat.rb +34 -24
  43. data/lib/airbrake-ruby/sync_sender.rb +3 -2
  44. data/lib/airbrake-ruby/tdigest.rb +43 -58
  45. data/lib/airbrake-ruby/thread_pool.rb +138 -0
  46. data/lib/airbrake-ruby/timed_trace.rb +58 -0
  47. data/lib/airbrake-ruby/truncator.rb +10 -4
  48. data/lib/airbrake-ruby/version.rb +11 -1
  49. data/lib/airbrake-ruby.rb +219 -53
  50. data/spec/airbrake_spec.rb +428 -9
  51. data/spec/async_sender_spec.rb +26 -110
  52. data/spec/backtrace_spec.rb +44 -44
  53. data/spec/benchmark_spec.rb +33 -0
  54. data/spec/code_hunk_spec.rb +11 -11
  55. data/spec/config/processor_spec.rb +209 -0
  56. data/spec/config/validator_spec.rb +23 -6
  57. data/spec/config_spec.rb +77 -7
  58. data/spec/deploy_notifier_spec.rb +2 -2
  59. data/spec/{file_cache.rb → file_cache_spec.rb} +2 -4
  60. data/spec/filter_chain_spec.rb +28 -1
  61. data/spec/filters/dependency_filter_spec.rb +1 -1
  62. data/spec/filters/gem_root_filter_spec.rb +9 -9
  63. data/spec/filters/git_last_checkout_filter_spec.rb +21 -4
  64. data/spec/filters/git_repository_filter.rb +1 -1
  65. data/spec/filters/git_revision_filter_spec.rb +13 -11
  66. data/spec/filters/{keys_whitelist_spec.rb → keys_allowlist_spec.rb} +29 -28
  67. data/spec/filters/{keys_blacklist_spec.rb → keys_blocklist_spec.rb} +39 -29
  68. data/spec/filters/root_directory_filter_spec.rb +9 -9
  69. data/spec/filters/sql_filter_spec.rb +110 -55
  70. data/spec/filters/system_exit_filter_spec.rb +1 -1
  71. data/spec/filters/thread_filter_spec.rb +33 -31
  72. data/spec/fixtures/project_root/code.rb +9 -9
  73. data/spec/loggable_spec.rb +17 -0
  74. data/spec/monotonic_time_spec.rb +23 -0
  75. data/spec/{notice_notifier_spec → notice_notifier}/options_spec.rb +19 -21
  76. data/spec/notice_notifier_spec.rb +20 -80
  77. data/spec/notice_spec.rb +9 -11
  78. data/spec/performance_breakdown_spec.rb +11 -0
  79. data/spec/performance_notifier_spec.rb +360 -85
  80. data/spec/query_spec.rb +11 -0
  81. data/spec/queue_spec.rb +18 -0
  82. data/spec/remote_settings/settings_data_spec.rb +365 -0
  83. data/spec/remote_settings_spec.rb +230 -0
  84. data/spec/request_spec.rb +9 -0
  85. data/spec/response_spec.rb +8 -8
  86. data/spec/spec_helper.rb +9 -13
  87. data/spec/stashable_spec.rb +23 -0
  88. data/spec/stat_spec.rb +17 -15
  89. data/spec/sync_sender_spec.rb +14 -12
  90. data/spec/tdigest_spec.rb +6 -6
  91. data/spec/thread_pool_spec.rb +187 -0
  92. data/spec/timed_trace_spec.rb +125 -0
  93. data/spec/truncator_spec.rb +12 -12
  94. metadata +55 -18
@@ -0,0 +1,145 @@
1
+ module Airbrake
2
+ # RemoteSettings polls the remote config of the passed project at fixed
3
+ # intervals. The fetched config is yielded as a callback parameter so that the
4
+ # invoker can define read config values.
5
+ #
6
+ # @example Disable/enable error notifications based on the remote value
7
+ # RemoteSettings.poll do |data|
8
+ # config.error_notifications = data.error_notifications?
9
+ # end
10
+ #
11
+ # When {#poll} is called, it will try to load remote settings from disk, so
12
+ # that it doesn't wait on the result from the API call.
13
+ #
14
+ # When {#stop_polling} is called, the current config will be dumped to disk.
15
+ #
16
+ # @since 5.0.0
17
+ # @api private
18
+ class RemoteSettings
19
+ include Airbrake::Loggable
20
+
21
+ # @return [String] the path to the persistent config
22
+ CONFIG_DUMP_PATH = File.join(
23
+ File.expand_path(__dir__),
24
+ '../../config/config.json',
25
+ ).freeze
26
+
27
+ # @return [Hash{Symbol=>String}] metadata to be attached to every GET
28
+ # request
29
+ QUERY_PARAMS = URI.encode_www_form(
30
+ notifier_name: Airbrake::NOTIFIER_INFO[:name],
31
+ notifier_version: Airbrake::NOTIFIER_INFO[:version],
32
+ os: RUBY_PLATFORM,
33
+ language: "#{RUBY_ENGINE}/#{RUBY_VERSION}".freeze,
34
+ ).freeze
35
+
36
+ # Polls remote config of the given project.
37
+ #
38
+ # @param [Integer] project_id
39
+ # @param [String] host
40
+ # @yield [data]
41
+ # @yieldparam data [Airbrake::RemoteSettings::SettingsData]
42
+ # @return [Airbrake::RemoteSettings]
43
+ def self.poll(project_id, host, &block)
44
+ new(project_id, host, &block).poll
45
+ end
46
+
47
+ # @param [Integer] project_id
48
+ # @yield [data]
49
+ # @yieldparam data [Airbrake::RemoteSettings::SettingsData]
50
+ def initialize(project_id, host, &block)
51
+ @data = SettingsData.new(project_id, {})
52
+ @host = host
53
+ @block = block
54
+ @poll = nil
55
+ end
56
+
57
+ # Polls remote config of the given project in background. Loads local config
58
+ # first (if exists).
59
+ #
60
+ # @return [self]
61
+ def poll
62
+ @poll ||= Thread.new do
63
+ begin
64
+ load_config
65
+ rescue StandardError => ex
66
+ logger.error("#{LOG_LABEL} config loading failed: #{ex}")
67
+ end
68
+
69
+ @block.call(@data)
70
+
71
+ loop do
72
+ @block.call(@data.merge!(fetch_config))
73
+ sleep(@data.interval)
74
+ end
75
+ end
76
+
77
+ self
78
+ end
79
+
80
+ # Stops the background poller thread. Dumps current config to disk.
81
+ #
82
+ # @return [void]
83
+ def stop_polling
84
+ @poll.kill if @poll
85
+
86
+ begin
87
+ dump_config
88
+ rescue StandardError => ex
89
+ logger.error("#{LOG_LABEL} config dumping failed: #{ex}")
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def fetch_config
96
+ response = nil
97
+ begin
98
+ response = Net::HTTP.get(build_config_uri)
99
+ rescue StandardError => ex
100
+ logger.error(ex)
101
+ return {}
102
+ end
103
+
104
+ # AWS S3 API returns XML when request is not valid. In this case we just
105
+ # print the returned body and exit the method.
106
+ if response.start_with?('<?xml ')
107
+ logger.error(response)
108
+ return {}
109
+ end
110
+
111
+ json = nil
112
+ begin
113
+ json = JSON.parse(response)
114
+ rescue JSON::ParserError => ex
115
+ logger.error(ex)
116
+ return {}
117
+ end
118
+
119
+ json
120
+ end
121
+
122
+ def build_config_uri
123
+ uri = URI(@data.config_route(@host))
124
+ uri.query = QUERY_PARAMS
125
+ uri
126
+ end
127
+
128
+ def load_config
129
+ config_dir = File.dirname(CONFIG_DUMP_PATH)
130
+ Dir.mkdir(config_dir) unless File.directory?(config_dir)
131
+
132
+ return unless File.exist?(CONFIG_DUMP_PATH)
133
+
134
+ config = File.read(CONFIG_DUMP_PATH)
135
+ @data.merge!(JSON.parse(config))
136
+ end
137
+
138
+ def dump_config
139
+ config_dir = File.dirname(CONFIG_DUMP_PATH)
140
+ Dir.mkdir(config_dir) unless File.directory?(config_dir)
141
+
142
+ File.write(CONFIG_DUMP_PATH, JSON.dump(@data.to_h))
143
+ end
144
+ end
145
+ end
@@ -4,21 +4,35 @@ module Airbrake
4
4
  # @see Airbrake.notify_request
5
5
  # @api public
6
6
  # @since v3.2.0
7
- Request = Struct.new(:method, :route, :status_code, :start_time, :end_time) do
7
+ class Request
8
8
  include HashKeyable
9
9
  include Ignorable
10
+ include Stashable
11
+ include Mergeable
12
+ include Grouppable
13
+
14
+ attr_accessor :method, :route, :status_code, :timing, :time
10
15
 
11
16
  def initialize(
12
17
  method:,
13
18
  route:,
14
19
  status_code:,
15
- start_time:,
16
- end_time: Time.now
20
+ timing: nil,
21
+ time: Time.now
17
22
  )
18
- super(method, route, status_code, start_time, end_time)
23
+ @time_utc = TimeTruncate.utc_truncate_minutes(time)
24
+ @method = method
25
+ @route = route
26
+ @status_code = status_code
27
+ @timing = timing
28
+ @time = time
29
+ end
30
+
31
+ def destination
32
+ 'routes-stats'
19
33
  end
20
34
 
21
- def name
35
+ def cargo
22
36
  'routes'
23
37
  end
24
38
 
@@ -27,7 +41,7 @@ module Airbrake
27
41
  'method' => method,
28
42
  'route' => route,
29
43
  'statusCode' => status_code,
30
- 'time' => TimeTruncate.utc_truncate_minutes(start_time)
44
+ 'time' => @time_utc,
31
45
  }.delete_if { |_key, val| val.nil? }
32
46
  end
33
47
  end
@@ -0,0 +1,15 @@
1
+ module Airbrake
2
+ # Stashable should be included in any class that wants the ability to stash
3
+ # arbitrary objects. It is mainly used by data objects that users can access
4
+ # through filters.
5
+ #
6
+ # @since v4.4.0
7
+ # @api private
8
+ module Stashable
9
+ # @return [Hash{Symbol=>Object}] the hash with arbitrary objects to be used
10
+ # in filters
11
+ def stash
12
+ @stash ||= {}
13
+ end
14
+ end
15
+ end
@@ -9,48 +9,58 @@ module Airbrake
9
9
  #
10
10
  # @example
11
11
  # stat = Airbrake::Stat.new
12
- # stat.increment(Time.now - 200)
12
+ # stat.increment_ms(2000)
13
13
  # stat.to_h # Pack and serialize data so it can be transmitted.
14
14
  #
15
15
  # @since v3.2.0
16
- Stat = Struct.new(:count, :sum, :sumsq, :tdigest) do
17
- # @param [Integer] count How many times this stat was incremented
16
+ class Stat
17
+ attr_accessor :sum, :sumsq, :tdigest
18
+
18
19
  # @param [Float] sum The sum of duration in milliseconds
19
20
  # @param [Float] sumsq The squared sum of duration in milliseconds
20
21
  # @param [TDigest::TDigest] tdigest Packed durations. By default,
21
22
  # compression is 20
22
- def initialize(count: 0, sum: 0.0, sumsq: 0.0, tdigest: TDigest.new(0.05))
23
- super(count, sum, sumsq, tdigest)
23
+ def initialize(sum: 0.0, sumsq: 0.0, tdigest: TDigest.new(0.05))
24
+ @sum = sum
25
+ @sumsq = sumsq
26
+ @tdigest = tdigest
27
+ @mutex = Mutex.new
24
28
  end
25
29
 
26
30
  # @return [Hash{String=>Object}] stats as a hash with compressed TDigest
27
31
  # (serialized as base64)
28
32
  def to_h
29
- tdigest.compress!
30
- {
31
- 'count' => count,
32
- 'sum' => sum,
33
- 'sumsq' => sumsq,
34
- 'tdigest' => Base64.strict_encode64(tdigest.as_small_bytes)
35
- }
33
+ @mutex.synchronize do
34
+ tdigest.compress!
35
+ {
36
+ 'count' => tdigest.size,
37
+ 'sum' => sum,
38
+ 'sumsq' => sumsq,
39
+ 'tdigest' => Base64.strict_encode64(tdigest.as_small_bytes),
40
+ }
41
+ end
36
42
  end
37
43
 
38
- # Increments count and updates performance with the difference of +end_time+
39
- # and +start_time+.
44
+ # Increments tdigest timings and updates tdigest with given +ms+ value.
40
45
  #
41
- # @param [Date] start_time
42
- # @param [Date] end_time
46
+ # @param [Float] ms
43
47
  # @return [void]
44
- def increment(start_time, end_time = nil)
45
- end_time ||= Time.new
46
-
47
- self.count += 1
48
+ def increment_ms(ms)
49
+ @mutex.synchronize do
50
+ self.sum += ms
51
+ self.sumsq += ms * ms
48
52
 
49
- ms = (end_time - start_time) * 1000
50
- self.sum += ms
51
- self.sumsq += ms * ms
53
+ tdigest.push(ms)
54
+ end
55
+ end
52
56
 
53
- tdigest.push(ms)
57
+ # We define custom inspect so that we weed out uninformative TDigest, which
58
+ # is also very slow to dump when we log Airbrake::Stat.
59
+ #
60
+ # @return [String]
61
+ def inspect
62
+ "#<struct Airbrake::Stat count=#{tdigest.size}, sum=#{sum}, sumsq=#{sumsq}>"
54
63
  end
64
+ alias pretty_print inspect
55
65
  end
56
66
  end
@@ -23,7 +23,7 @@ module Airbrake
23
23
  # @param [#to_json] data
24
24
  # @param [URI::HTTPS] endpoint
25
25
  # @return [Hash{String=>String}] the parsed HTTP response
26
- def send(data, promise, endpoint = @config.endpoint)
26
+ def send(data, promise, endpoint = @config.error_endpoint)
27
27
  return promise if rate_limited_ip?(promise)
28
28
 
29
29
  response = nil
@@ -47,6 +47,7 @@ module Airbrake
47
47
  end
48
48
 
49
49
  return promise.reject(parsed_resp['error']) if parsed_resp.key?('error')
50
+
50
51
  promise.resolve(parsed_resp)
51
52
  end
52
53
 
@@ -79,7 +80,7 @@ module Airbrake
79
80
  req['Authorization'] = "Bearer #{@config.project_key}"
80
81
  req['Content-Type'] = CONTENT_TYPE
81
82
  req['User-Agent'] =
82
- "#{Airbrake::Notice::NOTIFIER[:name]}/#{Airbrake::AIRBRAKE_RUBY_VERSION}" \
83
+ "#{Airbrake::NOTIFIER_INFO[:name]}/#{Airbrake::AIRBRAKE_RUBY_VERSION}" \
83
84
  " Ruby/#{RUBY_VERSION}"
84
85
 
85
86
  req
@@ -37,14 +37,15 @@ module Airbrake
37
37
  end
38
38
 
39
39
  attr_accessor :centroids
40
+ attr_reader :size
41
+
40
42
  def initialize(delta = 0.01, k = 25, cx = 1.1)
41
43
  @delta = delta
42
44
  @k = k
43
45
  @cx = cx
44
46
  @centroids = RBTree.new
45
- @nreset = 0
46
- @n = 0
47
- reset!
47
+ @size = 0
48
+ @last_cumulate = 0
48
49
  end
49
50
 
50
51
  def +(other)
@@ -59,8 +60,8 @@ module Airbrake
59
60
  # compression as defined by Java implementation
60
61
  size = @centroids.size
61
62
  output = [VERBOSE_ENCODING, compression, size]
62
- output += @centroids.map { |_, c| c.mean }
63
- output += @centroids.map { |_, c| c.n }
63
+ output += @centroids.each_value.map(&:mean)
64
+ output += @centroids.each_value.map(&:n)
64
65
  output.pack("NGNG#{size}N#{size}")
65
66
  end
66
67
 
@@ -70,14 +71,14 @@ module Airbrake
70
71
  output = [self.class::SMALL_ENCODING, compression, size]
71
72
  x = 0
72
73
  # delta encoding allows saving 4-bytes floats
73
- mean_arr = @centroids.map do |_, c|
74
+ mean_arr = @centroids.each_value.map do |c|
74
75
  val = c.mean - x
75
76
  x = c.mean
76
77
  val
77
78
  end
78
79
  output += mean_arr
79
80
  # Variable length encoding of numbers
80
- c_arr = @centroids.each_with_object([]) do |(_, c), arr|
81
+ c_arr = @centroids.each_value.each_with_object([]) do |c, arr|
81
82
  k = 0
82
83
  n = c.n
83
84
  while n < 0 || n > 0x7f
@@ -95,7 +96,7 @@ module Airbrake
95
96
  # rubocop:enable Metrics/AbcSize
96
97
 
97
98
  def as_json(_ = nil)
98
- @centroids.map { |_, c| c.as_json }
99
+ @centroids.each_value.map(&:as_json)
99
100
  end
100
101
 
101
102
  def bound_mean(x)
@@ -138,21 +139,17 @@ module Airbrake
138
139
  end
139
140
 
140
141
  def find_nearest(x)
141
- return nil if size == 0
142
-
143
- ceil = @centroids.upper_bound(x)
144
- floor = @centroids.lower_bound(x)
145
-
146
- return floor[1] if ceil.nil?
147
- return ceil[1] if floor.nil?
142
+ return if size == 0
148
143
 
149
- ceil_key = ceil[0]
150
- floor_key = floor[0]
144
+ upper_key, upper = @centroids.upper_bound(x)
145
+ lower_key, lower = @centroids.lower_bound(x)
146
+ return lower unless upper_key
147
+ return upper unless lower_key
151
148
 
152
- if (floor_key - x).abs < (ceil_key - x).abs
153
- floor[1]
149
+ if (lower_key - x).abs < (upper_key - x).abs
150
+ lower
154
151
  else
155
- ceil[1]
152
+ upper
156
153
  end
157
154
  end
158
155
 
@@ -186,7 +183,7 @@ module Airbrake
186
183
  mean_cumn += (item - lower.mean) * (upper.mean_cumn - lower.mean_cumn) \
187
184
  / (upper.mean - lower.mean)
188
185
  end
189
- mean_cumn / @n
186
+ mean_cumn / @size
190
187
  end
191
188
  end
192
189
  is_array ? x : x.first
@@ -203,11 +200,12 @@ module Airbrake
203
200
  unless (0..1).cover?(item)
204
201
  raise ArgumentError, "p should be in [0,1], got #{item}"
205
202
  end
203
+
206
204
  if size == 0
207
205
  nil
208
206
  else
209
207
  _cumulate(true)
210
- h = @n * item
208
+ h = @size * item
211
209
  lower, upper = bound_mean_cumn(h)
212
210
  if lower.nil? && upper.nil?
213
211
  nil
@@ -237,17 +235,12 @@ module Airbrake
237
235
 
238
236
  def reset!
239
237
  @centroids.clear
240
- @n = 0
241
- @nreset += 1
238
+ @size = 0
242
239
  @last_cumulate = 0
243
240
  end
244
241
 
245
- def size
246
- @n || 0
247
- end
248
-
249
242
  def to_a
250
- @centroids.map { |_, c| c }
243
+ @centroids.each_value.to_a
251
244
  end
252
245
 
253
246
  # rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength
@@ -279,6 +272,7 @@ module Airbrake
279
272
  shift = 7
280
273
  while (v & 0x80) != 0
281
274
  raise 'Shift too large in decode' if shift > 28
275
+
282
276
  v = counts_bytes.shift || 0
283
277
  z += (v & 0x7f) << shift
284
278
  shift += 7
@@ -307,16 +301,16 @@ module Airbrake
307
301
 
308
302
  private
309
303
 
310
- def _add_weight(nearest, x, n)
311
- nearest.mean += n * (x - nearest.mean) / (nearest.n + n) unless x == nearest.mean
312
-
313
- _cumulate(false, true) if nearest.mean_cumn.nil?
304
+ def _add_weight(centroid, x, n)
305
+ unless x == centroid.mean
306
+ centroid.mean += n * (x - centroid.mean) / (centroid.n + n)
307
+ end
314
308
 
315
- nearest.cumn += n
316
- nearest.mean_cumn += n / 2.0
317
- nearest.n += n
309
+ _cumulate(false, true) if centroid.mean_cumn.nil?
318
310
 
319
- nil
311
+ centroid.cumn += n
312
+ centroid.mean_cumn += n / 2.0
313
+ centroid.n += n
320
314
  end
321
315
 
322
316
  # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
@@ -325,17 +319,17 @@ module Airbrake
325
319
  factor = if @last_cumulate == 0
326
320
  Float::INFINITY
327
321
  else
328
- (@n.to_f / @last_cumulate)
322
+ (@size.to_f / @last_cumulate)
329
323
  end
330
- return if @n == @last_cumulate || (!exact && @cx && @cx > factor)
324
+ return if @size == @last_cumulate || (!exact && @cx && @cx > factor)
331
325
  end
332
326
 
333
327
  cumn = 0
334
- @centroids.each do |_, c|
328
+ @centroids.each_value do |c|
335
329
  c.mean_cumn = cumn + c.n / 2.0
336
330
  cumn = c.cumn = cumn + c.n
337
331
  end
338
- @n = @last_cumulate = cumn
332
+ @size = @last_cumulate = cumn
339
333
  nil
340
334
  end
341
335
  # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
@@ -345,28 +339,25 @@ module Airbrake
345
339
  def _digest(x, n)
346
340
  # Use 'first' and 'last' instead of min/max because of performance reasons
347
341
  # This works because RBTree is sorted
348
- min = @centroids.first
349
- max = @centroids.last
350
-
351
- min = min.nil? ? nil : min[1]
352
- max = max.nil? ? nil : max[1]
342
+ min = min.last if (min = @centroids.first)
343
+ max = max.last if (max = @centroids.last)
353
344
  nearest = find_nearest(x)
354
345
 
355
- @n += n
346
+ @size += n
356
347
 
357
348
  if nearest && nearest.mean == x
358
349
  _add_weight(nearest, x, n)
359
350
  elsif nearest == min
360
- _new_centroid(x, n, 0)
351
+ @centroids[x] = Centroid.new(x, n, 0)
361
352
  elsif nearest == max
362
- _new_centroid(x, n, @n)
353
+ @centroids[x] = Centroid.new(x, n, @size)
363
354
  else
364
- p = nearest.mean_cumn.to_f / @n
365
- max_n = (4 * @n * @delta * p * (1 - p)).floor
355
+ p = nearest.mean_cumn.to_f / @size
356
+ max_n = (4 * @size * @delta * p * (1 - p)).floor
366
357
  if max_n - nearest.n >= n
367
358
  _add_weight(nearest, x, n)
368
359
  else
369
- _new_centroid(x, n, nearest.cumn)
360
+ @centroids[x] = Centroid.new(x, n, nearest.cumn)
370
361
  end
371
362
  end
372
363
 
@@ -382,12 +373,6 @@ module Airbrake
382
373
  end
383
374
  # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity,
384
375
  # rubocop:enable Metrics/AbcSize
385
-
386
- def _new_centroid(x, n, cumn)
387
- c = Centroid.new(x, n, cumn)
388
- @centroids[x] = c
389
- c
390
- end
391
376
  end
392
377
  # rubocop:enable Metrics/ClassLength
393
378
  end
@@ -0,0 +1,138 @@
1
+ module Airbrake
2
+ # ThreadPool implements a simple thread pool that can configure the number of
3
+ # worker threads and the size of the queue to process.
4
+ #
5
+ # @example
6
+ # # Initialize a new thread pool with 5 workers and a queue size of 100. Set
7
+ # # the block to be run concurrently.
8
+ # thread_pool = ThreadPool.new(
9
+ # worker_size: 5,
10
+ # queue_size: 100,
11
+ # block: proc { |message| print "ECHO: #{message}..."}
12
+ # )
13
+ #
14
+ # # Send work.
15
+ # 10.times { |i| thread_pool << i }
16
+ # #=> ECHO: 0...ECHO: 1...ECHO: 2...
17
+ #
18
+ # @api private
19
+ # @since v4.6.1
20
+ class ThreadPool
21
+ include Loggable
22
+
23
+ # @return [ThreadGroup] the list of workers
24
+ # @note This is exposed for eaiser unit testing
25
+ attr_reader :workers
26
+
27
+ def initialize(worker_size:, queue_size:, block:)
28
+ @worker_size = worker_size
29
+ @queue_size = queue_size
30
+ @block = block
31
+
32
+ @queue = SizedQueue.new(queue_size)
33
+ @workers = ThreadGroup.new
34
+ @mutex = Mutex.new
35
+ @pid = nil
36
+ @closed = false
37
+
38
+ has_workers?
39
+ end
40
+
41
+ # Adds a new message to the thread pool. Rejects messages if the queue is at
42
+ # its capacity.
43
+ #
44
+ # @param [Object] message The message that gets passed to the block
45
+ # @return [Boolean] true if the message was successfully sent to the pool,
46
+ # false if the queue is full
47
+ def <<(message)
48
+ if backlog >= @queue_size
49
+ logger.error(
50
+ "#{LOG_LABEL} ThreadPool has reached its capacity of " \
51
+ "#{@queue_size} and the following message will not be " \
52
+ "processed: #{message.inspect}",
53
+ )
54
+ return false
55
+ end
56
+
57
+ @queue << message
58
+ true
59
+ end
60
+
61
+ # @return [Integer] how big the queue is at the moment
62
+ def backlog
63
+ @queue.size
64
+ end
65
+
66
+ # Checks if a thread pool has any workers. A thread pool doesn't have any
67
+ # workers only in two cases: when it was closed or when all workers
68
+ # crashed. An *active* thread pool doesn't have any workers only when
69
+ # something went wrong.
70
+ #
71
+ # Workers are expected to crash when you +fork+ the process the workers are
72
+ # living in. In this case we detect a +fork+ and try to revive them here.
73
+ #
74
+ # Another possible scenario that crashes workers is when you close the
75
+ # instance on +at_exit+, but some other +at_exit+ hook prevents the process
76
+ # from exiting.
77
+ #
78
+ # @return [Boolean] true if an instance wasn't closed, but has no workers
79
+ # @see https://goo.gl/oydz8h Example of at_exit that prevents exit
80
+ def has_workers?
81
+ @mutex.synchronize do
82
+ return false if @closed
83
+
84
+ if @pid != Process.pid && @workers.list.empty?
85
+ @pid = Process.pid
86
+ @workers = ThreadGroup.new
87
+ spawn_workers
88
+ end
89
+
90
+ !@closed && @workers.list.any?
91
+ end
92
+ end
93
+
94
+ # Closes the thread pool making it a no-op (it shut downs all worker
95
+ # threads). Before closing, waits on all unprocessed tasks to be processed.
96
+ #
97
+ # @return [void]
98
+ # @raise [Airbrake::Error] when invoked more than one time
99
+ def close
100
+ threads = @mutex.synchronize do
101
+ raise Airbrake::Error, 'this thread pool is closed already' if @closed
102
+
103
+ unless @queue.empty?
104
+ msg = "#{LOG_LABEL} waiting to process #{@queue.size} task(s)..."
105
+ logger.debug(msg + ' (Ctrl-C to abort)')
106
+ end
107
+
108
+ @worker_size.times { @queue << :stop }
109
+ @closed = true
110
+ @workers.list.dup
111
+ end
112
+
113
+ threads.each(&:join)
114
+ logger.debug("#{LOG_LABEL} thread pool closed")
115
+ end
116
+
117
+ def closed?
118
+ @closed
119
+ end
120
+
121
+ def spawn_workers
122
+ @worker_size.times { @workers.add(spawn_worker) }
123
+ @workers.enclose
124
+ end
125
+
126
+ private
127
+
128
+ def spawn_worker
129
+ Thread.new do
130
+ while (message = @queue.pop)
131
+ break if message == :stop
132
+
133
+ @block.call(message)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end