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.
- checksums.yaml +5 -5
- data/lib/airbrake-ruby/async_sender.rb +22 -96
- data/lib/airbrake-ruby/backtrace.rb +8 -7
- data/lib/airbrake-ruby/benchmark.rb +39 -0
- data/lib/airbrake-ruby/code_hunk.rb +1 -1
- data/lib/airbrake-ruby/config/processor.rb +84 -0
- data/lib/airbrake-ruby/config/validator.rb +9 -3
- data/lib/airbrake-ruby/config.rb +76 -20
- data/lib/airbrake-ruby/deploy_notifier.rb +1 -1
- data/lib/airbrake-ruby/file_cache.rb +6 -0
- data/lib/airbrake-ruby/filter_chain.rb +16 -1
- data/lib/airbrake-ruby/filters/dependency_filter.rb +1 -0
- data/lib/airbrake-ruby/filters/exception_attributes_filter.rb +2 -2
- data/lib/airbrake-ruby/filters/gem_root_filter.rb +1 -0
- data/lib/airbrake-ruby/filters/git_last_checkout_filter.rb +5 -5
- data/lib/airbrake-ruby/filters/git_repository_filter.rb +3 -0
- data/lib/airbrake-ruby/filters/git_revision_filter.rb +2 -0
- data/lib/airbrake-ruby/filters/{keys_whitelist.rb → keys_allowlist.rb} +3 -3
- data/lib/airbrake-ruby/filters/{keys_blacklist.rb → keys_blocklist.rb} +3 -3
- data/lib/airbrake-ruby/filters/keys_filter.rb +39 -20
- data/lib/airbrake-ruby/filters/root_directory_filter.rb +1 -0
- data/lib/airbrake-ruby/filters/sql_filter.rb +30 -6
- data/lib/airbrake-ruby/filters/system_exit_filter.rb +1 -0
- data/lib/airbrake-ruby/filters/thread_filter.rb +4 -2
- data/lib/airbrake-ruby/grouppable.rb +12 -0
- data/lib/airbrake-ruby/ignorable.rb +1 -0
- data/lib/airbrake-ruby/inspectable.rb +2 -2
- data/lib/airbrake-ruby/loggable.rb +2 -2
- data/lib/airbrake-ruby/mergeable.rb +12 -0
- data/lib/airbrake-ruby/monotonic_time.rb +48 -0
- data/lib/airbrake-ruby/notice.rb +10 -20
- data/lib/airbrake-ruby/notice_notifier.rb +23 -42
- data/lib/airbrake-ruby/performance_breakdown.rb +52 -0
- data/lib/airbrake-ruby/performance_notifier.rb +126 -49
- data/lib/airbrake-ruby/promise.rb +1 -0
- data/lib/airbrake-ruby/query.rb +26 -11
- data/lib/airbrake-ruby/queue.rb +65 -0
- data/lib/airbrake-ruby/remote_settings/settings_data.rb +120 -0
- data/lib/airbrake-ruby/remote_settings.rb +145 -0
- data/lib/airbrake-ruby/request.rb +20 -6
- data/lib/airbrake-ruby/stashable.rb +15 -0
- data/lib/airbrake-ruby/stat.rb +34 -24
- data/lib/airbrake-ruby/sync_sender.rb +3 -2
- data/lib/airbrake-ruby/tdigest.rb +43 -58
- data/lib/airbrake-ruby/thread_pool.rb +138 -0
- data/lib/airbrake-ruby/timed_trace.rb +58 -0
- data/lib/airbrake-ruby/truncator.rb +10 -4
- data/lib/airbrake-ruby/version.rb +11 -1
- data/lib/airbrake-ruby.rb +219 -53
- data/spec/airbrake_spec.rb +428 -9
- data/spec/async_sender_spec.rb +26 -110
- data/spec/backtrace_spec.rb +44 -44
- data/spec/benchmark_spec.rb +33 -0
- data/spec/code_hunk_spec.rb +11 -11
- data/spec/config/processor_spec.rb +209 -0
- data/spec/config/validator_spec.rb +23 -6
- data/spec/config_spec.rb +77 -7
- data/spec/deploy_notifier_spec.rb +2 -2
- data/spec/{file_cache.rb → file_cache_spec.rb} +2 -4
- data/spec/filter_chain_spec.rb +28 -1
- data/spec/filters/dependency_filter_spec.rb +1 -1
- data/spec/filters/gem_root_filter_spec.rb +9 -9
- data/spec/filters/git_last_checkout_filter_spec.rb +21 -4
- data/spec/filters/git_repository_filter.rb +1 -1
- data/spec/filters/git_revision_filter_spec.rb +13 -11
- data/spec/filters/{keys_whitelist_spec.rb → keys_allowlist_spec.rb} +29 -28
- data/spec/filters/{keys_blacklist_spec.rb → keys_blocklist_spec.rb} +39 -29
- data/spec/filters/root_directory_filter_spec.rb +9 -9
- data/spec/filters/sql_filter_spec.rb +110 -55
- data/spec/filters/system_exit_filter_spec.rb +1 -1
- data/spec/filters/thread_filter_spec.rb +33 -31
- data/spec/fixtures/project_root/code.rb +9 -9
- data/spec/loggable_spec.rb +17 -0
- data/spec/monotonic_time_spec.rb +23 -0
- data/spec/{notice_notifier_spec → notice_notifier}/options_spec.rb +19 -21
- data/spec/notice_notifier_spec.rb +20 -80
- data/spec/notice_spec.rb +9 -11
- data/spec/performance_breakdown_spec.rb +11 -0
- data/spec/performance_notifier_spec.rb +360 -85
- data/spec/query_spec.rb +11 -0
- data/spec/queue_spec.rb +18 -0
- data/spec/remote_settings/settings_data_spec.rb +365 -0
- data/spec/remote_settings_spec.rb +230 -0
- data/spec/request_spec.rb +9 -0
- data/spec/response_spec.rb +8 -8
- data/spec/spec_helper.rb +9 -13
- data/spec/stashable_spec.rb +23 -0
- data/spec/stat_spec.rb +17 -15
- data/spec/sync_sender_spec.rb +14 -12
- data/spec/tdigest_spec.rb +6 -6
- data/spec/thread_pool_spec.rb +187 -0
- data/spec/timed_trace_spec.rb +125 -0
- data/spec/truncator_spec.rb +12 -12
- 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
|
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
|
-
|
16
|
-
|
20
|
+
timing: nil,
|
21
|
+
time: Time.now
|
17
22
|
)
|
18
|
-
|
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
|
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' =>
|
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
|
data/lib/airbrake-ruby/stat.rb
CHANGED
@@ -9,48 +9,58 @@ module Airbrake
|
|
9
9
|
#
|
10
10
|
# @example
|
11
11
|
# stat = Airbrake::Stat.new
|
12
|
-
# stat.
|
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
|
17
|
-
|
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(
|
23
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
39
|
-
# and +start_time+.
|
44
|
+
# Increments tdigest timings and updates tdigest with given +ms+ value.
|
40
45
|
#
|
41
|
-
# @param [
|
42
|
-
# @param [Date] end_time
|
46
|
+
# @param [Float] ms
|
43
47
|
# @return [void]
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
+
def increment_ms(ms)
|
49
|
+
@mutex.synchronize do
|
50
|
+
self.sum += ms
|
51
|
+
self.sumsq += ms * ms
|
48
52
|
|
49
|
-
|
50
|
-
|
51
|
-
|
53
|
+
tdigest.push(ms)
|
54
|
+
end
|
55
|
+
end
|
52
56
|
|
53
|
-
|
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.
|
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::
|
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
|
-
@
|
46
|
-
@
|
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
|
63
|
-
output += @centroids.map
|
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 |
|
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 |
|
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
|
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
|
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
|
-
|
150
|
-
|
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 (
|
153
|
-
|
149
|
+
if (lower_key - x).abs < (upper_key - x).abs
|
150
|
+
lower
|
154
151
|
else
|
155
|
-
|
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 / @
|
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 = @
|
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
|
-
@
|
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.
|
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(
|
311
|
-
|
312
|
-
|
313
|
-
|
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
|
-
|
316
|
-
nearest.mean_cumn += n / 2.0
|
317
|
-
nearest.n += n
|
309
|
+
_cumulate(false, true) if centroid.mean_cumn.nil?
|
318
310
|
|
319
|
-
|
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
|
-
(@
|
322
|
+
(@size.to_f / @last_cumulate)
|
329
323
|
end
|
330
|
-
return if @
|
324
|
+
return if @size == @last_cumulate || (!exact && @cx && @cx > factor)
|
331
325
|
end
|
332
326
|
|
333
327
|
cumn = 0
|
334
|
-
@centroids.
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
351
|
+
@centroids[x] = Centroid.new(x, n, 0)
|
361
352
|
elsif nearest == max
|
362
|
-
|
353
|
+
@centroids[x] = Centroid.new(x, n, @size)
|
363
354
|
else
|
364
|
-
p = nearest.mean_cumn.to_f / @
|
365
|
-
max_n = (4 * @
|
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
|
-
|
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
|