dalli 3.2.8 → 4.3.3
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 +169 -1
- data/Gemfile +15 -2
- data/README.md +92 -0
- data/lib/dalli/client.rb +246 -11
- data/lib/dalli/instrumentation.rb +141 -0
- data/lib/dalli/key_manager.rb +23 -8
- data/lib/dalli/pipelined_deleter.rb +82 -0
- data/lib/dalli/pipelined_getter.rb +46 -20
- data/lib/dalli/pipelined_setter.rb +87 -0
- data/lib/dalli/protocol/base.rb +82 -10
- data/lib/dalli/protocol/binary/response_processor.rb +5 -15
- data/lib/dalli/protocol/binary.rb +27 -0
- data/lib/dalli/protocol/connection_manager.rb +16 -11
- data/lib/dalli/protocol/meta/key_regularizer.rb +1 -1
- data/lib/dalli/protocol/meta/request_formatter.rb +42 -10
- data/lib/dalli/protocol/meta/response_processor.rb +72 -26
- data/lib/dalli/protocol/meta.rb +96 -5
- data/lib/dalli/protocol/response_buffer.rb +36 -12
- data/lib/dalli/protocol/server_config_parser.rb +1 -1
- data/lib/dalli/protocol/string_marshaller.rb +65 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
- data/lib/dalli/protocol/value_compressor.rb +2 -11
- data/lib/dalli/protocol/value_marshaller.rb +1 -1
- data/lib/dalli/protocol/value_serializer.rb +59 -40
- data/lib/dalli/protocol.rb +10 -0
- data/lib/dalli/protocol_deprecations.rb +45 -0
- data/lib/dalli/socket.rb +70 -14
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +11 -2
- data/lib/rack/session/dalli.rb +43 -8
- metadata +25 -10
- data/lib/dalli/server.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 57b78e30ee409a2d742fc47ee57ccdd482fe9c75f369366ae315b7ef8b649934
|
|
4
|
+
data.tar.gz: b8cad66f3cba53bbcb84b18f406eed60f22209c10dd4a312063a1e13189185ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18b26e3c4aa30e5f8b4195d95307afca6c9240dd1154a36966d40d52047e638758850ec06e2a40fd1bd8e69fe150dee7409b1f4a565f0a80e397aaf115315463
|
|
7
|
+
data.tar.gz: b6db69ecbc1e6d34587d8ec29b861f6a71c863b36229d3c642e3d0e646ba6234e3bac04225d64ff174d12f35b728663e82e1d2b5f7e93dc9ac3e1ff33fd58db3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,177 @@
|
|
|
1
1
|
Dalli Changelog
|
|
2
2
|
=====================
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
4.3.3
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
Performance:
|
|
8
|
+
|
|
9
|
+
- Reduce object allocations in pipelined get response processing (#1072)
|
|
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 in both binary and meta protocols
|
|
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
|
+
- Skip OTel integration tests when meta protocol is unavailable (#1072)
|
|
19
|
+
|
|
20
|
+
4.3.2
|
|
21
|
+
==========
|
|
22
|
+
|
|
23
|
+
OpenTelemetry:
|
|
24
|
+
|
|
25
|
+
- Migrate to stable OTel semantic conventions
|
|
26
|
+
- `db.system` renamed to `db.system.name`
|
|
27
|
+
- `db.operation` renamed to `db.operation.name`
|
|
28
|
+
- `server.address` now contains hostname only; `server.port` is a separate integer attribute
|
|
29
|
+
- `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
|
|
30
|
+
- Add `db.query.text` span attribute with configurable modes
|
|
31
|
+
- `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
|
|
32
|
+
- Add `peer.service` span attribute
|
|
33
|
+
- `:otel_peer_service` option for logical service naming
|
|
34
|
+
|
|
35
|
+
4.3.1
|
|
36
|
+
==========
|
|
37
|
+
|
|
38
|
+
Bug Fixes:
|
|
39
|
+
|
|
40
|
+
- Fix socket compatibility with gems that monkey-patch TCPSocket (#996, #1012)
|
|
41
|
+
- Gems like `socksify` and `resolv-replace` modify `TCPSocket#initialize`, breaking Ruby 3.0+'s `connect_timeout:` keyword argument
|
|
42
|
+
- Detection now uses parameter signature checking instead of gem-specific method detection
|
|
43
|
+
- Falls back to `Timeout.timeout` when monkey-patching is detected
|
|
44
|
+
- Detection result is cached for performance
|
|
45
|
+
|
|
46
|
+
- Fix network retry bug with `socket_max_failures: 0` (#1065)
|
|
47
|
+
- Previously, setting `socket_max_failures: 0` could still cause retries due to error handling
|
|
48
|
+
- Introduced `RetryableNetworkError` subclass to distinguish retryable vs non-retryable errors
|
|
49
|
+
- `down!` now raises non-retryable `NetworkError`, `reconnect!` raises `RetryableNetworkError`
|
|
50
|
+
- Thanks to Graham Cooper (Shopify) for this fix
|
|
51
|
+
|
|
52
|
+
- Fix "character class has duplicated range" Ruby warning (#1067)
|
|
53
|
+
- Fixed regex in `KeyManager::VALID_NAMESPACE_SEPARATORS` that caused warnings on newer Ruby versions
|
|
54
|
+
- Thanks to Hartley McGuire for this fix
|
|
55
|
+
|
|
56
|
+
Improvements:
|
|
57
|
+
|
|
58
|
+
- Add StrictWarnings test helper to catch Ruby warnings early (#1067)
|
|
59
|
+
|
|
60
|
+
- Use bulk attribute setter for OpenTelemetry spans (#1068)
|
|
61
|
+
- Reduces lock acquisitions when setting span attributes
|
|
62
|
+
- Thanks to Robert Laurin (Shopify) for this optimization
|
|
63
|
+
|
|
64
|
+
- Fix double recording of exceptions on OpenTelemetry spans (#1069)
|
|
65
|
+
- OpenTelemetry's `in_span` method already records exceptions and sets error status automatically
|
|
66
|
+
- Removed redundant explicit exception recording that caused exceptions to appear twice in traces
|
|
67
|
+
- Thanks to Robert Laurin (Shopify) for this fix
|
|
68
|
+
|
|
69
|
+
4.3.0
|
|
70
|
+
==========
|
|
71
|
+
|
|
72
|
+
New Features:
|
|
73
|
+
|
|
74
|
+
- Add `namespace_separator` option to customize the separator between namespace and key (#1019)
|
|
75
|
+
- Default is `:` for backward compatibility
|
|
76
|
+
- Must be a single non-alphanumeric character (e.g., `:`, `/`, `|`, `.`)
|
|
77
|
+
- Example: `Dalli::Client.new(servers, namespace: 'myapp', namespace_separator: '/')`
|
|
78
|
+
|
|
79
|
+
Bug Fixes:
|
|
80
|
+
|
|
81
|
+
- Fix architecture-dependent struct timeval packing for socket timeouts (#1034)
|
|
82
|
+
- Detects correct pack format for time_t and suseconds_t on each platform
|
|
83
|
+
- Fixes timeout issues on architectures with 64-bit time_t
|
|
84
|
+
|
|
85
|
+
- Fix get_multi hanging with large key counts (#776, #941)
|
|
86
|
+
- Add interleaved read/write for pipelined gets to prevent socket buffer deadlock
|
|
87
|
+
- For batches over 10,000 keys per server, requests are now sent in chunks
|
|
88
|
+
|
|
89
|
+
- **Breaking:** Enforce string-only values in raw mode (#1022)
|
|
90
|
+
- `set(key, nil, raw: true)` now raises `MarshalError` instead of storing `""`
|
|
91
|
+
- `set(key, 123, raw: true)` now raises `MarshalError` instead of storing `"123"`
|
|
92
|
+
- This matches the behavior of client-level `raw: true` mode
|
|
93
|
+
- To store counters, use string values: `set('counter', '0', raw: true)`
|
|
94
|
+
|
|
95
|
+
CI:
|
|
96
|
+
|
|
97
|
+
- Add TruffleRuby to CI test matrix (#988)
|
|
98
|
+
|
|
99
|
+
4.2.0
|
|
100
|
+
==========
|
|
101
|
+
|
|
102
|
+
Performance:
|
|
103
|
+
|
|
104
|
+
- Buffered I/O: Use `socket.sync = false` with explicit flush to reduce syscalls for pipelined operations
|
|
105
|
+
- get_multi optimizations: Use Set for O(1) server tracking lookups
|
|
106
|
+
- Raw mode optimization: Skip bitflags request in meta protocol when in raw mode (saves 2 bytes per request)
|
|
107
|
+
|
|
108
|
+
New Features:
|
|
109
|
+
|
|
110
|
+
- OpenTelemetry tracing support: Automatically instruments operations when OpenTelemetry SDK is present
|
|
111
|
+
- Zero overhead when OpenTelemetry is not loaded
|
|
112
|
+
- Traces `get`, `set`, `delete`, `get_multi`, `set_multi`, `delete_multi`, `get_with_metadata`, and `fetch_with_lock`
|
|
113
|
+
- Spans include `db.system: memcached` and `db.operation` attributes
|
|
114
|
+
- Single-key operations include `server.address` attribute
|
|
115
|
+
- Multi-key operations include `db.memcached.key_count` attribute
|
|
116
|
+
- `get_multi` spans include `db.memcached.hit_count` and `db.memcached.miss_count` for cache efficiency metrics
|
|
117
|
+
- Exceptions are automatically recorded on spans with error status
|
|
118
|
+
|
|
119
|
+
4.1.0
|
|
120
|
+
==========
|
|
121
|
+
|
|
122
|
+
New Features:
|
|
123
|
+
|
|
124
|
+
- Add `set_multi` for efficient bulk set operations using pipelined requests
|
|
125
|
+
- Add `delete_multi` for efficient bulk delete operations using pipelined requests
|
|
126
|
+
- Add `fetch_with_lock` for thundering herd protection using meta protocol's vivify/recache flags (requires memcached 1.6+)
|
|
127
|
+
- Add thundering herd protection support to meta protocol (requires memcached 1.6+):
|
|
128
|
+
- `N` (vivify) flag for creating stubs on cache miss
|
|
129
|
+
- `R` (recache) flag for winning recache race when TTL is below threshold
|
|
130
|
+
- Response flags `W` (won recache), `X` (stale), `Z` (lost race)
|
|
131
|
+
- `delete_stale` method for marking items as stale instead of deleting
|
|
132
|
+
- Add `get_with_metadata` for advanced cache operations with metadata retrieval (requires memcached 1.6+):
|
|
133
|
+
- Returns hash with `:value`, `:cas`, `:won_recache`, `:stale`, `:lost_recache`
|
|
134
|
+
- Optional `:return_hit_status` returns `:hit_before` (true/false for previous access)
|
|
135
|
+
- Optional `:return_last_access` returns `:last_access` (seconds since last access)
|
|
136
|
+
- Optional `:skip_lru_bump` prevents LRU update on access
|
|
137
|
+
- Optional `:vivify_ttl` and `:recache_ttl` for thundering herd protection
|
|
138
|
+
|
|
139
|
+
Deprecations:
|
|
140
|
+
|
|
141
|
+
- Binary protocol is deprecated and will be removed in Dalli 5.0. Use `protocol: :meta` instead (requires memcached 1.6+)
|
|
142
|
+
- SASL authentication is deprecated and will be removed in Dalli 5.0. Consider using network-level security or memcached's TLS support
|
|
143
|
+
|
|
144
|
+
4.0.1
|
|
145
|
+
==========
|
|
146
|
+
|
|
147
|
+
- Add `:raw` client option to skip serialization entirely, returning raw byte strings
|
|
148
|
+
- Handle `OpenSSL::SSL::SSLError` in connection manager
|
|
149
|
+
|
|
150
|
+
4.0.0
|
|
151
|
+
==========
|
|
152
|
+
|
|
153
|
+
BREAKING CHANGES:
|
|
154
|
+
|
|
155
|
+
- Require Ruby 3.1+ (dropped support for Ruby 2.6, 2.7, and 3.0)
|
|
156
|
+
- Removed `Dalli::Server` deprecated alias - use `Dalli::Protocol::Binary` instead
|
|
157
|
+
- Removed `:compression` option - use `:compress` instead
|
|
158
|
+
- Removed `close_on_fork` method - use `reconnect_on_fork` instead
|
|
159
|
+
|
|
160
|
+
Other changes:
|
|
161
|
+
|
|
162
|
+
- Add security warning when using default Marshal serializer (silence with `silence_marshal_warning: true`)
|
|
163
|
+
- Add defense-in-depth input validation for stats command arguments
|
|
164
|
+
- Add `string_fastpath` option to skip serialization for simple strings (byroot)
|
|
165
|
+
- Meta protocol set performance improvement (danmayer)
|
|
166
|
+
- Fix connection_pool 3.0 compatibility for Rack session store
|
|
167
|
+
- Fix session recovery after deletion (stengineering0)
|
|
168
|
+
- Fix cannot read response data included terminator `\r\n` when use meta protocol (matsubara0507)
|
|
169
|
+
- Support SERVER_ERROR response from Memcached as per the [memcached spec](https://github.com/memcached/memcached/blob/e43364402195c8e822bb8f88755a60ab8bbed62a/doc/protocol.txt#L172) (grcooper)
|
|
170
|
+
- Update Socket timeout handling to use Socket#timeout= when available (nickamorim)
|
|
171
|
+
- Serializer: reraise all .load errors as UnmarshalError (olleolleolle)
|
|
172
|
+
- Reconnect gracefully when a fork is detected instead of crashing (PatrickTulskie)
|
|
173
|
+
- Update CI to test against memcached 1.6.40
|
|
174
|
+
|
|
7
175
|
3.2.8
|
|
8
176
|
==========
|
|
9
177
|
|
data/Gemfile
CHANGED
|
@@ -5,9 +5,18 @@ 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 '
|
|
10
|
-
|
|
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'
|
|
@@ -18,4 +27,8 @@ end
|
|
|
18
27
|
|
|
19
28
|
group :test do
|
|
20
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
|
|
21
34
|
end
|
data/README.md
CHANGED
|
@@ -11,9 +11,101 @@ 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
|
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
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
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration Options
|
|
35
|
+
|
|
36
|
+
### Namespace
|
|
37
|
+
|
|
38
|
+
Use namespaces to partition your cache and avoid key collisions between different applications or environments:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
# All keys will be prefixed with "myapp:"
|
|
42
|
+
Dalli::Client.new('localhost:11211', namespace: 'myapp')
|
|
43
|
+
|
|
44
|
+
# Dynamic namespace using a Proc (evaluated on each operation)
|
|
45
|
+
Dalli::Client.new('localhost:11211', namespace: -> { "tenant:#{Thread.current[:tenant_id]}" })
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Namespace Separator
|
|
49
|
+
|
|
50
|
+
By default, the namespace and key are joined with a colon (`:`). You can customize this with the `namespace_separator` option:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# Keys will be prefixed with "myapp/" instead of "myapp:"
|
|
54
|
+
Dalli::Client.new('localhost:11211', namespace: 'myapp', namespace_separator: '/')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The separator must be a single non-alphanumeric character. Valid examples: `:`, `/`, `|`, `.`, `-`, `_`, `#`
|
|
58
|
+
|
|
59
|
+
## Security Note
|
|
60
|
+
|
|
61
|
+
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:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
Dalli::Client.new('localhost:11211', serializer: JSON)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
See the [4.0-Upgrade.md](4.0-Upgrade.md) guide for more information.
|
|
68
|
+
|
|
69
|
+
## OpenTelemetry Tracing
|
|
70
|
+
|
|
71
|
+
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:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# Gemfile
|
|
75
|
+
gem 'opentelemetry-sdk'
|
|
76
|
+
gem 'opentelemetry-exporter-otlp' # or your preferred exporter
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When OpenTelemetry is loaded, Dalli creates spans for:
|
|
80
|
+
- Single key operations: `get`, `set`, `delete`, `add`, `replace`, `incr`, `decr`, etc.
|
|
81
|
+
- Multi-key operations: `get_multi`, `set_multi`, `delete_multi`
|
|
82
|
+
- Advanced operations: `get_with_metadata`, `fetch_with_lock`
|
|
83
|
+
|
|
84
|
+
### Span Attributes
|
|
85
|
+
|
|
86
|
+
All spans include:
|
|
87
|
+
- `db.system`: `memcached`
|
|
88
|
+
- `db.operation`: The operation name (e.g., `get`, `set_multi`)
|
|
89
|
+
|
|
90
|
+
Single-key operations also include:
|
|
91
|
+
- `server.address`: The memcached server that handled the request (e.g., `localhost:11211`)
|
|
92
|
+
|
|
93
|
+
Multi-key operations include cache efficiency metrics:
|
|
94
|
+
- `db.memcached.key_count`: Number of keys in the request
|
|
95
|
+
- `db.memcached.hit_count`: Number of keys found (for `get_multi`)
|
|
96
|
+
- `db.memcached.miss_count`: Number of keys not found (for `get_multi`)
|
|
97
|
+
|
|
98
|
+
### Error Handling
|
|
99
|
+
|
|
100
|
+
Exceptions are automatically recorded on spans with error status. When an operation fails:
|
|
101
|
+
1. The exception is recorded on the span via `span.record_exception(e)`
|
|
102
|
+
2. The span status is set to error with the exception message
|
|
103
|
+
3. The exception is re-raised to the caller
|
|
104
|
+
|
|
105
|
+
### Zero Overhead
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
17
109
|

|
|
18
110
|
|
|
19
111
|
|
data/lib/dalli/client.rb
CHANGED
|
@@ -41,16 +41,26 @@ module Dalli
|
|
|
41
41
|
# - :compressor - defaults to Dalli::Compressor, a Zlib-based implementation
|
|
42
42
|
# - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for
|
|
43
43
|
# #fetch operations.
|
|
44
|
+
# - :raw - If set, disables serialization and compression entirely at the client level.
|
|
45
|
+
# Only String values are supported. This is useful when the caller handles its own
|
|
46
|
+
# serialization (e.g., Rails' ActiveSupport::Cache). Note: this is different from
|
|
47
|
+
# the per-request :raw option which converts values to strings but still uses the
|
|
48
|
+
# serialization pipeline.
|
|
44
49
|
# - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
|
|
45
50
|
# useful for injecting a FIPS compliant hash object.
|
|
46
51
|
# - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
|
|
47
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.
|
|
48
57
|
#
|
|
49
58
|
def initialize(servers = nil, options = {})
|
|
50
59
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
51
60
|
@options = normalize_options(options)
|
|
52
61
|
@key_manager = ::Dalli::KeyManager.new(@options)
|
|
53
62
|
@ring = nil
|
|
63
|
+
emit_deprecation_warnings
|
|
54
64
|
end
|
|
55
65
|
|
|
56
66
|
#
|
|
@@ -91,24 +101,70 @@ module Dalli
|
|
|
91
101
|
yield value, cas
|
|
92
102
|
end
|
|
93
103
|
|
|
104
|
+
##
|
|
105
|
+
# Get value with extended metadata using the meta protocol.
|
|
106
|
+
#
|
|
107
|
+
# IMPORTANT: This method requires memcached 1.6+ and the meta protocol (protocol: :meta).
|
|
108
|
+
# It will raise an error if used with the binary protocol.
|
|
109
|
+
#
|
|
110
|
+
# @param key [String] the cache key
|
|
111
|
+
# @param options [Hash] options controlling what metadata to return
|
|
112
|
+
# - :return_cas [Boolean] return the CAS value (default: true)
|
|
113
|
+
# - :return_hit_status [Boolean] return whether item was previously accessed
|
|
114
|
+
# - :return_last_access [Boolean] return seconds since last access
|
|
115
|
+
# - :skip_lru_bump [Boolean] don't bump LRU or update access stats
|
|
116
|
+
#
|
|
117
|
+
# @return [Hash] containing:
|
|
118
|
+
# - :value - the cached value (or nil on miss)
|
|
119
|
+
# - :cas - the CAS value
|
|
120
|
+
# - :hit_before - true/false if previously accessed (only if return_hit_status: true)
|
|
121
|
+
# - :last_access - seconds since last access (only if return_last_access: true)
|
|
122
|
+
#
|
|
123
|
+
# @example Get with hit status
|
|
124
|
+
# result = client.get_with_metadata('key', return_hit_status: true)
|
|
125
|
+
# # => { value: "data", cas: 123, hit_before: true }
|
|
126
|
+
#
|
|
127
|
+
# @example Get with all metadata without affecting LRU
|
|
128
|
+
# result = client.get_with_metadata('key',
|
|
129
|
+
# return_hit_status: true,
|
|
130
|
+
# return_last_access: true,
|
|
131
|
+
# skip_lru_bump: true
|
|
132
|
+
# )
|
|
133
|
+
# # => { value: "data", cas: 123, hit_before: true, last_access: 42 }
|
|
134
|
+
#
|
|
135
|
+
def get_with_metadata(key, options = {})
|
|
136
|
+
raise_unless_meta_protocol!
|
|
137
|
+
|
|
138
|
+
key = key.to_s
|
|
139
|
+
key = @key_manager.validate_key(key)
|
|
140
|
+
|
|
141
|
+
server = ring.server_for_key(key)
|
|
142
|
+
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', key, server)) do
|
|
143
|
+
server.request(:meta_get, key, options)
|
|
144
|
+
end
|
|
145
|
+
rescue NetworkError => e
|
|
146
|
+
Dalli.logger.debug { e.inspect }
|
|
147
|
+
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
148
|
+
retry
|
|
149
|
+
end
|
|
150
|
+
|
|
94
151
|
##
|
|
95
152
|
# Fetch multiple keys efficiently.
|
|
96
153
|
# If a block is given, yields key/value pairs one at a time.
|
|
97
154
|
# Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
|
|
155
|
+
# rubocop:disable Style/ExplicitBlockArgument
|
|
98
156
|
def get_multi(*keys)
|
|
99
157
|
keys.flatten!
|
|
100
158
|
keys.compact!
|
|
101
|
-
|
|
102
159
|
return {} if keys.empty?
|
|
103
160
|
|
|
104
161
|
if block_given?
|
|
105
|
-
|
|
162
|
+
get_multi_yielding(keys) { |k, v| yield k, v }
|
|
106
163
|
else
|
|
107
|
-
|
|
108
|
-
pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
|
|
109
|
-
end
|
|
164
|
+
get_multi_hash(keys)
|
|
110
165
|
end
|
|
111
166
|
end
|
|
167
|
+
# rubocop:enable Style/ExplicitBlockArgument
|
|
112
168
|
|
|
113
169
|
##
|
|
114
170
|
# Fetch multiple keys efficiently, including available metadata such as CAS.
|
|
@@ -144,6 +200,57 @@ module Dalli
|
|
|
144
200
|
new_val
|
|
145
201
|
end
|
|
146
202
|
|
|
203
|
+
##
|
|
204
|
+
# Fetch the value with thundering herd protection using the meta protocol's
|
|
205
|
+
# N (vivify) and R (recache) flags.
|
|
206
|
+
#
|
|
207
|
+
# This method prevents multiple clients from simultaneously regenerating the same
|
|
208
|
+
# cache entry (the "thundering herd" problem). Only one client wins the right to
|
|
209
|
+
# regenerate; other clients receive the stale value (if available) or wait.
|
|
210
|
+
#
|
|
211
|
+
# IMPORTANT: This method requires memcached 1.6+ and the meta protocol (protocol: :meta).
|
|
212
|
+
# It will raise an error if used with the binary protocol.
|
|
213
|
+
#
|
|
214
|
+
# @param key [String] the cache key
|
|
215
|
+
# @param ttl [Integer] time-to-live for the cached value in seconds
|
|
216
|
+
# @param lock_ttl [Integer] how long the lock/stub lives (default: 30 seconds)
|
|
217
|
+
# This is the maximum time other clients will return stale data while
|
|
218
|
+
# waiting for regeneration. Should be longer than your expected regeneration time.
|
|
219
|
+
# @param recache_threshold [Integer, nil] if set, win the recache race when the
|
|
220
|
+
# item's remaining TTL is below this threshold. Useful for proactive recaching.
|
|
221
|
+
# @param req_options [Hash] options passed to set operations (e.g., raw: true)
|
|
222
|
+
#
|
|
223
|
+
# @yield Block to regenerate the value (only called if this client won the race)
|
|
224
|
+
# @return [Object] the cached value (may be stale if another client is regenerating)
|
|
225
|
+
#
|
|
226
|
+
# @example Basic usage
|
|
227
|
+
# client.fetch_with_lock('expensive_key', ttl: 300, lock_ttl: 30) do
|
|
228
|
+
# expensive_database_query
|
|
229
|
+
# end
|
|
230
|
+
#
|
|
231
|
+
# @example With proactive recaching (recache before expiry)
|
|
232
|
+
# client.fetch_with_lock('key', ttl: 300, lock_ttl: 30, recache_threshold: 60) do
|
|
233
|
+
# expensive_operation
|
|
234
|
+
# end
|
|
235
|
+
#
|
|
236
|
+
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
|
|
237
|
+
raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
|
|
238
|
+
|
|
239
|
+
raise_unless_meta_protocol!
|
|
240
|
+
|
|
241
|
+
key = key.to_s
|
|
242
|
+
key = @key_manager.validate_key(key)
|
|
243
|
+
|
|
244
|
+
server = ring.server_for_key(key)
|
|
245
|
+
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', key, server)) do
|
|
246
|
+
fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
247
|
+
end
|
|
248
|
+
rescue NetworkError => e
|
|
249
|
+
Dalli.logger.debug { e.inspect }
|
|
250
|
+
Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
|
|
251
|
+
retry
|
|
252
|
+
end
|
|
253
|
+
|
|
147
254
|
##
|
|
148
255
|
# compare and swap values using optimistic locking.
|
|
149
256
|
# Fetch the existing value for key.
|
|
@@ -155,8 +262,8 @@ module Dalli
|
|
|
155
262
|
# - nil if the key did not exist.
|
|
156
263
|
# - false if the value was changed by someone else.
|
|
157
264
|
# - true if the value was successfully updated.
|
|
158
|
-
def cas(key, ttl = nil, req_options = nil, &
|
|
159
|
-
cas_core(key, false, ttl, req_options, &
|
|
265
|
+
def cas(key, ttl = nil, req_options = nil, &)
|
|
266
|
+
cas_core(key, false, ttl, req_options, &)
|
|
160
267
|
end
|
|
161
268
|
|
|
162
269
|
##
|
|
@@ -166,8 +273,8 @@ module Dalli
|
|
|
166
273
|
# Returns:
|
|
167
274
|
# - false if the value was changed by someone else.
|
|
168
275
|
# - true if the value was successfully updated.
|
|
169
|
-
def cas!(key, ttl = nil, req_options = nil, &
|
|
170
|
-
cas_core(key, true, ttl, req_options, &
|
|
276
|
+
def cas!(key, ttl = nil, req_options = nil, &)
|
|
277
|
+
cas_core(key, true, ttl, req_options, &)
|
|
171
278
|
end
|
|
172
279
|
|
|
173
280
|
##
|
|
@@ -201,6 +308,26 @@ module Dalli
|
|
|
201
308
|
set_cas(key, value, 0, ttl, req_options)
|
|
202
309
|
end
|
|
203
310
|
|
|
311
|
+
##
|
|
312
|
+
# Set multiple keys and values efficiently using pipelining.
|
|
313
|
+
# This method is more efficient than calling set() in a loop because
|
|
314
|
+
# it batches requests by server and uses quiet mode.
|
|
315
|
+
#
|
|
316
|
+
# @param hash [Hash] key-value pairs to set
|
|
317
|
+
# @param ttl [Integer] time-to-live in seconds (optional, uses default if not provided)
|
|
318
|
+
# @param req_options [Hash] options passed to each set operation
|
|
319
|
+
# @return [void]
|
|
320
|
+
#
|
|
321
|
+
# Example:
|
|
322
|
+
# client.set_multi({ 'key1' => 'value1', 'key2' => 'value2' }, 300)
|
|
323
|
+
def set_multi(hash, ttl = nil, req_options = nil)
|
|
324
|
+
return if hash.empty?
|
|
325
|
+
|
|
326
|
+
Instrumentation.trace('set_multi', multi_trace_attrs('set_multi', hash.size, hash.keys)) do
|
|
327
|
+
pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
204
331
|
##
|
|
205
332
|
# Set the key-value pair, verifying existing CAS.
|
|
206
333
|
# Returns the resulting CAS value if succeeded, and falsy otherwise.
|
|
@@ -240,6 +367,24 @@ module Dalli
|
|
|
240
367
|
delete_cas(key, 0)
|
|
241
368
|
end
|
|
242
369
|
|
|
370
|
+
##
|
|
371
|
+
# Delete multiple keys efficiently using pipelining.
|
|
372
|
+
# This method is more efficient than calling delete() in a loop because
|
|
373
|
+
# it batches requests by server and uses quiet mode.
|
|
374
|
+
#
|
|
375
|
+
# @param keys [Array<String>] keys to delete
|
|
376
|
+
# @return [void]
|
|
377
|
+
#
|
|
378
|
+
# Example:
|
|
379
|
+
# client.delete_multi(['key1', 'key2', 'key3'])
|
|
380
|
+
def delete_multi(keys)
|
|
381
|
+
return if keys.empty?
|
|
382
|
+
|
|
383
|
+
Instrumentation.trace('delete_multi', multi_trace_attrs('delete_multi', keys.size, keys)) do
|
|
384
|
+
pipelined_deleter.process(keys)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
243
388
|
##
|
|
244
389
|
# Append value to the value already stored on the server for 'key'.
|
|
245
390
|
# Appending only works for values stored with :raw => true.
|
|
@@ -369,6 +514,65 @@ module Dalli
|
|
|
369
514
|
|
|
370
515
|
private
|
|
371
516
|
|
|
517
|
+
# Records hit/miss metrics on a span for cache observability.
|
|
518
|
+
# @param span [OpenTelemetry::Trace::Span, nil] the span to record on
|
|
519
|
+
# @param key_count [Integer] total keys requested
|
|
520
|
+
# @param hit_count [Integer] keys found in cache
|
|
521
|
+
def record_hit_miss_metrics(span, key_count, hit_count)
|
|
522
|
+
return unless span
|
|
523
|
+
|
|
524
|
+
span.add_attributes('db.memcached.hit_count' => hit_count,
|
|
525
|
+
'db.memcached.miss_count' => key_count - hit_count)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def get_multi_yielding(keys)
|
|
529
|
+
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
530
|
+
hit_count = 0
|
|
531
|
+
pipelined_getter.process(keys) do |k, data|
|
|
532
|
+
hit_count += 1
|
|
533
|
+
yield k, data.first
|
|
534
|
+
end
|
|
535
|
+
record_hit_miss_metrics(span, keys.size, hit_count)
|
|
536
|
+
nil
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def get_multi_hash(keys)
|
|
541
|
+
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
542
|
+
{}.tap do |hash|
|
|
543
|
+
pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
|
|
544
|
+
record_hit_miss_metrics(span, keys.size, hash.size)
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def get_multi_attributes(keys)
|
|
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
|
|
574
|
+
end
|
|
575
|
+
|
|
372
576
|
def check_positive!(amt)
|
|
373
577
|
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
|
374
578
|
end
|
|
@@ -381,6 +585,17 @@ module Dalli
|
|
|
381
585
|
perform(:set, key, newvalue, ttl_or_default(ttl), cas, req_options)
|
|
382
586
|
end
|
|
383
587
|
|
|
588
|
+
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
589
|
+
server = ring.server_for_key(key)
|
|
590
|
+
result = server.request(:meta_get, key, { vivify_ttl: lock_ttl, recache_ttl: recache_threshold })
|
|
591
|
+
|
|
592
|
+
return result[:value] unless result[:won_recache]
|
|
593
|
+
|
|
594
|
+
new_val = yield
|
|
595
|
+
set(key, new_val, ttl_or_default(ttl), req_options)
|
|
596
|
+
new_val
|
|
597
|
+
end
|
|
598
|
+
|
|
384
599
|
##
|
|
385
600
|
# Uses the argument TTL or the client-wide default. Ensures
|
|
386
601
|
# that the value is an integer
|
|
@@ -423,8 +638,10 @@ module Dalli
|
|
|
423
638
|
key = @key_manager.validate_key(key)
|
|
424
639
|
|
|
425
640
|
server = ring.server_for_key(key)
|
|
426
|
-
|
|
427
|
-
|
|
641
|
+
Instrumentation.trace(op.to_s, trace_attrs(op.to_s, key, server)) do
|
|
642
|
+
server.request(op, key, *args)
|
|
643
|
+
end
|
|
644
|
+
rescue RetryableNetworkError => e
|
|
428
645
|
Dalli.logger.debug { e.inspect }
|
|
429
646
|
Dalli.logger.debug { 'retrying request with new server' }
|
|
430
647
|
retry
|
|
@@ -440,5 +657,23 @@ module Dalli
|
|
|
440
657
|
def pipelined_getter
|
|
441
658
|
PipelinedGetter.new(ring, @key_manager)
|
|
442
659
|
end
|
|
660
|
+
|
|
661
|
+
def pipelined_setter
|
|
662
|
+
PipelinedSetter.new(ring, @key_manager)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def pipelined_deleter
|
|
666
|
+
PipelinedDeleter.new(ring, @key_manager)
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def raise_unless_meta_protocol!
|
|
670
|
+
return if protocol_implementation == Dalli::Protocol::Meta
|
|
671
|
+
|
|
672
|
+
raise Dalli::DalliError,
|
|
673
|
+
'This operation requires the meta protocol (memcached 1.6+). ' \
|
|
674
|
+
'Use protocol: :meta when creating the client.'
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
include ProtocolDeprecations
|
|
443
678
|
end
|
|
444
679
|
end
|