dalli 5.0.6 → 5.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 +57 -0
- data/README.md +6 -1
- data/lib/dalli/client.rb +259 -49
- data/lib/dalli/pipelined_deleter.rb +29 -12
- data/lib/dalli/pipelined_getter.rb +57 -8
- data/lib/dalli/pipelined_setter.rb +15 -3
- data/lib/dalli/protocol/base.rb +48 -4
- data/lib/dalli/protocol/key_regularizer.rb +18 -1
- data/lib/dalli/protocol/meta.rb +83 -30
- data/lib/dalli/protocol/request_formatter.rb +105 -13
- data/lib/dalli/protocol/response_processor.rb +23 -2
- data/lib/dalli/version.rb +2 -2
- metadata +2 -2
|
@@ -15,17 +15,24 @@ module Dalli
|
|
|
15
15
|
##
|
|
16
16
|
# Deletes multiple keys from memcached.
|
|
17
17
|
#
|
|
18
|
+
# `req_options` is applied to every delete in the batch (see
|
|
19
|
+
# Dalli::Client#delete for the supported meta-delete keys).
|
|
20
|
+
#
|
|
18
21
|
# @param keys [Array<String>] keys to delete
|
|
19
|
-
# @return [Integer] the number of keys
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
22
|
+
# @return [Integer] the number of keys the server found and acted on. Only
|
|
23
|
+
# a key that did not exist decrements this count, so with
|
|
24
|
+
# `req_options: {invalidate: true}` it reports how many keys were
|
|
25
|
+
# tombstoned rather than removed. This is best-effort: a transient
|
|
26
|
+
# network error is retried automatically, and keys handled before the
|
|
27
|
+
# error are not recounted, so the result may under-report when a retry
|
|
28
|
+
# occurs. If a server remains unreachable after retrying, raises
|
|
29
|
+
# Dalli::NetworkError.
|
|
23
30
|
##
|
|
24
|
-
def process(keys)
|
|
31
|
+
def process(keys, req_options = nil)
|
|
25
32
|
return 0 if keys.empty?
|
|
26
33
|
|
|
27
34
|
@ring.lock do
|
|
28
|
-
groups = setup_requests(keys)
|
|
35
|
+
groups = setup_requests(keys, req_options)
|
|
29
36
|
finish_requests(groups)
|
|
30
37
|
end
|
|
31
38
|
rescue Dalli::RetryableNetworkError => e
|
|
@@ -36,9 +43,9 @@ module Dalli
|
|
|
36
43
|
|
|
37
44
|
private
|
|
38
45
|
|
|
39
|
-
def setup_requests(keys)
|
|
46
|
+
def setup_requests(keys, req_options = nil)
|
|
40
47
|
groups = groups_for_keys(keys)
|
|
41
|
-
make_delete_requests(groups)
|
|
48
|
+
make_delete_requests(groups, req_options)
|
|
42
49
|
groups
|
|
43
50
|
end
|
|
44
51
|
|
|
@@ -46,12 +53,20 @@ module Dalli
|
|
|
46
53
|
# Loop through the server-grouped sets of keys, writing
|
|
47
54
|
# the corresponding quiet delete requests to the appropriate servers
|
|
48
55
|
##
|
|
49
|
-
|
|
56
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate: the
|
|
57
|
+
# top-level rescue in #process retries the whole pipelined delete on it. A
|
|
58
|
+
# combined `rescue DalliError, NetworkError` would silently swallow
|
|
59
|
+
# RetryableNetworkError too -- since NetworkError < DalliError -- dropping
|
|
60
|
+
# this server's keys on a transient hiccup instead of retrying. Only a
|
|
61
|
+
# non-network DalliError should be swallowed here.
|
|
62
|
+
def make_delete_requests(groups, req_options = nil)
|
|
50
63
|
groups.each do |server, keys_for_server|
|
|
51
64
|
keys_for_server.select! do |key|
|
|
52
|
-
server.request(:pipelined_delete, key)
|
|
65
|
+
server.request(:pipelined_delete, key, req_options)
|
|
53
66
|
true
|
|
54
|
-
rescue
|
|
67
|
+
rescue Dalli::NetworkError
|
|
68
|
+
raise
|
|
69
|
+
rescue DalliError => e
|
|
55
70
|
Dalli.logger.debug { e.inspect }
|
|
56
71
|
Dalli.logger.debug { "unable to delete key #{key} for server #{server.name}" }
|
|
57
72
|
false
|
|
@@ -66,7 +81,9 @@ module Dalli
|
|
|
66
81
|
def finish_requests(groups)
|
|
67
82
|
groups.sum do |server, keys_for_server|
|
|
68
83
|
server.request(:finish_pipelined_delete, keys_for_server.size)
|
|
69
|
-
rescue
|
|
84
|
+
rescue Dalli::NetworkError
|
|
85
|
+
raise
|
|
86
|
+
rescue DalliError => e
|
|
70
87
|
Dalli.logger.debug { e.inspect }
|
|
71
88
|
Dalli.logger.debug { "unable to complete pipelined delete on server #{server.name}" }
|
|
72
89
|
0
|
|
@@ -20,13 +20,18 @@ module Dalli
|
|
|
20
20
|
##
|
|
21
21
|
# Yields, one at a time, keys and their values+attributes.
|
|
22
22
|
#
|
|
23
|
-
|
|
23
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
24
|
+
#
|
|
25
|
+
# A transient network error is retried automatically. If a server remains
|
|
26
|
+
# unreachable after retrying, raises Dalli::NetworkError.
|
|
27
|
+
#
|
|
28
|
+
def process(keys, req_options = nil, &block)
|
|
24
29
|
return {} if keys.empty?
|
|
25
30
|
|
|
26
31
|
@ring.lock do
|
|
27
32
|
# Stores partial results collected during interleaved send phase
|
|
28
33
|
@partial_results = {}
|
|
29
|
-
servers = setup_requests(keys)
|
|
34
|
+
servers = setup_requests(keys, req_options)
|
|
30
35
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
36
|
|
|
32
37
|
# First yield any partial results collected during interleaved send
|
|
@@ -40,6 +45,40 @@ module Dalli
|
|
|
40
45
|
retry
|
|
41
46
|
end
|
|
42
47
|
|
|
48
|
+
##
|
|
49
|
+
# Stale-aware bulk get across servers. Returns { key => metadata Hash } for
|
|
50
|
+
# the keys that were found; see Protocol::Meta#read_multi_with_metadata_req
|
|
51
|
+
# for why misses are absent rather than present with miss: true.
|
|
52
|
+
#
|
|
53
|
+
# Unlike #process this issues one request per server and reads its full
|
|
54
|
+
# response before moving on, rather than pipelining across servers: the
|
|
55
|
+
# metadata path has no interleaving support, and the stale-aware callers it
|
|
56
|
+
# serves fetch far smaller batches than get_multi does.
|
|
57
|
+
#
|
|
58
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
59
|
+
#
|
|
60
|
+
def process_with_metadata(keys, req_options = nil)
|
|
61
|
+
return {} if keys.empty?
|
|
62
|
+
|
|
63
|
+
@ring.lock do
|
|
64
|
+
results = {}
|
|
65
|
+
groups_for_keys(keys).each do |server, keys_for_server|
|
|
66
|
+
results.merge!(server.request(:read_multi_with_metadata_req, keys_for_server, req_options))
|
|
67
|
+
rescue Dalli::RetryableNetworkError
|
|
68
|
+
raise
|
|
69
|
+
rescue DalliError, NetworkError => e
|
|
70
|
+
Dalli.logger.debug { e.inspect }
|
|
71
|
+
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
72
|
+
end
|
|
73
|
+
results.transform_keys! { |key| @key_manager.key_without_namespace(key) }
|
|
74
|
+
results
|
|
75
|
+
end
|
|
76
|
+
rescue Dalli::RetryableNetworkError => e
|
|
77
|
+
Dalli.logger.debug { e.inspect }
|
|
78
|
+
Dalli.logger.debug { 'retrying pipelined get with metadata because of network error' }
|
|
79
|
+
retry
|
|
80
|
+
end
|
|
81
|
+
|
|
43
82
|
private
|
|
44
83
|
|
|
45
84
|
def yield_partial_results
|
|
@@ -49,9 +88,9 @@ module Dalli
|
|
|
49
88
|
@partial_results.clear
|
|
50
89
|
end
|
|
51
90
|
|
|
52
|
-
def setup_requests(keys)
|
|
91
|
+
def setup_requests(keys, req_options = nil)
|
|
53
92
|
groups = groups_for_keys(keys)
|
|
54
|
-
make_getkq_requests(groups)
|
|
93
|
+
make_getkq_requests(groups, req_options)
|
|
55
94
|
|
|
56
95
|
# TODO: How does this exit on a NetworkError
|
|
57
96
|
finish_queries(groups.keys)
|
|
@@ -65,17 +104,27 @@ module Dalli
|
|
|
65
104
|
# on the wire by switching from getkq to getq, and using
|
|
66
105
|
# the opaque value to match requests to responses.
|
|
67
106
|
##
|
|
68
|
-
def make_getkq_requests(groups)
|
|
107
|
+
def make_getkq_requests(groups, req_options = nil)
|
|
69
108
|
groups.each do |server, keys_for_server|
|
|
70
109
|
if keys_for_server.size <= INTERLEAVE_THRESHOLD
|
|
71
110
|
# Small batch - send all at once (existing behavior)
|
|
72
|
-
server.request(:pipelined_get, keys_for_server)
|
|
111
|
+
server.request(:pipelined_get, keys_for_server, req_options)
|
|
73
112
|
else
|
|
74
113
|
# Large batch - interleave sends with response draining
|
|
75
114
|
# Pass @partial_results directly to avoid hash allocation/merge overhead
|
|
76
|
-
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results)
|
|
115
|
+
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results, req_options)
|
|
77
116
|
end
|
|
78
|
-
|
|
117
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate:
|
|
118
|
+
# #process's top-level rescue retries the whole pipelined get on it. This
|
|
119
|
+
# rescue used to catch DalliError and NetworkError together -- since
|
|
120
|
+
# NetworkError < DalliError, that silently swallowed RetryableNetworkError
|
|
121
|
+
# too, dropping this server's keys from the result on a transient hiccup
|
|
122
|
+
# instead of retrying, with nothing surfaced above debug-level logging.
|
|
123
|
+
# Only a non-network DalliError (this server genuinely can't serve these
|
|
124
|
+
# keys) should be swallowed here.
|
|
125
|
+
rescue Dalli::NetworkError
|
|
126
|
+
raise
|
|
127
|
+
rescue DalliError => e
|
|
79
128
|
Dalli.logger.debug { e.inspect }
|
|
80
129
|
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
81
130
|
end
|
|
@@ -14,7 +14,9 @@ module Dalli
|
|
|
14
14
|
|
|
15
15
|
##
|
|
16
16
|
# Writes multiple key-value pairs to memcached.
|
|
17
|
-
#
|
|
17
|
+
#
|
|
18
|
+
# A transient network error is retried automatically. If a server remains
|
|
19
|
+
# unreachable after retrying, raises Dalli::NetworkError.
|
|
18
20
|
#
|
|
19
21
|
# @param hash [Hash] key-value pairs to set
|
|
20
22
|
# @param ttl [Integer] time-to-live in seconds
|
|
@@ -46,13 +48,21 @@ module Dalli
|
|
|
46
48
|
# Loop through the server-grouped sets of keys, writing
|
|
47
49
|
# the corresponding quiet set requests to the appropriate servers
|
|
48
50
|
##
|
|
51
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate: the
|
|
52
|
+
# top-level rescue in #process retries the whole pipelined set on it. A
|
|
53
|
+
# combined `rescue DalliError, NetworkError` would silently swallow
|
|
54
|
+
# RetryableNetworkError too -- since NetworkError < DalliError -- dropping
|
|
55
|
+
# this server's keys on a transient hiccup instead of retrying. Only a
|
|
56
|
+
# non-network DalliError should be swallowed here.
|
|
49
57
|
def make_set_requests(groups, hash, ttl, req_options)
|
|
50
58
|
groups.each do |server, keys_for_server|
|
|
51
59
|
keys_for_server.each do |key|
|
|
52
60
|
original_key = @key_manager.key_without_namespace(key)
|
|
53
61
|
value = hash[original_key]
|
|
54
62
|
server.request(:pipelined_set, key, value, ttl, req_options)
|
|
55
|
-
rescue
|
|
63
|
+
rescue Dalli::NetworkError
|
|
64
|
+
raise
|
|
65
|
+
rescue DalliError => e
|
|
56
66
|
Dalli.logger.debug { e.inspect }
|
|
57
67
|
Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
|
|
58
68
|
end
|
|
@@ -65,7 +75,9 @@ module Dalli
|
|
|
65
75
|
def finish_requests(servers)
|
|
66
76
|
servers.each do |server|
|
|
67
77
|
server.request(:noop)
|
|
68
|
-
rescue
|
|
78
|
+
rescue Dalli::NetworkError
|
|
79
|
+
raise
|
|
80
|
+
rescue DalliError => e
|
|
69
81
|
Dalli.logger.debug { e.inspect }
|
|
70
82
|
Dalli.logger.debug { "unable to complete pipelined set on server #{server.name}" }
|
|
71
83
|
end
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -78,6 +78,19 @@ module Dalli
|
|
|
78
78
|
# particular memcached instance is available for use.
|
|
79
79
|
def alive?
|
|
80
80
|
ensure_connected!
|
|
81
|
+
rescue Dalli::RetryableNetworkError => e
|
|
82
|
+
# A single connection attempt failure is retryable -- the same
|
|
83
|
+
# contract #request enforces on the send/receive path. Retrying here
|
|
84
|
+
# lets error_on_request!'s own fail-count threshold decide when to
|
|
85
|
+
# give up: it keeps raising RetryableNetworkError until
|
|
86
|
+
# socket_max_failures is reached, then raises a terminal
|
|
87
|
+
# NetworkError via down!, which the next rescue converts to false.
|
|
88
|
+
# Without this, a single transient hiccup during the liveness check
|
|
89
|
+
# itself -- as opposed to an actual request -- would report this
|
|
90
|
+
# server as not alive even though it would have reconnected fine.
|
|
91
|
+
Dalli.logger.debug { e.inspect }
|
|
92
|
+
Dalli.logger.debug { "retrying connection attempt to #{name} because of network error" }
|
|
93
|
+
retry
|
|
81
94
|
rescue Dalli::NetworkError
|
|
82
95
|
# ensure_connected! raises a NetworkError if connection fails. We
|
|
83
96
|
# want to capture that error and convert it to a boolean value here.
|
|
@@ -237,13 +250,44 @@ module Dalli
|
|
|
237
250
|
opts[:cache_nils] ? true : false
|
|
238
251
|
end
|
|
239
252
|
|
|
253
|
+
# Extracts opaque routing-token kwargs (:p_token, :l_token) from a
|
|
254
|
+
# request-options Hash so they can be splatted into a RequestFormatter
|
|
255
|
+
# call. Returns {} when neither is set, so the splat is a no-op on the
|
|
256
|
+
# common path. Validation (type, forbidden bytes) happens at the
|
|
257
|
+
# wire-formatter level, where it can raise uniformly regardless of how
|
|
258
|
+
# the token reached the formatter.
|
|
259
|
+
def routing_token_kwargs(opts)
|
|
260
|
+
return {} unless opts.is_a?(Hash)
|
|
261
|
+
return {} unless opts[:p_token] || opts[:l_token]
|
|
262
|
+
|
|
263
|
+
{ p_token: opts[:p_token], l_token: opts[:l_token] }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Maps the client-facing meta-delete options onto RequestFormatter's flag
|
|
267
|
+
# names, so they can be splatted into a meta_delete call. Returns {} when
|
|
268
|
+
# none are set, keeping the splat a no-op on the common path.
|
|
269
|
+
#
|
|
270
|
+
# :tombstone_ttl becomes the T flag, which is the same TTL the formatter
|
|
271
|
+
# already accepted -- deliberately not a second TTL parameter, since two
|
|
272
|
+
# would allow emitting two T tokens in one request. It is sanitized like
|
|
273
|
+
# every other TTL Dalli sends.
|
|
274
|
+
def tombstone_kwargs(opts)
|
|
275
|
+
return {} unless opts.is_a?(Hash)
|
|
276
|
+
|
|
277
|
+
kwargs = {}
|
|
278
|
+
kwargs[:stale] = true if opts[:invalidate]
|
|
279
|
+
kwargs[:ttl] = TtlSanitizer.sanitize(Integer(opts[:tombstone_ttl])) if opts[:tombstone_ttl]
|
|
280
|
+
kwargs[:drop_value] = true if opts[:drop_value]
|
|
281
|
+
kwargs
|
|
282
|
+
end
|
|
283
|
+
|
|
240
284
|
def connect
|
|
241
285
|
@connection_manager.establish_connection
|
|
242
286
|
@version = version
|
|
243
287
|
up!
|
|
244
288
|
end
|
|
245
289
|
|
|
246
|
-
def pipelined_get(keys)
|
|
290
|
+
def pipelined_get(keys, options = nil)
|
|
247
291
|
# Clear buffer to remove any stale data from interrupted operations.
|
|
248
292
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
249
293
|
# the expected state before pipeline_response_setup is called.
|
|
@@ -251,7 +295,7 @@ module Dalli
|
|
|
251
295
|
|
|
252
296
|
req = +''
|
|
253
297
|
keys.each do |key|
|
|
254
|
-
req << quiet_get_request(key)
|
|
298
|
+
req << quiet_get_request(key, options)
|
|
255
299
|
end
|
|
256
300
|
# Could send noop here instead of in pipeline_response_setup
|
|
257
301
|
write(req)
|
|
@@ -260,7 +304,7 @@ module Dalli
|
|
|
260
304
|
# For large batches, interleave writing requests with draining responses.
|
|
261
305
|
# This prevents socket buffer deadlock when sending many keys.
|
|
262
306
|
# Populates the provided results hash with any responses drained during send.
|
|
263
|
-
def pipelined_get_interleaved(keys, chunk_size, results)
|
|
307
|
+
def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
|
|
264
308
|
# Initialize the response buffer for draining during send phase
|
|
265
309
|
response_buffer.ensure_ready
|
|
266
310
|
|
|
@@ -268,7 +312,7 @@ module Dalli
|
|
|
268
312
|
# Build and write this chunk of requests
|
|
269
313
|
req = +''
|
|
270
314
|
chunk.each do |key|
|
|
271
|
-
req << quiet_get_request(key)
|
|
315
|
+
req << quiet_get_request(key, options)
|
|
272
316
|
end
|
|
273
317
|
write(req)
|
|
274
318
|
@connection_manager.flush
|
|
@@ -12,8 +12,25 @@ module Dalli
|
|
|
12
12
|
module KeyRegularizer
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
|
+
# protocol.txt requires that a key "must not include control
|
|
16
|
+
# characters or whitespace" -- \p{Cntrl} is C0 (0x00-0x1F) plus DEL
|
|
17
|
+
# (0x7F). \s alone misses NUL and the rest of that range: a key
|
|
18
|
+
# containing one of those bytes but no whitespace is ASCII-only, so
|
|
19
|
+
# it would otherwise be written to the wire unencoded. Not a
|
|
20
|
+
# protocol-injection risk (the text protocol splits on CRLF, not
|
|
21
|
+
# other control bytes), but a downstream consumer that treats the key
|
|
22
|
+
# specially at one of those bytes (a C string terminating at NUL, a
|
|
23
|
+
# terminal or log line interpreting an escape byte) could silently
|
|
24
|
+
# act on a different key than Dalli believes it sent.
|
|
25
|
+
#
|
|
26
|
+
# Written as \p{Cntrl} rather than the POSIX [:cntrl:] bracket class:
|
|
27
|
+
# \s and [:cntrl:] overlap (tab, newline, CR are in both), and Ruby
|
|
28
|
+
# warns "character class has duplicated range" when they're combined
|
|
29
|
+
# in one -- fatal here, since this suite's -w run treats warnings as
|
|
30
|
+
# errors (see test_strict_warnings.rb). \p{Cntrl} matches the same
|
|
31
|
+
# bytes without the overlap warning.
|
|
15
32
|
def required?(key)
|
|
16
|
-
!key.ascii_only? ||
|
|
33
|
+
!key.ascii_only? || /[\p{Cntrl}\s]/.match?(key)
|
|
17
34
|
end
|
|
18
35
|
|
|
19
36
|
def encode(key)
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -26,20 +26,21 @@ module Dalli
|
|
|
26
26
|
def get(key, options = nil)
|
|
27
27
|
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
28
28
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
29
|
-
req = RequestFormatter.meta_get(key: key, skip_flags: skip_flags)
|
|
29
|
+
req = RequestFormatter.meta_get(key: key, skip_flags: skip_flags, **routing_token_kwargs(options))
|
|
30
30
|
flushed_write(req)
|
|
31
31
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def quiet_get_request(key)
|
|
34
|
+
def quiet_get_request(key, options = nil)
|
|
35
35
|
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
36
|
-
RequestFormatter.meta_get(key: key, return_cas: true, quiet: true, skip_flags: raw_mode
|
|
36
|
+
RequestFormatter.meta_get(key: key, return_cas: true, quiet: true, skip_flags: raw_mode?,
|
|
37
|
+
**routing_token_kwargs(options))
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def gat(key, ttl, options = nil)
|
|
40
41
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
41
42
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
42
|
-
req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags)
|
|
43
|
+
req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags, **routing_token_kwargs(options))
|
|
43
44
|
flushed_write(req)
|
|
44
45
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
45
46
|
end
|
|
@@ -53,8 +54,8 @@ module Dalli
|
|
|
53
54
|
|
|
54
55
|
# TODO: This is confusing, as there's a cas command in memcached
|
|
55
56
|
# and this isn't it. Maybe rename? Maybe eliminate?
|
|
56
|
-
def cas(key)
|
|
57
|
-
req = RequestFormatter.meta_get(key: key, value: true, return_cas: true)
|
|
57
|
+
def cas(key, options = nil)
|
|
58
|
+
req = RequestFormatter.meta_get(key: key, value: true, return_cas: true, **routing_token_kwargs(options))
|
|
58
59
|
flushed_write(req)
|
|
59
60
|
response_processor.meta_get_with_value_and_cas
|
|
60
61
|
end
|
|
@@ -88,12 +89,16 @@ module Dalli
|
|
|
88
89
|
key: key, value: true, return_cas: true,
|
|
89
90
|
vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
|
|
90
91
|
return_hit_status: options[:return_hit_status],
|
|
91
|
-
return_last_access: options[:return_last_access],
|
|
92
|
+
return_last_access: options[:return_last_access],
|
|
93
|
+
return_ttl_remaining: options[:return_ttl_remaining],
|
|
94
|
+
skip_lru_bump: options[:skip_lru_bump],
|
|
95
|
+
**routing_token_kwargs(options)
|
|
92
96
|
)
|
|
93
97
|
flushed_write(req)
|
|
94
98
|
response_processor.meta_get_with_metadata(
|
|
95
99
|
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
96
|
-
return_last_access: options[:return_last_access]
|
|
100
|
+
return_last_access: options[:return_last_access],
|
|
101
|
+
return_ttl_remaining: options[:return_ttl_remaining]
|
|
97
102
|
)
|
|
98
103
|
end
|
|
99
104
|
|
|
@@ -137,35 +142,41 @@ module Dalli
|
|
|
137
142
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
138
143
|
req = RequestFormatter.meta_set(key: key, value: value,
|
|
139
144
|
bitflags: bitflags, cas: cas,
|
|
140
|
-
ttl: ttl, mode: mode, quiet: quiet
|
|
145
|
+
ttl: ttl, mode: mode, quiet: quiet,
|
|
146
|
+
**routing_token_kwargs(options))
|
|
141
147
|
write("#{req}#{value}#{TERMINATOR}")
|
|
142
148
|
@connection_manager.flush unless quiet
|
|
143
149
|
end
|
|
144
150
|
# rubocop:enable Metrics/ParameterLists
|
|
145
151
|
|
|
146
|
-
def append(key, value)
|
|
147
|
-
write_append_prepend_req(:append, key, value)
|
|
152
|
+
def append(key, value, options = nil)
|
|
153
|
+
write_append_prepend_req(:append, key, value, nil, nil, options)
|
|
148
154
|
response_processor.meta_set_append_prepend unless quiet?
|
|
149
155
|
end
|
|
150
156
|
|
|
151
|
-
def prepend(key, value)
|
|
152
|
-
write_append_prepend_req(:prepend, key, value)
|
|
157
|
+
def prepend(key, value, options = nil)
|
|
158
|
+
write_append_prepend_req(:prepend, key, value, nil, nil, options)
|
|
153
159
|
response_processor.meta_set_append_prepend unless quiet?
|
|
154
160
|
end
|
|
155
161
|
|
|
156
162
|
# rubocop:disable Metrics/ParameterLists
|
|
157
|
-
def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil,
|
|
163
|
+
def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, options = nil)
|
|
158
164
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
159
165
|
req = RequestFormatter.meta_set(key: key, value: value,
|
|
160
|
-
cas: cas, ttl: ttl, mode: mode, quiet: quiet
|
|
166
|
+
cas: cas, ttl: ttl, mode: mode, quiet: quiet?,
|
|
167
|
+
**routing_token_kwargs(options))
|
|
161
168
|
write("#{req}#{value}#{TERMINATOR}")
|
|
162
169
|
@connection_manager.flush unless quiet?
|
|
163
170
|
end
|
|
164
171
|
# rubocop:enable Metrics/ParameterLists
|
|
165
172
|
|
|
166
173
|
# Delete Commands
|
|
167
|
-
|
|
168
|
-
|
|
174
|
+
#
|
|
175
|
+
# `options` supports the meta-delete keys :invalidate, :tombstone_ttl and
|
|
176
|
+
# :drop_value, plus :p_token/:l_token; see Dalli::Client#delete.
|
|
177
|
+
def delete(key, cas, options = nil)
|
|
178
|
+
req = RequestFormatter.meta_delete(key: key, cas: cas, quiet: quiet?,
|
|
179
|
+
**tombstone_kwargs(options), **routing_token_kwargs(options))
|
|
169
180
|
write(req)
|
|
170
181
|
@connection_manager.flush unless quiet?
|
|
171
182
|
response_processor.meta_delete unless quiet?
|
|
@@ -173,8 +184,9 @@ module Dalli
|
|
|
173
184
|
|
|
174
185
|
# Pipelined delete - writes a quiet delete request without reading response.
|
|
175
186
|
# Used by PipelinedDeleter for bulk operations.
|
|
176
|
-
def pipelined_delete(key)
|
|
177
|
-
req = RequestFormatter.meta_delete(key: key, quiet: true
|
|
187
|
+
def pipelined_delete(key, req_options = nil)
|
|
188
|
+
req = RequestFormatter.meta_delete(key: key, quiet: true,
|
|
189
|
+
**tombstone_kwargs(req_options), **routing_token_kwargs(req_options))
|
|
178
190
|
write(req)
|
|
179
191
|
end
|
|
180
192
|
|
|
@@ -184,21 +196,23 @@ module Dalli
|
|
|
184
196
|
end
|
|
185
197
|
|
|
186
198
|
# Arithmetic Commands
|
|
187
|
-
def decr(key, count, ttl, initial)
|
|
188
|
-
decr_incr false, key, count, ttl, initial
|
|
199
|
+
def decr(key, count, ttl, initial, options = nil)
|
|
200
|
+
decr_incr false, key, count, ttl, initial, options
|
|
189
201
|
end
|
|
190
202
|
|
|
191
|
-
def incr(key, count, ttl, initial)
|
|
192
|
-
decr_incr true, key, count, ttl, initial
|
|
203
|
+
def incr(key, count, ttl, initial, options = nil)
|
|
204
|
+
decr_incr true, key, count, ttl, initial, options
|
|
193
205
|
end
|
|
194
206
|
|
|
195
|
-
|
|
207
|
+
# rubocop:disable Metrics/ParameterLists
|
|
208
|
+
def decr_incr(incr, key, delta, ttl, initial, options = nil)
|
|
196
209
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
197
210
|
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
198
|
-
quiet: quiet
|
|
211
|
+
quiet: quiet?, **routing_token_kwargs(options)))
|
|
199
212
|
@connection_manager.flush unless quiet?
|
|
200
213
|
response_processor.decr_incr unless quiet?
|
|
201
214
|
end
|
|
215
|
+
# rubocop:enable Metrics/ParameterLists
|
|
202
216
|
|
|
203
217
|
# Other Commands
|
|
204
218
|
def flush(delay = 0)
|
|
@@ -236,14 +250,52 @@ module Dalli
|
|
|
236
250
|
# Single-server fast path for get_multi. Inlines request formatting and
|
|
237
251
|
# response parsing to minimize per-key overhead. Avoids the PipelinedGetter
|
|
238
252
|
# machinery (IO.select, response buffering, server grouping).
|
|
239
|
-
def read_multi_req(keys)
|
|
253
|
+
def read_multi_req(keys, options = nil)
|
|
240
254
|
is_raw = raw_mode?
|
|
241
|
-
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw)
|
|
255
|
+
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw, **routing_token_kwargs(options))
|
|
242
256
|
flushed_write(buffer)
|
|
243
257
|
buffer.clear
|
|
244
258
|
read_multi_get_responses(is_raw)
|
|
245
259
|
end
|
|
246
260
|
|
|
261
|
+
# Stale-aware bulk get. Returns { key => { value:, cas:, stale:, miss: } }
|
|
262
|
+
# for the keys the server returned. Keys that were not found are absent
|
|
263
|
+
# from the hash, matching read_multi_req and the get_multi family; a
|
|
264
|
+
# tombstoned item is present (it answers VA with the X flag) with
|
|
265
|
+
# stale: true, which is the distinction callers need.
|
|
266
|
+
#
|
|
267
|
+
# Shared by both the single-server fast path and PipelinedGetter's
|
|
268
|
+
# per-server-group request, so this one change covers both routes.
|
|
269
|
+
def read_multi_with_metadata_req(keys, options = nil)
|
|
270
|
+
is_raw = raw_mode?
|
|
271
|
+
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw, return_cas: true,
|
|
272
|
+
**routing_token_kwargs(options))
|
|
273
|
+
flushed_write(buffer)
|
|
274
|
+
buffer.clear
|
|
275
|
+
read_multi_metadata_responses(is_raw)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Unlike read_multi_get_responses this locates tokens by flag rather than
|
|
279
|
+
# by position, because the c flag shifts the key's index.
|
|
280
|
+
def read_multi_metadata_responses(is_raw)
|
|
281
|
+
hash = {}
|
|
282
|
+
while (line = @connection_manager.read_line)
|
|
283
|
+
break if line.start_with?('MN')
|
|
284
|
+
next unless line.start_with?('VA ')
|
|
285
|
+
|
|
286
|
+
tokens = line.chomp!(TERMINATOR).split
|
|
287
|
+
value = @connection_manager.read(tokens[1].to_i + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
288
|
+
stale = response_processor.stale_from_tokens(tokens)
|
|
289
|
+
cas = response_processor.cas_from_tokens(tokens)
|
|
290
|
+
bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
|
|
291
|
+
key = response_processor.key_from_tokens(tokens)
|
|
292
|
+
next if key.nil?
|
|
293
|
+
|
|
294
|
+
hash[key] = { value: @value_marshaller.retrieve(value, bitflags), cas: cas, stale: stale, miss: false }
|
|
295
|
+
end
|
|
296
|
+
hash
|
|
297
|
+
end
|
|
298
|
+
|
|
247
299
|
def read_multi_get_responses(is_raw)
|
|
248
300
|
hash = {}
|
|
249
301
|
key_index = is_raw ? 2 : 3
|
|
@@ -277,7 +329,7 @@ module Dalli
|
|
|
277
329
|
[key, @value_marshaller.store(key, raw_value, req_options)]
|
|
278
330
|
end
|
|
279
331
|
|
|
280
|
-
buffer = RequestFormatter.multi_meta_set(entries, ttl: ttl)
|
|
332
|
+
buffer = RequestFormatter.multi_meta_set(entries, ttl: ttl, **routing_token_kwargs(req_options))
|
|
281
333
|
flushed_write(buffer)
|
|
282
334
|
buffer.clear
|
|
283
335
|
response_processor.consume_all_responses_until_mn
|
|
@@ -285,8 +337,9 @@ module Dalli
|
|
|
285
337
|
|
|
286
338
|
# Single-server fast path for delete_multi. Writes all quiet delete requests
|
|
287
339
|
# terminated by a noop, then consumes all responses.
|
|
288
|
-
def delete_multi_req(keys)
|
|
289
|
-
buffer = RequestFormatter.multi_meta_delete(keys)
|
|
340
|
+
def delete_multi_req(keys, req_options = nil)
|
|
341
|
+
buffer = RequestFormatter.multi_meta_delete(keys, **tombstone_kwargs(req_options),
|
|
342
|
+
**routing_token_kwargs(req_options))
|
|
290
343
|
flushed_write(buffer)
|
|
291
344
|
buffer.clear
|
|
292
345
|
keys.size - response_processor.pipelined_delete_non_deletions
|