dalli 4.3.0 → 4.3.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: e97e2a407956737b411c33627d1f771be758017b881c68fab66edee95dc3249e
4
- data.tar.gz: b57638f133a592e9d57b71530bc925ff6589b27549e8785bc6c7f334906636ff
3
+ metadata.gz: 7eda4c85eb715e06ef3d8b34ea8e33a3cde89f19379a758f105363457451a034
4
+ data.tar.gz: 36ae8b603f8f778edbbf3bed9b7504ff1a440382bf5520423f49c39961f04a55
5
5
  SHA512:
6
- metadata.gz: 7dd43e9e5b09b65174f2e46b84de40e4e5bcfae9645c6bb67017ca81a64d344e7345cc4bf685f11d4779aeaafd7c7163df5a67bf8410153034f4adf523385b95
7
- data.tar.gz: b57506702dbdcb387490b3c0c56d2407aff4f0f789aec01e2b1a92c9f8d925a0225b8563e3dbc1ccf3c3ca72a25ca58f4199f1738d93bdf65a07893ed2930b6d
6
+ metadata.gz: b9a37ecfc73734a141979a34f2e155baa40cea5b22b016c192514c9d5847335d949ee642620c4508cc2ec8a2ceeb4dcec381715e633893623c42454975999d1a
7
+ data.tar.gz: 2a5f38c78c371e0710354a2b915a074512a87225f33fca95c9088ded76cad0dfc22e454fc9f64bddd40d8b0c0a6ab65bb108f467cd5b2b2c39d651285665f845
data/CHANGELOG.md CHANGED
@@ -1,6 +1,55 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ 4.3.2
5
+ ==========
6
+
7
+ OpenTelemetry:
8
+
9
+ - Migrate to stable OTel semantic conventions
10
+ - `db.system` renamed to `db.system.name`
11
+ - `db.operation` renamed to `db.operation.name`
12
+ - `server.address` now contains hostname only; `server.port` is a separate integer attribute
13
+ - `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
14
+ - Add `db.query.text` span attribute with configurable modes
15
+ - `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
16
+ - Add `peer.service` span attribute
17
+ - `:otel_peer_service` option for logical service naming
18
+
19
+ 4.3.1
20
+ ==========
21
+
22
+ Bug Fixes:
23
+
24
+ - Fix socket compatibility with gems that monkey-patch TCPSocket (#996, #1012)
25
+ - Gems like `socksify` and `resolv-replace` modify `TCPSocket#initialize`, breaking Ruby 3.0+'s `connect_timeout:` keyword argument
26
+ - Detection now uses parameter signature checking instead of gem-specific method detection
27
+ - Falls back to `Timeout.timeout` when monkey-patching is detected
28
+ - Detection result is cached for performance
29
+
30
+ - Fix network retry bug with `socket_max_failures: 0` (#1065)
31
+ - Previously, setting `socket_max_failures: 0` could still cause retries due to error handling
32
+ - Introduced `RetryableNetworkError` subclass to distinguish retryable vs non-retryable errors
33
+ - `down!` now raises non-retryable `NetworkError`, `reconnect!` raises `RetryableNetworkError`
34
+ - Thanks to Graham Cooper (Shopify) for this fix
35
+
36
+ - Fix "character class has duplicated range" Ruby warning (#1067)
37
+ - Fixed regex in `KeyManager::VALID_NAMESPACE_SEPARATORS` that caused warnings on newer Ruby versions
38
+ - Thanks to Hartley McGuire for this fix
39
+
40
+ Improvements:
41
+
42
+ - Add StrictWarnings test helper to catch Ruby warnings early (#1067)
43
+
44
+ - Use bulk attribute setter for OpenTelemetry spans (#1068)
45
+ - Reduces lock acquisitions when setting span attributes
46
+ - Thanks to Robert Laurin (Shopify) for this optimization
47
+
48
+ - Fix double recording of exceptions on OpenTelemetry spans (#1069)
49
+ - OpenTelemetry's `in_span` method already records exceptions and sets error status automatically
50
+ - Removed redundant explicit exception recording that caused exceptions to appear twice in traces
51
+ - Thanks to Robert Laurin (Shopify) for this fix
52
+
4
53
  4.3.0
5
54
  ==========
6
55
 
data/Gemfile CHANGED
@@ -27,4 +27,8 @@ end
27
27
 
28
28
  group :test do
29
29
  gem 'ruby-prof', platform: :mri
30
+
31
+ # For socket compatibility testing (these gems monkey-patch TCPSocket)
32
+ gem 'resolv-replace', require: false
33
+ gem 'socksify', require: false
30
34
  end
data/lib/dalli/client.rb CHANGED
@@ -50,6 +50,10 @@ module Dalli
50
50
  # useful for injecting a FIPS compliant hash object.
51
51
  # - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
52
52
  # to communicate with memcached.
53
+ # - :otel_db_statement - controls the +db.query.text+ span attribute when OpenTelemetry is loaded.
54
+ # +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
55
+ # +nil+ (default) omits the attribute entirely.
56
+ # - :otel_peer_service - when set, adds a +peer.service+ span attribute with this value for logical service naming.
53
57
  #
54
58
  def initialize(servers = nil, options = {})
55
59
  @normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
@@ -134,8 +138,8 @@ module Dalli
134
138
  key = key.to_s
135
139
  key = @key_manager.validate_key(key)
136
140
 
137
- Instrumentation.trace('get_with_metadata', { 'db.operation' => 'get_with_metadata' }) do
138
- server = ring.server_for_key(key)
141
+ server = ring.server_for_key(key)
142
+ Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', key, server)) do
139
143
  server.request(:meta_get, key, options)
140
144
  end
141
145
  rescue NetworkError => e
@@ -237,7 +241,8 @@ module Dalli
237
241
  key = key.to_s
238
242
  key = @key_manager.validate_key(key)
239
243
 
240
- Instrumentation.trace('fetch_with_lock', { 'db.operation' => 'fetch_with_lock' }) do
244
+ server = ring.server_for_key(key)
245
+ Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', key, server)) do
241
246
  fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
242
247
  end
243
248
  rescue NetworkError => e
@@ -318,10 +323,7 @@ module Dalli
318
323
  def set_multi(hash, ttl = nil, req_options = nil)
319
324
  return if hash.empty?
320
325
 
321
- Instrumentation.trace('set_multi', {
322
- 'db.operation' => 'set_multi',
323
- 'db.memcached.key_count' => hash.size
324
- }) do
326
+ Instrumentation.trace('set_multi', multi_trace_attrs('set_multi', hash.size, hash.keys)) do
325
327
  pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
326
328
  end
327
329
  end
@@ -378,10 +380,7 @@ module Dalli
378
380
  def delete_multi(keys)
379
381
  return if keys.empty?
380
382
 
381
- Instrumentation.trace('delete_multi', {
382
- 'db.operation' => 'delete_multi',
383
- 'db.memcached.key_count' => keys.size
384
- }) do
383
+ Instrumentation.trace('delete_multi', multi_trace_attrs('delete_multi', keys.size, keys)) do
385
384
  pipelined_deleter.process(keys)
386
385
  end
387
386
  end
@@ -522,8 +521,8 @@ module Dalli
522
521
  def record_hit_miss_metrics(span, key_count, hit_count)
523
522
  return unless span
524
523
 
525
- span.set_attribute('db.memcached.hit_count', hit_count)
526
- span.set_attribute('db.memcached.miss_count', key_count - hit_count)
524
+ span.add_attributes('db.memcached.hit_count' => hit_count,
525
+ 'db.memcached.miss_count' => key_count - hit_count)
527
526
  end
528
527
 
529
528
  def get_multi_yielding(keys)
@@ -548,7 +547,30 @@ module Dalli
548
547
  end
549
548
 
550
549
  def get_multi_attributes(keys)
551
- { 'db.operation' => 'get_multi', 'db.memcached.key_count' => keys.size }
550
+ multi_trace_attrs('get_multi', keys.size, keys)
551
+ end
552
+
553
+ def trace_attrs(operation, key, server)
554
+ attrs = { 'db.operation.name' => operation, 'server.address' => server.hostname }
555
+ attrs['server.port'] = server.port if server.socket_type == :tcp
556
+ attrs['peer.service'] = @options[:otel_peer_service] if @options[:otel_peer_service]
557
+ add_query_text(attrs, operation, key)
558
+ end
559
+
560
+ def multi_trace_attrs(operation, key_count, keys)
561
+ attrs = { 'db.operation.name' => operation, 'db.memcached.key_count' => key_count }
562
+ attrs['peer.service'] = @options[:otel_peer_service] if @options[:otel_peer_service]
563
+ add_query_text(attrs, operation, keys)
564
+ end
565
+
566
+ def add_query_text(attrs, operation, key_or_keys)
567
+ case @options[:otel_db_statement]
568
+ when :include
569
+ attrs['db.query.text'] = "#{operation} #{Array(key_or_keys).join(' ')}"
570
+ when :obfuscate
571
+ attrs['db.query.text'] = "#{operation} ?"
572
+ end
573
+ attrs
552
574
  end
553
575
 
554
576
  def check_positive!(amt)
@@ -616,13 +638,10 @@ module Dalli
616
638
  key = @key_manager.validate_key(key)
617
639
 
618
640
  server = ring.server_for_key(key)
619
- Instrumentation.trace(op.to_s, {
620
- 'db.operation' => op.to_s,
621
- 'server.address' => server.name
622
- }) do
641
+ Instrumentation.trace(op.to_s, trace_attrs(op.to_s, key, server)) do
623
642
  server.request(op, key, *args)
624
643
  end
625
- rescue NetworkError => e
644
+ rescue RetryableNetworkError => e
626
645
  Dalli.logger.debug { e.inspect }
627
646
  Dalli.logger.debug { 'retrying request with new server' }
628
647
  retry
@@ -8,25 +8,36 @@ module Dalli
8
8
  # When OpenTelemetry is loaded, Dalli automatically creates spans for cache operations.
9
9
  # When OpenTelemetry is not available, all tracing methods are no-ops with zero overhead.
10
10
  #
11
+ # Dalli 4.3.2 uses the stable OTel semantic conventions for database spans.
12
+ #
11
13
  # == Span Attributes
12
14
  #
13
15
  # All spans include the following default attributes:
14
- # - +db.system+ - Always "memcached"
16
+ # - +db.system.name+ - Always "memcached"
15
17
  #
16
18
  # Single-key operations (+get+, +set+, +delete+, +incr+, +decr+, etc.) add:
17
- # - +db.operation+ - The operation name (e.g., "get", "set")
18
- # - +server.address+ - The memcached server handling the request (e.g., "localhost:11211")
19
+ # - +db.operation.name+ - The operation name (e.g., "get", "set")
20
+ # - +server.address+ - The server hostname (e.g., "localhost")
21
+ # - +server.port+ - The server port as an integer (e.g., 11211); omitted for Unix sockets
19
22
  #
20
23
  # Multi-key operations (+get_multi+) add:
21
- # - +db.operation+ - "get_multi"
24
+ # - +db.operation.name+ - "get_multi"
22
25
  # - +db.memcached.key_count+ - Number of keys requested
23
26
  # - +db.memcached.hit_count+ - Number of keys found in cache
24
27
  # - +db.memcached.miss_count+ - Number of keys not found
25
28
  #
26
29
  # Bulk write operations (+set_multi+, +delete_multi+) add:
27
- # - +db.operation+ - The operation name
30
+ # - +db.operation.name+ - The operation name
28
31
  # - +db.memcached.key_count+ - Number of keys in the operation
29
32
  #
33
+ # == Optional Attributes
34
+ #
35
+ # - +db.query.text+ - The operation and key(s), controlled by the +:otel_db_statement+ client option:
36
+ # - +:include+ - Full text (e.g., "get mykey")
37
+ # - +:obfuscate+ - Obfuscated (e.g., "get ?")
38
+ # - +nil+ (default) - Attribute omitted
39
+ # - +peer.service+ - Logical service name, set via the +:otel_peer_service+ client option
40
+ #
30
41
  # == Error Handling
31
42
  #
32
43
  # When an exception occurs during a traced operation:
@@ -40,8 +51,8 @@ module Dalli
40
51
  ##
41
52
  module Instrumentation
42
53
  # Default attributes included on all memcached spans.
43
- # @return [Hash] frozen hash with 'db.system' => 'memcached'
44
- DEFAULT_ATTRIBUTES = { 'db.system' => 'memcached' }.freeze
54
+ # @return [Hash] frozen hash with 'db.system.name' => 'memcached'
55
+ DEFAULT_ATTRIBUTES = { 'db.system.name' => 'memcached' }.freeze
45
56
 
46
57
  class << self
47
58
  # Returns the OpenTelemetry tracer if available, nil otherwise.
@@ -75,27 +86,24 @@ module Dalli
75
86
  # @param name [String] the span name (e.g., 'get', 'set', 'delete')
76
87
  # @param attributes [Hash] span attributes to merge with defaults.
77
88
  # Common attributes include:
78
- # - 'db.operation' - the operation name
79
- # - 'server.address' - the target server
89
+ # - 'db.operation.name' - the operation name
90
+ # - 'server.address' - the server hostname
91
+ # - 'server.port' - the server port (integer)
80
92
  # - 'db.memcached.key_count' - number of keys (for multi operations)
81
93
  # @yield the cache operation to trace
82
94
  # @return [Object] the result of the block
83
95
  # @raise [StandardError] re-raises any exception from the block
84
96
  #
85
97
  # @example Tracing a set operation
86
- # trace('set', { 'db.operation' => 'set', 'server.address' => 'localhost:11211' }) do
98
+ # trace('set', { 'db.operation.name' => 'set', 'server.address' => 'localhost', 'server.port' => 11211 }) do
87
99
  # server.set(key, value, ttl)
88
100
  # end
89
101
  #
90
102
  def trace(name, attributes = {})
91
103
  return yield unless enabled?
92
104
 
93
- tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |span|
105
+ tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |_span|
94
106
  yield
95
- rescue StandardError => e
96
- span.record_exception(e)
97
- span.status = OpenTelemetry::Trace::Status.error(e.message)
98
- raise
99
107
  end
100
108
  end
101
109
 
@@ -114,7 +122,7 @@ module Dalli
114
122
  # @raise [StandardError] re-raises any exception from the block
115
123
  #
116
124
  # @example Recording hit/miss metrics after get_multi
117
- # trace_with_result('get_multi', { 'db.operation' => 'get_multi' }) do |span|
125
+ # trace_with_result('get_multi', { 'db.operation.name' => 'get_multi' }) do |span|
118
126
  # results = fetch_from_cache(keys)
119
127
  # if span
120
128
  # span.set_attribute('db.memcached.hit_count', results.size)
@@ -123,16 +131,10 @@ module Dalli
123
131
  # results
124
132
  # end
125
133
  #
126
- def trace_with_result(name, attributes = {})
134
+ def trace_with_result(name, attributes = {}, &)
127
135
  return yield(nil) unless enabled?
128
136
 
129
- tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |span|
130
- yield(span)
131
- rescue StandardError => e
132
- span.record_exception(e)
133
- span.status = OpenTelemetry::Trace::Status.error(e.message)
134
- raise
135
- end
137
+ tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client, &)
136
138
  end
137
139
  end
138
140
  end
@@ -31,7 +31,7 @@ module Dalli
31
31
 
32
32
  # Valid separators: non-alphanumeric, single printable ASCII characters
33
33
  # Excludes: alphanumerics, whitespace, control characters
34
- VALID_NAMESPACE_SEPARATORS = /\A[^a-zA-Z0-9\s\x00-\x1F\x7F]\z/
34
+ VALID_NAMESPACE_SEPARATORS = /\A[^a-zA-Z0-9 \x00-\x1F\x7F]\z/
35
35
 
36
36
  def initialize(client_options)
37
37
  @key_options =
@@ -36,7 +36,7 @@ module Dalli
36
36
 
37
37
  servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
38
38
  end
39
- rescue NetworkError => e
39
+ rescue Dalli::RetryableNetworkError => e
40
40
  Dalli.logger.debug { e.inspect }
41
41
  Dalli.logger.debug { 'retrying pipelined gets because of timeout' }
42
42
  retry
@@ -143,7 +143,7 @@ module Dalli
143
143
  servers
144
144
  rescue NetworkError
145
145
  # Abort and raise if we encountered a network error. This triggers
146
- # a retry at the top level.
146
+ # a retry at the top level on RetryableNetworkError.
147
147
  abort_without_timeout(servers)
148
148
  raise
149
149
  end
@@ -28,7 +28,7 @@ module Dalli
28
28
  servers = setup_requests(hash, ttl, req_options)
29
29
  finish_requests(servers)
30
30
  end
31
- rescue NetworkError => e
31
+ rescue Dalli::RetryableNetworkError => e
32
32
  Dalli.logger.debug { e.inspect }
33
33
  Dalli.logger.debug { 'retrying pipelined sets because of network error' }
34
34
  retry
@@ -199,7 +199,7 @@ module Dalli
199
199
  def reconnect!(message)
200
200
  close
201
201
  sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
202
- raise Dalli::NetworkError, message
202
+ raise Dalli::RetryableNetworkError, message
203
203
  end
204
204
 
205
205
  def reset_down_info
data/lib/dalli/socket.rb CHANGED
@@ -90,6 +90,12 @@ module Dalli
90
90
  # options - supports enhanced logging in the case of a timeout
91
91
  attr_accessor :options
92
92
 
93
+ # Expected parameter signature for unmodified TCPSocket#initialize.
94
+ # Used to detect when gems like socksify or resolv-replace have monkey-patched
95
+ # TCPSocket, which breaks the connect_timeout: keyword argument.
96
+ TCPSOCKET_NATIVE_PARAMETERS = [[:rest]].freeze
97
+ private_constant :TCPSOCKET_NATIVE_PARAMETERS
98
+
93
99
  def self.open(host, port, options = {})
94
100
  create_socket_with_timeout(host, port, options) do |sock|
95
101
  sock.options = { host: host, port: port }.merge(options)
@@ -99,15 +105,18 @@ module Dalli
99
105
  end
100
106
  end
101
107
 
108
+ # Detect and cache whether TCPSocket supports the connect_timeout: keyword argument.
109
+ # Returns false if TCPSocket#initialize has been monkey-patched by gems like
110
+ # socksify or resolv-replace, which don't support keyword arguments.
111
+ def self.supports_connect_timeout?
112
+ return @supports_connect_timeout if defined?(@supports_connect_timeout)
113
+
114
+ @supports_connect_timeout = RUBY_VERSION >= '3.0' &&
115
+ ::TCPSocket.instance_method(:initialize).parameters == TCPSOCKET_NATIVE_PARAMETERS
116
+ end
117
+
102
118
  def self.create_socket_with_timeout(host, port, options)
103
- # Check that TCPSocket#initialize was not overwritten by resolv-replace gem
104
- # (part of ruby standard library since 3.0.0, should be removed in 3.4.0),
105
- # as it does not handle keyword arguments correctly.
106
- # To check this we are using the fact that resolv-replace
107
- # aliases TCPSocket#initialize method to #original_resolv_initialize.
108
- # https://github.com/ruby/resolv-replace/blob/v0.1.1/lib/resolv-replace.rb#L21
109
- if RUBY_VERSION >= '3.0' &&
110
- !::TCPSocket.private_method_defined?(:original_resolv_initialize)
119
+ if supports_connect_timeout?
111
120
  sock = new(host, port, connect_timeout: options[:socket_timeout])
112
121
  yield(sock)
113
122
  else
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '4.3.0'
4
+ VERSION = '4.3.2'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -28,6 +28,9 @@ module Dalli
28
28
  # raised when Memcached response with a SERVER_ERROR
29
29
  class ServerError < DalliError; end
30
30
 
31
+ # socket/server communication error that can be retried
32
+ class RetryableNetworkError < NetworkError; end
33
+
31
34
  # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
32
35
  class NilObject; end # rubocop:disable Lint/EmptyClass
33
36
  NOT_FOUND = NilObject.new
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dalli
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.0
4
+ version: 4.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -93,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
93
  - !ruby/object:Gem::Version
94
94
  version: '0'
95
95
  requirements: []
96
- rubygems_version: 4.0.4
96
+ rubygems_version: 4.0.6
97
97
  specification_version: 4
98
98
  summary: High performance memcached client for Ruby
99
99
  test_files: []