dalli 5.0.7 → 5.1.1
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 +80 -1
- data/Gemfile +1 -1
- data/README.md +6 -1
- data/lib/dalli/client.rb +265 -56
- data/lib/dalli/pipelined_deleter.rb +29 -12
- data/lib/dalli/pipelined_getter.rb +84 -47
- data/lib/dalli/pipelined_setter.rb +15 -3
- data/lib/dalli/protocol/base.rb +73 -11
- data/lib/dalli/protocol/key_regularizer.rb +25 -1
- data/lib/dalli/protocol/meta.rb +101 -33
- data/lib/dalli/protocol/request_formatter.rb +118 -18
- data/lib/dalli/protocol/response_processor.rb +85 -16
- data/lib/dalli/ring.rb +31 -17
- data/lib/dalli/socket.rb +2 -4
- data/lib/dalli/version.rb +2 -2
- data/lib/rack/session/dalli.rb +1 -2
- metadata +1 -1
|
@@ -20,13 +20,21 @@ 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
|
+
# With return_cas: false the CAS value is not requested, and each yielded
|
|
26
|
+
# CAS is 0.
|
|
27
|
+
#
|
|
28
|
+
# A transient network error is retried automatically. If a server remains
|
|
29
|
+
# unreachable after retrying, raises Dalli::NetworkError.
|
|
30
|
+
#
|
|
31
|
+
def process(keys, req_options = nil, return_cas: true, &block)
|
|
24
32
|
return {} if keys.empty?
|
|
25
33
|
|
|
26
34
|
@ring.lock do
|
|
27
35
|
# Stores partial results collected during interleaved send phase
|
|
28
36
|
@partial_results = {}
|
|
29
|
-
servers = setup_requests(keys)
|
|
37
|
+
servers = setup_requests(keys, req_options, return_cas: return_cas)
|
|
30
38
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
39
|
|
|
32
40
|
# First yield any partial results collected during interleaved send
|
|
@@ -40,6 +48,40 @@ module Dalli
|
|
|
40
48
|
retry
|
|
41
49
|
end
|
|
42
50
|
|
|
51
|
+
##
|
|
52
|
+
# Stale-aware bulk get across servers. Returns { key => metadata Hash } for
|
|
53
|
+
# the keys that were found; see Protocol::Meta#read_multi_with_metadata_req
|
|
54
|
+
# for why misses are absent rather than present with miss: true.
|
|
55
|
+
#
|
|
56
|
+
# Unlike #process this issues one request per server and reads its full
|
|
57
|
+
# response before moving on, rather than pipelining across servers: the
|
|
58
|
+
# metadata path has no interleaving support, and the stale-aware callers it
|
|
59
|
+
# serves fetch far smaller batches than get_multi does.
|
|
60
|
+
#
|
|
61
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
62
|
+
#
|
|
63
|
+
def process_with_metadata(keys, req_options = nil)
|
|
64
|
+
return {} if keys.empty?
|
|
65
|
+
|
|
66
|
+
@ring.lock do
|
|
67
|
+
results = {}
|
|
68
|
+
groups_for_keys(keys).each do |server, keys_for_server|
|
|
69
|
+
results.merge!(server.request(:read_multi_with_metadata_req, keys_for_server, req_options))
|
|
70
|
+
rescue Dalli::RetryableNetworkError
|
|
71
|
+
raise
|
|
72
|
+
rescue DalliError, NetworkError => e
|
|
73
|
+
Dalli.logger.debug { e.inspect }
|
|
74
|
+
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
75
|
+
end
|
|
76
|
+
results.transform_keys! { |key| @key_manager.key_without_namespace(key) }
|
|
77
|
+
results
|
|
78
|
+
end
|
|
79
|
+
rescue Dalli::RetryableNetworkError => e
|
|
80
|
+
Dalli.logger.debug { e.inspect }
|
|
81
|
+
Dalli.logger.debug { 'retrying pipelined get with metadata because of network error' }
|
|
82
|
+
retry
|
|
83
|
+
end
|
|
84
|
+
|
|
43
85
|
private
|
|
44
86
|
|
|
45
87
|
def yield_partial_results
|
|
@@ -49,61 +91,55 @@ module Dalli
|
|
|
49
91
|
@partial_results.clear
|
|
50
92
|
end
|
|
51
93
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
# Sends each server's queries and its terminating noop before building
|
|
95
|
+
# the next server's, so memcached is already answering the first servers
|
|
96
|
+
# while the rest are prepared. Returns the servers with a pending response.
|
|
97
|
+
def setup_requests(keys, req_options = nil, return_cas: true)
|
|
98
|
+
started = []
|
|
99
|
+
groups_for_keys(keys).each do |server, keys_for_server|
|
|
100
|
+
make_getkq_request(server, keys_for_server, req_options, return_cas: return_cas)
|
|
101
|
+
next unless server.connected?
|
|
55
102
|
|
|
56
|
-
|
|
57
|
-
|
|
103
|
+
started << server
|
|
104
|
+
finish_query_for_server(server)
|
|
105
|
+
rescue Dalli::NetworkError
|
|
106
|
+
abort_without_timeout(started)
|
|
107
|
+
raise
|
|
108
|
+
rescue Dalli::DalliError
|
|
109
|
+
started.delete(server)
|
|
110
|
+
end
|
|
111
|
+
started
|
|
58
112
|
end
|
|
59
113
|
|
|
60
114
|
##
|
|
61
|
-
#
|
|
62
|
-
# the corresponding getkq requests to the appropriate servers
|
|
115
|
+
# Writes the getkq requests for one server's keys
|
|
63
116
|
#
|
|
64
117
|
# It's worth noting that we could potentially reduce bytes
|
|
65
118
|
# on the wire by switching from getkq to getq, and using
|
|
66
119
|
# the opaque value to match requests to responses.
|
|
67
120
|
##
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results)
|
|
77
|
-
end
|
|
78
|
-
rescue DalliError, NetworkError => e
|
|
79
|
-
Dalli.logger.debug { e.inspect }
|
|
80
|
-
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
121
|
+
def make_getkq_request(server, keys_for_server, req_options = nil, return_cas: true)
|
|
122
|
+
if keys_for_server.size <= INTERLEAVE_THRESHOLD
|
|
123
|
+
# Small batch - send all at once (existing behavior)
|
|
124
|
+
server.request(:pipelined_get, keys_for_server, req_options, return_cas)
|
|
125
|
+
else
|
|
126
|
+
# Large batch - interleave sends with response draining
|
|
127
|
+
# Pass @partial_results directly to avoid hash allocation/merge overhead
|
|
128
|
+
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results, req_options)
|
|
81
129
|
end
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
servers.each do |server|
|
|
92
|
-
next unless server.connected?
|
|
93
|
-
|
|
94
|
-
begin
|
|
95
|
-
finish_query_for_server(server)
|
|
96
|
-
rescue Dalli::NetworkError
|
|
97
|
-
raise
|
|
98
|
-
rescue Dalli::DalliError
|
|
99
|
-
deleted << server
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
servers.delete_if { |server| deleted.include?(server) }
|
|
130
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate:
|
|
131
|
+
# #process's top-level rescue retries the whole pipelined get on it. This
|
|
132
|
+
# rescue used to catch DalliError and NetworkError together -- since
|
|
133
|
+
# NetworkError < DalliError, that silently swallowed RetryableNetworkError
|
|
134
|
+
# too, dropping this server's keys from the result on a transient hiccup
|
|
135
|
+
# instead of retrying, with nothing surfaced above debug-level logging.
|
|
136
|
+
# Only a non-network DalliError (this server genuinely can't serve these
|
|
137
|
+
# keys) should be swallowed here.
|
|
104
138
|
rescue Dalli::NetworkError
|
|
105
|
-
abort_without_timeout(servers)
|
|
106
139
|
raise
|
|
140
|
+
rescue DalliError => e
|
|
141
|
+
Dalli.logger.debug { e.inspect }
|
|
142
|
+
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
107
143
|
end
|
|
108
144
|
|
|
109
145
|
def finish_query_for_server(server)
|
|
@@ -181,8 +217,9 @@ module Dalli
|
|
|
181
217
|
return [] if readable.nil?
|
|
182
218
|
|
|
183
219
|
# For typical server counts (1-5), linear scan is faster than
|
|
184
|
-
# building and looking up a hash map
|
|
185
|
-
|
|
220
|
+
# building and looking up a hash map. Array#index scans in C and
|
|
221
|
+
# reuses the sockets already fetched above.
|
|
222
|
+
readable.filter_map { |sock| (idx = sockets.index(sock)) && servers[idx] }
|
|
186
223
|
end
|
|
187
224
|
|
|
188
225
|
def groups_for_keys(*keys)
|
|
@@ -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
|
@@ -17,8 +17,24 @@ module Dalli
|
|
|
17
17
|
attr_accessor :weight, :options
|
|
18
18
|
|
|
19
19
|
def_delegators :@value_marshaller, :serializer, :compressor, :compression_min_size, :compress_by_default?
|
|
20
|
-
def_delegators :@connection_manager, :name, :
|
|
21
|
-
:socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
|
|
20
|
+
def_delegators :@connection_manager, :name, :hostname, :port, :close, :socket_timeout,
|
|
21
|
+
:socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
|
|
22
|
+
|
|
23
|
+
# Delegated by hand rather than with def_delegators because they run on
|
|
24
|
+
# every request, and a plain method call is cheaper than a Forwardable one.
|
|
25
|
+
def connected?
|
|
26
|
+
@connection_manager.connected?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def flushed_write(bytes)
|
|
30
|
+
@connection_manager.flushed_write(bytes)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Written out rather than delegated: a multi-server get_multi calls it for
|
|
34
|
+
# every server, and a plain method call is cheaper than a Forwardable one.
|
|
35
|
+
def sock
|
|
36
|
+
@connection_manager.sock
|
|
37
|
+
end
|
|
22
38
|
|
|
23
39
|
def initialize(attribs, client_options = {})
|
|
24
40
|
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
|
@@ -78,6 +94,19 @@ module Dalli
|
|
|
78
94
|
# particular memcached instance is available for use.
|
|
79
95
|
def alive?
|
|
80
96
|
ensure_connected!
|
|
97
|
+
rescue Dalli::RetryableNetworkError => e
|
|
98
|
+
# A single connection attempt failure is retryable -- the same
|
|
99
|
+
# contract #request enforces on the send/receive path. Retrying here
|
|
100
|
+
# lets error_on_request!'s own fail-count threshold decide when to
|
|
101
|
+
# give up: it keeps raising RetryableNetworkError until
|
|
102
|
+
# socket_max_failures is reached, then raises a terminal
|
|
103
|
+
# NetworkError via down!, which the next rescue converts to false.
|
|
104
|
+
# Without this, a single transient hiccup during the liveness check
|
|
105
|
+
# itself -- as opposed to an actual request -- would report this
|
|
106
|
+
# server as not alive even though it would have reconnected fine.
|
|
107
|
+
Dalli.logger.debug { e.inspect }
|
|
108
|
+
Dalli.logger.debug { "retrying connection attempt to #{name} because of network error" }
|
|
109
|
+
retry
|
|
81
110
|
rescue Dalli::NetworkError
|
|
82
111
|
# ensure_connected! raises a NetworkError if connection fails. We
|
|
83
112
|
# want to capture that error and convert it to a boolean value here.
|
|
@@ -237,30 +266,63 @@ module Dalli
|
|
|
237
266
|
opts[:cache_nils] ? true : false
|
|
238
267
|
end
|
|
239
268
|
|
|
269
|
+
# Extracts opaque routing-token kwargs (:p_token, :l_token) from a
|
|
270
|
+
# request-options Hash so they can be splatted into a RequestFormatter
|
|
271
|
+
# call. Returns {} when neither is set, so the splat is a no-op on the
|
|
272
|
+
# common path. Validation (type, forbidden bytes) happens at the
|
|
273
|
+
# wire-formatter level, where it can raise uniformly regardless of how
|
|
274
|
+
# the token reached the formatter.
|
|
275
|
+
def routing_token_kwargs(opts)
|
|
276
|
+
return {} unless routing_tokens?(opts)
|
|
277
|
+
|
|
278
|
+
{ p_token: opts[:p_token], l_token: opts[:l_token] }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def routing_tokens?(opts)
|
|
282
|
+
opts.is_a?(Hash) && (opts[:p_token] || opts[:l_token]) ? true : false
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Maps the client-facing meta-delete options onto RequestFormatter's flag
|
|
286
|
+
# names, so they can be splatted into a meta_delete call. Returns {} when
|
|
287
|
+
# none are set, keeping the splat a no-op on the common path.
|
|
288
|
+
#
|
|
289
|
+
# :tombstone_ttl becomes the T flag, which is the same TTL the formatter
|
|
290
|
+
# already accepted -- deliberately not a second TTL parameter, since two
|
|
291
|
+
# would allow emitting two T tokens in one request. It is sanitized like
|
|
292
|
+
# every other TTL Dalli sends.
|
|
293
|
+
def tombstone_kwargs(opts)
|
|
294
|
+
return {} unless opts.is_a?(Hash)
|
|
295
|
+
|
|
296
|
+
kwargs = {}
|
|
297
|
+
kwargs[:stale] = true if opts[:invalidate]
|
|
298
|
+
kwargs[:ttl] = TtlSanitizer.sanitize(Integer(opts[:tombstone_ttl])) if opts[:tombstone_ttl]
|
|
299
|
+
kwargs[:drop_value] = true if opts[:drop_value]
|
|
300
|
+
kwargs
|
|
301
|
+
end
|
|
302
|
+
|
|
240
303
|
def connect
|
|
241
304
|
@connection_manager.establish_connection
|
|
242
305
|
@version = version
|
|
243
306
|
up!
|
|
244
307
|
end
|
|
245
308
|
|
|
246
|
-
|
|
309
|
+
# return_cas = false leaves the c flag off each request, for callers
|
|
310
|
+
# (plain get_multi) that discard the CAS value. It is positional because
|
|
311
|
+
# #request forwards only positional arguments.
|
|
312
|
+
def pipelined_get(keys, options = nil, return_cas = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
247
313
|
# Clear buffer to remove any stale data from interrupted operations.
|
|
248
314
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
249
315
|
# the expected state before pipeline_response_setup is called.
|
|
250
316
|
response_buffer.clear
|
|
251
317
|
|
|
252
|
-
|
|
253
|
-
keys
|
|
254
|
-
req << quiet_get_request(key)
|
|
255
|
-
end
|
|
256
|
-
# Could send noop here instead of in pipeline_response_setup
|
|
257
|
-
write(req)
|
|
318
|
+
# The terminating noop is sent by pipeline_response_setup
|
|
319
|
+
write(quiet_get_requests(keys, options, return_cas: return_cas))
|
|
258
320
|
end
|
|
259
321
|
|
|
260
322
|
# For large batches, interleave writing requests with draining responses.
|
|
261
323
|
# This prevents socket buffer deadlock when sending many keys.
|
|
262
324
|
# Populates the provided results hash with any responses drained during send.
|
|
263
|
-
def pipelined_get_interleaved(keys, chunk_size, results)
|
|
325
|
+
def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
|
|
264
326
|
# Initialize the response buffer for draining during send phase
|
|
265
327
|
response_buffer.ensure_ready
|
|
266
328
|
|
|
@@ -268,7 +330,7 @@ module Dalli
|
|
|
268
330
|
# Build and write this chunk of requests
|
|
269
331
|
req = +''
|
|
270
332
|
chunk.each do |key|
|
|
271
|
-
req << quiet_get_request(key)
|
|
333
|
+
req << quiet_get_request(key, options)
|
|
272
334
|
end
|
|
273
335
|
write(req)
|
|
274
336
|
@connection_manager.flush
|
|
@@ -12,8 +12,32 @@ 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.
|
|
32
|
+
#
|
|
33
|
+
# The ascii_only? check runs first, so the regexp only ever sees ASCII
|
|
34
|
+
# keys. Over ASCII, [\p{Cntrl}\s] is exactly 0x00-0x20 plus 0x7F, and
|
|
35
|
+
# the plain byte class below is about 5x faster to match.
|
|
36
|
+
ASCII_CNTRL_OR_SPACE = /[\x00-\x20\x7F]/
|
|
37
|
+
private_constant :ASCII_CNTRL_OR_SPACE
|
|
38
|
+
|
|
15
39
|
def required?(key)
|
|
16
|
-
!key.ascii_only? ||
|
|
40
|
+
!key.ascii_only? || ASCII_CNTRL_OR_SPACE.match?(key)
|
|
17
41
|
end
|
|
18
42
|
|
|
19
43
|
def encode(key)
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -24,22 +24,40 @@ module Dalli
|
|
|
24
24
|
|
|
25
25
|
# Retrieval Commands
|
|
26
26
|
def get(key, options = nil)
|
|
27
|
+
# Fast path for the common case of no per-request options (nil or false)
|
|
28
|
+
unless options
|
|
29
|
+
flushed_write(RequestFormatter.plain_meta_get(key, raw_mode?))
|
|
30
|
+
return response_processor.meta_get_with_value
|
|
31
|
+
end
|
|
32
|
+
|
|
27
33
|
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
28
|
-
skip_flags = raw_mode? ||
|
|
29
|
-
req =
|
|
34
|
+
skip_flags = raw_mode? || options[:raw]
|
|
35
|
+
req = if routing_tokens?(options)
|
|
36
|
+
RequestFormatter.meta_get(key: key, skip_flags: skip_flags, **routing_token_kwargs(options))
|
|
37
|
+
else
|
|
38
|
+
RequestFormatter.plain_meta_get(key, skip_flags)
|
|
39
|
+
end
|
|
30
40
|
flushed_write(req)
|
|
31
41
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
32
42
|
end
|
|
33
43
|
|
|
34
|
-
def quiet_get_request(key)
|
|
44
|
+
def quiet_get_request(key, options = nil)
|
|
35
45
|
# 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
|
|
46
|
+
RequestFormatter.meta_get(key: key, return_cas: true, quiet: true, skip_flags: raw_mode?,
|
|
47
|
+
**routing_token_kwargs(options))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Same requests as quiet_get_request for each key, built in one pass
|
|
51
|
+
# and without the trailing noop.
|
|
52
|
+
def quiet_get_requests(keys, options = nil, return_cas: true)
|
|
53
|
+
RequestFormatter.multi_meta_get(keys, skip_flags: raw_mode?, return_cas: return_cas, terminate: false,
|
|
54
|
+
**routing_token_kwargs(options))
|
|
37
55
|
end
|
|
38
56
|
|
|
39
57
|
def gat(key, ttl, options = nil)
|
|
40
58
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
41
59
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
42
|
-
req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags)
|
|
60
|
+
req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags, **routing_token_kwargs(options))
|
|
43
61
|
flushed_write(req)
|
|
44
62
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
45
63
|
end
|
|
@@ -53,8 +71,8 @@ module Dalli
|
|
|
53
71
|
|
|
54
72
|
# TODO: This is confusing, as there's a cas command in memcached
|
|
55
73
|
# 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)
|
|
74
|
+
def cas(key, options = nil)
|
|
75
|
+
req = RequestFormatter.meta_get(key: key, value: true, return_cas: true, **routing_token_kwargs(options))
|
|
58
76
|
flushed_write(req)
|
|
59
77
|
response_processor.meta_get_with_value_and_cas
|
|
60
78
|
end
|
|
@@ -88,12 +106,16 @@ module Dalli
|
|
|
88
106
|
key: key, value: true, return_cas: true,
|
|
89
107
|
vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
|
|
90
108
|
return_hit_status: options[:return_hit_status],
|
|
91
|
-
return_last_access: options[:return_last_access],
|
|
109
|
+
return_last_access: options[:return_last_access],
|
|
110
|
+
return_ttl_remaining: options[:return_ttl_remaining],
|
|
111
|
+
skip_lru_bump: options[:skip_lru_bump],
|
|
112
|
+
**routing_token_kwargs(options)
|
|
92
113
|
)
|
|
93
114
|
flushed_write(req)
|
|
94
115
|
response_processor.meta_get_with_metadata(
|
|
95
116
|
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
96
|
-
return_last_access: options[:return_last_access]
|
|
117
|
+
return_last_access: options[:return_last_access],
|
|
118
|
+
return_ttl_remaining: options[:return_ttl_remaining]
|
|
97
119
|
)
|
|
98
120
|
end
|
|
99
121
|
|
|
@@ -137,35 +159,40 @@ module Dalli
|
|
|
137
159
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
138
160
|
req = RequestFormatter.meta_set(key: key, value: value,
|
|
139
161
|
bitflags: bitflags, cas: cas,
|
|
140
|
-
ttl: ttl, mode: mode, quiet: quiet
|
|
162
|
+
ttl: ttl, mode: mode, quiet: quiet,
|
|
163
|
+
**routing_token_kwargs(options))
|
|
141
164
|
write("#{req}#{value}#{TERMINATOR}")
|
|
142
165
|
@connection_manager.flush unless quiet
|
|
143
166
|
end
|
|
144
167
|
# rubocop:enable Metrics/ParameterLists
|
|
145
168
|
|
|
146
|
-
def append(key, value)
|
|
147
|
-
write_append_prepend_req(:append, key, value)
|
|
169
|
+
def append(key, value, options = nil)
|
|
170
|
+
write_append_prepend_req(:append, key, value, nil, nil, options)
|
|
148
171
|
response_processor.meta_set_append_prepend unless quiet?
|
|
149
172
|
end
|
|
150
173
|
|
|
151
|
-
def prepend(key, value)
|
|
152
|
-
write_append_prepend_req(:prepend, key, value)
|
|
174
|
+
def prepend(key, value, options = nil)
|
|
175
|
+
write_append_prepend_req(:prepend, key, value, nil, nil, options)
|
|
153
176
|
response_processor.meta_set_append_prepend unless quiet?
|
|
154
177
|
end
|
|
155
178
|
|
|
156
|
-
# rubocop:disable Metrics/ParameterLists
|
|
157
|
-
def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil,
|
|
179
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
180
|
+
def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, options = nil)
|
|
158
181
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
159
182
|
req = RequestFormatter.meta_set(key: key, value: value,
|
|
160
|
-
cas: cas, ttl: ttl, mode: mode, quiet: quiet
|
|
183
|
+
cas: cas, ttl: ttl, mode: mode, quiet: quiet?,
|
|
184
|
+
**routing_token_kwargs(options))
|
|
161
185
|
write("#{req}#{value}#{TERMINATOR}")
|
|
162
186
|
@connection_manager.flush unless quiet?
|
|
163
187
|
end
|
|
164
|
-
# rubocop:enable Metrics/ParameterLists
|
|
165
188
|
|
|
166
189
|
# Delete Commands
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
#
|
|
191
|
+
# `options` supports the meta-delete keys :invalidate, :tombstone_ttl and
|
|
192
|
+
# :drop_value, plus :p_token/:l_token; see Dalli::Client#delete.
|
|
193
|
+
def delete(key, cas, options = nil)
|
|
194
|
+
req = RequestFormatter.meta_delete(key: key, cas: cas, quiet: quiet?,
|
|
195
|
+
**tombstone_kwargs(options), **routing_token_kwargs(options))
|
|
169
196
|
write(req)
|
|
170
197
|
@connection_manager.flush unless quiet?
|
|
171
198
|
response_processor.meta_delete unless quiet?
|
|
@@ -173,8 +200,9 @@ module Dalli
|
|
|
173
200
|
|
|
174
201
|
# Pipelined delete - writes a quiet delete request without reading response.
|
|
175
202
|
# Used by PipelinedDeleter for bulk operations.
|
|
176
|
-
def pipelined_delete(key)
|
|
177
|
-
req = RequestFormatter.meta_delete(key: key, quiet: true
|
|
203
|
+
def pipelined_delete(key, req_options = nil)
|
|
204
|
+
req = RequestFormatter.meta_delete(key: key, quiet: true,
|
|
205
|
+
**tombstone_kwargs(req_options), **routing_token_kwargs(req_options))
|
|
178
206
|
write(req)
|
|
179
207
|
end
|
|
180
208
|
|
|
@@ -184,18 +212,19 @@ module Dalli
|
|
|
184
212
|
end
|
|
185
213
|
|
|
186
214
|
# Arithmetic Commands
|
|
187
|
-
def decr(key, count, ttl, initial)
|
|
188
|
-
decr_incr false, key, count, ttl, initial
|
|
215
|
+
def decr(key, count, ttl, initial, options = nil)
|
|
216
|
+
decr_incr false, key, count, ttl, initial, options
|
|
189
217
|
end
|
|
190
218
|
|
|
191
|
-
def incr(key, count, ttl, initial)
|
|
192
|
-
decr_incr true, key, count, ttl, initial
|
|
219
|
+
def incr(key, count, ttl, initial, options = nil)
|
|
220
|
+
decr_incr true, key, count, ttl, initial, options
|
|
193
221
|
end
|
|
194
222
|
|
|
195
|
-
|
|
223
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
224
|
+
def decr_incr(incr, key, delta, ttl, initial, options = nil)
|
|
196
225
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
197
226
|
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
198
|
-
quiet: quiet
|
|
227
|
+
quiet: quiet?, **routing_token_kwargs(options)))
|
|
199
228
|
@connection_manager.flush unless quiet?
|
|
200
229
|
response_processor.decr_incr unless quiet?
|
|
201
230
|
end
|
|
@@ -236,14 +265,52 @@ module Dalli
|
|
|
236
265
|
# Single-server fast path for get_multi. Inlines request formatting and
|
|
237
266
|
# response parsing to minimize per-key overhead. Avoids the PipelinedGetter
|
|
238
267
|
# machinery (IO.select, response buffering, server grouping).
|
|
239
|
-
def read_multi_req(keys)
|
|
268
|
+
def read_multi_req(keys, options = nil)
|
|
240
269
|
is_raw = raw_mode?
|
|
241
|
-
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw)
|
|
270
|
+
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw, **routing_token_kwargs(options))
|
|
242
271
|
flushed_write(buffer)
|
|
243
272
|
buffer.clear
|
|
244
273
|
read_multi_get_responses(is_raw)
|
|
245
274
|
end
|
|
246
275
|
|
|
276
|
+
# Stale-aware bulk get. Returns { key => { value:, cas:, stale:, miss: } }
|
|
277
|
+
# for the keys the server returned. Keys that were not found are absent
|
|
278
|
+
# from the hash, matching read_multi_req and the get_multi family; a
|
|
279
|
+
# tombstoned item is present (it answers VA with the X flag) with
|
|
280
|
+
# stale: true, which is the distinction callers need.
|
|
281
|
+
#
|
|
282
|
+
# Shared by both the single-server fast path and PipelinedGetter's
|
|
283
|
+
# per-server-group request, so this one change covers both routes.
|
|
284
|
+
def read_multi_with_metadata_req(keys, options = nil)
|
|
285
|
+
is_raw = raw_mode?
|
|
286
|
+
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw, return_cas: true,
|
|
287
|
+
**routing_token_kwargs(options))
|
|
288
|
+
flushed_write(buffer)
|
|
289
|
+
buffer.clear
|
|
290
|
+
read_multi_metadata_responses(is_raw)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Unlike read_multi_get_responses this locates tokens by flag rather than
|
|
294
|
+
# by position, because the c flag shifts the key's index.
|
|
295
|
+
def read_multi_metadata_responses(is_raw)
|
|
296
|
+
hash = {}
|
|
297
|
+
while (line = @connection_manager.read_line)
|
|
298
|
+
break if line.start_with?('MN')
|
|
299
|
+
next unless line.start_with?('VA ')
|
|
300
|
+
|
|
301
|
+
tokens = line.chomp!(TERMINATOR).split
|
|
302
|
+
value = @connection_manager.read(tokens[1].to_i + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
303
|
+
stale = response_processor.stale_from_tokens(tokens)
|
|
304
|
+
cas = response_processor.cas_from_tokens(tokens)
|
|
305
|
+
bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
|
|
306
|
+
key = response_processor.key_from_tokens(tokens)
|
|
307
|
+
next if key.nil?
|
|
308
|
+
|
|
309
|
+
hash[key] = { value: @value_marshaller.retrieve(value, bitflags), cas: cas, stale: stale, miss: false }
|
|
310
|
+
end
|
|
311
|
+
hash
|
|
312
|
+
end
|
|
313
|
+
|
|
247
314
|
def read_multi_get_responses(is_raw)
|
|
248
315
|
hash = {}
|
|
249
316
|
key_index = is_raw ? 2 : 3
|
|
@@ -277,7 +344,7 @@ module Dalli
|
|
|
277
344
|
[key, @value_marshaller.store(key, raw_value, req_options)]
|
|
278
345
|
end
|
|
279
346
|
|
|
280
|
-
buffer = RequestFormatter.multi_meta_set(entries, ttl: ttl)
|
|
347
|
+
buffer = RequestFormatter.multi_meta_set(entries, ttl: ttl, **routing_token_kwargs(req_options))
|
|
281
348
|
flushed_write(buffer)
|
|
282
349
|
buffer.clear
|
|
283
350
|
response_processor.consume_all_responses_until_mn
|
|
@@ -285,8 +352,9 @@ module Dalli
|
|
|
285
352
|
|
|
286
353
|
# Single-server fast path for delete_multi. Writes all quiet delete requests
|
|
287
354
|
# terminated by a noop, then consumes all responses.
|
|
288
|
-
def delete_multi_req(keys)
|
|
289
|
-
buffer = RequestFormatter.multi_meta_delete(keys)
|
|
355
|
+
def delete_multi_req(keys, req_options = nil)
|
|
356
|
+
buffer = RequestFormatter.multi_meta_delete(keys, **tombstone_kwargs(req_options),
|
|
357
|
+
**routing_token_kwargs(req_options))
|
|
290
358
|
flushed_write(buffer)
|
|
291
359
|
buffer.clear
|
|
292
360
|
keys.size - response_processor.pipelined_delete_non_deletions
|