celerbrake-ruby 0.1.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 +7 -0
- data/lib/celerbrake-ruby/async_sender.rb +57 -0
- data/lib/celerbrake-ruby/backlog.rb +123 -0
- data/lib/celerbrake-ruby/backtrace.rb +197 -0
- data/lib/celerbrake-ruby/benchmark.rb +39 -0
- data/lib/celerbrake-ruby/code_hunk.rb +51 -0
- data/lib/celerbrake-ruby/config/processor.rb +77 -0
- data/lib/celerbrake-ruby/config/validator.rb +97 -0
- data/lib/celerbrake-ruby/config.rb +291 -0
- data/lib/celerbrake-ruby/context.rb +51 -0
- data/lib/celerbrake-ruby/deploy_notifier.rb +36 -0
- data/lib/celerbrake-ruby/file_cache.rb +54 -0
- data/lib/celerbrake-ruby/filter_chain.rb +112 -0
- data/lib/celerbrake-ruby/filters/context_filter.rb +28 -0
- data/lib/celerbrake-ruby/filters/dependency_filter.rb +32 -0
- data/lib/celerbrake-ruby/filters/exception_attributes_filter.rb +46 -0
- data/lib/celerbrake-ruby/filters/gem_root_filter.rb +34 -0
- data/lib/celerbrake-ruby/filters/git_last_checkout_filter.rb +92 -0
- data/lib/celerbrake-ruby/filters/git_repository_filter.rb +73 -0
- data/lib/celerbrake-ruby/filters/git_revision_filter.rb +68 -0
- data/lib/celerbrake-ruby/filters/keys_allowlist.rb +48 -0
- data/lib/celerbrake-ruby/filters/keys_blocklist.rb +49 -0
- data/lib/celerbrake-ruby/filters/keys_filter.rb +159 -0
- data/lib/celerbrake-ruby/filters/root_directory_filter.rb +29 -0
- data/lib/celerbrake-ruby/filters/sql_filter.rb +128 -0
- data/lib/celerbrake-ruby/filters/system_exit_filter.rb +24 -0
- data/lib/celerbrake-ruby/filters/thread_filter.rb +93 -0
- data/lib/celerbrake-ruby/grouppable.rb +12 -0
- data/lib/celerbrake-ruby/hash_keyable.rb +37 -0
- data/lib/celerbrake-ruby/ignorable.rb +43 -0
- data/lib/celerbrake-ruby/inspectable.rb +39 -0
- data/lib/celerbrake-ruby/loggable.rb +34 -0
- data/lib/celerbrake-ruby/mergeable.rb +12 -0
- data/lib/celerbrake-ruby/monotonic_time.rb +48 -0
- data/lib/celerbrake-ruby/nested_exception.rb +59 -0
- data/lib/celerbrake-ruby/notice.rb +157 -0
- data/lib/celerbrake-ruby/notice_notifier.rb +142 -0
- data/lib/celerbrake-ruby/performance_breakdown.rb +52 -0
- data/lib/celerbrake-ruby/performance_notifier.rb +177 -0
- data/lib/celerbrake-ruby/promise.rb +110 -0
- data/lib/celerbrake-ruby/query.rb +59 -0
- data/lib/celerbrake-ruby/queue.rb +65 -0
- data/lib/celerbrake-ruby/remote_settings/callback.rb +44 -0
- data/lib/celerbrake-ruby/remote_settings/settings_data.rb +116 -0
- data/lib/celerbrake-ruby/remote_settings.rb +128 -0
- data/lib/celerbrake-ruby/request.rb +48 -0
- data/lib/celerbrake-ruby/response.rb +125 -0
- data/lib/celerbrake-ruby/stashable.rb +15 -0
- data/lib/celerbrake-ruby/stat.rb +66 -0
- data/lib/celerbrake-ruby/sync_sender.rb +145 -0
- data/lib/celerbrake-ruby/tdigest.rb +379 -0
- data/lib/celerbrake-ruby/thread_pool.rb +139 -0
- data/lib/celerbrake-ruby/time_truncate.rb +17 -0
- data/lib/celerbrake-ruby/timed_trace.rb +56 -0
- data/lib/celerbrake-ruby/truncator.rb +121 -0
- data/lib/celerbrake-ruby/version.rb +16 -0
- data/lib/celerbrake-ruby.rb +592 -0
- metadata +251 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
require 'rbtree'
|
|
2
|
+
|
|
3
|
+
module Celerbrake
|
|
4
|
+
# Ruby implementation of Ted Dunning's t-digest data structure.
|
|
5
|
+
#
|
|
6
|
+
# This implementation is imported from https://github.com/castle/tdigest with
|
|
7
|
+
# custom modifications. Huge thanks to Castle for the implementation :beer:
|
|
8
|
+
#
|
|
9
|
+
# The difference is that we pack with Big Endian (unlike Native Endian in
|
|
10
|
+
# Castle's version). Our backend does not permit little endian.
|
|
11
|
+
#
|
|
12
|
+
# @see https://github.com/tdunning/t-digest
|
|
13
|
+
# @see https://github.com/castle/tdigest
|
|
14
|
+
# @api private
|
|
15
|
+
# @since v3.2.0
|
|
16
|
+
#
|
|
17
|
+
# rubocop:disable Metrics/ClassLength
|
|
18
|
+
class TDigest
|
|
19
|
+
VERBOSE_ENCODING = 1
|
|
20
|
+
SMALL_ENCODING = 2
|
|
21
|
+
|
|
22
|
+
# Centroid represents a number of data points.
|
|
23
|
+
# @api private
|
|
24
|
+
# @since v3.2.0
|
|
25
|
+
class Centroid
|
|
26
|
+
attr_accessor :mean, :n, :cumn, :mean_cumn
|
|
27
|
+
|
|
28
|
+
def initialize(mean, n, cumn, mean_cumn = nil)
|
|
29
|
+
@mean = mean
|
|
30
|
+
@n = n
|
|
31
|
+
@cumn = cumn
|
|
32
|
+
@mean_cumn = mean_cumn
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def as_json(_ = nil)
|
|
36
|
+
{ m: mean, n: n }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attr_accessor :centroids
|
|
41
|
+
attr_reader :size
|
|
42
|
+
|
|
43
|
+
def initialize(delta = 0.01, k = 25, cx = 1.1)
|
|
44
|
+
@delta = delta
|
|
45
|
+
@k = k
|
|
46
|
+
@cx = cx
|
|
47
|
+
@centroids = RBTree.new
|
|
48
|
+
@size = 0
|
|
49
|
+
@last_cumulate = 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def +(other)
|
|
53
|
+
# Uses delta, k and cx from the caller
|
|
54
|
+
t = self.class.new(@delta, @k, @cx)
|
|
55
|
+
data = centroids.values + other.centroids.values
|
|
56
|
+
t.push_centroid(data.delete_at(rand(data.length))) while data.any?
|
|
57
|
+
t
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def as_bytes
|
|
61
|
+
# compression as defined by Java implementation
|
|
62
|
+
size = @centroids.size
|
|
63
|
+
output = [VERBOSE_ENCODING, compression, size]
|
|
64
|
+
output += @centroids.each_value.map(&:mean)
|
|
65
|
+
output += @centroids.each_value.map(&:n)
|
|
66
|
+
output.pack("NGNG#{size}N#{size}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# rubocop:disable Metrics/AbcSize
|
|
70
|
+
def as_small_bytes
|
|
71
|
+
size = @centroids.size
|
|
72
|
+
output = [self.class::SMALL_ENCODING, compression, size]
|
|
73
|
+
x = 0
|
|
74
|
+
# delta encoding allows saving 4-bytes floats
|
|
75
|
+
mean_arr = @centroids.each_value.map do |c|
|
|
76
|
+
val = c.mean - x
|
|
77
|
+
x = c.mean
|
|
78
|
+
val
|
|
79
|
+
end
|
|
80
|
+
output += mean_arr
|
|
81
|
+
# Variable length encoding of numbers
|
|
82
|
+
c_arr = @centroids.each_value.with_object([]) do |c, arr|
|
|
83
|
+
k = 0
|
|
84
|
+
n = c.n
|
|
85
|
+
while n < 0 || n > 0x7f
|
|
86
|
+
b = 0x80 | (0x7f & n)
|
|
87
|
+
arr << b
|
|
88
|
+
n = n >> 7
|
|
89
|
+
k += 1
|
|
90
|
+
raise 'Unreasonable large number' if k > 6
|
|
91
|
+
end
|
|
92
|
+
arr << n
|
|
93
|
+
end
|
|
94
|
+
output += c_arr
|
|
95
|
+
output.pack("NGNg#{size}C#{size}")
|
|
96
|
+
end
|
|
97
|
+
# rubocop:enable Metrics/AbcSize
|
|
98
|
+
|
|
99
|
+
def as_json(_ = nil)
|
|
100
|
+
@centroids.each_value.map(&:as_json)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def bound_mean(x)
|
|
104
|
+
upper = @centroids.upper_bound(x)
|
|
105
|
+
lower = @centroids.lower_bound(x)
|
|
106
|
+
[lower[1], upper[1]]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def bound_mean_cumn(cumn)
|
|
110
|
+
last_c = nil
|
|
111
|
+
bounds = []
|
|
112
|
+
@centroids.each_value do |v|
|
|
113
|
+
if v.mean_cumn == cumn
|
|
114
|
+
bounds << v
|
|
115
|
+
break
|
|
116
|
+
elsif v.mean_cumn > cumn
|
|
117
|
+
bounds << last_c
|
|
118
|
+
bounds << v
|
|
119
|
+
break
|
|
120
|
+
else
|
|
121
|
+
last_c = v
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
# If still no results, pick lagging value if any
|
|
125
|
+
bounds << last_c if bounds.empty? && !last_c.nil?
|
|
126
|
+
|
|
127
|
+
bounds
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def compress!
|
|
131
|
+
points = to_a
|
|
132
|
+
reset!
|
|
133
|
+
push_centroid(points.shuffle)
|
|
134
|
+
_cumulate(exact: true, force: true)
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def compression
|
|
139
|
+
1 / @delta
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def find_nearest(x)
|
|
143
|
+
return if size == 0
|
|
144
|
+
|
|
145
|
+
upper_key, upper = @centroids.upper_bound(x)
|
|
146
|
+
lower_key, lower = @centroids.lower_bound(x)
|
|
147
|
+
return lower unless upper_key
|
|
148
|
+
return upper unless lower_key
|
|
149
|
+
|
|
150
|
+
if (lower_key - x).abs < (upper_key - x).abs
|
|
151
|
+
lower
|
|
152
|
+
else
|
|
153
|
+
upper
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def merge!(other)
|
|
158
|
+
push_centroid(other.centroids.values.shuffle)
|
|
159
|
+
self
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
163
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
164
|
+
def p_rank(x)
|
|
165
|
+
is_array = x.is_a? Array
|
|
166
|
+
x = [x] unless is_array
|
|
167
|
+
|
|
168
|
+
min = @centroids.first
|
|
169
|
+
max = @centroids.last
|
|
170
|
+
|
|
171
|
+
x.map! do |item|
|
|
172
|
+
if size == 0
|
|
173
|
+
nil
|
|
174
|
+
elsif item < min[1].mean
|
|
175
|
+
0.0
|
|
176
|
+
elsif item > max[1].mean
|
|
177
|
+
1.0
|
|
178
|
+
else
|
|
179
|
+
_cumulate(exact: true)
|
|
180
|
+
bound = bound_mean(item)
|
|
181
|
+
lower, upper = bound
|
|
182
|
+
mean_cumn = lower.mean_cumn
|
|
183
|
+
if lower != upper
|
|
184
|
+
mean_cumn += (item - lower.mean) * (upper.mean_cumn - lower.mean_cumn) \
|
|
185
|
+
/ (upper.mean - lower.mean)
|
|
186
|
+
end
|
|
187
|
+
mean_cumn / @size
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
is_array ? x : x.first
|
|
191
|
+
end
|
|
192
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
193
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
194
|
+
|
|
195
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
196
|
+
# rubocop:disable Metrics/AbcSize
|
|
197
|
+
def percentile(p)
|
|
198
|
+
is_array = p.is_a? Array
|
|
199
|
+
p = [p] unless is_array
|
|
200
|
+
p.map! do |item|
|
|
201
|
+
unless (0..1).cover?(item)
|
|
202
|
+
raise ArgumentError, "p should be in [0,1], got #{item}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
if size == 0
|
|
206
|
+
nil
|
|
207
|
+
else
|
|
208
|
+
_cumulate(exact: true)
|
|
209
|
+
h = @size * item
|
|
210
|
+
lower, upper = bound_mean_cumn(h)
|
|
211
|
+
if lower.nil? && upper.nil?
|
|
212
|
+
nil
|
|
213
|
+
elsif upper == lower || lower.nil? || upper.nil?
|
|
214
|
+
(lower || upper).mean
|
|
215
|
+
elsif h == lower.mean_cumn
|
|
216
|
+
lower.mean
|
|
217
|
+
else
|
|
218
|
+
upper.mean
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
is_array ? p : p.first
|
|
223
|
+
end
|
|
224
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
225
|
+
# rubocop:enable Metrics/AbcSize
|
|
226
|
+
|
|
227
|
+
def push(x, n = 1)
|
|
228
|
+
x = [x] unless x.is_a? Array
|
|
229
|
+
x.each { |value| _digest(value, n) }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def push_centroid(c)
|
|
233
|
+
c = [c] unless c.is_a? Array
|
|
234
|
+
c.each { |centroid| _digest(centroid.mean, centroid.n) }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def reset!
|
|
238
|
+
@centroids.clear
|
|
239
|
+
@size = 0
|
|
240
|
+
@last_cumulate = 0
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def to_a
|
|
244
|
+
@centroids.each_value.to_a
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
248
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize
|
|
249
|
+
def self.from_bytes(bytes)
|
|
250
|
+
format, compression, size = bytes.unpack('NGN')
|
|
251
|
+
tdigest = new(1 / compression)
|
|
252
|
+
|
|
253
|
+
start_idx = 16 # after header
|
|
254
|
+
case format
|
|
255
|
+
when VERBOSE_ENCODING
|
|
256
|
+
array = bytes[start_idx..-1].unpack("G#{size}N#{size}")
|
|
257
|
+
means, counts = array.each_slice(size).to_a if array.any?
|
|
258
|
+
when SMALL_ENCODING
|
|
259
|
+
means = bytes[start_idx..(start_idx + (4 * size))].unpack("g#{size}")
|
|
260
|
+
# Decode delta encoding of means
|
|
261
|
+
x = 0
|
|
262
|
+
means.map! do |m|
|
|
263
|
+
m += x
|
|
264
|
+
x = m
|
|
265
|
+
m
|
|
266
|
+
end
|
|
267
|
+
counts_bytes = bytes[(start_idx + (4 * size))..-1].unpack('C*')
|
|
268
|
+
counts = []
|
|
269
|
+
# Decode variable length integer bytes
|
|
270
|
+
size.times do
|
|
271
|
+
v = counts_bytes.shift
|
|
272
|
+
z = 0x7f & v
|
|
273
|
+
shift = 7
|
|
274
|
+
while (v & 0x80) != 0
|
|
275
|
+
raise 'Shift too large in decode' if shift > 28
|
|
276
|
+
|
|
277
|
+
v = counts_bytes.shift || 0
|
|
278
|
+
z += (v & 0x7f) << shift
|
|
279
|
+
shift += 7
|
|
280
|
+
end
|
|
281
|
+
counts << z
|
|
282
|
+
end
|
|
283
|
+
# This shouldn't happen
|
|
284
|
+
raise 'Mismatch' unless counts.size == means.size
|
|
285
|
+
else
|
|
286
|
+
raise 'Unknown compression format'
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
means.zip(counts).each { |val| tdigest.push(val[0], val[1]) } if means && counts
|
|
290
|
+
|
|
291
|
+
tdigest
|
|
292
|
+
end
|
|
293
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/MethodLength
|
|
294
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/AbcSize
|
|
295
|
+
|
|
296
|
+
def self.from_json(array)
|
|
297
|
+
tdigest = new
|
|
298
|
+
# Handle both string and symbol keys
|
|
299
|
+
array.each { |a| tdigest.push(a['m'] || a[:m], a['n'] || a[:n]) }
|
|
300
|
+
tdigest
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
private
|
|
304
|
+
|
|
305
|
+
def _add_weight(centroid, x, n)
|
|
306
|
+
unless x == centroid.mean
|
|
307
|
+
centroid.mean += n * (x - centroid.mean) / (centroid.n + n)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
_cumulate(exact: false, force: true) if centroid.mean_cumn.nil?
|
|
311
|
+
|
|
312
|
+
centroid.cumn += n
|
|
313
|
+
centroid.mean_cumn += n / 2.0
|
|
314
|
+
centroid.n += n
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
318
|
+
def _cumulate(exact: false, force: false)
|
|
319
|
+
unless force
|
|
320
|
+
factor = if @last_cumulate == 0
|
|
321
|
+
Float::INFINITY
|
|
322
|
+
else
|
|
323
|
+
(@size.to_f / @last_cumulate)
|
|
324
|
+
end
|
|
325
|
+
return if @size == @last_cumulate || (!exact && @cx && @cx > factor)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
cumn = 0
|
|
329
|
+
@centroids.each_value do |c|
|
|
330
|
+
c.mean_cumn = cumn + (c.n / 2.0)
|
|
331
|
+
cumn = c.cumn = cumn + c.n
|
|
332
|
+
end
|
|
333
|
+
@size = @last_cumulate = cumn
|
|
334
|
+
nil
|
|
335
|
+
end
|
|
336
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
337
|
+
|
|
338
|
+
# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
339
|
+
# rubocop:disable Metrics/AbcSize
|
|
340
|
+
def _digest(x, n)
|
|
341
|
+
# Use 'first' and 'last' instead of min/max because of performance reasons
|
|
342
|
+
# This works because RBTree is sorted
|
|
343
|
+
min = min.last if (min = @centroids.first)
|
|
344
|
+
max = max.last if (max = @centroids.last)
|
|
345
|
+
nearest = find_nearest(x)
|
|
346
|
+
|
|
347
|
+
@size += n
|
|
348
|
+
|
|
349
|
+
if nearest && nearest.mean == x
|
|
350
|
+
_add_weight(nearest, x, n)
|
|
351
|
+
elsif nearest == min
|
|
352
|
+
@centroids[x] = Centroid.new(x, n, 0)
|
|
353
|
+
elsif nearest == max
|
|
354
|
+
@centroids[x] = Centroid.new(x, n, @size)
|
|
355
|
+
else
|
|
356
|
+
p = nearest.mean_cumn.to_f / @size
|
|
357
|
+
max_n = (4 * @size * @delta * p * (1 - p)).floor
|
|
358
|
+
if max_n - nearest.n >= n
|
|
359
|
+
_add_weight(nearest, x, n)
|
|
360
|
+
else
|
|
361
|
+
@centroids[x] = Centroid.new(x, n, nearest.cumn)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
_cumulate(exact: false)
|
|
366
|
+
|
|
367
|
+
# If the number of centroids has grown to a very large size,
|
|
368
|
+
# it may be due to values being inserted in sorted order.
|
|
369
|
+
# We combat that by replaying the centroids in random order,
|
|
370
|
+
# which is what compress! does
|
|
371
|
+
compress! if @centroids.size > (@k / @delta)
|
|
372
|
+
|
|
373
|
+
nil
|
|
374
|
+
end
|
|
375
|
+
# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity,
|
|
376
|
+
# rubocop:enable Metrics/AbcSize
|
|
377
|
+
end
|
|
378
|
+
# rubocop:enable Metrics/ClassLength
|
|
379
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
module Celerbrake
|
|
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
|
+
# name: 'performance-notifier',
|
|
10
|
+
# worker_size: 5,
|
|
11
|
+
# queue_size: 100,
|
|
12
|
+
# block: proc { |message| print "ECHO: #{message}..."}
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# # Send work.
|
|
16
|
+
# 10.times { |i| thread_pool << i }
|
|
17
|
+
# #=> ECHO: 0...ECHO: 1...ECHO: 2...
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
# @since v4.6.1
|
|
21
|
+
class ThreadPool
|
|
22
|
+
include Loggable
|
|
23
|
+
|
|
24
|
+
# @return [ThreadGroup] the list of workers
|
|
25
|
+
# @note This is exposed for eaiser unit testing
|
|
26
|
+
attr_reader :workers
|
|
27
|
+
|
|
28
|
+
def initialize(worker_size:, queue_size:, block:, name: nil)
|
|
29
|
+
@name = name
|
|
30
|
+
@worker_size = worker_size
|
|
31
|
+
@queue_size = queue_size
|
|
32
|
+
@block = block
|
|
33
|
+
|
|
34
|
+
@queue = SizedQueue.new(queue_size)
|
|
35
|
+
@workers = ThreadGroup.new
|
|
36
|
+
@mutex = Mutex.new
|
|
37
|
+
@pid = nil
|
|
38
|
+
@closed = false
|
|
39
|
+
|
|
40
|
+
has_workers?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Adds a new message to the thread pool. Rejects messages if the queue is at
|
|
44
|
+
# its capacity.
|
|
45
|
+
#
|
|
46
|
+
# @param [Object] message The message that gets passed to the block
|
|
47
|
+
# @return [Boolean] true if the message was successfully sent to the pool,
|
|
48
|
+
# false if the queue is full
|
|
49
|
+
def <<(message)
|
|
50
|
+
if backlog >= @queue_size
|
|
51
|
+
logger.info do
|
|
52
|
+
"#{LOG_LABEL} ThreadPool has reached its capacity of " \
|
|
53
|
+
"#{@queue_size} and the following message will not be " \
|
|
54
|
+
"processed: #{message.inspect}"
|
|
55
|
+
end
|
|
56
|
+
return false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@queue << message
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @return [Integer] how big the queue is at the moment
|
|
64
|
+
def backlog
|
|
65
|
+
@queue.size
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Checks if a thread pool has any workers. A thread pool doesn't have any
|
|
69
|
+
# workers only in two cases: when it was closed or when all workers
|
|
70
|
+
# crashed. An *active* thread pool doesn't have any workers only when
|
|
71
|
+
# something went wrong.
|
|
72
|
+
#
|
|
73
|
+
# Workers are expected to crash when you +fork+ the process the workers are
|
|
74
|
+
# living in. In this case we detect a +fork+ and try to revive them here.
|
|
75
|
+
#
|
|
76
|
+
# Another possible scenario that crashes workers is when you close the
|
|
77
|
+
# instance on +at_exit+, but some other +at_exit+ hook prevents the process
|
|
78
|
+
# from exiting.
|
|
79
|
+
#
|
|
80
|
+
# @return [Boolean] true if an instance wasn't closed, but has no workers
|
|
81
|
+
# @see https://goo.gl/oydz8h Example of at_exit that prevents exit
|
|
82
|
+
def has_workers?
|
|
83
|
+
@mutex.synchronize do
|
|
84
|
+
return false if @closed
|
|
85
|
+
|
|
86
|
+
if @pid != Process.pid && @workers.list.empty?
|
|
87
|
+
@pid = Process.pid
|
|
88
|
+
@workers = ThreadGroup.new
|
|
89
|
+
spawn_workers
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
!@closed && @workers.list.any?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Closes the thread pool making it a no-op (it shut downs all worker
|
|
97
|
+
# threads). Before closing, waits on all unprocessed tasks to be processed.
|
|
98
|
+
#
|
|
99
|
+
# @return [void]
|
|
100
|
+
# @raise [Celerbrake::Error] when invoked more than one time
|
|
101
|
+
def close
|
|
102
|
+
threads = @mutex.synchronize do
|
|
103
|
+
raise Celerbrake::Error, 'this thread pool is closed already' if @closed
|
|
104
|
+
|
|
105
|
+
unless @queue.empty?
|
|
106
|
+
msg = "#{LOG_LABEL} waiting to process #{@queue.size} task(s)..."
|
|
107
|
+
logger.debug("#{msg} (Ctrl-C to abort)")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@worker_size.times { @queue << :stop }
|
|
111
|
+
@closed = true
|
|
112
|
+
@workers.list.dup
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
threads.each(&:join)
|
|
116
|
+
logger.debug("#{LOG_LABEL} #{@name} thread pool closed")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def closed?
|
|
120
|
+
@closed
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def spawn_workers
|
|
124
|
+
@worker_size.times { @workers.add(spawn_worker) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def spawn_worker
|
|
130
|
+
Thread.new do
|
|
131
|
+
while (message = @queue.pop)
|
|
132
|
+
break if message == :stop
|
|
133
|
+
|
|
134
|
+
@block.call(message)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# TimeTruncate contains methods for truncating time.
|
|
3
|
+
#
|
|
4
|
+
# @api private
|
|
5
|
+
# @since v3.2.0
|
|
6
|
+
module TimeTruncate
|
|
7
|
+
# Truncate +time+ to floor minute and turn it into an RFC3339 timestamp.
|
|
8
|
+
#
|
|
9
|
+
# @param [Time, Integer, Float] time
|
|
10
|
+
# @return [String]
|
|
11
|
+
def self.utc_truncate_minutes(time)
|
|
12
|
+
tm = Time.at(time).getutc
|
|
13
|
+
|
|
14
|
+
Time.utc(tm.year, tm.month, tm.day, tm.hour, tm.min).to_datetime.rfc3339
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# TimedTrace represents a chunk of code performance of which was measured and
|
|
3
|
+
# stored under a label. The chunk is called a "span".
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# timed_trace = TimedTrace.new
|
|
7
|
+
# timed_trace.span('http request') do
|
|
8
|
+
# http.get('example.com')
|
|
9
|
+
# end
|
|
10
|
+
# timed_trace.spans #=> { 'http request' => 0.123 }
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
13
|
+
# @since v4.3.0
|
|
14
|
+
class TimedTrace
|
|
15
|
+
# @param [String] label
|
|
16
|
+
# @return [Celerbrake::TimedTrace]
|
|
17
|
+
def self.span(label, &block)
|
|
18
|
+
new.tap { |timed_trace| timed_trace.span(label, &block) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@spans = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param [String] label
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def span(label)
|
|
28
|
+
start_span(label)
|
|
29
|
+
yield
|
|
30
|
+
stop_span(label)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param [String] label
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def start_span(label)
|
|
36
|
+
return false if @spans.key?(label)
|
|
37
|
+
|
|
38
|
+
@spans[label] = Celerbrake::Benchmark.new
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param [String] label
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def stop_span(label)
|
|
45
|
+
return false unless @spans.key?(label)
|
|
46
|
+
|
|
47
|
+
@spans[label].stop
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Hash<String=>Float>]
|
|
52
|
+
def spans
|
|
53
|
+
@spans.transform_values(&:duration)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
module Celerbrake
|
|
2
|
+
# This class is responsible for truncation of too big objects. Mainly, you
|
|
3
|
+
# should use it for simple objects such as strings, hashes, & arrays.
|
|
4
|
+
#
|
|
5
|
+
# @api private
|
|
6
|
+
# @since v1.0.0
|
|
7
|
+
class Truncator
|
|
8
|
+
# @return [Hash] the options for +String#encode+
|
|
9
|
+
ENCODING_OPTIONS = { invalid: :replace, undef: :replace }.freeze
|
|
10
|
+
|
|
11
|
+
# @return [String] the temporary encoding to be used when fixing invalid
|
|
12
|
+
# strings with +ENCODING_OPTIONS+
|
|
13
|
+
TEMP_ENCODING = 'utf-16'.freeze
|
|
14
|
+
|
|
15
|
+
# @return [Array<Encoding>] encodings that are eligible for fixing invalid
|
|
16
|
+
# characters
|
|
17
|
+
SUPPORTED_ENCODINGS = [Encoding::UTF_8, Encoding::ASCII].freeze
|
|
18
|
+
|
|
19
|
+
# @return [String] what to append when something is a circular reference
|
|
20
|
+
CIRCULAR = '[Circular]'.freeze
|
|
21
|
+
|
|
22
|
+
# @return [String] what to append when something is truncated
|
|
23
|
+
TRUNCATED = '[Truncated]'.freeze
|
|
24
|
+
|
|
25
|
+
# @return [Array<Class>] The types that can contain references to itself
|
|
26
|
+
CIRCULAR_TYPES = [Array, Hash, Set].freeze
|
|
27
|
+
|
|
28
|
+
# @param [Integer] max_size maximum size of hashes, arrays and strings
|
|
29
|
+
def initialize(max_size)
|
|
30
|
+
@max_size = max_size
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Performs deep truncation of arrays, hashes, sets & strings. Uses a
|
|
34
|
+
# placeholder for recursive objects (`[Circular]`).
|
|
35
|
+
#
|
|
36
|
+
# @param [Object] object The object to truncate
|
|
37
|
+
# @param [Set] seen The cache that helps to detect recursion
|
|
38
|
+
# @return [Object] truncated object
|
|
39
|
+
def truncate(object, seen = Set.new)
|
|
40
|
+
if seen.include?(object.object_id)
|
|
41
|
+
return CIRCULAR if CIRCULAR_TYPES.any? { |t| object.is_a?(t) }
|
|
42
|
+
|
|
43
|
+
return object
|
|
44
|
+
end
|
|
45
|
+
truncate_object(object, seen << object.object_id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Reduces maximum allowed size of hashes, arrays, sets & strings by half.
|
|
49
|
+
# @return [Integer] current +max_size+ value
|
|
50
|
+
def reduce_max_size
|
|
51
|
+
@max_size /= 2
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def truncate_object(object, seen)
|
|
57
|
+
case object
|
|
58
|
+
when Hash then truncate_hash(object, seen)
|
|
59
|
+
when Array then truncate_array(object, seen)
|
|
60
|
+
when Set then truncate_set(object, seen)
|
|
61
|
+
when String then truncate_string(object)
|
|
62
|
+
when Numeric, TrueClass, FalseClass, Symbol, NilClass then object
|
|
63
|
+
else
|
|
64
|
+
truncate_string(stringify_object(object))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def truncate_string(str)
|
|
69
|
+
fixed_str = replace_invalid_characters(str)
|
|
70
|
+
return fixed_str if fixed_str.length <= @max_size
|
|
71
|
+
|
|
72
|
+
(fixed_str.slice(0, @max_size) + TRUNCATED).freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def stringify_object(object)
|
|
76
|
+
object.to_json
|
|
77
|
+
rescue *Notice::JSON_EXCEPTIONS
|
|
78
|
+
object.to_s
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def truncate_hash(hash, seen)
|
|
82
|
+
truncated_hash = {}
|
|
83
|
+
hash.each_with_index do |(key, val), idx|
|
|
84
|
+
break if idx + 1 > @max_size
|
|
85
|
+
|
|
86
|
+
truncated_hash[key] = truncate(val, seen)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
truncated_hash.freeze
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def truncate_array(array, seen)
|
|
93
|
+
array.slice(0, @max_size).map! { |elem| truncate(elem, seen) }.freeze
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def truncate_set(set, seen)
|
|
97
|
+
truncated_set = Set.new
|
|
98
|
+
|
|
99
|
+
set.each do |elem|
|
|
100
|
+
truncated_set << truncate(elem, seen)
|
|
101
|
+
break if truncated_set.size >= @max_size
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
truncated_set.freeze
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Replaces invalid characters in a string with arbitrary encoding.
|
|
108
|
+
#
|
|
109
|
+
# @param [String] str The string to replace characters
|
|
110
|
+
# @return [String] a UTF-8 encoded string
|
|
111
|
+
# @see https://github.com/flori/json/commit/3e158410e81f94dbbc3da6b7b35f4f64983aa4e3
|
|
112
|
+
def replace_invalid_characters(str)
|
|
113
|
+
utf8_string = SUPPORTED_ENCODINGS.include?(str.encoding)
|
|
114
|
+
return str if utf8_string && str.valid_encoding?
|
|
115
|
+
|
|
116
|
+
temp_str = str.dup
|
|
117
|
+
temp_str.encode!(TEMP_ENCODING, **ENCODING_OPTIONS) if utf8_string
|
|
118
|
+
temp_str.encode!('utf-8', **ENCODING_OPTIONS)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|