airbrake-ruby 4.1.0 → 5.0.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 +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
|