dalli 4.3.0 → 5.0.1

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: 02d0fa949b7a065f86fb3ac7b511a3feacc50b700e82dcb385e0e79c56583a9d
4
+ data.tar.gz: d72b9e4b014ae1ae0b0d5a1ebe09ea48cdf9fbf0a2cda000efc19f17d5d34368
5
5
  SHA512:
6
- metadata.gz: 7dd43e9e5b09b65174f2e46b84de40e4e5bcfae9645c6bb67017ca81a64d344e7345cc4bf685f11d4779aeaafd7c7163df5a67bf8410153034f4adf523385b95
7
- data.tar.gz: b57506702dbdcb387490b3c0c56d2407aff4f0f789aec01e2b1a92c9f8d925a0225b8563e3dbc1ccf3c3ca72a25ca58f4199f1738d93bdf65a07893ed2930b6d
6
+ metadata.gz: bf26484aa345df2d43a78da570027b68a7fd0dd70d7a62275ebe95a5e29ed36c11a8b1385ff0c71818f55184245e085cd75962510f9f8559aad10b0d284f8d71
7
+ data.tar.gz: 0b3913a33f61873d6e3da0d62ec643dbf49f679740d3469b7cb826083f02f3b32d37e2a20548634d09cecdc4389da7c5bdc43af530bb48956cb4084f91f10d9e
data/CHANGELOG.md CHANGED
@@ -1,6 +1,137 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ 5.0.1
5
+ ==========
6
+
7
+ Performance:
8
+
9
+ - Reduce object allocations in pipelined get response processing (#1072, #1078)
10
+ - Offset-based `ResponseBuffer`: track a read offset instead of slicing a new string after every parsed response; compact only when the consumed portion exceeds 4KB and more than half the buffer
11
+ - Inline response processor parsing: avoid intermediate array allocations from `split`-based header parsing
12
+ - Block-based `pipeline_next_responses`: yield `(key, value, cas)` directly when a block is given, avoiding per-call Hash allocation
13
+ - `PipelinedGetter`: replace Hash-based socket-to-server mapping with linear scan (faster for typical 1-5 server counts); use `Process.clock_gettime(CLOCK_MONOTONIC)` instead of `Time.now`
14
+ - Add cross-version benchmark script (`bin/compare_versions`) for reproducible performance comparisons across Dalli versions
15
+
16
+ Bug Fixes:
17
+
18
+ - Rescue `IOError` in connection manager `write`/`flush` methods (#1075)
19
+ - Prevents unhandled exceptions when a connection is closed mid-operation
20
+ - Thanks to Graham Cooper (Shopify) for this fix
21
+
22
+ Development:
23
+
24
+ - Add `rubocop-thread_safety` for detecting thread-safety issues (#1076)
25
+ - Add CONTRIBUTING.md with AI contribution policy (#1074)
26
+
27
+ 5.0.0
28
+ ==========
29
+
30
+ **Breaking Changes:**
31
+
32
+ - **Removed binary protocol** - The meta protocol is now the only supported protocol
33
+ - The `:protocol` option is no longer used
34
+ - Requires memcached 1.6+ (for meta protocol support)
35
+ - Users on older memcached versions must upgrade or stay on Dalli 4.x
36
+
37
+ - **Removed SASL authentication** - The meta protocol does not support authentication
38
+ - Use network-level security (firewall rules, VPN) or memcached's TLS support instead
39
+ - Users requiring SASL authentication must stay on Dalli 4.x with binary protocol
40
+
41
+ - **Ruby 3.3+ required** - Dropped support for Ruby 3.1 and 3.2
42
+ - Ruby 3.2 reached end-of-life in March 2026
43
+ - JRuby remains supported
44
+
45
+ Performance:
46
+
47
+ - **~7% read performance improvement** (CRuby only)
48
+ - Use native `IO#read` instead of custom `readfull` implementation
49
+ - Enabled by Ruby 3.3's `IO#timeout=` support
50
+ - JRuby continues to use `readfull` for compatibility
51
+
52
+ OpenTelemetry:
53
+
54
+ - Migrate to stable OTel semantic conventions (#1070)
55
+ - `db.system` renamed to `db.system.name`
56
+ - `db.operation` renamed to `db.operation.name`
57
+ - `server.address` now contains hostname only; `server.port` is a separate integer attribute
58
+ - `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
59
+ - Add `db.query.text` span attribute with configurable modes
60
+ - `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
61
+ - Add `peer.service` span attribute
62
+ - `:otel_peer_service` option for logical service naming
63
+
64
+ Internal:
65
+
66
+ - Simplified protocol directory structure: moved `lib/dalli/protocol/meta/*` to `lib/dalli/protocol/`
67
+ - Removed deprecated binary protocol files and SASL authentication code
68
+ - Removed `require 'set'` (autoloaded in Ruby 3.3+)
69
+
70
+ 4.3.3
71
+ ==========
72
+
73
+ Performance:
74
+
75
+ - Reduce object allocations in pipelined get response processing (#1072)
76
+ - Offset-based `ResponseBuffer`: track a read offset instead of slicing a new string after every parsed response; compact only when the consumed portion exceeds 4KB and more than half the buffer
77
+ - Inline response processor parsing: avoid intermediate array allocations from `split`-based header parsing in both binary and meta protocols
78
+ - Block-based `pipeline_next_responses`: yield `(key, value, cas)` directly when a block is given, avoiding per-call Hash allocation
79
+ - `PipelinedGetter`: replace Hash-based socket-to-server mapping with linear scan (faster for typical 1-5 server counts); use `Process.clock_gettime(CLOCK_MONOTONIC)` instead of `Time.now`
80
+ - Add cross-version benchmark script (`bin/compare_versions`) for reproducible performance comparisons across Dalli versions
81
+
82
+ Bug Fixes:
83
+
84
+ - Skip OTel integration tests when meta protocol is unavailable (#1072)
85
+
86
+ 4.3.2
87
+ ==========
88
+
89
+ OpenTelemetry:
90
+
91
+ - Migrate to stable OTel semantic conventions
92
+ - `db.system` renamed to `db.system.name`
93
+ - `db.operation` renamed to `db.operation.name`
94
+ - `server.address` now contains hostname only; `server.port` is a separate integer attribute
95
+ - `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
96
+ - Add `db.query.text` span attribute with configurable modes
97
+ - `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
98
+ - Add `peer.service` span attribute
99
+ - `:otel_peer_service` option for logical service naming
100
+
101
+ 4.3.1
102
+ ==========
103
+
104
+ Bug Fixes:
105
+
106
+ - Fix socket compatibility with gems that monkey-patch TCPSocket (#996, #1012)
107
+ - Gems like `socksify` and `resolv-replace` modify `TCPSocket#initialize`, breaking Ruby 3.0+'s `connect_timeout:` keyword argument
108
+ - Detection now uses parameter signature checking instead of gem-specific method detection
109
+ - Falls back to `Timeout.timeout` when monkey-patching is detected
110
+ - Detection result is cached for performance
111
+
112
+ - Fix network retry bug with `socket_max_failures: 0` (#1065)
113
+ - Previously, setting `socket_max_failures: 0` could still cause retries due to error handling
114
+ - Introduced `RetryableNetworkError` subclass to distinguish retryable vs non-retryable errors
115
+ - `down!` now raises non-retryable `NetworkError`, `reconnect!` raises `RetryableNetworkError`
116
+ - Thanks to Graham Cooper (Shopify) for this fix
117
+
118
+ - Fix "character class has duplicated range" Ruby warning (#1067)
119
+ - Fixed regex in `KeyManager::VALID_NAMESPACE_SEPARATORS` that caused warnings on newer Ruby versions
120
+ - Thanks to Hartley McGuire for this fix
121
+
122
+ Improvements:
123
+
124
+ - Add StrictWarnings test helper to catch Ruby warnings early (#1067)
125
+
126
+ - Use bulk attribute setter for OpenTelemetry spans (#1068)
127
+ - Reduces lock acquisitions when setting span attributes
128
+ - Thanks to Robert Laurin (Shopify) for this optimization
129
+
130
+ - Fix double recording of exceptions on OpenTelemetry spans (#1069)
131
+ - OpenTelemetry's `in_span` method already records exceptions and sets error status automatically
132
+ - Removed redundant explicit exception recording that caused exceptions to appear twice in traces
133
+ - Thanks to Robert Laurin (Shopify) for this fix
134
+
4
135
  4.3.0
5
136
  ==========
6
137
 
data/Gemfile CHANGED
@@ -22,9 +22,14 @@ group :development, :test do
22
22
  gem 'rubocop-minitest'
23
23
  gem 'rubocop-performance'
24
24
  gem 'rubocop-rake'
25
+ gem 'rubocop-thread_safety'
25
26
  gem 'simplecov'
26
27
  end
27
28
 
28
29
  group :test do
29
30
  gem 'ruby-prof', platform: :mri
31
+
32
+ # For socket compatibility testing (these gems monkey-patch TCPSocket)
33
+ gem 'resolv-replace', require: false
34
+ gem 'socksify', require: false
30
35
  end
data/README.md CHANGED
@@ -10,26 +10,14 @@ Dalli supports:
10
10
  * Fine-grained control of data serialization and compression
11
11
  * Thread-safe operation (either through use of a connection pool, or by using the Dalli client in threadsafe mode)
12
12
  * SSL/TLS connections to memcached
13
- * SASL authentication
14
13
  * OpenTelemetry distributed tracing (automatic when SDK is present)
15
14
 
16
15
  The name is a variant of Salvador Dali for his famous painting [The Persistence of Memory](http://en.wikipedia.org/wiki/The_Persistence_of_Memory).
17
16
 
18
17
  ## Requirements
19
18
 
20
- * Ruby 3.1 or later
21
- * memcached 1.4 or later (1.6+ recommended for meta protocol support)
22
-
23
- ## Protocol Options
24
-
25
- Dalli supports two protocols for communicating with memcached:
26
-
27
- * `:binary` (default) - Works with all memcached versions, supports SASL authentication
28
- * `:meta` - Requires memcached 1.6+, better performance for some operations, no authentication support
29
-
30
- ```ruby
31
- Dalli::Client.new('localhost:11211', protocol: :meta)
32
- ```
19
+ * Ruby 3.3 or later (JRuby also supported)
20
+ * memcached 1.6 or later
33
21
 
34
22
  ## Configuration Options
35
23
 
@@ -64,7 +52,7 @@ By default, Dalli uses Ruby's Marshal for serialization. Deserializing untrusted
64
52
  Dalli::Client.new('localhost:11211', serializer: JSON)
65
53
  ```
66
54
 
67
- See the [4.0-Upgrade.md](4.0-Upgrade.md) guide for more information.
55
+ See the [5.0-Upgrade.md](5.0-Upgrade.md) guide for upgrade information.
68
56
 
69
57
  ## OpenTelemetry Tracing
70
58
 
@@ -125,7 +113,7 @@ To install this gem onto your local machine, run `bundle exec rake install`.
125
113
 
126
114
  ## Contributing
127
115
 
128
- If you have a fix you wish to provide, please fork the code, fix in your local project and then send a pull request on github. Please ensure that you include a test which verifies your fix and update the [changelog](CHANGELOG.md) with a one sentence description of your fix so you get credit as a contributor.
116
+ Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute, including our policy on AI-authored contributions.
129
117
 
130
118
  ## Appreciation
131
119
 
data/lib/dalli/client.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'digest/md5'
4
- require 'set'
5
4
 
6
5
  # encoding: ascii
7
6
  module Dalli
@@ -48,15 +47,17 @@ module Dalli
48
47
  # serialization pipeline.
49
48
  # - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
50
49
  # useful for injecting a FIPS compliant hash object.
51
- # - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
52
- # to communicate with memcached.
50
+ # - :otel_db_statement - controls the +db.query.text+ span attribute when OpenTelemetry is loaded.
51
+ # +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
52
+ # +nil+ (default) omits the attribute entirely.
53
+ # - :otel_peer_service - when set, adds a +peer.service+ span attribute with this value for logical service naming.
53
54
  #
54
55
  def initialize(servers = nil, options = {})
55
56
  @normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
56
57
  @options = normalize_options(options)
58
+ warn_removed_options(@options)
57
59
  @key_manager = ::Dalli::KeyManager.new(@options)
58
60
  @ring = nil
59
- emit_deprecation_warnings
60
61
  end
61
62
 
62
63
  #
@@ -98,10 +99,7 @@ module Dalli
98
99
  end
99
100
 
100
101
  ##
101
- # Get value with extended metadata using the meta protocol.
102
- #
103
- # IMPORTANT: This method requires memcached 1.6+ and the meta protocol (protocol: :meta).
104
- # It will raise an error if used with the binary protocol.
102
+ # Get value with extended metadata.
105
103
  #
106
104
  # @param key [String] the cache key
107
105
  # @param options [Hash] options controlling what metadata to return
@@ -129,13 +127,11 @@ module Dalli
129
127
  # # => { value: "data", cas: 123, hit_before: true, last_access: 42 }
130
128
  #
131
129
  def get_with_metadata(key, options = {})
132
- raise_unless_meta_protocol!
133
-
134
130
  key = key.to_s
135
131
  key = @key_manager.validate_key(key)
136
132
 
137
- Instrumentation.trace('get_with_metadata', { 'db.operation' => 'get_with_metadata' }) do
138
- server = ring.server_for_key(key)
133
+ server = ring.server_for_key(key)
134
+ Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', key, server)) do
139
135
  server.request(:meta_get, key, options)
140
136
  end
141
137
  rescue NetworkError => e
@@ -204,9 +200,6 @@ module Dalli
204
200
  # cache entry (the "thundering herd" problem). Only one client wins the right to
205
201
  # regenerate; other clients receive the stale value (if available) or wait.
206
202
  #
207
- # IMPORTANT: This method requires memcached 1.6+ and the meta protocol (protocol: :meta).
208
- # It will raise an error if used with the binary protocol.
209
- #
210
203
  # @param key [String] the cache key
211
204
  # @param ttl [Integer] time-to-live for the cached value in seconds
212
205
  # @param lock_ttl [Integer] how long the lock/stub lives (default: 30 seconds)
@@ -232,12 +225,11 @@ module Dalli
232
225
  def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
233
226
  raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
234
227
 
235
- raise_unless_meta_protocol!
236
-
237
228
  key = key.to_s
238
229
  key = @key_manager.validate_key(key)
239
230
 
240
- Instrumentation.trace('fetch_with_lock', { 'db.operation' => 'fetch_with_lock' }) do
231
+ server = ring.server_for_key(key)
232
+ Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', key, server)) do
241
233
  fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
242
234
  end
243
235
  rescue NetworkError => e
@@ -318,10 +310,7 @@ module Dalli
318
310
  def set_multi(hash, ttl = nil, req_options = nil)
319
311
  return if hash.empty?
320
312
 
321
- Instrumentation.trace('set_multi', {
322
- 'db.operation' => 'set_multi',
323
- 'db.memcached.key_count' => hash.size
324
- }) do
313
+ Instrumentation.trace('set_multi', multi_trace_attrs('set_multi', hash.size, hash.keys)) do
325
314
  pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
326
315
  end
327
316
  end
@@ -378,10 +367,7 @@ module Dalli
378
367
  def delete_multi(keys)
379
368
  return if keys.empty?
380
369
 
381
- Instrumentation.trace('delete_multi', {
382
- 'db.operation' => 'delete_multi',
383
- 'db.memcached.key_count' => keys.size
384
- }) do
370
+ Instrumentation.trace('delete_multi', multi_trace_attrs('delete_multi', keys.size, keys)) do
385
371
  pipelined_deleter.process(keys)
386
372
  end
387
373
  end
@@ -515,15 +501,11 @@ module Dalli
515
501
 
516
502
  private
517
503
 
518
- # Records hit/miss metrics on a span for cache observability.
519
- # @param span [OpenTelemetry::Trace::Span, nil] the span to record on
520
- # @param key_count [Integer] total keys requested
521
- # @param hit_count [Integer] keys found in cache
522
504
  def record_hit_miss_metrics(span, key_count, hit_count)
523
505
  return unless span
524
506
 
525
- span.set_attribute('db.memcached.hit_count', hit_count)
526
- span.set_attribute('db.memcached.miss_count', key_count - hit_count)
507
+ span.add_attributes('db.memcached.hit_count' => hit_count,
508
+ 'db.memcached.miss_count' => key_count - hit_count)
527
509
  end
528
510
 
529
511
  def get_multi_yielding(keys)
@@ -548,7 +530,30 @@ module Dalli
548
530
  end
549
531
 
550
532
  def get_multi_attributes(keys)
551
- { 'db.operation' => 'get_multi', 'db.memcached.key_count' => keys.size }
533
+ multi_trace_attrs('get_multi', keys.size, keys)
534
+ end
535
+
536
+ def trace_attrs(operation, key, server)
537
+ attrs = { 'db.operation.name' => operation, 'server.address' => server.hostname }
538
+ attrs['server.port'] = server.port if server.socket_type == :tcp
539
+ attrs['peer.service'] = @options[:otel_peer_service] if @options[:otel_peer_service]
540
+ add_query_text(attrs, operation, key)
541
+ end
542
+
543
+ def multi_trace_attrs(operation, key_count, keys)
544
+ attrs = { 'db.operation.name' => operation, 'db.memcached.key_count' => key_count }
545
+ attrs['peer.service'] = @options[:otel_peer_service] if @options[:otel_peer_service]
546
+ add_query_text(attrs, operation, keys)
547
+ end
548
+
549
+ def add_query_text(attrs, operation, key_or_keys)
550
+ case @options[:otel_db_statement]
551
+ when :include
552
+ attrs['db.query.text'] = "#{operation} #{Array(key_or_keys).join(' ')}"
553
+ when :obfuscate
554
+ attrs['db.query.text'] = "#{operation} ?"
555
+ end
556
+ attrs
552
557
  end
553
558
 
554
559
  def check_positive!(amt)
@@ -585,16 +590,7 @@ module Dalli
585
590
  end
586
591
 
587
592
  def ring
588
- @ring ||= Dalli::Ring.new(@normalized_servers, protocol_implementation, @options)
589
- end
590
-
591
- def protocol_implementation
592
- @protocol_implementation ||= case @options[:protocol]&.to_s
593
- when 'meta'
594
- Dalli::Protocol::Meta
595
- else
596
- Dalli::Protocol::Binary
597
- end
593
+ @ring ||= Dalli::Ring.new(@normalized_servers, @options)
598
594
  end
599
595
 
600
596
  ##
@@ -605,7 +601,7 @@ module Dalli
605
601
  #
606
602
  # This method also forces retries on network errors - when
607
603
  # a particular memcached instance becomes unreachable, or the
608
- # operational times out.
604
+ # operation times out.
609
605
  ##
610
606
  def perform(*all_args)
611
607
  return yield if block_given?
@@ -616,13 +612,10 @@ module Dalli
616
612
  key = @key_manager.validate_key(key)
617
613
 
618
614
  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
615
+ Instrumentation.trace(op.to_s, trace_attrs(op.to_s, key, server)) do
623
616
  server.request(op, key, *args)
624
617
  end
625
- rescue NetworkError => e
618
+ rescue RetryableNetworkError => e
626
619
  Dalli.logger.debug { e.inspect }
627
620
  Dalli.logger.debug { 'retrying request with new server' }
628
621
  retry
@@ -635,6 +628,21 @@ module Dalli
635
628
  raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
636
629
  end
637
630
 
631
+ REMOVED_OPTIONS = {
632
+ protocol: 'Dalli 5.0 only supports the meta protocol. The :protocol option has been removed.',
633
+ username: 'Dalli 5.0 removed SASL authentication support. The :username option is ignored.',
634
+ password: 'Dalli 5.0 removed SASL authentication support. The :password option is ignored.'
635
+ }.freeze
636
+ private_constant :REMOVED_OPTIONS
637
+
638
+ def warn_removed_options(opts)
639
+ REMOVED_OPTIONS.each do |key, message|
640
+ next unless opts.key?(key)
641
+
642
+ Dalli.logger.warn(message)
643
+ end
644
+ end
645
+
638
646
  def pipelined_getter
639
647
  PipelinedGetter.new(ring, @key_manager)
640
648
  end
@@ -646,15 +654,5 @@ module Dalli
646
654
  def pipelined_deleter
647
655
  PipelinedDeleter.new(ring, @key_manager)
648
656
  end
649
-
650
- def raise_unless_meta_protocol!
651
- return if protocol_implementation == Dalli::Protocol::Meta
652
-
653
- raise Dalli::DalliError,
654
- 'This operation requires the meta protocol (memcached 1.6+). ' \
655
- 'Use protocol: :meta when creating the client.'
656
- end
657
-
658
- include ProtocolDeprecations
659
657
  end
660
658
  end
@@ -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 5.0 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.
@@ -50,11 +61,13 @@ module Dalli
50
61
  # Uses the library name 'dalli' and current Dalli::VERSION.
51
62
  #
52
63
  # @return [OpenTelemetry::Trace::Tracer, nil] the tracer or nil if OTel unavailable
64
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
53
65
  def tracer
54
66
  return @tracer if defined?(@tracer)
55
67
 
56
68
  @tracer = (OpenTelemetry.tracer_provider.tracer('dalli', Dalli::VERSION) if defined?(OpenTelemetry))
57
69
  end
70
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
58
71
 
59
72
  # Returns true if instrumentation is enabled (OpenTelemetry SDK is available).
60
73
  #
@@ -75,27 +88,24 @@ module Dalli
75
88
  # @param name [String] the span name (e.g., 'get', 'set', 'delete')
76
89
  # @param attributes [Hash] span attributes to merge with defaults.
77
90
  # Common attributes include:
78
- # - 'db.operation' - the operation name
79
- # - 'server.address' - the target server
91
+ # - 'db.operation.name' - the operation name
92
+ # - 'server.address' - the server hostname
93
+ # - 'server.port' - the server port (integer)
80
94
  # - 'db.memcached.key_count' - number of keys (for multi operations)
81
95
  # @yield the cache operation to trace
82
96
  # @return [Object] the result of the block
83
97
  # @raise [StandardError] re-raises any exception from the block
84
98
  #
85
99
  # @example Tracing a set operation
86
- # trace('set', { 'db.operation' => 'set', 'server.address' => 'localhost:11211' }) do
100
+ # trace('set', { 'db.operation.name' => 'set', 'server.address' => 'localhost', 'server.port' => 11211 }) do
87
101
  # server.set(key, value, ttl)
88
102
  # end
89
103
  #
90
104
  def trace(name, attributes = {})
91
105
  return yield unless enabled?
92
106
 
93
- tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |span|
107
+ tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |_span|
94
108
  yield
95
- rescue StandardError => e
96
- span.record_exception(e)
97
- span.status = OpenTelemetry::Trace::Status.error(e.message)
98
- raise
99
109
  end
100
110
  end
101
111
 
@@ -114,7 +124,7 @@ module Dalli
114
124
  # @raise [StandardError] re-raises any exception from the block
115
125
  #
116
126
  # @example Recording hit/miss metrics after get_multi
117
- # trace_with_result('get_multi', { 'db.operation' => 'get_multi' }) do |span|
127
+ # trace_with_result('get_multi', { 'db.operation.name' => 'get_multi' }) do |span|
118
128
  # results = fetch_from_cache(keys)
119
129
  # if span
120
130
  # span.set_attribute('db.memcached.hit_count', results.size)
@@ -123,16 +133,10 @@ module Dalli
123
133
  # results
124
134
  # end
125
135
  #
126
- def trace_with_result(name, attributes = {})
136
+ def trace_with_result(name, attributes = {}, &)
127
137
  return yield(nil) unless enabled?
128
138
 
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
139
+ tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client, &)
136
140
  end
137
141
  end
138
142
  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 =
data/lib/dalli/options.rb CHANGED
@@ -6,7 +6,7 @@ module Dalli
6
6
  # Make Dalli threadsafe by using a lock around all
7
7
  # public server methods.
8
8
  #
9
- # Dalli::Protocol::Binary.extend(Dalli::Threadsafe)
9
+ # Dalli::Protocol::Meta.extend(Dalli::Threadsafe)
10
10
  #
11
11
  module Threadsafe
12
12
  def self.extended(obj)
@@ -13,7 +13,7 @@ module Dalli
13
13
  attr_reader :pid
14
14
 
15
15
  def update!
16
- @pid = Process.pid
16
+ @pid = Process.pid # rubocop:disable ThreadSafety/ClassInstanceVariable
17
17
  end
18
18
  end
19
19
  update!