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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3da8fd9b1f80c7cf776c41d335ab14858dd625299bbec536c7f8994dfe93baf6
4
- data.tar.gz: b1ca1c4049a5f3956ea510074027bb577d26ab8a2033cf96d4af5fcc39fc90c9
3
+ metadata.gz: 51853f9d4b70fc39649e251e7e93ff3d77b2bcca1d776182f82de8219ea86886
4
+ data.tar.gz: fce22a816682f0bc25510b6a68248a64c93ba983e7640b57ec01dbb0b985c9d2
5
5
  SHA512:
6
- metadata.gz: f8774b020de05ec35687a82aca947a19b095ea34867fdcd73518748c9ccddfe28e41cdc41c2dfe1b9c2e4f657b4416218d67fe3b0e259a8bf10a810580f8ea60
7
- data.tar.gz: 420ea1eb853426b83d2d5f546dde091d402aaff31ef243b912df3869dd02882e8ef24bde4b124f860beccb0e9528dcfbcd7ab6b25b36b3d2fc3e02798fa209e1
6
+ metadata.gz: 4a06bd60427001260e206243f4db74ecb3d8225c1c5e3f8f9764239bc40901457cf621d7f5fbd0936d39fa0765145499672a9fb0a9c22025ac8611f7051f7154
7
+ data.tar.gz: f74279e49141844c564d896e1d04b88b6b9f66585e56401a4ee52f7f4cb8dcc5fbe6a99b8427fbfa45ca1be843fef5a8c9357fb629129372880ac5020aceab31
@@ -9,7 +9,7 @@ jobs:
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
- ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', 'ruby-head', 'jruby-9.4.9.0', 'truffleruby-25.0.0']
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. We mask to 32 bits to avoid Bignum allocations
193
- # from left shifts on 64-bit hash values.
194
- __cache_key__ = #{tag_names.first}.hash & 0xFFFFFFFF
195
- #{tag_names.drop(1).map { |name| "__cache_key__ = (((__cache_key__ << 5) | (__cache_key__ >> 27)) ^ #{name}.hash) & 0xFFFFFFFF" }.join("\n")}
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__ =
@@ -77,7 +77,7 @@ module StatsD
77
77
  (?:\|\#(?<tags>(?:[^\|,]+(?:,[^\|,]+)*)))?
78
78
  \n? # In some implementations, the datagram may include a trailing newline.
79
79
  \z
80
- }x
80
+ }x.freeze
81
81
 
82
82
  def parsed_datagram
83
83
  @parsed ||= if (match_info = PARSER.match(@source))
@@ -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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StatsD
4
4
  module Instrument
5
- VERSION = "3.11.1"
5
+ VERSION = "3.11.2"
6
6
  end
7
7
  end
@@ -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 (32-bit bounded)
289
- # For single tag, it's the hash value masked to 32 bits
290
- cache_key_for_1 = 1.hash & 0xFFFFFFFF
291
- cache_key_for_2 = 2.hash & 0xFFFFFFFF
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.1
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.14
138
+ rubygems_version: 4.0.18
139
139
  specification_version: 4
140
140
  summary: A StatsD client for Ruby apps
141
141
  test_files: