dalli 4.0.1 → 4.2.0
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/CHANGELOG.md +45 -0
- data/README.md +41 -0
- data/lib/dalli/client.rb +217 -6
- data/lib/dalli/instrumentation.rb +139 -0
- data/lib/dalli/pipelined_deleter.rb +82 -0
- data/lib/dalli/pipelined_getter.rb +5 -3
- data/lib/dalli/pipelined_setter.rb +87 -0
- data/lib/dalli/protocol/base.rb +8 -1
- data/lib/dalli/protocol/binary.rb +26 -0
- data/lib/dalli/protocol/connection_manager.rb +7 -0
- data/lib/dalli/protocol/meta/request_formatter.rb +37 -3
- data/lib/dalli/protocol/meta/response_processor.rb +50 -0
- data/lib/dalli/protocol/meta.rb +92 -5
- data/lib/dalli/protocol_deprecations.rb +45 -0
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +4 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7966ac393d9c0f41d61b244c6a5832fbcab5705ed4daa4189b6e1ffc92db600f
|
|
4
|
+
data.tar.gz: da320eac7a8d553f33fe81315750297f97edb75c6da79197ef1f974d47ac8081
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d29cfbe8d2ee6e2993e8a292238f2762ac1de29b8d8623f0a521f810edf866ffd9d631905d5b7cb939d44216fb7e8d6edabf4c09ba388162fef97a772254352
|
|
7
|
+
data.tar.gz: ef1c636e6aab12a630ddb7e4d7081c29b0e257203b97f78ccde207170802d1c3619db7057093d4140fcc9cbc23f979bf6f60be4a64011bde983062a724ea59fc
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
Dalli Changelog
|
|
2
2
|
=====================
|
|
3
3
|
|
|
4
|
+
4.2.0
|
|
5
|
+
==========
|
|
6
|
+
|
|
7
|
+
Performance:
|
|
8
|
+
|
|
9
|
+
- Buffered I/O: Use `socket.sync = false` with explicit flush to reduce syscalls for pipelined operations
|
|
10
|
+
- get_multi optimizations: Use Set for O(1) server tracking lookups
|
|
11
|
+
- Raw mode optimization: Skip bitflags request in meta protocol when in raw mode (saves 2 bytes per request)
|
|
12
|
+
|
|
13
|
+
New Features:
|
|
14
|
+
|
|
15
|
+
- OpenTelemetry tracing support: Automatically instruments operations when OpenTelemetry SDK is present
|
|
16
|
+
- Zero overhead when OpenTelemetry is not loaded
|
|
17
|
+
- Traces `get`, `set`, `delete`, `get_multi`, `set_multi`, `delete_multi`, `get_with_metadata`, and `fetch_with_lock`
|
|
18
|
+
- Spans include `db.system: memcached` and `db.operation` attributes
|
|
19
|
+
- Single-key operations include `server.address` attribute
|
|
20
|
+
- Multi-key operations include `db.memcached.key_count` attribute
|
|
21
|
+
- `get_multi` spans include `db.memcached.hit_count` and `db.memcached.miss_count` for cache efficiency metrics
|
|
22
|
+
- Exceptions are automatically recorded on spans with error status
|
|
23
|
+
|
|
24
|
+
4.1.0
|
|
25
|
+
==========
|
|
26
|
+
|
|
27
|
+
New Features:
|
|
28
|
+
|
|
29
|
+
- Add `set_multi` for efficient bulk set operations using pipelined requests
|
|
30
|
+
- Add `delete_multi` for efficient bulk delete operations using pipelined requests
|
|
31
|
+
- Add `fetch_with_lock` for thundering herd protection using meta protocol's vivify/recache flags (requires memcached 1.6+)
|
|
32
|
+
- Add thundering herd protection support to meta protocol (requires memcached 1.6+):
|
|
33
|
+
- `N` (vivify) flag for creating stubs on cache miss
|
|
34
|
+
- `R` (recache) flag for winning recache race when TTL is below threshold
|
|
35
|
+
- Response flags `W` (won recache), `X` (stale), `Z` (lost race)
|
|
36
|
+
- `delete_stale` method for marking items as stale instead of deleting
|
|
37
|
+
- Add `get_with_metadata` for advanced cache operations with metadata retrieval (requires memcached 1.6+):
|
|
38
|
+
- Returns hash with `:value`, `:cas`, `:won_recache`, `:stale`, `:lost_recache`
|
|
39
|
+
- Optional `:return_hit_status` returns `:hit_before` (true/false for previous access)
|
|
40
|
+
- Optional `:return_last_access` returns `:last_access` (seconds since last access)
|
|
41
|
+
- Optional `:skip_lru_bump` prevents LRU update on access
|
|
42
|
+
- Optional `:vivify_ttl` and `:recache_ttl` for thundering herd protection
|
|
43
|
+
|
|
44
|
+
Deprecations:
|
|
45
|
+
|
|
46
|
+
- Binary protocol is deprecated and will be removed in Dalli 5.0. Use `protocol: :meta` instead (requires memcached 1.6+)
|
|
47
|
+
- SASL authentication is deprecated and will be removed in Dalli 5.0. Consider using network-level security or memcached's TLS support
|
|
48
|
+
|
|
4
49
|
4.0.1
|
|
5
50
|
==========
|
|
6
51
|
|
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Dalli supports:
|
|
|
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
13
|
* SASL authentication
|
|
14
|
+
* OpenTelemetry distributed tracing (automatic when SDK is present)
|
|
14
15
|
|
|
15
16
|
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
17
|
|
|
@@ -40,6 +41,46 @@ Dalli::Client.new('localhost:11211', serializer: JSON)
|
|
|
40
41
|
|
|
41
42
|
See the [4.0-Upgrade.md](4.0-Upgrade.md) guide for more information.
|
|
42
43
|
|
|
44
|
+
## OpenTelemetry Tracing
|
|
45
|
+
|
|
46
|
+
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:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# Gemfile
|
|
50
|
+
gem 'opentelemetry-sdk'
|
|
51
|
+
gem 'opentelemetry-exporter-otlp' # or your preferred exporter
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When OpenTelemetry is loaded, Dalli creates spans for:
|
|
55
|
+
- Single key operations: `get`, `set`, `delete`, `add`, `replace`, `incr`, `decr`, etc.
|
|
56
|
+
- Multi-key operations: `get_multi`, `set_multi`, `delete_multi`
|
|
57
|
+
- Advanced operations: `get_with_metadata`, `fetch_with_lock`
|
|
58
|
+
|
|
59
|
+
### Span Attributes
|
|
60
|
+
|
|
61
|
+
All spans include:
|
|
62
|
+
- `db.system`: `memcached`
|
|
63
|
+
- `db.operation`: The operation name (e.g., `get`, `set_multi`)
|
|
64
|
+
|
|
65
|
+
Single-key operations also include:
|
|
66
|
+
- `server.address`: The memcached server that handled the request (e.g., `localhost:11211`)
|
|
67
|
+
|
|
68
|
+
Multi-key operations include cache efficiency metrics:
|
|
69
|
+
- `db.memcached.key_count`: Number of keys in the request
|
|
70
|
+
- `db.memcached.hit_count`: Number of keys found (for `get_multi`)
|
|
71
|
+
- `db.memcached.miss_count`: Number of keys not found (for `get_multi`)
|
|
72
|
+
|
|
73
|
+
### Error Handling
|
|
74
|
+
|
|
75
|
+
Exceptions are automatically recorded on spans with error status. When an operation fails:
|
|
76
|
+
1. The exception is recorded on the span via `span.record_exception(e)`
|
|
77
|
+
2. The span status is set to error with the exception message
|
|
78
|
+
3. The exception is re-raised to the caller
|
|
79
|
+
|
|
80
|
+
### Zero Overhead
|
|
81
|
+
|
|
82
|
+
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.
|
|
83
|
+
|
|
43
84
|

|
|
44
85
|
|
|
45
86
|
|
data/lib/dalli/client.rb
CHANGED
|
@@ -56,6 +56,7 @@ module Dalli
|
|
|
56
56
|
@options = normalize_options(options)
|
|
57
57
|
@key_manager = ::Dalli::KeyManager.new(@options)
|
|
58
58
|
@ring = nil
|
|
59
|
+
emit_deprecation_warnings
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
#
|
|
@@ -96,24 +97,70 @@ module Dalli
|
|
|
96
97
|
yield value, cas
|
|
97
98
|
end
|
|
98
99
|
|
|
100
|
+
##
|
|
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.
|
|
105
|
+
#
|
|
106
|
+
# @param key [String] the cache key
|
|
107
|
+
# @param options [Hash] options controlling what metadata to return
|
|
108
|
+
# - :return_cas [Boolean] return the CAS value (default: true)
|
|
109
|
+
# - :return_hit_status [Boolean] return whether item was previously accessed
|
|
110
|
+
# - :return_last_access [Boolean] return seconds since last access
|
|
111
|
+
# - :skip_lru_bump [Boolean] don't bump LRU or update access stats
|
|
112
|
+
#
|
|
113
|
+
# @return [Hash] containing:
|
|
114
|
+
# - :value - the cached value (or nil on miss)
|
|
115
|
+
# - :cas - the CAS value
|
|
116
|
+
# - :hit_before - true/false if previously accessed (only if return_hit_status: true)
|
|
117
|
+
# - :last_access - seconds since last access (only if return_last_access: true)
|
|
118
|
+
#
|
|
119
|
+
# @example Get with hit status
|
|
120
|
+
# result = client.get_with_metadata('key', return_hit_status: true)
|
|
121
|
+
# # => { value: "data", cas: 123, hit_before: true }
|
|
122
|
+
#
|
|
123
|
+
# @example Get with all metadata without affecting LRU
|
|
124
|
+
# result = client.get_with_metadata('key',
|
|
125
|
+
# return_hit_status: true,
|
|
126
|
+
# return_last_access: true,
|
|
127
|
+
# skip_lru_bump: true
|
|
128
|
+
# )
|
|
129
|
+
# # => { value: "data", cas: 123, hit_before: true, last_access: 42 }
|
|
130
|
+
#
|
|
131
|
+
def get_with_metadata(key, options = {})
|
|
132
|
+
raise_unless_meta_protocol!
|
|
133
|
+
|
|
134
|
+
key = key.to_s
|
|
135
|
+
key = @key_manager.validate_key(key)
|
|
136
|
+
|
|
137
|
+
Instrumentation.trace('get_with_metadata', { 'db.operation' => 'get_with_metadata' }) do
|
|
138
|
+
server = ring.server_for_key(key)
|
|
139
|
+
server.request(:meta_get, key, options)
|
|
140
|
+
end
|
|
141
|
+
rescue NetworkError => e
|
|
142
|
+
Dalli.logger.debug { e.inspect }
|
|
143
|
+
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
144
|
+
retry
|
|
145
|
+
end
|
|
146
|
+
|
|
99
147
|
##
|
|
100
148
|
# Fetch multiple keys efficiently.
|
|
101
149
|
# If a block is given, yields key/value pairs one at a time.
|
|
102
150
|
# Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
|
|
151
|
+
# rubocop:disable Style/ExplicitBlockArgument
|
|
103
152
|
def get_multi(*keys)
|
|
104
153
|
keys.flatten!
|
|
105
154
|
keys.compact!
|
|
106
|
-
|
|
107
155
|
return {} if keys.empty?
|
|
108
156
|
|
|
109
157
|
if block_given?
|
|
110
|
-
|
|
158
|
+
get_multi_yielding(keys) { |k, v| yield k, v }
|
|
111
159
|
else
|
|
112
|
-
|
|
113
|
-
pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
|
|
114
|
-
end
|
|
160
|
+
get_multi_hash(keys)
|
|
115
161
|
end
|
|
116
162
|
end
|
|
163
|
+
# rubocop:enable Style/ExplicitBlockArgument
|
|
117
164
|
|
|
118
165
|
##
|
|
119
166
|
# Fetch multiple keys efficiently, including available metadata such as CAS.
|
|
@@ -149,6 +196,56 @@ module Dalli
|
|
|
149
196
|
new_val
|
|
150
197
|
end
|
|
151
198
|
|
|
199
|
+
##
|
|
200
|
+
# Fetch the value with thundering herd protection using the meta protocol's
|
|
201
|
+
# N (vivify) and R (recache) flags.
|
|
202
|
+
#
|
|
203
|
+
# This method prevents multiple clients from simultaneously regenerating the same
|
|
204
|
+
# cache entry (the "thundering herd" problem). Only one client wins the right to
|
|
205
|
+
# regenerate; other clients receive the stale value (if available) or wait.
|
|
206
|
+
#
|
|
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
|
+
# @param key [String] the cache key
|
|
211
|
+
# @param ttl [Integer] time-to-live for the cached value in seconds
|
|
212
|
+
# @param lock_ttl [Integer] how long the lock/stub lives (default: 30 seconds)
|
|
213
|
+
# This is the maximum time other clients will return stale data while
|
|
214
|
+
# waiting for regeneration. Should be longer than your expected regeneration time.
|
|
215
|
+
# @param recache_threshold [Integer, nil] if set, win the recache race when the
|
|
216
|
+
# item's remaining TTL is below this threshold. Useful for proactive recaching.
|
|
217
|
+
# @param req_options [Hash] options passed to set operations (e.g., raw: true)
|
|
218
|
+
#
|
|
219
|
+
# @yield Block to regenerate the value (only called if this client won the race)
|
|
220
|
+
# @return [Object] the cached value (may be stale if another client is regenerating)
|
|
221
|
+
#
|
|
222
|
+
# @example Basic usage
|
|
223
|
+
# client.fetch_with_lock('expensive_key', ttl: 300, lock_ttl: 30) do
|
|
224
|
+
# expensive_database_query
|
|
225
|
+
# end
|
|
226
|
+
#
|
|
227
|
+
# @example With proactive recaching (recache before expiry)
|
|
228
|
+
# client.fetch_with_lock('key', ttl: 300, lock_ttl: 30, recache_threshold: 60) do
|
|
229
|
+
# expensive_operation
|
|
230
|
+
# end
|
|
231
|
+
#
|
|
232
|
+
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
|
|
233
|
+
raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
|
|
234
|
+
|
|
235
|
+
raise_unless_meta_protocol!
|
|
236
|
+
|
|
237
|
+
key = key.to_s
|
|
238
|
+
key = @key_manager.validate_key(key)
|
|
239
|
+
|
|
240
|
+
Instrumentation.trace('fetch_with_lock', { 'db.operation' => 'fetch_with_lock' }) do
|
|
241
|
+
fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
242
|
+
end
|
|
243
|
+
rescue NetworkError => e
|
|
244
|
+
Dalli.logger.debug { e.inspect }
|
|
245
|
+
Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
|
|
246
|
+
retry
|
|
247
|
+
end
|
|
248
|
+
|
|
152
249
|
##
|
|
153
250
|
# compare and swap values using optimistic locking.
|
|
154
251
|
# Fetch the existing value for key.
|
|
@@ -206,6 +303,29 @@ module Dalli
|
|
|
206
303
|
set_cas(key, value, 0, ttl, req_options)
|
|
207
304
|
end
|
|
208
305
|
|
|
306
|
+
##
|
|
307
|
+
# Set multiple keys and values efficiently using pipelining.
|
|
308
|
+
# This method is more efficient than calling set() in a loop because
|
|
309
|
+
# it batches requests by server and uses quiet mode.
|
|
310
|
+
#
|
|
311
|
+
# @param hash [Hash] key-value pairs to set
|
|
312
|
+
# @param ttl [Integer] time-to-live in seconds (optional, uses default if not provided)
|
|
313
|
+
# @param req_options [Hash] options passed to each set operation
|
|
314
|
+
# @return [void]
|
|
315
|
+
#
|
|
316
|
+
# Example:
|
|
317
|
+
# client.set_multi({ 'key1' => 'value1', 'key2' => 'value2' }, 300)
|
|
318
|
+
def set_multi(hash, ttl = nil, req_options = nil)
|
|
319
|
+
return if hash.empty?
|
|
320
|
+
|
|
321
|
+
Instrumentation.trace('set_multi', {
|
|
322
|
+
'db.operation' => 'set_multi',
|
|
323
|
+
'db.memcached.key_count' => hash.size
|
|
324
|
+
}) do
|
|
325
|
+
pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
209
329
|
##
|
|
210
330
|
# Set the key-value pair, verifying existing CAS.
|
|
211
331
|
# Returns the resulting CAS value if succeeded, and falsy otherwise.
|
|
@@ -245,6 +365,27 @@ module Dalli
|
|
|
245
365
|
delete_cas(key, 0)
|
|
246
366
|
end
|
|
247
367
|
|
|
368
|
+
##
|
|
369
|
+
# Delete multiple keys efficiently using pipelining.
|
|
370
|
+
# This method is more efficient than calling delete() in a loop because
|
|
371
|
+
# it batches requests by server and uses quiet mode.
|
|
372
|
+
#
|
|
373
|
+
# @param keys [Array<String>] keys to delete
|
|
374
|
+
# @return [void]
|
|
375
|
+
#
|
|
376
|
+
# Example:
|
|
377
|
+
# client.delete_multi(['key1', 'key2', 'key3'])
|
|
378
|
+
def delete_multi(keys)
|
|
379
|
+
return if keys.empty?
|
|
380
|
+
|
|
381
|
+
Instrumentation.trace('delete_multi', {
|
|
382
|
+
'db.operation' => 'delete_multi',
|
|
383
|
+
'db.memcached.key_count' => keys.size
|
|
384
|
+
}) do
|
|
385
|
+
pipelined_deleter.process(keys)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
248
389
|
##
|
|
249
390
|
# Append value to the value already stored on the server for 'key'.
|
|
250
391
|
# Appending only works for values stored with :raw => true.
|
|
@@ -374,6 +515,42 @@ module Dalli
|
|
|
374
515
|
|
|
375
516
|
private
|
|
376
517
|
|
|
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
|
+
def record_hit_miss_metrics(span, key_count, hit_count)
|
|
523
|
+
return unless span
|
|
524
|
+
|
|
525
|
+
span.set_attribute('db.memcached.hit_count', hit_count)
|
|
526
|
+
span.set_attribute('db.memcached.miss_count', key_count - hit_count)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def get_multi_yielding(keys)
|
|
530
|
+
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
531
|
+
hit_count = 0
|
|
532
|
+
pipelined_getter.process(keys) do |k, data|
|
|
533
|
+
hit_count += 1
|
|
534
|
+
yield k, data.first
|
|
535
|
+
end
|
|
536
|
+
record_hit_miss_metrics(span, keys.size, hit_count)
|
|
537
|
+
nil
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def get_multi_hash(keys)
|
|
542
|
+
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
543
|
+
{}.tap do |hash|
|
|
544
|
+
pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
|
|
545
|
+
record_hit_miss_metrics(span, keys.size, hash.size)
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def get_multi_attributes(keys)
|
|
551
|
+
{ 'db.operation' => 'get_multi', 'db.memcached.key_count' => keys.size }
|
|
552
|
+
end
|
|
553
|
+
|
|
377
554
|
def check_positive!(amt)
|
|
378
555
|
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
|
379
556
|
end
|
|
@@ -386,6 +563,17 @@ module Dalli
|
|
|
386
563
|
perform(:set, key, newvalue, ttl_or_default(ttl), cas, req_options)
|
|
387
564
|
end
|
|
388
565
|
|
|
566
|
+
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
567
|
+
server = ring.server_for_key(key)
|
|
568
|
+
result = server.request(:meta_get, key, { vivify_ttl: lock_ttl, recache_ttl: recache_threshold })
|
|
569
|
+
|
|
570
|
+
return result[:value] unless result[:won_recache]
|
|
571
|
+
|
|
572
|
+
new_val = yield
|
|
573
|
+
set(key, new_val, ttl_or_default(ttl), req_options)
|
|
574
|
+
new_val
|
|
575
|
+
end
|
|
576
|
+
|
|
389
577
|
##
|
|
390
578
|
# Uses the argument TTL or the client-wide default. Ensures
|
|
391
579
|
# that the value is an integer
|
|
@@ -428,7 +616,12 @@ module Dalli
|
|
|
428
616
|
key = @key_manager.validate_key(key)
|
|
429
617
|
|
|
430
618
|
server = ring.server_for_key(key)
|
|
431
|
-
|
|
619
|
+
Instrumentation.trace(op.to_s, {
|
|
620
|
+
'db.operation' => op.to_s,
|
|
621
|
+
'server.address' => server.name
|
|
622
|
+
}) do
|
|
623
|
+
server.request(op, key, *args)
|
|
624
|
+
end
|
|
432
625
|
rescue NetworkError => e
|
|
433
626
|
Dalli.logger.debug { e.inspect }
|
|
434
627
|
Dalli.logger.debug { 'retrying request with new server' }
|
|
@@ -445,5 +638,23 @@ module Dalli
|
|
|
445
638
|
def pipelined_getter
|
|
446
639
|
PipelinedGetter.new(ring, @key_manager)
|
|
447
640
|
end
|
|
641
|
+
|
|
642
|
+
def pipelined_setter
|
|
643
|
+
PipelinedSetter.new(ring, @key_manager)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def pipelined_deleter
|
|
647
|
+
PipelinedDeleter.new(ring, @key_manager)
|
|
648
|
+
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
|
|
448
659
|
end
|
|
449
660
|
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# Instrumentation support for Dalli. Provides hooks for distributed tracing
|
|
6
|
+
# via OpenTelemetry when the SDK is available.
|
|
7
|
+
#
|
|
8
|
+
# When OpenTelemetry is loaded, Dalli automatically creates spans for cache operations.
|
|
9
|
+
# When OpenTelemetry is not available, all tracing methods are no-ops with zero overhead.
|
|
10
|
+
#
|
|
11
|
+
# == Span Attributes
|
|
12
|
+
#
|
|
13
|
+
# All spans include the following default attributes:
|
|
14
|
+
# - +db.system+ - Always "memcached"
|
|
15
|
+
#
|
|
16
|
+
# 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
|
+
#
|
|
20
|
+
# Multi-key operations (+get_multi+) add:
|
|
21
|
+
# - +db.operation+ - "get_multi"
|
|
22
|
+
# - +db.memcached.key_count+ - Number of keys requested
|
|
23
|
+
# - +db.memcached.hit_count+ - Number of keys found in cache
|
|
24
|
+
# - +db.memcached.miss_count+ - Number of keys not found
|
|
25
|
+
#
|
|
26
|
+
# Bulk write operations (+set_multi+, +delete_multi+) add:
|
|
27
|
+
# - +db.operation+ - The operation name
|
|
28
|
+
# - +db.memcached.key_count+ - Number of keys in the operation
|
|
29
|
+
#
|
|
30
|
+
# == Error Handling
|
|
31
|
+
#
|
|
32
|
+
# When an exception occurs during a traced operation:
|
|
33
|
+
# - The exception is recorded on the span via +record_exception+
|
|
34
|
+
# - The span status is set to error with the exception message
|
|
35
|
+
# - The exception is re-raised to the caller
|
|
36
|
+
#
|
|
37
|
+
# @example Checking if tracing is enabled
|
|
38
|
+
# Dalli::Instrumentation.enabled? # => true if OpenTelemetry is loaded
|
|
39
|
+
#
|
|
40
|
+
##
|
|
41
|
+
module Instrumentation
|
|
42
|
+
# Default attributes included on all memcached spans.
|
|
43
|
+
# @return [Hash] frozen hash with 'db.system' => 'memcached'
|
|
44
|
+
DEFAULT_ATTRIBUTES = { 'db.system' => 'memcached' }.freeze
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
# Returns the OpenTelemetry tracer if available, nil otherwise.
|
|
48
|
+
#
|
|
49
|
+
# The tracer is cached after first lookup for performance.
|
|
50
|
+
# Uses the library name 'dalli' and current Dalli::VERSION.
|
|
51
|
+
#
|
|
52
|
+
# @return [OpenTelemetry::Trace::Tracer, nil] the tracer or nil if OTel unavailable
|
|
53
|
+
def tracer
|
|
54
|
+
return @tracer if defined?(@tracer)
|
|
55
|
+
|
|
56
|
+
@tracer = (OpenTelemetry.tracer_provider.tracer('dalli', Dalli::VERSION) if defined?(OpenTelemetry))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns true if instrumentation is enabled (OpenTelemetry SDK is available).
|
|
60
|
+
#
|
|
61
|
+
# @return [Boolean] true if tracing is active, false otherwise
|
|
62
|
+
def enabled?
|
|
63
|
+
!tracer.nil?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Wraps a block with a span if instrumentation is enabled.
|
|
67
|
+
#
|
|
68
|
+
# Creates a client span with the given name and attributes merged with
|
|
69
|
+
# DEFAULT_ATTRIBUTES. The block is executed within the span context.
|
|
70
|
+
# If an exception occurs, it is recorded on the span before re-raising.
|
|
71
|
+
#
|
|
72
|
+
# When tracing is disabled (OpenTelemetry not loaded), this method
|
|
73
|
+
# simply yields directly with zero overhead.
|
|
74
|
+
#
|
|
75
|
+
# @param name [String] the span name (e.g., 'get', 'set', 'delete')
|
|
76
|
+
# @param attributes [Hash] span attributes to merge with defaults.
|
|
77
|
+
# Common attributes include:
|
|
78
|
+
# - 'db.operation' - the operation name
|
|
79
|
+
# - 'server.address' - the target server
|
|
80
|
+
# - 'db.memcached.key_count' - number of keys (for multi operations)
|
|
81
|
+
# @yield the cache operation to trace
|
|
82
|
+
# @return [Object] the result of the block
|
|
83
|
+
# @raise [StandardError] re-raises any exception from the block
|
|
84
|
+
#
|
|
85
|
+
# @example Tracing a set operation
|
|
86
|
+
# trace('set', { 'db.operation' => 'set', 'server.address' => 'localhost:11211' }) do
|
|
87
|
+
# server.set(key, value, ttl)
|
|
88
|
+
# end
|
|
89
|
+
#
|
|
90
|
+
def trace(name, attributes = {})
|
|
91
|
+
return yield unless enabled?
|
|
92
|
+
|
|
93
|
+
tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |span|
|
|
94
|
+
yield
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
span.record_exception(e)
|
|
97
|
+
span.status = OpenTelemetry::Trace::Status.error(e.message)
|
|
98
|
+
raise
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Like trace, but yields the span to allow adding attributes after execution.
|
|
103
|
+
#
|
|
104
|
+
# This is useful for operations where metrics are only known after the
|
|
105
|
+
# operation completes, such as get_multi where hit/miss counts depend
|
|
106
|
+
# on the cache response.
|
|
107
|
+
#
|
|
108
|
+
# When tracing is disabled, yields nil as the span argument.
|
|
109
|
+
#
|
|
110
|
+
# @param name [String] the span name (e.g., 'get_multi')
|
|
111
|
+
# @param attributes [Hash] initial span attributes to merge with defaults
|
|
112
|
+
# @yield [OpenTelemetry::Trace::Span, nil] the span object, or nil if disabled
|
|
113
|
+
# @return [Object] the result of the block
|
|
114
|
+
# @raise [StandardError] re-raises any exception from the block
|
|
115
|
+
#
|
|
116
|
+
# @example Recording hit/miss metrics after get_multi
|
|
117
|
+
# trace_with_result('get_multi', { 'db.operation' => 'get_multi' }) do |span|
|
|
118
|
+
# results = fetch_from_cache(keys)
|
|
119
|
+
# if span
|
|
120
|
+
# span.set_attribute('db.memcached.hit_count', results.size)
|
|
121
|
+
# span.set_attribute('db.memcached.miss_count', keys.size - results.size)
|
|
122
|
+
# end
|
|
123
|
+
# results
|
|
124
|
+
# end
|
|
125
|
+
#
|
|
126
|
+
def trace_with_result(name, attributes = {})
|
|
127
|
+
return yield(nil) unless enabled?
|
|
128
|
+
|
|
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
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# Contains logic for the pipelined delete operations implemented by the client.
|
|
6
|
+
# Efficiently deletes multiple keys by grouping requests by server
|
|
7
|
+
# and using quiet mode to minimize round trips.
|
|
8
|
+
##
|
|
9
|
+
class PipelinedDeleter
|
|
10
|
+
def initialize(ring, key_manager)
|
|
11
|
+
@ring = ring
|
|
12
|
+
@key_manager = key_manager
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# Deletes multiple keys from memcached.
|
|
17
|
+
#
|
|
18
|
+
# @param keys [Array<String>] keys to delete
|
|
19
|
+
# @return [void]
|
|
20
|
+
##
|
|
21
|
+
def process(keys)
|
|
22
|
+
return if keys.empty?
|
|
23
|
+
|
|
24
|
+
@ring.lock do
|
|
25
|
+
servers = setup_requests(keys)
|
|
26
|
+
finish_requests(servers)
|
|
27
|
+
end
|
|
28
|
+
rescue NetworkError => e
|
|
29
|
+
Dalli.logger.debug { e.inspect }
|
|
30
|
+
Dalli.logger.debug { 'retrying pipelined deletes because of network error' }
|
|
31
|
+
retry
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def setup_requests(keys)
|
|
37
|
+
groups = groups_for_keys(keys)
|
|
38
|
+
make_delete_requests(groups)
|
|
39
|
+
groups.keys
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Loop through the server-grouped sets of keys, writing
|
|
44
|
+
# the corresponding quiet delete requests to the appropriate servers
|
|
45
|
+
##
|
|
46
|
+
def make_delete_requests(groups)
|
|
47
|
+
groups.each do |server, keys_for_server|
|
|
48
|
+
keys_for_server.each do |key|
|
|
49
|
+
server.request(:pipelined_delete, key)
|
|
50
|
+
rescue DalliError, NetworkError => e
|
|
51
|
+
Dalli.logger.debug { e.inspect }
|
|
52
|
+
Dalli.logger.debug { "unable to delete key #{key} for server #{server.name}" }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Sends noop to each server to flush responses and ensure all deletes complete.
|
|
59
|
+
##
|
|
60
|
+
def finish_requests(servers)
|
|
61
|
+
servers.each do |server|
|
|
62
|
+
server.request(:noop)
|
|
63
|
+
rescue DalliError, NetworkError => e
|
|
64
|
+
Dalli.logger.debug { e.inspect }
|
|
65
|
+
Dalli.logger.debug { "unable to complete pipelined delete on server #{server.name}" }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def groups_for_keys(keys)
|
|
70
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
71
|
+
groups = @ring.keys_grouped_by_server(validated_keys)
|
|
72
|
+
|
|
73
|
+
if (unfound_keys = groups.delete(nil))
|
|
74
|
+
Dalli.logger.debug do
|
|
75
|
+
"unable to delete #{unfound_keys.length} keys because no matching server was found"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
groups
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
3
5
|
module Dalli
|
|
4
6
|
##
|
|
5
7
|
# Contains logic for the pipelined gets implemented by the client.
|
|
@@ -57,7 +59,7 @@ module Dalli
|
|
|
57
59
|
# our set, sending the noop to terminate the set of queries.
|
|
58
60
|
##
|
|
59
61
|
def finish_queries(servers)
|
|
60
|
-
deleted =
|
|
62
|
+
deleted = Set.new
|
|
61
63
|
|
|
62
64
|
servers.each do |server|
|
|
63
65
|
next unless server.connected?
|
|
@@ -67,7 +69,7 @@ module Dalli
|
|
|
67
69
|
rescue Dalli::NetworkError
|
|
68
70
|
raise
|
|
69
71
|
rescue Dalli::DalliError
|
|
70
|
-
deleted
|
|
72
|
+
deleted << server
|
|
71
73
|
end
|
|
72
74
|
end
|
|
73
75
|
|
|
@@ -94,7 +96,7 @@ module Dalli
|
|
|
94
96
|
|
|
95
97
|
def fetch_responses(servers, start_time, timeout, &block)
|
|
96
98
|
# Remove any servers which are not connected
|
|
97
|
-
servers.
|
|
99
|
+
servers.select!(&:connected?)
|
|
98
100
|
return [] if servers.empty?
|
|
99
101
|
|
|
100
102
|
time_left = remaining_time(start_time, timeout)
|