statsd-instrument 3.11.1 → 3.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/tests.yml +1 -1
- data/CHANGELOG.md +6 -0
- data/Gemfile +2 -5
- data/lib/statsd/instrument/compiled_metric.rb +7 -4
- data/lib/statsd/instrument/datagram.rb +1 -1
- data/lib/statsd/instrument/dogstatsd_datagram.rb +2 -2
- data/lib/statsd/instrument/rubocop/metric_return_value.rb +1 -1
- data/lib/statsd/instrument/rubocop.rb +3 -3
- data/lib/statsd/instrument/version.rb +1 -1
- data/test/compiled_metric_test.rb +75 -4
- data/test/rubocop/singleton_configuration_test.rb +3 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 51853f9d4b70fc39649e251e7e93ff3d77b2bcca1d776182f82de8219ea86886
|
|
4
|
+
data.tar.gz: fce22a816682f0bc25510b6a68248a64c93ba983e7640b57ec01dbb0b985c9d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a06bd60427001260e206243f4db74ecb3d8225c1c5e3f8f9764239bc40901457cf621d7f5fbd0936d39fa0765145499672a9fb0a9c22025ac8611f7051f7154
|
|
7
|
+
data.tar.gz: f74279e49141844c564d896e1d04b88b6b9f66585e56401a4ee52f7f4cb8dcc5fbe6a99b8427fbfa45ca1be843fef5a8c9357fb629129372880ac5020aceab31
|
data/.github/workflows/tests.yml
CHANGED
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
strategy:
|
|
10
10
|
fail-fast: false
|
|
11
11
|
matrix:
|
|
12
|
-
ruby: ['
|
|
12
|
+
ruby: ['3.0', '3.1', '3.2', '3.3', '3.4', 'ruby-head', 'jruby-10.1.1.0', 'truffleruby-34.0.1']
|
|
13
13
|
|
|
14
14
|
steps:
|
|
15
15
|
- uses: actions/checkout@v4
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ section below.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased changes
|
|
8
8
|
|
|
9
|
+
## Version 3.11.2
|
|
10
|
+
|
|
11
|
+
- Widen the `CompiledMetric` tag-combination cache key from 32 to 57 bits, eliminating
|
|
12
|
+
recurring hash collisions on hot metrics while keeping all intermediates within Fixnum
|
|
13
|
+
range (no new allocations on the emit path).
|
|
14
|
+
|
|
9
15
|
## Version 3.11.1
|
|
10
16
|
|
|
11
17
|
- Fix `CompiledMetric::Distribution` applying `sample_rate` twice.
|
data/Gemfile
CHANGED
|
@@ -13,15 +13,12 @@ gem "rubocop-shopify", require: false
|
|
|
13
13
|
gem "benchmark-ips"
|
|
14
14
|
gem "dogstatsd-ruby", "~> 5.0", require: false
|
|
15
15
|
gem "simplecov", require: false, group: :test
|
|
16
|
+
# logger is no longer bundled with newer Ruby and JRuby releases.
|
|
17
|
+
gem "logger"
|
|
16
18
|
|
|
17
19
|
platform :mri do
|
|
18
20
|
# only if Ruby is MRI && >= 3.2
|
|
19
21
|
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.2")
|
|
20
22
|
gem "vernier", require: false
|
|
21
23
|
end
|
|
22
|
-
|
|
23
|
-
# From Ruby >= 3.5, logger is not part of the stdlib anymore
|
|
24
|
-
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.5")
|
|
25
|
-
gem "logger"
|
|
26
|
-
end
|
|
27
24
|
end
|
|
@@ -189,10 +189,13 @@ module StatsD
|
|
|
189
189
|
|
|
190
190
|
# Compute hash of tag values for cache lookup using rotate-left + XOR.
|
|
191
191
|
# Rotation makes it order-dependent (unlike plain XOR), preventing collisions
|
|
192
|
-
# when tag values are swapped.
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
#
|
|
192
|
+
# when tag values are swapped. The mask keeps 57 bits of entropy, wide enough
|
|
193
|
+
# that birthday collisions are negligible at real tag-combination volumes.
|
|
194
|
+
# 57 bits is the widest key for which the intermediate (__cache_key__ << 5)
|
|
195
|
+
# stays within Fixnum range (2**62 - 1 on 64-bit CRuby) and never allocates
|
|
196
|
+
# a Bignum.
|
|
197
|
+
__cache_key__ = #{tag_names.first}.hash & 0x01FFFFFFFFFFFFFF
|
|
198
|
+
#{tag_names.drop(1).map { |name| "__cache_key__ = (((__cache_key__ << 5) | (__cache_key__ >> 52)) ^ #{name}.hash) & 0x01FFFFFFFFFFFFFF" }.join("\n")}
|
|
196
199
|
|
|
197
200
|
# Look up or create a PrecompiledDatagram
|
|
198
201
|
__datagram__ =
|
|
@@ -69,7 +69,7 @@ module StatsD
|
|
|
69
69
|
(?:\|m:(?<message>[^\|]+))?
|
|
70
70
|
\n? # In some implementations, the datagram may include a trailing newline.
|
|
71
71
|
\z
|
|
72
|
-
}x
|
|
72
|
+
}x.freeze
|
|
73
73
|
|
|
74
74
|
# |k:my-key|p:low|s:source|t:success|
|
|
75
75
|
EVENT_PARSER = %r{
|
|
@@ -84,7 +84,7 @@ module StatsD
|
|
|
84
84
|
(?:\|\#(?<tags>(?:[^\|,]+(?:,[^\|,]+)*)))?
|
|
85
85
|
\n? # In some implementations, the datagram may include a trailing newline.
|
|
86
86
|
\z
|
|
87
|
-
}x
|
|
87
|
+
}x.freeze
|
|
88
88
|
|
|
89
89
|
PARSER = Regexp.union(StatsD::Instrument::Datagram::PARSER, SERVICE_CHECK_PARSER, EVENT_PARSER)
|
|
90
90
|
end
|
|
@@ -19,7 +19,7 @@ module RuboCop
|
|
|
19
19
|
|
|
20
20
|
MSG = "Do not use the return value of StatsD metric methods"
|
|
21
21
|
|
|
22
|
-
INVALID_PARENTS = [:lvasgn, :array, :pair, :send, :return, :yield]
|
|
22
|
+
INVALID_PARENTS = [:lvasgn, :array, :pair, :send, :return, :yield].freeze
|
|
23
23
|
|
|
24
24
|
def on_send(node)
|
|
25
25
|
if metric_method?(node) && node.arguments.last&.type != :block_pass
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
5
|
module StatsD
|
|
6
|
-
METRIC_METHODS = [:increment, :gauge, :measure, :set, :histogram, :distribution, :key_value]
|
|
6
|
+
METRIC_METHODS = [:increment, :gauge, :measure, :set, :histogram, :distribution, :key_value].freeze
|
|
7
7
|
|
|
8
8
|
METAPROGRAMMING_METHODS = [
|
|
9
9
|
:statsd_measure,
|
|
@@ -11,7 +11,7 @@ module RuboCop
|
|
|
11
11
|
:statsd_count_success,
|
|
12
12
|
:statsd_count_if,
|
|
13
13
|
:statsd_count,
|
|
14
|
-
]
|
|
14
|
+
].freeze
|
|
15
15
|
|
|
16
16
|
SINGLETON_CONFIGURATION_METHODS = [
|
|
17
17
|
:backend,
|
|
@@ -22,7 +22,7 @@ module RuboCop
|
|
|
22
22
|
:"default_tags=",
|
|
23
23
|
:default_sample_rate,
|
|
24
24
|
:"default_sample_rate=",
|
|
25
|
-
]
|
|
25
|
+
].freeze
|
|
26
26
|
|
|
27
27
|
private
|
|
28
28
|
|
|
@@ -285,10 +285,10 @@ class CompiledMetricDefinitionTest < Minitest::Test
|
|
|
285
285
|
|
|
286
286
|
cache = metric.instance_variable_get(:@tag_combination_cache)
|
|
287
287
|
|
|
288
|
-
# Compute cache keys using the rotate-left + XOR formula (
|
|
289
|
-
# For single tag, it's the hash value masked to
|
|
290
|
-
cache_key_for_1 = 1.hash &
|
|
291
|
-
cache_key_for_2 = 2.hash &
|
|
288
|
+
# Compute cache keys using the rotate-left + XOR formula (57-bit bounded)
|
|
289
|
+
# For single tag, it's the hash value masked to 57 bits
|
|
290
|
+
cache_key_for_1 = 1.hash & 0x01FFFFFFFFFFFFFF
|
|
291
|
+
cache_key_for_2 = 2.hash & 0x01FFFFFFFFFFFFFF
|
|
292
292
|
|
|
293
293
|
# Store the cached datagram under the collision key
|
|
294
294
|
cached_datagram = cache[cache_key_for_1]
|
|
@@ -312,6 +312,77 @@ class CompiledMetricDefinitionTest < Minitest::Test
|
|
|
312
312
|
assert_includes(hash_collision_metric.tags, "metric_name:foo.bar")
|
|
313
313
|
end
|
|
314
314
|
|
|
315
|
+
def test_cache_key_space_is_wider_than_32_bits
|
|
316
|
+
metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
|
|
317
|
+
define(
|
|
318
|
+
name: "foo.bar",
|
|
319
|
+
tags: { shop_id: Integer },
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Hash seeds are process-local, so find a pair of tag values at runtime that
|
|
324
|
+
# shares its low 32 hash bits (collided under the previous 32-bit cache key)
|
|
325
|
+
# while still differing across the full 57-bit key space.
|
|
326
|
+
seen = {}
|
|
327
|
+
pair = nil
|
|
328
|
+
i = 0
|
|
329
|
+
until pair
|
|
330
|
+
key = i.hash & 0xFFFFFFFF
|
|
331
|
+
other = seen[key]
|
|
332
|
+
if other && other != i && (other.hash & 0x01FFFFFFFFFFFFFF) != (i.hash & 0x01FFFFFFFFFFFFFF)
|
|
333
|
+
pair = [other, i]
|
|
334
|
+
end
|
|
335
|
+
seen[key] ||= i
|
|
336
|
+
i += 1
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
metric.increment(1, shop_id: pair[0])
|
|
340
|
+
metric.increment(1, shop_id: pair[1])
|
|
341
|
+
|
|
342
|
+
cache = metric.instance_variable_get(:@tag_combination_cache)
|
|
343
|
+
assert_equal(2, cache.size)
|
|
344
|
+
|
|
345
|
+
collision_metric = @sink.datagrams.find do |datagram|
|
|
346
|
+
datagram.name == "test.statsd_instrument.compiled_metric.hash_collision_detected"
|
|
347
|
+
end
|
|
348
|
+
assert_nil(collision_metric, "Expected no hash collision metric for 57-bit-distinct keys")
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def test_cache_key_computation_does_not_allocate
|
|
352
|
+
skip_on_jruby("JRuby does not support GC.stat(:total_allocated_objects)")
|
|
353
|
+
|
|
354
|
+
single_tag_metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
|
|
355
|
+
define(name: "foo.single", tags: { a: String })
|
|
356
|
+
end
|
|
357
|
+
multi_tag_metric = Class.new(StatsD::Instrument::CompiledMetric::Counter) do
|
|
358
|
+
define(name: "foo.multi", tags: { a: String, b: String, c: String, d: String, e: String, f: String, g: String, h: String })
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
single_call = -> { single_tag_metric.increment(1, a: "a") }
|
|
362
|
+
multi_call = -> { multi_tag_metric.increment(1, a: "a", b: "b", c: "c", d: "d", e: "e", f: "f", g: "g", h: "h") }
|
|
363
|
+
measure = ->(callable) { count_allocations { 10.times { callable.call } } }
|
|
364
|
+
[single_call, multi_call].each { |c| measure.call(c) }
|
|
365
|
+
|
|
366
|
+
# With frozen string literals, the rotate-left + XOR chain is the only per-tag
|
|
367
|
+
# work on a cache hit. A cache key wide enough to escape Fixnum range would
|
|
368
|
+
# allocate one heap integer per chain step and show up as extra allocations
|
|
369
|
+
# for the multi-tag metric.
|
|
370
|
+
assert_equal(
|
|
371
|
+
measure.call(single_call),
|
|
372
|
+
measure.call(multi_call),
|
|
373
|
+
"Expected per-tag cache-key computation to allocate nothing",
|
|
374
|
+
)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
private
|
|
378
|
+
|
|
379
|
+
def count_allocations
|
|
380
|
+
GC.start
|
|
381
|
+
before = GC.stat(:total_allocated_objects)
|
|
382
|
+
yield
|
|
383
|
+
GC.stat(:total_allocated_objects) - before
|
|
384
|
+
end
|
|
385
|
+
|
|
315
386
|
def test_handles_default_tags_as_array
|
|
316
387
|
StatsD.singleton_client = StatsD::Instrument::Client.new(
|
|
317
388
|
sink: @sink,
|
|
@@ -18,7 +18,10 @@ module Rubocop
|
|
|
18
18
|
|
|
19
19
|
def test_offense_statsd_prefix
|
|
20
20
|
assert_offense('StatsD.prefix = "foo"')
|
|
21
|
+
# The interpolation is intentionally literal source passed to the cop under test.
|
|
22
|
+
# rubocop:disable Lint/InterpolationCheck
|
|
21
23
|
assert_offense('"#{StatsD.prefix}.foo"')
|
|
24
|
+
# rubocop:enable Lint/InterpolationCheck
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
def test_offense_statsd_default_tags
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: statsd-instrument
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.11.
|
|
4
|
+
version: 3.11.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jesse Storimer
|
|
@@ -135,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
135
135
|
- !ruby/object:Gem::Version
|
|
136
136
|
version: '0'
|
|
137
137
|
requirements: []
|
|
138
|
-
rubygems_version: 4.0.
|
|
138
|
+
rubygems_version: 4.0.18
|
|
139
139
|
specification_version: 4
|
|
140
140
|
summary: A StatsD client for Ruby apps
|
|
141
141
|
test_files:
|