dalli 4.0.0 → 4.1.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 +31 -0
- data/Gemfile +9 -2
- data/README.md +26 -0
- data/lib/dalli/client.rb +166 -0
- data/lib/dalli/pipelined_deleter.rb +82 -0
- data/lib/dalli/pipelined_setter.rb +87 -0
- data/lib/dalli/protocol/base.rb +2 -2
- data/lib/dalli/protocol/binary.rb +13 -0
- data/lib/dalli/protocol/connection_manager.rb +3 -3
- data/lib/dalli/protocol/meta/request_formatter.rb +33 -2
- data/lib/dalli/protocol/meta/response_processor.rb +50 -0
- data/lib/dalli/protocol/meta.rb +69 -2
- data/lib/dalli/protocol/string_marshaller.rb +65 -0
- data/lib/dalli/protocol.rb +10 -0
- data/lib/dalli/protocol_deprecations.rb +45 -0
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +4 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e342d39d58d552607783486a9c9f0ffa23d9c479079c62cb760ec84a97d064da
|
|
4
|
+
data.tar.gz: 1ae923ede204d0a3e82426404cfb4882f4414a12cb351cf3d04b9ef8653051ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aeb1bec84092f4e07db884b415d6b25375dc995fff04cc58a1764d9ad0937986ba7ff5db5a665a831bd8ab488b3fa4868fce79834e93a26848bc37ac1c9f9855
|
|
7
|
+
data.tar.gz: 8c5a4e21f4b0a86279bf28edc729c0a1981964438342117f3bc180d15d5cdfc93e395913267db7af66b3907416051be7629e687708c163f4d4015d4e2a768508
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
Dalli Changelog
|
|
2
2
|
=====================
|
|
3
3
|
|
|
4
|
+
4.1.0
|
|
5
|
+
==========
|
|
6
|
+
|
|
7
|
+
New Features:
|
|
8
|
+
|
|
9
|
+
- Add `set_multi` for efficient bulk set operations using pipelined requests
|
|
10
|
+
- Add `delete_multi` for efficient bulk delete operations using pipelined requests
|
|
11
|
+
- Add `fetch_with_lock` for thundering herd protection using meta protocol's vivify/recache flags (requires memcached 1.6+)
|
|
12
|
+
- Add thundering herd protection support to meta protocol (requires memcached 1.6+):
|
|
13
|
+
- `N` (vivify) flag for creating stubs on cache miss
|
|
14
|
+
- `R` (recache) flag for winning recache race when TTL is below threshold
|
|
15
|
+
- Response flags `W` (won recache), `X` (stale), `Z` (lost race)
|
|
16
|
+
- `delete_stale` method for marking items as stale instead of deleting
|
|
17
|
+
- Add `get_with_metadata` for advanced cache operations with metadata retrieval (requires memcached 1.6+):
|
|
18
|
+
- Returns hash with `:value`, `:cas`, `:won_recache`, `:stale`, `:lost_recache`
|
|
19
|
+
- Optional `:return_hit_status` returns `:hit_before` (true/false for previous access)
|
|
20
|
+
- Optional `:return_last_access` returns `:last_access` (seconds since last access)
|
|
21
|
+
- Optional `:skip_lru_bump` prevents LRU update on access
|
|
22
|
+
- Optional `:vivify_ttl` and `:recache_ttl` for thundering herd protection
|
|
23
|
+
|
|
24
|
+
Deprecations:
|
|
25
|
+
|
|
26
|
+
- Binary protocol is deprecated and will be removed in Dalli 5.0. Use `protocol: :meta` instead (requires memcached 1.6+)
|
|
27
|
+
- SASL authentication is deprecated and will be removed in Dalli 5.0. Consider using network-level security or memcached's TLS support
|
|
28
|
+
|
|
29
|
+
4.0.1
|
|
30
|
+
==========
|
|
31
|
+
|
|
32
|
+
- Add `:raw` client option to skip serialization entirely, returning raw byte strings
|
|
33
|
+
- Handle `OpenSSL::SSL::SSLError` in connection manager
|
|
34
|
+
|
|
4
35
|
4.0.0
|
|
5
36
|
==========
|
|
6
37
|
|
data/Gemfile
CHANGED
|
@@ -5,11 +5,18 @@ source 'https://rubygems.org'
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
7
|
group :development, :test do
|
|
8
|
+
gem 'benchmark'
|
|
8
9
|
gem 'cgi'
|
|
9
10
|
gem 'connection_pool'
|
|
10
11
|
gem 'debug' unless RUBY_PLATFORM == 'java'
|
|
11
|
-
|
|
12
|
-
|
|
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'
|
|
13
20
|
gem 'rake', '~> 13.0'
|
|
14
21
|
gem 'rubocop'
|
|
15
22
|
gem 'rubocop-minitest'
|
data/README.md
CHANGED
|
@@ -14,6 +14,32 @@ Dalli supports:
|
|
|
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.1 or later
|
|
20
|
+
* memcached 1.4 or later (1.6+ recommended for meta protocol support)
|
|
21
|
+
|
|
22
|
+
## Protocol Options
|
|
23
|
+
|
|
24
|
+
Dalli supports two protocols for communicating with memcached:
|
|
25
|
+
|
|
26
|
+
* `:binary` (default) - Works with all memcached versions, supports SASL authentication
|
|
27
|
+
* `:meta` - Requires memcached 1.6+, better performance for some operations, no authentication support
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
Dalli::Client.new('localhost:11211', protocol: :meta)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Security Note
|
|
34
|
+
|
|
35
|
+
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:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
Dalli::Client.new('localhost:11211', serializer: JSON)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
See the [4.0-Upgrade.md](4.0-Upgrade.md) guide for more information.
|
|
42
|
+
|
|
17
43
|

