dalli 3.2.8 → 5.0.5

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +305 -0
  3. data/Gemfile +16 -2
  4. data/README.md +96 -2
  5. data/lib/dalli/client.rb +289 -28
  6. data/lib/dalli/flags.rb +15 -0
  7. data/lib/dalli/instrumentation.rb +153 -0
  8. data/lib/dalli/key_manager.rb +23 -8
  9. data/lib/dalli/options.rb +1 -1
  10. data/lib/dalli/pid_cache.rb +1 -1
  11. data/lib/dalli/pipelined_deleter.rb +82 -0
  12. data/lib/dalli/pipelined_getter.rb +44 -20
  13. data/lib/dalli/pipelined_setter.rb +87 -0
  14. data/lib/dalli/protocol/base.rb +95 -25
  15. data/lib/dalli/protocol/connection_manager.rb +37 -14
  16. data/lib/dalli/protocol/{meta/key_regularizer.rb → key_regularizer.rb} +1 -1
  17. data/lib/dalli/protocol/meta.rb +185 -20
  18. data/lib/dalli/protocol/{meta/request_formatter.rb → request_formatter.rb} +49 -16
  19. data/lib/dalli/protocol/response_buffer.rb +36 -12
  20. data/lib/dalli/protocol/{meta/response_processor.rb → response_processor.rb} +93 -37
  21. data/lib/dalli/protocol/server_config_parser.rb +2 -2
  22. data/lib/dalli/protocol/string_marshaller.rb +65 -0
  23. data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
  24. data/lib/dalli/protocol/value_compressor.rb +3 -16
  25. data/lib/dalli/protocol/value_marshaller.rb +1 -1
  26. data/lib/dalli/protocol/value_serializer.rb +61 -44
  27. data/lib/dalli/protocol.rb +10 -0
  28. data/lib/dalli/ring.rb +2 -2
  29. data/lib/dalli/servers_arg_normalizer.rb +1 -1
  30. data/lib/dalli/socket.rb +79 -14
  31. data/lib/dalli/version.rb +2 -2
  32. data/lib/dalli.rb +13 -5
  33. data/lib/rack/session/dalli.rb +43 -8
  34. metadata +27 -17
  35. data/lib/dalli/protocol/binary/request_formatter.rb +0 -117
  36. data/lib/dalli/protocol/binary/response_header.rb +0 -36
  37. data/lib/dalli/protocol/binary/response_processor.rb +0 -239
  38. data/lib/dalli/protocol/binary/sasl_authentication.rb +0 -60
  39. data/lib/dalli/protocol/binary.rb +0 -173
  40. data/lib/dalli/server.rb +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7d8b3dd9e76a224d876f901dc44c3cf8016326209df3efc7fe522053a4a3c22
4
- data.tar.gz: 0ced058a6a170cadd4b74268de5417c4a1e700baf5767fc8f260db9477e2b735
3
+ metadata.gz: 5c110d569b03993cefe140fdc4b54fb5720ef407e15c0fe100e4a49f90b5ce36
4
+ data.tar.gz: 8a3967c3d9d82dd3a28172fdb06e7f235a8ebb6598fd742e04770b458d24d041
5
5
  SHA512:
6
- metadata.gz: 3e316a4d60c3327cecec46a0a34c52536130199124035c375bf1933676d85fbf09e99a985ae44faf4e27b0fb1f8b8faef1656a812e6bdfc387406c5e18874461
7
- data.tar.gz: bce3e0e41c280e99889bea6835c1b2f701cbcb5e4f398203ae2b4d6ebc05cdc505ee5955365715f8e00441d69701ceac84de37a98eac8581d003d1ab411a33e9
6
+ metadata.gz: 486715371411ddcf9118f87296abaaf7a30597a340bba41f26d26a750268d85751fef62be52f332662d427fa79e0ed3ea9bf0e503747203f5039194875a878de
7
+ data.tar.gz: d1bdf88817ba281d7b88525b1e0579c6efefdd68cb1b437dbbc0cf4fa2f386a6d9612e579cbfc409b1c1458a05ee5b4c7af841599e1930433912a909550b84f1
data/CHANGELOG.md CHANGED
@@ -4,6 +4,311 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 5.0.5
8
+ ==========
9
+
10
+ Performance:
11
+
12
+ - Batch multi-key commands into a single write to reduce packet overhead (#1107)
13
+ - With `TCP_NODELAY` set on sockets, each `write` call emits a separate packet; the meta protocol was calling `write` up to 3 times per key in multi-key operations (`get_multi`, `set_multi`, `delete_multi`), significantly increasing network traffic compared to the old binary protocol
14
+ - Multi-key request paths now buffer all per-key commands into a single binary string and flush once; single-key paths combine the write and flush into one `flushed_write` call
15
+ - Thanks to Jean Boussier for this contribution
16
+
17
+ - Avoid repeated `RUBY_ENGINE` checks on every socket read (#1103)
18
+ - Moved the JRuby branch from a runtime `if` inside `ConnectionManager#read` to a class-level conditional method definition, so the check happens once at load time rather than on every read call
19
+ - Thanks to Jean Boussier for this contribution
20
+
21
+ - Eliminate per-call array allocations in `ResponseProcessor` (#1104)
22
+ - Token sets passed to `error_on_unexpected!` (e.g. `[VA, EN, HD]`) were allocated as new arrays on every invocation; replaced with frozen constants defined once at class load time
23
+ - Thanks to Jean Boussier for this contribution
24
+
25
+ - Avoid string copies when building request commands in `RequestFormatter` (#1106)
26
+ - Changed `cmd + TERMINATOR` to `cmd << TERMINATOR`; since `cmd` is always a mutable string, the in-place append avoids copying the entire command string just to append two bytes
27
+ - Thanks to Jean Boussier for this contribution
28
+
29
+ 5.0.4
30
+ ==========
31
+
32
+ Bug fixes:
33
+
34
+ - Fix `string_fastpath` flag collision with compression (#1099)
35
+ - `ValueSerializer::FLAG_UTF8` and `ValueCompressor::FLAG_COMPRESSED` were both `0x2`, causing `Dalli::UnmarshalError` on any UTF-8 string written with `string_fastpath: true` when compression is enabled, and silent encoding corruption for binary strings
36
+ - Introduces `Dalli::Flags` to centralise bit flag constants; UTF8 is reassigned to `0x4`
37
+ - Adds regression test covering short/long UTF-8, binary, and cross-client read scenarios
38
+ - Thanks to Jean Boussier and Mikael Henriksson for the fix and regression test
39
+
40
+ - Fix client-level `string_fastpath: true` being silently ignored (#1101)
41
+ - `Dalli::Client.new(servers, string_fastpath: true)` had no effect; the fast path was only taken when `string_fastpath: true` was passed as a per-request option on each `set` call
42
+ - Per-request option continues to take precedence over the client-level setting in both directions
43
+
44
+ 5.0.3
45
+ ==========
46
+
47
+ Performance:
48
+
49
+ - Eliminate double array allocation in `Client#perform` (#1093)
50
+ - Changed method signature from `perform(*all_args)` with destructuring to `perform(op, key, *args)`, letting Ruby decompose arguments directly without intermediate array allocations
51
+ - Reduces benchmark time by ~39% across all Dalli operations (get, set, delete, etc.)
52
+ - Thanks to Sam Obeid for this contribution
53
+
54
+ Features:
55
+
56
+ - Support `connect_timeout:` keyword argument with `resolv-replace` >= 0.2.0, which now correctly forwards keyword arguments through its `TCPSocket` patch (#1096)
57
+
58
+ - Add `Dalli::Instrumentation.disable!` to allow disabling OpenTelemetry instrumentation at runtime (#1088)
59
+ - Also exposes `Dalli::Instrumentation.tracer=` for setting a custom tracer
60
+
61
+ 5.0.2
62
+ ==========
63
+
64
+ Performance:
65
+
66
+ - Add single-server fast path for `get_multi`, `set_multi`, and `delete_multi` (#1077)
67
+ - When only one memcached server is configured, bypass the `Pipelined*` machinery (IO.select, response buffering, server grouping) and issue all quiet meta requests inline followed by a noop terminator
68
+ - `get_multi` shows ~1.5x improvement at 10 keys and ~1.75x at 100–500 keys compared to the `PipelinedGetter` path
69
+ - Thanks to Dan Mayer (Shopify) for this contribution
70
+
71
+ Development:
72
+
73
+ - Add `bin/benchmark_branch` script for benchmarking against the current branch
74
+
75
+ 5.0.1
76
+ ==========
77
+
78
+ Performance:
79
+
80
+ - Reduce object allocations in pipelined get response processing (#1072, #1078)
81
+ - 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
82
+ - Inline response processor parsing: avoid intermediate array allocations from `split`-based header parsing
83
+ - Block-based `pipeline_next_responses`: yield `(key, value, cas)` directly when a block is given, avoiding per-call Hash allocation
84
+ - `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`
85
+ - Add cross-version benchmark script (`bin/compare_versions`) for reproducible performance comparisons across Dalli versions
86
+
87
+ Bug Fixes:
88
+
89
+ - Rescue `IOError` in connection manager `write`/`flush` methods (#1075)
90
+ - Prevents unhandled exceptions when a connection is closed mid-operation
91
+ - Thanks to Graham Cooper (Shopify) for this fix
92
+
93
+ Development:
94
+
95
+ - Add `rubocop-thread_safety` for detecting thread-safety issues (#1076)
96
+ - Add CONTRIBUTING.md with AI contribution policy (#1074)
97
+
98
+ 5.0.0
99
+ ==========
100
+
101
+ **Breaking Changes:**
102
+
103
+ - **Removed binary protocol** - The meta protocol is now the only supported protocol
104
+ - The `:protocol` option is no longer used
105
+ - Requires memcached 1.6+ (for meta protocol support)
106
+ - Users on older memcached versions must upgrade or stay on Dalli 4.x
107
+
108
+ - **Removed SASL authentication** - The meta protocol does not support authentication
109
+ - Use network-level security (firewall rules, VPN) or memcached's TLS support instead
110
+ - Users requiring SASL authentication must stay on Dalli 4.x with binary protocol
111
+
112
+ - **Ruby 3.3+ required** - Dropped support for Ruby 3.1 and 3.2
113
+ - Ruby 3.2 reached end-of-life in March 2026
114
+ - JRuby remains supported
115
+
116
+ Performance:
117
+
118
+ - **~7% read performance improvement** (CRuby only)
119
+ - Use native `IO#read` instead of custom `readfull` implementation
120
+ - Enabled by Ruby 3.3's `IO#timeout=` support
121
+ - JRuby continues to use `readfull` for compatibility
122
+
123
+ OpenTelemetry:
124
+
125
+ - Migrate to stable OTel semantic conventions (#1070)
126
+ - `db.system` renamed to `db.system.name`
127
+ - `db.operation` renamed to `db.operation.name`
128
+ - `server.address` now contains hostname only; `server.port` is a separate integer attribute
129
+ - `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
130
+ - Add `db.query.text` span attribute with configurable modes
131
+ - `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
132
+ - Add `peer.service` span attribute
133
+ - `:otel_peer_service` option for logical service naming
134
+
135
+ Internal:
136
+
137
+ - Simplified protocol directory structure: moved `lib/dalli/protocol/meta/*` to `lib/dalli/protocol/`
138
+ - Removed deprecated binary protocol files and SASL authentication code
139
+ - Removed `require 'set'` (autoloaded in Ruby 3.3+)
140
+
141
+ 4.3.3
142
+ ==========
143
+
144
+ Performance:
145
+
146
+ - Reduce object allocations in pipelined get response processing (#1072)
147
+ - 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
148
+ - Inline response processor parsing: avoid intermediate array allocations from `split`-based header parsing in both binary and meta protocols
149
+ - Block-based `pipeline_next_responses`: yield `(key, value, cas)` directly when a block is given, avoiding per-call Hash allocation
150
+ - `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`
151
+ - Add cross-version benchmark script (`bin/compare_versions`) for reproducible performance comparisons across Dalli versions
152
+
153
+ Bug Fixes:
154
+
155
+ - Skip OTel integration tests when meta protocol is unavailable (#1072)
156
+
157
+ 4.3.2
158
+ ==========
159
+
160
+ OpenTelemetry:
161
+
162
+ - Migrate to stable OTel semantic conventions
163
+ - `db.system` renamed to `db.system.name`
164
+ - `db.operation` renamed to `db.operation.name`
165
+ - `server.address` now contains hostname only; `server.port` is a separate integer attribute
166
+ - `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
167
+ - Add `db.query.text` span attribute with configurable modes
168
+ - `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
169
+ - Add `peer.service` span attribute
170
+ - `:otel_peer_service` option for logical service naming
171
+
172
+ 4.3.1
173
+ ==========
174
+
175
+ Bug Fixes:
176
+
177
+ - Fix socket compatibility with gems that monkey-patch TCPSocket (#996, #1012)
178
+ - Gems like `socksify` and `resolv-replace` modify `TCPSocket#initialize`, breaking Ruby 3.0+'s `connect_timeout:` keyword argument
179
+ - Detection now uses parameter signature checking instead of gem-specific method detection
180
+ - Falls back to `Timeout.timeout` when monkey-patching is detected
181
+ - Detection result is cached for performance
182
+
183
+ - Fix network retry bug with `socket_max_failures: 0` (#1065)
184
+ - Previously, setting `socket_max_failures: 0` could still cause retries due to error handling
185
+ - Introduced `RetryableNetworkError` subclass to distinguish retryable vs non-retryable errors
186
+ - `down!` now raises non-retryable `NetworkError`, `reconnect!` raises `RetryableNetworkError`
187
+ - Thanks to Graham Cooper (Shopify) for this fix
188
+
189
+ - Fix "character class has duplicated range" Ruby warning (#1067)
190
+ - Fixed regex in `KeyManager::VALID_NAMESPACE_SEPARATORS` that caused warnings on newer Ruby versions
191
+ - Thanks to Hartley McGuire for this fix
192
+
193
+ Improvements:
194
+
195
+ - Add StrictWarnings test helper to catch Ruby warnings early (#1067)
196
+
197
+ - Use bulk attribute setter for OpenTelemetry spans (#1068)
198
+ - Reduces lock acquisitions when setting span attributes
199
+ - Thanks to Robert Laurin (Shopify) for this optimization
200
+
201
+ - Fix double recording of exceptions on OpenTelemetry spans (#1069)
202
+ - OpenTelemetry's `in_span` method already records exceptions and sets error status automatically
203
+ - Removed redundant explicit exception recording that caused exceptions to appear twice in traces
204
+ - Thanks to Robert Laurin (Shopify) for this fix
205
+
206
+ 4.3.0
207
+ ==========
208
+
209
+ New Features:
210
+
211
+ - Add `namespace_separator` option to customize the separator between namespace and key (#1019)
212
+ - Default is `:` for backward compatibility
213
+ - Must be a single non-alphanumeric character (e.g., `:`, `/`, `|`, `.`)
214
+ - Example: `Dalli::Client.new(servers, namespace: 'myapp', namespace_separator: '/')`
215
+
216
+ Bug Fixes:
217
+
218
+ - Fix architecture-dependent struct timeval packing for socket timeouts (#1034)
219
+ - Detects correct pack format for time_t and suseconds_t on each platform
220
+ - Fixes timeout issues on architectures with 64-bit time_t
221
+
222
+ - Fix get_multi hanging with large key counts (#776, #941)
223
+ - Add interleaved read/write for pipelined gets to prevent socket buffer deadlock
224
+ - For batches over 10,000 keys per server, requests are now sent in chunks
225
+
226
+ - **Breaking:** Enforce string-only values in raw mode (#1022)
227
+ - `set(key, nil, raw: true)` now raises `MarshalError` instead of storing `""`
228
+ - `set(key, 123, raw: true)` now raises `MarshalError` instead of storing `"123"`
229
+ - This matches the behavior of client-level `raw: true` mode
230
+ - To store counters, use string values: `set('counter', '0', raw: true)`
231
+
232
+ CI:
233
+
234
+ - Add TruffleRuby to CI test matrix (#988)
235
+
236
+ 4.2.0
237
+ ==========
238
+
239
+ Performance:
240
+
241
+ - Buffered I/O: Use `socket.sync = false` with explicit flush to reduce syscalls for pipelined operations
242
+ - get_multi optimizations: Use Set for O(1) server tracking lookups
243
+ - Raw mode optimization: Skip bitflags request in meta protocol when in raw mode (saves 2 bytes per request)
244
+
245
+ New Features:
246
+
247
+ - OpenTelemetry tracing support: Automatically instruments operations when OpenTelemetry SDK is present
248
+ - Zero overhead when OpenTelemetry is not loaded
249
+ - Traces `get`, `set`, `delete`, `get_multi`, `set_multi`, `delete_multi`, `get_with_metadata`, and `fetch_with_lock`
250
+ - Spans include `db.system: memcached` and `db.operation` attributes
251
+ - Single-key operations include `server.address` attribute
252
+ - Multi-key operations include `db.memcached.key_count` attribute
253
+ - `get_multi` spans include `db.memcached.hit_count` and `db.memcached.miss_count` for cache efficiency metrics
254
+ - Exceptions are automatically recorded on spans with error status
255
+
256
+ 4.1.0
257
+ ==========
258
+
259
+ New Features:
260
+
261
+ - Add `set_multi` for efficient bulk set operations using pipelined requests
262
+ - Add `delete_multi` for efficient bulk delete operations using pipelined requests
263
+ - Add `fetch_with_lock` for thundering herd protection using meta protocol's vivify/recache flags (requires memcached 1.6+)
264
+ - Add thundering herd protection support to meta protocol (requires memcached 1.6+):
265
+ - `N` (vivify) flag for creating stubs on cache miss
266
+ - `R` (recache) flag for winning recache race when TTL is below threshold
267
+ - Response flags `W` (won recache), `X` (stale), `Z` (lost race)
268
+ - `delete_stale` method for marking items as stale instead of deleting
269
+ - Add `get_with_metadata` for advanced cache operations with metadata retrieval (requires memcached 1.6+):
270
+ - Returns hash with `:value`, `:cas`, `:won_recache`, `:stale`, `:lost_recache`
271
+ - Optional `:return_hit_status` returns `:hit_before` (true/false for previous access)
272
+ - Optional `:return_last_access` returns `:last_access` (seconds since last access)
273
+ - Optional `:skip_lru_bump` prevents LRU update on access
274
+ - Optional `:vivify_ttl` and `:recache_ttl` for thundering herd protection
275
+
276
+ Deprecations:
277
+
278
+ - Binary protocol is deprecated and will be removed in Dalli 5.0. Use `protocol: :meta` instead (requires memcached 1.6+)
279
+ - SASL authentication is deprecated and will be removed in Dalli 5.0. Consider using network-level security or memcached's TLS support
280
+
281
+ 4.0.1
282
+ ==========
283
+
284
+ - Add `:raw` client option to skip serialization entirely, returning raw byte strings
285
+ - Handle `OpenSSL::SSL::SSLError` in connection manager
286
+
287
+ 4.0.0
288
+ ==========
289
+
290
+ BREAKING CHANGES:
291
+
292
+ - Require Ruby 3.1+ (dropped support for Ruby 2.6, 2.7, and 3.0)
293
+ - Removed `Dalli::Server` deprecated alias - use `Dalli::Protocol::Binary` instead
294
+ - Removed `:compression` option - use `:compress` instead
295
+ - Removed `close_on_fork` method - use `reconnect_on_fork` instead
296
+
297
+ Other changes:
298
+
299
+ - Add security warning when using default Marshal serializer (silence with `silence_marshal_warning: true`)
300
+ - Add defense-in-depth input validation for stats command arguments
301
+ - Add `string_fastpath` option to skip serialization for simple strings (byroot)
302
+ - Meta protocol set performance improvement (danmayer)
303
+ - Fix connection_pool 3.0 compatibility for Rack session store
304
+ - Fix session recovery after deletion (stengineering0)
305
+ - Fix cannot read response data included terminator `\r\n` when use meta protocol (matsubara0507)
306
+ - Support SERVER_ERROR response from Memcached as per the [memcached spec](https://github.com/memcached/memcached/blob/e43364402195c8e822bb8f88755a60ab8bbed62a/doc/protocol.txt#L172) (grcooper)
307
+ - Update Socket timeout handling to use Socket#timeout= when available (nickamorim)
308
+ - Serializer: reraise all .load errors as UnmarshalError (olleolleolle)
309
+ - Reconnect gracefully when a fork is detected instead of crashing (PatrickTulskie)
310
+ - Update CI to test against memcached 1.6.40
311
+
7
312
  3.2.8
8
313
  ==========
9
314
 
data/Gemfile CHANGED
@@ -5,17 +5,31 @@ source 'https://rubygems.org'
5
5
  gemspec
6
6
 
7
7
  group :development, :test do
8
+ gem 'benchmark'
9
+ gem 'cgi'
8
10
  gem 'connection_pool'
9
- gem 'minitest', '~> 5'
10
- gem 'rack', '~> 2.0', '>= 2.2.0'
11
+ gem 'debug' unless RUBY_PLATFORM == 'java'
12
+ if RUBY_VERSION >= '3.2'
13
+ gem 'minitest', '~> 6'
14
+ gem 'minitest-mock'
15
+ else
16
+ gem 'minitest', '~> 5'
17
+ end
18
+ gem 'rack', '~> 3'
19
+ gem 'rack-session'
11
20
  gem 'rake', '~> 13.0'
12
21
  gem 'rubocop'
13
22
  gem 'rubocop-minitest'
14
23
  gem 'rubocop-performance'
15
24
  gem 'rubocop-rake'
25
+ gem 'rubocop-thread_safety'
16
26
  gem 'simplecov'
17
27
  end
18
28
 
19
29
  group :test do
20
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
21
35
  end
data/README.md CHANGED
@@ -10,10 +10,104 @@ 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
13
+ * OpenTelemetry distributed tracing (automatic when SDK is present)
14
14
 
15
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).
16
16
 
17
+ ## Requirements
18
+
19
+ * Ruby 3.3 or later (JRuby also supported)
20
+ * memcached 1.6 or later
21
+
22
+ ## Configuration Options
23
+
24
+ ### Namespace
25
+
26
+ Use namespaces to partition your cache and avoid key collisions between different applications or environments:
27
+
28
+ ```ruby
29
+ # All keys will be prefixed with "myapp:"
30
+ Dalli::Client.new('localhost:11211', namespace: 'myapp')
31
+
32
+ # Dynamic namespace using a Proc (evaluated on each operation)
33
+ Dalli::Client.new('localhost:11211', namespace: -> { "tenant:#{Thread.current[:tenant_id]}" })
34
+ ```
35
+
36
+ ### Namespace Separator
37
+
38
+ By default, the namespace and key are joined with a colon (`:`). You can customize this with the `namespace_separator` option:
39
+
40
+ ```ruby
41
+ # Keys will be prefixed with "myapp/" instead of "myapp:"
42
+ Dalli::Client.new('localhost:11211', namespace: 'myapp', namespace_separator: '/')
43
+ ```
44
+
45
+ The separator must be a single non-alphanumeric character. Valid examples: `:`, `/`, `|`, `.`, `-`, `_`, `#`
46
+
47
+ ## Security Note
48
+
49
+ By default, Dalli uses Ruby's Marshal for serialization. Deserializing untrusted data with Marshal can lead to remote code execution. If you cache user-controlled data, consider using a safer serializer:
50
+
51
+ ```ruby
52
+ Dalli::Client.new('localhost:11211', serializer: JSON)
53
+ ```
54
+
55
+ See the [5.0-Upgrade.md](5.0-Upgrade.md) guide for upgrade information.
56
+
57
+ ## OpenTelemetry Tracing
58
+
59
+ Dalli automatically instruments operations with [OpenTelemetry](https://opentelemetry.io/) when the SDK is present. No configuration is required - just add the OpenTelemetry gems to your application:
60
+
61
+ ```ruby
62
+ # Gemfile
63
+ gem 'opentelemetry-sdk'
64
+ gem 'opentelemetry-exporter-otlp' # or your preferred exporter
65
+ ```
66
+
67
+ When OpenTelemetry is loaded, Dalli creates spans for:
68
+ - Single key operations: `get`, `set`, `delete`, `add`, `replace`, `incr`, `decr`, etc.
69
+ - Multi-key operations: `get_multi`, `set_multi`, `delete_multi`
70
+ - Advanced operations: `get_with_metadata`, `fetch_with_lock`
71
+
72
+ ### Span Attributes
73
+
74
+ All spans include:
75
+ - `db.system`: `memcached`
76
+ - `db.operation`: The operation name (e.g., `get`, `set_multi`)
77
+
78
+ Single-key operations also include:
79
+ - `server.address`: The memcached server that handled the request (e.g., `localhost:11211`)
80
+
81
+ Multi-key operations include cache efficiency metrics:
82
+ - `db.memcached.key_count`: Number of keys in the request
83
+ - `db.memcached.hit_count`: Number of keys found (for `get_multi`)
84
+ - `db.memcached.miss_count`: Number of keys not found (for `get_multi`)
85
+
86
+ ### Error Handling
87
+
88
+ Exceptions are automatically recorded on spans with error status. When an operation fails:
89
+ 1. The exception is recorded on the span via `span.record_exception(e)`
90
+ 2. The span status is set to error with the exception message
91
+ 3. The exception is re-raised to the caller
92
+
93
+ ### Disabling Instrumentation
94
+
95
+ To disable instrumentation at runtime (e.g., in tests or specific environments):
96
+
97
+ ```ruby
98
+ Dalli::Instrumentation.disable!
99
+ ```
100
+
101
+ You can also assign a custom tracer directly:
102
+
103
+ ```ruby
104
+ Dalli::Instrumentation.tracer = my_custom_tracer
105
+ ```
106
+
107
+ ### Zero Overhead
108
+
109
+ When OpenTelemetry is not present, there is zero overhead - the tracing code checks once at startup and bypasses all instrumentation logic entirely when the SDK is not loaded.
110
+
17
111
  ![Persistence of Memory](https://upload.wikimedia.org/wikipedia/en/d/dd/The_Persistence_of_Memory.jpg)
18
112
 
19
113
 
@@ -33,7 +127,7 @@ To install this gem onto your local machine, run `bundle exec rake install`.
33
127
 
34
128
  ## Contributing
35
129
 
36
- 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.
130
+ Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to contribute, including our policy on AI-authored contributions.
37
131
 
38
132
  ## Appreciation
39
133