|
|
18
44
|
|
|
19
45
|
|
data/lib/dalli/client.rb
CHANGED
|
@@ -41,6 +41,11 @@ 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
|
|
@@ -51,6 +56,7 @@ module Dalli
|
|
|
51
56
|
@options = normalize_options(options)
|
|
52
57
|
@key_manager = ::Dalli::KeyManager.new(@options)
|
|
53
58
|
@ring = nil
|
|
59
|
+
emit_deprecation_warnings
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
#
|
|
@@ -91,6 +97,51 @@ module Dalli
|
|
|
91
97
|
yield value, cas
|
|
92
98
|
end
|
|
93
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
|
+
server = ring.server_for_key(key)
|
|
138
|
+
server.request(:meta_get, key, options)
|
|
139
|
+
rescue NetworkError => e
|
|
140
|
+
Dalli.logger.debug { e.inspect }
|
|
141
|
+
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
142
|
+
retry
|
|
143
|
+
end
|
|
144
|
+
|
|
94
145
|
##
|
|
95
146
|
# Fetch multiple keys efficiently.
|
|
96
147
|
# If a block is given, yields key/value pairs one at a time.
|
|
@@ -144,6 +195,69 @@ module Dalli
|
|
|
144
195
|
new_val
|
|
145
196
|
end
|
|
146
197
|
|
|
198
|
+
##
|
|
199
|
+
# Fetch the value with thundering herd protection using the meta protocol's
|
|
200
|
+
# N (vivify) and R (recache) flags.
|
|
201
|
+
#
|
|
202
|
+
# This method prevents multiple clients from simultaneously regenerating the same
|
|
203
|
+
# cache entry (the "thundering herd" problem). Only one client wins the right to
|
|
204
|
+
# regenerate; other clients receive the stale value (if available) or wait.
|
|
205
|
+
#
|
|
206
|
+
# IMPORTANT: This method requires memcached 1.6+ and the meta protocol (protocol: :meta).
|
|
207
|
+
# It will raise an error if used with the binary protocol.
|
|
208
|
+
#
|
|
209
|
+
# @param key [String] the cache key
|
|
210
|
+
# @param ttl [Integer] time-to-live for the cached value in seconds
|
|
211
|
+
# @param lock_ttl [Integer] how long the lock/stub lives (default: 30 seconds)
|
|
212
|
+
# This is the maximum time other clients will return stale data while
|
|
213
|
+
# waiting for regeneration. Should be longer than your expected regeneration time.
|
|
214
|
+
# @param recache_threshold [Integer, nil] if set, win the recache race when the
|
|
215
|
+
# item's remaining TTL is below this threshold. Useful for proactive recaching.
|
|
216
|
+
# @param req_options [Hash] options passed to set operations (e.g., raw: true)
|
|
217
|
+
#
|
|
218
|
+
# @yield Block to regenerate the value (only called if this client won the race)
|
|
219
|
+
# @return [Object] the cached value (may be stale if another client is regenerating)
|
|
220
|
+
#
|
|
221
|
+
# @example Basic usage
|
|
222
|
+
# client.fetch_with_lock('expensive_key', ttl: 300, lock_ttl: 30) do
|
|
223
|
+
# expensive_database_query
|
|
224
|
+
# end
|
|
225
|
+
#
|
|
226
|
+
# @example With proactive recaching (recache before expiry)
|
|
227
|
+
# client.fetch_with_lock('key', ttl: 300, lock_ttl: 30, recache_threshold: 60) do
|
|
228
|
+
# expensive_operation
|
|
229
|
+
# end
|
|
230
|
+
#
|
|
231
|
+
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil)
|
|
232
|
+
raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
|
|
233
|
+
|
|
234
|
+
raise_unless_meta_protocol!
|
|
235
|
+
|
|
236
|
+
key = key.to_s
|
|
237
|
+
key = @key_manager.validate_key(key)
|
|
238
|
+
|
|
239
|
+
server = ring.server_for_key(key)
|
|
240
|
+
result = server.request(:meta_get, key, {
|
|
241
|
+
vivify_ttl: lock_ttl,
|
|
242
|
+
recache_ttl: recache_threshold
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
if result[:won_recache]
|
|
246
|
+
# This client won the race - regenerate the value
|
|
247
|
+
new_val = yield
|
|
248
|
+
set(key, new_val, ttl_or_default(ttl), req_options)
|
|
249
|
+
new_val
|
|
250
|
+
else
|
|
251
|
+
# Another client is regenerating, or value exists and isn't stale
|
|
252
|
+
# Return the existing value
|
|
253
|
+
result[:value]
|
|
254
|
+
end
|
|
255
|
+
rescue NetworkError => e
|
|
256
|
+
Dalli.logger.debug { e.inspect }
|
|
257
|
+
Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
|
|
258
|
+
retry
|
|
259
|
+
end
|
|
260
|
+
|
|
147
261
|
##
|
|
148
262
|
# compare and swap values using optimistic locking.
|
|
149
263
|
# Fetch the existing value for key.
|
|
@@ -201,6 +315,24 @@ module Dalli
|
|
|
201
315
|
set_cas(key, value, 0, ttl, req_options)
|
|
202
316
|
end
|
|
203
317
|
|
|
318
|
+
##
|
|
319
|
+
# Set multiple keys and values efficiently using pipelining.
|
|
320
|
+
# This method is more efficient than calling set() in a loop because
|
|
321
|
+
# it batches requests by server and uses quiet mode.
|
|
322
|
+
#
|
|
323
|
+
# @param hash [Hash] key-value pairs to set
|
|
324
|
+
# @param ttl [Integer] time-to-live in seconds (optional, uses default if not provided)
|
|
325
|
+
# @param req_options [Hash] options passed to each set operation
|
|
326
|
+
# @return [void]
|
|
327
|
+
#
|
|
328
|
+
# Example:
|
|
329
|
+
# client.set_multi({ 'key1' => 'value1', 'key2' => 'value2' }, 300)
|
|
330
|
+
def set_multi(hash, ttl = nil, req_options = nil)
|
|
331
|
+
return if hash.empty?
|
|
332
|
+
|
|
333
|
+
pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
|
|
334
|
+
end
|
|
335
|
+
|
|
204
336
|
##
|
|
205
337
|
# Set the key-value pair, verifying existing CAS.
|
|
206
338
|
# Returns the resulting CAS value if succeeded, and falsy otherwise.
|
|
@@ -240,6 +372,22 @@ module Dalli
|
|
|
240
372
|
delete_cas(key, 0)
|
|
241
373
|
end
|
|
242
374
|
|
|
375
|
+
##
|
|
376
|
+
# Delete multiple keys efficiently using pipelining.
|
|
377
|
+
# This method is more efficient than calling delete() in a loop because
|
|
378
|
+
# it batches requests by server and uses quiet mode.
|
|
379
|
+
#
|
|
380
|
+
# @param keys [Array<String>] keys to delete
|
|
381
|
+
# @return [void]
|
|
382
|
+
#
|
|
383
|
+
# Example:
|
|
384
|
+
# client.delete_multi(['key1', 'key2', 'key3'])
|
|
385
|
+
def delete_multi(keys)
|
|
386
|
+
return if keys.empty?
|
|
387
|
+
|
|
388
|
+
pipelined_deleter.process(keys)
|
|
389
|
+
end
|
|
390
|
+
|
|
243
391
|
##
|
|
244
392
|
# Append value to the value already stored on the server for 'key'.
|
|
245
393
|
# Appending only works for values stored with :raw => true.
|
|
@@ -440,5 +588,23 @@ module Dalli
|
|
|
440
588
|
def pipelined_getter
|
|
441
589
|
PipelinedGetter.new(ring, @key_manager)
|
|
442
590
|
end
|
|
591
|
+
|
|
592
|
+
def pipelined_setter
|
|
593
|
+
PipelinedSetter.new(ring, @key_manager)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def pipelined_deleter
|
|
597
|
+
PipelinedDeleter.new(ring, @key_manager)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def raise_unless_meta_protocol!
|
|
601
|
+
return if protocol_implementation == Dalli::Protocol::Meta
|
|
602
|
+
|
|
603
|
+
raise Dalli::DalliError,
|
|
604
|
+
'This operation requires the meta protocol (memcached 1.6+). ' \
|
|
605
|
+
'Use protocol: :meta when creating the client.'
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
include ProtocolDeprecations
|
|
443
609
|
end
|
|
444
610
|
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
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# Contains logic for the pipelined set operations implemented by the client.
|
|
6
|
+
# Efficiently writes multiple key-value pairs by grouping requests by server
|
|
7
|
+
# and using quiet mode to minimize round trips.
|
|
8
|
+
##
|
|
9
|
+
class PipelinedSetter
|
|
10
|
+
def initialize(ring, key_manager)
|
|
11
|
+
@ring = ring
|
|
12
|
+
@key_manager = key_manager
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# Writes multiple key-value pairs to memcached.
|
|
17
|
+
# Raises an error if any server is unavailable.
|
|
18
|
+
#
|
|
19
|
+
# @param hash [Hash] key-value pairs to set
|
|
20
|
+
# @param ttl [Integer] time-to-live in seconds
|
|
21
|
+
# @param req_options [Hash] options passed to each set operation
|
|
22
|
+
# @return [void]
|
|
23
|
+
##
|
|
24
|
+
def process(hash, ttl, req_options)
|
|
25
|
+
return if hash.empty?
|
|
26
|
+
|
|
27
|
+
@ring.lock do
|
|
28
|
+
servers = setup_requests(hash, ttl, req_options)
|
|
29
|
+
finish_requests(servers)
|
|
30
|
+
end
|
|
31
|
+
rescue NetworkError => e
|
|
32
|
+
Dalli.logger.debug { e.inspect }
|
|
33
|
+
Dalli.logger.debug { 'retrying pipelined sets because of network error' }
|
|
34
|
+
retry
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def setup_requests(hash, ttl, req_options)
|
|
40
|
+
groups = groups_for_keys(hash.keys)
|
|
41
|
+
make_set_requests(groups, hash, ttl, req_options)
|
|
42
|
+
groups.keys
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Loop through the server-grouped sets of keys, writing
|
|
47
|
+
# the corresponding quiet set requests to the appropriate servers
|
|
48
|
+
##
|
|
49
|
+
def make_set_requests(groups, hash, ttl, req_options)
|
|
50
|
+
groups.each do |server, keys_for_server|
|
|
51
|
+
keys_for_server.each do |key|
|
|
52
|
+
original_key = @key_manager.key_without_namespace(key)
|
|
53
|
+
value = hash[original_key]
|
|
54
|
+
server.request(:pipelined_set, key, value, ttl, req_options)
|
|
55
|
+
rescue DalliError, NetworkError => e
|
|
56
|
+
Dalli.logger.debug { e.inspect }
|
|
57
|
+
Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Sends noop to each server to flush responses and ensure all writes complete.
|
|
64
|
+
##
|
|
65
|
+
def finish_requests(servers)
|
|
66
|
+
servers.each do |server|
|
|
67
|
+
server.request(:noop)
|
|
68
|
+
rescue DalliError, NetworkError => e
|
|
69
|
+
Dalli.logger.debug { e.inspect }
|
|
70
|
+
Dalli.logger.debug { "unable to complete pipelined set on server #{server.name}" }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def groups_for_keys(keys)
|
|
75
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
76
|
+
groups = @ring.keys_grouped_by_server(validated_keys)
|
|
77
|
+
|
|
78
|
+
if (unfound_keys = groups.delete(nil))
|
|
79
|
+
Dalli.logger.debug do
|
|
80
|
+
"unable to set #{unfound_keys.length} keys because no matching server was found"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
groups
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -23,7 +23,7 @@ module Dalli
|
|
|
23
23
|
def initialize(attribs, client_options = {})
|
|
24
24
|
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
|
25
25
|
@options = client_options.merge(user_creds)
|
|
26
|
-
@value_marshaller = ValueMarshaller.new(@options)
|
|
26
|
+
@value_marshaller = client_options[:raw] ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
27
27
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -106,7 +106,7 @@ module Dalli
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
values
|
|
109
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
109
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
110
110
|
@connection_manager.error_on_request!(e)
|
|
111
111
|
end
|
|
112
112
|
|
|
@@ -56,6 +56,12 @@ module Dalli
|
|
|
56
56
|
storage_req(opkey, key, value, ttl, cas, options)
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Pipelined set - writes a quiet set request without reading response.
|
|
60
|
+
# Used by PipelinedSetter for bulk operations.
|
|
61
|
+
def pipelined_set(key, value, ttl, options)
|
|
62
|
+
storage_req(:setq, key, value, ttl, 0, options)
|
|
63
|
+
end
|
|
64
|
+
|
|
59
65
|
def add(key, value, ttl, options)
|
|
60
66
|
opkey = quiet? ? :addq : :add
|
|
61
67
|
storage_req(opkey, key, value, ttl, 0, options)
|
|
@@ -102,6 +108,13 @@ module Dalli
|
|
|
102
108
|
response_processor.delete unless quiet?
|
|
103
109
|
end
|
|
104
110
|
|
|
111
|
+
# Pipelined delete - writes a quiet delete request without reading response.
|
|
112
|
+
# Used by PipelinedDeleter for bulk operations.
|
|
113
|
+
def pipelined_delete(key)
|
|
114
|
+
req = RequestFormatter.standard_request(opkey: :deleteq, key: key, cas: 0)
|
|
115
|
+
write(req)
|
|
116
|
+
end
|
|
117
|
+
|
|
105
118
|
# Arithmetic Commands
|
|
106
119
|
def decr(key, count, ttl, initial)
|
|
107
120
|
opkey = quiet? ? :decrq : :decr
|
|
@@ -150,19 +150,19 @@ module Dalli
|
|
|
150
150
|
data = @sock.gets("\r\n")
|
|
151
151
|
error_on_request!('EOF in read_line') if data.nil?
|
|
152
152
|
data
|
|
153
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
153
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
154
154
|
error_on_request!(e)
|
|
155
155
|
end
|
|
156
156
|
|
|
157
157
|
def read(count)
|
|
158
158
|
@sock.readfull(count)
|
|
159
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
159
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
160
160
|
error_on_request!(e)
|
|
161
161
|
end
|
|
162
162
|
|
|
163
163
|
def write(bytes)
|
|
164
164
|
@sock.write(bytes)
|
|
165
|
-
rescue SystemCallError, *TIMEOUT_ERRORS => e
|
|
165
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
|
|
166
166
|
error_on_request!(e)
|
|
167
167
|
end
|
|
168
168
|
|
|
@@ -15,13 +15,40 @@ module Dalli
|
|
|
15
15
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
16
16
|
# rubocop:disable Metrics/ParameterLists
|
|
17
17
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
18
|
-
|
|
18
|
+
#
|
|
19
|
+
# Meta get flags:
|
|
20
|
+
#
|
|
21
|
+
# Thundering herd protection:
|
|
22
|
+
# - vivify_ttl (N flag): On miss, create a stub item and return W flag. The TTL
|
|
23
|
+
# specifies how long the stub lives. Other clients see X (stale) and Z (lost race).
|
|
24
|
+
# - recache_ttl (R flag): If item's remaining TTL is below this threshold, return W
|
|
25
|
+
# flag to indicate this client should recache. Other clients get Z (lost race).
|
|
26
|
+
#
|
|
27
|
+
# Metadata flags:
|
|
28
|
+
# - return_hit_status (h flag): Return whether item has been hit before (0 or 1)
|
|
29
|
+
# - return_last_access (l flag): Return seconds since item was last accessed
|
|
30
|
+
# - skip_lru_bump (u flag): Don't bump item in LRU, don't update hit status or last access
|
|
31
|
+
#
|
|
32
|
+
# Response flags (parsed by response processor):
|
|
33
|
+
# - W: Client won the right to recache this item
|
|
34
|
+
# - X: Item is stale (another client is regenerating)
|
|
35
|
+
# - Z: Client lost the recache race (another client is already regenerating)
|
|
36
|
+
# - h0/h1: Hit status (0 = first access, 1 = previously accessed)
|
|
37
|
+
# - l<N>: Seconds since last access
|
|
38
|
+
def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false,
|
|
39
|
+
vivify_ttl: nil, recache_ttl: nil,
|
|
40
|
+
return_hit_status: false, return_last_access: false, skip_lru_bump: false)
|
|
19
41
|
cmd = "mg #{key}"
|
|
20
42
|
cmd << ' v f' if value
|
|
21
43
|
cmd << ' c' if return_cas
|
|
22
44
|
cmd << ' b' if base64
|
|
23
45
|
cmd << " T#{ttl}" if ttl
|
|
24
46
|
cmd << ' k q s' if quiet # Return the key in the response if quiet
|
|
47
|
+
cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
|
|
48
|
+
cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
|
|
49
|
+
cmd << ' h' if return_hit_status # Return hit status (0 or 1)
|
|
50
|
+
cmd << ' l' if return_last_access # Return seconds since last access
|
|
51
|
+
cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
|
|
25
52
|
cmd + TERMINATOR
|
|
26
53
|
end
|
|
27
54
|
|
|
@@ -37,11 +64,15 @@ module Dalli
|
|
|
37
64
|
cmd << TERMINATOR
|
|
38
65
|
end
|
|
39
66
|
|
|
40
|
-
|
|
67
|
+
# Thundering herd protection flag:
|
|
68
|
+
# - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
|
|
69
|
+
# using N/R flags will see the X flag and know the item is being regenerated.
|
|
70
|
+
def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false, stale: false)
|
|
41
71
|
cmd = "md #{key}"
|
|
42
72
|
cmd << ' b' if base64
|
|
43
73
|
cmd << cas_string(cas)
|
|
44
74
|
cmd << " T#{ttl}" if ttl
|
|
75
|
+
cmd << ' I' if stale # Mark stale instead of deleting
|
|
45
76
|
cmd << ' q' if quiet
|
|
46
77
|
cmd + TERMINATOR
|
|
47
78
|
end
|
|
@@ -51,6 +51,41 @@ module Dalli
|
|
|
51
51
|
tokens.first == EN ? nil : true
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Returns a hash with all requested metadata:
|
|
55
|
+
# - :value - the cached value (or nil if miss)
|
|
56
|
+
# - :cas - the CAS value (if return_cas was requested)
|
|
57
|
+
# - :won_recache - true if client won the right to recache (W flag)
|
|
58
|
+
# - :stale - true if the item is stale (X flag)
|
|
59
|
+
# - :lost_recache - true if another client is already recaching (Z flag)
|
|
60
|
+
# - :hit_before - true/false if item was previously accessed (h flag, if requested)
|
|
61
|
+
# - :last_access - seconds since last access (l flag, if requested)
|
|
62
|
+
#
|
|
63
|
+
# Used by meta_get for comprehensive metadata retrieval.
|
|
64
|
+
# Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
|
|
65
|
+
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false)
|
|
66
|
+
tokens = error_on_unexpected!([VA, EN, HD])
|
|
67
|
+
result = build_metadata_result(tokens)
|
|
68
|
+
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
69
|
+
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
70
|
+
result[:value] = parse_value_from_tokens(tokens, cache_nils)
|
|
71
|
+
result
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_metadata_result(tokens)
|
|
75
|
+
{
|
|
76
|
+
value: nil, cas: cas_from_tokens(tokens),
|
|
77
|
+
won_recache: tokens.include?('W'), stale: tokens.include?('X'),
|
|
78
|
+
lost_recache: tokens.include?('Z')
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def parse_value_from_tokens(tokens, cache_nils)
|
|
83
|
+
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
84
|
+
return unless tokens.first == VA
|
|
85
|
+
|
|
86
|
+
@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
|
|
87
|
+
end
|
|
88
|
+
|
|
54
89
|
def meta_set_with_cas
|
|
55
90
|
tokens = error_on_unexpected!([HD, NS, NF, EX])
|
|
56
91
|
return false unless tokens.first == HD
|
|
@@ -190,6 +225,21 @@ module Dalli
|
|
|
190
225
|
KeyRegularizer.decode(encoded_key, base64_encoded)
|
|
191
226
|
end
|
|
192
227
|
|
|
228
|
+
# Returns true if item was previously hit, false if first access, nil if not requested
|
|
229
|
+
# The h flag returns h0 (first access) or h1 (previously accessed)
|
|
230
|
+
def hit_status_from_tokens(tokens)
|
|
231
|
+
hit_token = tokens.find { |t| t.start_with?('h') && t.length == 2 }
|
|
232
|
+
return nil unless hit_token
|
|
233
|
+
|
|
234
|
+
hit_token[1] == '1'
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Returns seconds since last access, or nil if not requested
|
|
238
|
+
# The l flag returns l<seconds>
|
|
239
|
+
def last_access_from_tokens(tokens)
|
|
240
|
+
value_from_tokens(tokens, 'l')&.to_i
|
|
241
|
+
end
|
|
242
|
+
|
|
193
243
|
def body_len_from_tokens(tokens)
|
|
194
244
|
value_from_tokens(tokens, 's')&.to_i
|
|
195
245
|
end
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -60,12 +60,71 @@ module Dalli
|
|
|
60
60
|
response_processor.meta_get_with_value_and_cas
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
# Comprehensive meta get with support for all metadata flags.
|
|
64
|
+
# @note Requires memcached 1.6+ (meta protocol feature)
|
|
65
|
+
#
|
|
66
|
+
# This is the full-featured get method that supports:
|
|
67
|
+
# - Thundering herd protection (vivify_ttl, recache_ttl)
|
|
68
|
+
# - Item metadata (hit_status, last_access)
|
|
69
|
+
# - LRU control (skip_lru_bump)
|
|
70
|
+
#
|
|
71
|
+
# @param key [String] the key to retrieve
|
|
72
|
+
# @param options [Hash] options controlling what metadata to return
|
|
73
|
+
# - :vivify_ttl [Integer] creates a stub on miss with this TTL (N flag)
|
|
74
|
+
# - :recache_ttl [Integer] wins recache race if remaining TTL is below this (R flag)
|
|
75
|
+
# - :return_hit_status [Boolean] return whether item was previously accessed (h flag)
|
|
76
|
+
# - :return_last_access [Boolean] return seconds since last access (l flag)
|
|
77
|
+
# - :skip_lru_bump [Boolean] don't bump LRU or update access stats (u flag)
|
|
78
|
+
# - :cache_nils [Boolean] whether to cache nil values
|
|
79
|
+
# @return [Hash] containing:
|
|
80
|
+
# - :value - the cached value (or nil on miss)
|
|
81
|
+
# - :cas - the CAS value
|
|
82
|
+
# - :won_recache - true if client won recache race (W flag)
|
|
83
|
+
# - :stale - true if item is stale (X flag)
|
|
84
|
+
# - :lost_recache - true if another client is recaching (Z flag)
|
|
85
|
+
# - :hit_before - true/false if previously accessed (only if return_hit_status: true)
|
|
86
|
+
# - :last_access - seconds since last access (only if return_last_access: true)
|
|
87
|
+
def meta_get(key, options = {})
|
|
88
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
89
|
+
req = RequestFormatter.meta_get(
|
|
90
|
+
key: encoded_key, value: true, return_cas: true, base64: base64,
|
|
91
|
+
vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
|
|
92
|
+
return_hit_status: options[:return_hit_status],
|
|
93
|
+
return_last_access: options[:return_last_access], skip_lru_bump: options[:skip_lru_bump]
|
|
94
|
+
)
|
|
95
|
+
write(req)
|
|
96
|
+
response_processor.meta_get_with_metadata(
|
|
97
|
+
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
98
|
+
return_last_access: options[:return_last_access]
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Delete with stale invalidation instead of actual deletion.
|
|
103
|
+
# Used with thundering herd protection to mark items as stale rather than removing them.
|
|
104
|
+
# @note Requires memcached 1.6+ (meta protocol feature)
|
|
105
|
+
#
|
|
106
|
+
# @param key [String] the key to invalidate
|
|
107
|
+
# @param cas [Integer] optional CAS value for compare-and-swap
|
|
108
|
+
# @return [Boolean] true if successful
|
|
109
|
+
def delete_stale(key, cas = nil)
|
|
110
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
111
|
+
req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
|
|
112
|
+
write(req)
|
|
113
|
+
response_processor.meta_delete
|
|
114
|
+
end
|
|
115
|
+
|
|
63
116
|
# Storage Commands
|
|
64
117
|
def set(key, value, ttl, cas, options)
|
|
65
118
|
write_storage_req(:set, key, value, ttl, cas, options)
|
|
66
119
|
response_processor.meta_set_with_cas unless quiet?
|
|
67
120
|
end
|
|
68
121
|
|
|
122
|
+
# Pipelined set - writes a quiet set request without reading response.
|
|
123
|
+
# Used by PipelinedSetter for bulk operations.
|
|
124
|
+
def pipelined_set(key, value, ttl, options)
|
|
125
|
+
write_storage_req(:set, key, value, ttl, nil, options, quiet: true)
|
|
126
|
+
end
|
|
127
|
+
|
|
69
128
|
def add(key, value, ttl, options)
|
|
70
129
|
write_storage_req(:add, key, value, ttl, nil, options)
|
|
71
130
|
response_processor.meta_set_with_cas unless quiet?
|
|
@@ -77,13 +136,13 @@ module Dalli
|
|
|
77
136
|
end
|
|
78
137
|
|
|
79
138
|
# rubocop:disable Metrics/ParameterLists
|
|
80
|
-
def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {})
|
|
139
|
+
def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
|
|
81
140
|
(value, bitflags) = @value_marshaller.store(key, raw_value, options)
|
|
82
141
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
83
142
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
84
143
|
req = RequestFormatter.meta_set(key: encoded_key, value: value,
|
|
85
144
|
bitflags: bitflags, cas: cas,
|
|
86
|
-
ttl: ttl, mode: mode, quiet: quiet
|
|
145
|
+
ttl: ttl, mode: mode, quiet: quiet, base64: base64)
|
|
87
146
|
write(req)
|
|
88
147
|
write(value)
|
|
89
148
|
write(TERMINATOR)
|
|
@@ -121,6 +180,14 @@ module Dalli
|
|
|
121
180
|
response_processor.meta_delete unless quiet?
|
|
122
181
|
end
|
|
123
182
|
|
|
183
|
+
# Pipelined delete - writes a quiet delete request without reading response.
|
|
184
|
+
# Used by PipelinedDeleter for bulk operations.
|
|
185
|
+
def pipelined_delete(key)
|
|
186
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
187
|
+
req = RequestFormatter.meta_delete(key: encoded_key, base64: base64, quiet: true)
|
|
188
|
+
write(req)
|
|
189
|
+
end
|
|
190
|
+
|
|
124
191
|
# Arithmetic Commands
|
|
125
192
|
def decr(key, count, ttl, initial)
|
|
126
193
|
decr_incr false, key, count, ttl, initial
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
module Protocol
|
|
5
|
+
##
|
|
6
|
+
# Dalli::Protocol::StringMarshaller is a pass-through marshaller for use with
|
|
7
|
+
# the :raw client option. It bypasses serialization and compression entirely,
|
|
8
|
+
# expecting values to already be strings (e.g., pre-serialized by Rails'
|
|
9
|
+
# ActiveSupport::Cache). It still enforces the maximum value size limit.
|
|
10
|
+
##
|
|
11
|
+
class StringMarshaller
|
|
12
|
+
DEFAULTS = {
|
|
13
|
+
# max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
|
|
14
|
+
value_max_bytes: 1024 * 1024
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :value_max_bytes
|
|
18
|
+
|
|
19
|
+
def initialize(client_options)
|
|
20
|
+
@value_max_bytes = client_options.fetch(:value_max_bytes) do
|
|
21
|
+
DEFAULTS.fetch(:value_max_bytes)
|
|
22
|
+
end.to_i
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def store(key, value, _options = nil)
|
|
26
|
+
raise MarshalError, "Dalli in :raw mode only supports strings, got: #{value.class}" unless value.is_a?(String)
|
|
27
|
+
|
|
28
|
+
error_if_over_max_value_bytes(key, value)
|
|
29
|
+
[value, 0]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def retrieve(value, _flags)
|
|
33
|
+
value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Interface compatibility methods - these return nil since
|
|
37
|
+
# StringMarshaller bypasses serialization and compression entirely.
|
|
38
|
+
|
|
39
|
+
def serializer
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def compressor
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def compression_min_size
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def compress_by_default?
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def error_if_over_max_value_bytes(key, value)
|
|
58
|
+
return if value.bytesize <= value_max_bytes
|
|
59
|
+
|
|
60
|
+
message = "Value for #{key} over max size: #{value_max_bytes} <= #{value.bytesize}"
|
|
61
|
+
raise Dalli::ValueOverMaxSize, message
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/dalli/protocol.rb
CHANGED
|
@@ -15,5 +15,15 @@ module Dalli
|
|
|
15
15
|
else
|
|
16
16
|
[Timeout::Error]
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
# SSL errors that occur during read/write operations (not during initial
|
|
20
|
+
# handshake) should trigger reconnection. These indicate transient network
|
|
21
|
+
# issues, not configuration problems.
|
|
22
|
+
SSL_ERRORS =
|
|
23
|
+
if defined?(OpenSSL::SSL::SSLError)
|
|
24
|
+
[OpenSSL::SSL::SSLError]
|
|
25
|
+
else
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
18
28
|
end
|
|
19
29
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# Handles deprecation warnings for protocol and authentication features
|
|
6
|
+
# that will be removed in Dalli 5.0.
|
|
7
|
+
##
|
|
8
|
+
module ProtocolDeprecations
|
|
9
|
+
BINARY_PROTOCOL_DEPRECATION_MESSAGE = <<~MSG.chomp
|
|
10
|
+
[DEPRECATION] The binary protocol is deprecated and will be removed in Dalli 5.0. \
|
|
11
|
+
Please use `protocol: :meta` instead. The meta protocol requires memcached 1.6+. \
|
|
12
|
+
See https://github.com/petergoldstein/dalli for migration details.
|
|
13
|
+
MSG
|
|
14
|
+
|
|
15
|
+
SASL_AUTH_DEPRECATION_MESSAGE = <<~MSG.chomp
|
|
16
|
+
[DEPRECATION] SASL authentication is deprecated and will be removed in Dalli 5.0. \
|
|
17
|
+
SASL is only supported by the binary protocol, which is being removed. \
|
|
18
|
+
Consider using network-level security (firewall rules, VPN) or memcached's TLS support instead.
|
|
19
|
+
MSG
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def emit_deprecation_warnings
|
|
24
|
+
emit_binary_protocol_deprecation_warning
|
|
25
|
+
emit_sasl_auth_deprecation_warning
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def emit_binary_protocol_deprecation_warning
|
|
29
|
+
protocol = @options[:protocol]
|
|
30
|
+
# Binary is used when protocol is nil, :binary, or 'binary'
|
|
31
|
+
return if protocol.to_s == 'meta'
|
|
32
|
+
|
|
33
|
+
warn BINARY_PROTOCOL_DEPRECATION_MESSAGE
|
|
34
|
+
Dalli.logger.warn(BINARY_PROTOCOL_DEPRECATION_MESSAGE)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def emit_sasl_auth_deprecation_warning
|
|
38
|
+
username = @options[:username] || ENV.fetch('MEMCACHE_USERNAME', nil)
|
|
39
|
+
return unless username
|
|
40
|
+
|
|
41
|
+
warn SASL_AUTH_DEPRECATION_MESSAGE
|
|
42
|
+
Dalli.logger.warn(SASL_AUTH_DEPRECATION_MESSAGE)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/dalli/version.rb
CHANGED
data/lib/dalli.rb
CHANGED
|
@@ -58,9 +58,12 @@ end
|
|
|
58
58
|
require_relative 'dalli/version'
|
|
59
59
|
|
|
60
60
|
require_relative 'dalli/compressor'
|
|
61
|
+
require_relative 'dalli/protocol_deprecations'
|
|
61
62
|
require_relative 'dalli/client'
|
|
62
63
|
require_relative 'dalli/key_manager'
|
|
63
64
|
require_relative 'dalli/pipelined_getter'
|
|
65
|
+
require_relative 'dalli/pipelined_setter'
|
|
66
|
+
require_relative 'dalli/pipelined_deleter'
|
|
64
67
|
require_relative 'dalli/ring'
|
|
65
68
|
require_relative 'dalli/protocol'
|
|
66
69
|
require_relative 'dalli/protocol/base'
|
|
@@ -72,6 +75,7 @@ require_relative 'dalli/protocol/server_config_parser'
|
|
|
72
75
|
require_relative 'dalli/protocol/ttl_sanitizer'
|
|
73
76
|
require_relative 'dalli/protocol/value_compressor'
|
|
74
77
|
require_relative 'dalli/protocol/value_marshaller'
|
|
78
|
+
require_relative 'dalli/protocol/string_marshaller'
|
|
75
79
|
require_relative 'dalli/protocol/value_serializer'
|
|
76
80
|
require_relative 'dalli/servers_arg_normalizer'
|
|
77
81
|
require_relative 'dalli/socket'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dalli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -43,7 +43,9 @@ files:
|
|
|
43
43
|
- lib/dalli/key_manager.rb
|
|
44
44
|
- lib/dalli/options.rb
|
|
45
45
|
- lib/dalli/pid_cache.rb
|
|
46
|
+
- lib/dalli/pipelined_deleter.rb
|
|
46
47
|
- lib/dalli/pipelined_getter.rb
|
|
48
|
+
- lib/dalli/pipelined_setter.rb
|
|
47
49
|
- lib/dalli/protocol.rb
|
|
48
50
|
- lib/dalli/protocol/base.rb
|
|
49
51
|
- lib/dalli/protocol/binary.rb
|
|
@@ -58,10 +60,12 @@ files:
|
|
|
58
60
|
- lib/dalli/protocol/meta/response_processor.rb
|
|
59
61
|
- lib/dalli/protocol/response_buffer.rb
|
|
60
62
|
- lib/dalli/protocol/server_config_parser.rb
|
|
63
|
+
- lib/dalli/protocol/string_marshaller.rb
|
|
61
64
|
- lib/dalli/protocol/ttl_sanitizer.rb
|
|
62
65
|
- lib/dalli/protocol/value_compressor.rb
|
|
63
66
|
- lib/dalli/protocol/value_marshaller.rb
|
|
64
67
|
- lib/dalli/protocol/value_serializer.rb
|
|
68
|
+
- lib/dalli/protocol_deprecations.rb
|
|
65
69
|
- lib/dalli/ring.rb
|
|
66
70
|
- lib/dalli/servers_arg_normalizer.rb
|
|
67
71
|
- lib/dalli/socket.rb
|
|
@@ -88,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
88
92
|
- !ruby/object:Gem::Version
|
|
89
93
|
version: '0'
|
|
90
94
|
requirements: []
|
|
91
|
-
rubygems_version: 4.0.
|
|
95
|
+
rubygems_version: 4.0.4
|
|
92
96
|
specification_version: 4
|
|
93
97
|
summary: High performance memcached client for Ruby
|
|
94
98
|
test_files: []
|