dalli 5.0.5 → 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 +172 -0
- data/README.md +6 -1
- data/lib/dalli/client.rb +274 -48
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pipelined_deleter.rb +41 -17
- data/lib/dalli/pipelined_getter.rb +57 -8
- data/lib/dalli/pipelined_setter.rb +15 -3
- data/lib/dalli/protocol/base.rb +62 -5
- data/lib/dalli/protocol/connection_manager.rb +34 -10
- data/lib/dalli/protocol/key_regularizer.rb +25 -9
- data/lib/dalli/protocol/meta.rb +100 -90
- data/lib/dalli/protocol/request_formatter.rb +166 -27
- data/lib/dalli/protocol/response_buffer.rb +21 -22
- data/lib/dalli/protocol/response_processor.rb +64 -17
- data/lib/dalli/socket.rb +47 -32
- data/lib/dalli/version.rb +2 -2
- metadata +2 -3
- data/lib/dalli/pid_cache.rb +0 -40
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -24,43 +24,38 @@ module Dalli
|
|
|
24
24
|
|
|
25
25
|
# Retrieval Commands
|
|
26
26
|
def get(key, options = nil)
|
|
27
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
28
27
|
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
29
28
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
30
|
-
req = RequestFormatter.meta_get(key:
|
|
29
|
+
req = RequestFormatter.meta_get(key: key, skip_flags: skip_flags, **routing_token_kwargs(options))
|
|
31
30
|
flushed_write(req)
|
|
32
31
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
33
32
|
end
|
|
34
33
|
|
|
35
|
-
def quiet_get_request(key)
|
|
36
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
34
|
+
def quiet_get_request(key, options = nil)
|
|
37
35
|
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
38
|
-
RequestFormatter.meta_get(key:
|
|
39
|
-
|
|
36
|
+
RequestFormatter.meta_get(key: key, return_cas: true, quiet: true, skip_flags: raw_mode?,
|
|
37
|
+
**routing_token_kwargs(options))
|
|
40
38
|
end
|
|
41
39
|
|
|
42
40
|
def gat(key, ttl, options = nil)
|
|
43
41
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
44
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
45
42
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
46
|
-
req = RequestFormatter.meta_get(key:
|
|
43
|
+
req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags, **routing_token_kwargs(options))
|
|
47
44
|
flushed_write(req)
|
|
48
45
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
def touch(key, ttl)
|
|
52
49
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
53
|
-
|
|
54
|
-
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
|
|
50
|
+
req = RequestFormatter.meta_get(key: key, ttl: ttl, value: false)
|
|
55
51
|
flushed_write(req)
|
|
56
52
|
response_processor.meta_get_without_value
|
|
57
53
|
end
|
|
58
54
|
|
|
59
55
|
# TODO: This is confusing, as there's a cas command in memcached
|
|
60
56
|
# and this isn't it. Maybe rename? Maybe eliminate?
|
|
61
|
-
def cas(key)
|
|
62
|
-
|
|
63
|
-
req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
|
|
57
|
+
def cas(key, options = nil)
|
|
58
|
+
req = RequestFormatter.meta_get(key: key, value: true, return_cas: true, **routing_token_kwargs(options))
|
|
64
59
|
flushed_write(req)
|
|
65
60
|
response_processor.meta_get_with_value_and_cas
|
|
66
61
|
end
|
|
@@ -90,17 +85,20 @@ module Dalli
|
|
|
90
85
|
# - :hit_before - true/false if previously accessed (only if return_hit_status: true)
|
|
91
86
|
# - :last_access - seconds since last access (only if return_last_access: true)
|
|
92
87
|
def meta_get(key, options = {})
|
|
93
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
94
88
|
req = RequestFormatter.meta_get(
|
|
95
|
-
key:
|
|
89
|
+
key: key, value: true, return_cas: true,
|
|
96
90
|
vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
|
|
97
91
|
return_hit_status: options[:return_hit_status],
|
|
98
|
-
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)
|
|
99
96
|
)
|
|
100
97
|
flushed_write(req)
|
|
101
98
|
response_processor.meta_get_with_metadata(
|
|
102
99
|
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
103
|
-
return_last_access: options[:return_last_access]
|
|
100
|
+
return_last_access: options[:return_last_access],
|
|
101
|
+
return_ttl_remaining: options[:return_ttl_remaining]
|
|
104
102
|
)
|
|
105
103
|
end
|
|
106
104
|
|
|
@@ -112,8 +110,7 @@ module Dalli
|
|
|
112
110
|
# @param cas [Integer] optional CAS value for compare-and-swap
|
|
113
111
|
# @return [Boolean] true if successful
|
|
114
112
|
def delete_stale(key, cas = nil)
|
|
115
|
-
|
|
116
|
-
req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
|
|
113
|
+
req = RequestFormatter.meta_delete(key: key, cas: cas, stale: true)
|
|
117
114
|
flushed_write(req)
|
|
118
115
|
response_processor.meta_delete
|
|
119
116
|
end
|
|
@@ -140,45 +137,46 @@ module Dalli
|
|
|
140
137
|
response_processor.meta_set_with_cas unless quiet?
|
|
141
138
|
end
|
|
142
139
|
|
|
143
|
-
# rubocop:disable Metrics/ParameterLists
|
|
144
140
|
def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
|
|
145
141
|
(value, bitflags) = @value_marshaller.store(key, raw_value, options)
|
|
146
142
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
147
|
-
|
|
148
|
-
req = RequestFormatter.meta_set(key: encoded_key, value: value,
|
|
143
|
+
req = RequestFormatter.meta_set(key: key, value: value,
|
|
149
144
|
bitflags: bitflags, cas: cas,
|
|
150
|
-
ttl: ttl, mode: mode, quiet: quiet,
|
|
145
|
+
ttl: ttl, mode: mode, quiet: quiet,
|
|
146
|
+
**routing_token_kwargs(options))
|
|
151
147
|
write("#{req}#{value}#{TERMINATOR}")
|
|
152
148
|
@connection_manager.flush unless quiet
|
|
153
149
|
end
|
|
154
150
|
# rubocop:enable Metrics/ParameterLists
|
|
155
151
|
|
|
156
|
-
def append(key, value)
|
|
157
|
-
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)
|
|
158
154
|
response_processor.meta_set_append_prepend unless quiet?
|
|
159
155
|
end
|
|
160
156
|
|
|
161
|
-
def prepend(key, value)
|
|
162
|
-
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)
|
|
163
159
|
response_processor.meta_set_append_prepend unless quiet?
|
|
164
160
|
end
|
|
165
161
|
|
|
166
162
|
# rubocop:disable Metrics/ParameterLists
|
|
167
|
-
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)
|
|
168
164
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
req = RequestFormatter.meta_set(key: key, value: value,
|
|
166
|
+
cas: cas, ttl: ttl, mode: mode, quiet: quiet?,
|
|
167
|
+
**routing_token_kwargs(options))
|
|
172
168
|
write("#{req}#{value}#{TERMINATOR}")
|
|
173
169
|
@connection_manager.flush unless quiet?
|
|
174
170
|
end
|
|
175
171
|
# rubocop:enable Metrics/ParameterLists
|
|
176
172
|
|
|
177
173
|
# Delete Commands
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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))
|
|
182
180
|
write(req)
|
|
183
181
|
@connection_manager.flush unless quiet?
|
|
184
182
|
response_processor.meta_delete unless quiet?
|
|
@@ -186,29 +184,35 @@ module Dalli
|
|
|
186
184
|
|
|
187
185
|
# Pipelined delete - writes a quiet delete request without reading response.
|
|
188
186
|
# Used by PipelinedDeleter for bulk operations.
|
|
189
|
-
def pipelined_delete(key)
|
|
190
|
-
|
|
191
|
-
|
|
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))
|
|
192
190
|
write(req)
|
|
193
191
|
end
|
|
194
192
|
|
|
193
|
+
def finish_pipelined_delete(sent)
|
|
194
|
+
write_noop
|
|
195
|
+
sent - response_processor.pipelined_delete_non_deletions
|
|
196
|
+
end
|
|
197
|
+
|
|
195
198
|
# Arithmetic Commands
|
|
196
|
-
def decr(key, count, ttl, initial)
|
|
197
|
-
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
|
|
198
201
|
end
|
|
199
202
|
|
|
200
|
-
def incr(key, count, ttl, initial)
|
|
201
|
-
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
|
|
202
205
|
end
|
|
203
206
|
|
|
204
|
-
|
|
207
|
+
# rubocop:disable Metrics/ParameterLists
|
|
208
|
+
def decr_incr(incr, key, delta, ttl, initial, options = nil)
|
|
205
209
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
quiet: quiet?, base64: base64))
|
|
210
|
+
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
211
|
+
quiet: quiet?, **routing_token_kwargs(options)))
|
|
209
212
|
@connection_manager.flush unless quiet?
|
|
210
213
|
response_processor.decr_incr unless quiet?
|
|
211
214
|
end
|
|
215
|
+
# rubocop:enable Metrics/ParameterLists
|
|
212
216
|
|
|
213
217
|
# Other Commands
|
|
214
218
|
def flush(delay = 0)
|
|
@@ -246,28 +250,52 @@ module Dalli
|
|
|
246
250
|
# Single-server fast path for get_multi. Inlines request formatting and
|
|
247
251
|
# response parsing to minimize per-key overhead. Avoids the PipelinedGetter
|
|
248
252
|
# machinery (IO.select, response buffering, server grouping).
|
|
249
|
-
def read_multi_req(keys)
|
|
253
|
+
def read_multi_req(keys, options = nil)
|
|
250
254
|
is_raw = raw_mode?
|
|
251
|
-
|
|
252
|
-
# In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
|
|
253
|
-
# Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
|
|
254
|
-
post_get = is_raw ? " v k q s\r\n" : " v f k q s\r\n"
|
|
255
|
-
buffer = ''.b
|
|
256
|
-
keys.each do |key|
|
|
257
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
258
|
-
if base64
|
|
259
|
-
buffer << 'mg ' << encoded_key << ' b' << post_get
|
|
260
|
-
else
|
|
261
|
-
buffer << 'mg ' << encoded_key << post_get
|
|
262
|
-
end
|
|
263
|
-
end
|
|
264
|
-
buffer << 'mn' << TERMINATOR
|
|
255
|
+
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw, **routing_token_kwargs(options))
|
|
265
256
|
flushed_write(buffer)
|
|
266
257
|
buffer.clear
|
|
267
|
-
|
|
268
258
|
read_multi_get_responses(is_raw)
|
|
269
259
|
end
|
|
270
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
|
+
|
|
271
299
|
def read_multi_get_responses(is_raw)
|
|
272
300
|
hash = {}
|
|
273
301
|
key_index = is_raw ? 2 : 3
|
|
@@ -287,52 +315,34 @@ module Dalli
|
|
|
287
315
|
raw_key = tokens[key_index]
|
|
288
316
|
return unless raw_key
|
|
289
317
|
|
|
290
|
-
key =
|
|
318
|
+
key = raw_key[1..]
|
|
319
|
+
key = KeyRegularizer.decode(key) if tokens.include?('b')
|
|
291
320
|
bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
|
|
292
321
|
[key, @value_marshaller.retrieve(value, bitflags)]
|
|
293
322
|
end
|
|
294
323
|
|
|
295
|
-
# rubocop:disable Metrics/AbcSize
|
|
296
|
-
|
|
297
324
|
# Single-server fast path for set_multi. Inlines request formatting to
|
|
298
325
|
# minimize per-key overhead. Avoids PipelinedSetter server grouping.
|
|
299
326
|
def write_multi_req(pairs, ttl, req_options)
|
|
300
327
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
(value, bitflags) = @value_marshaller.store(key, raw_value, req_options)
|
|
304
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
305
|
-
# Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
|
|
306
|
-
buffer << "ms #{encoded_key} #{value.bytesize} c"
|
|
307
|
-
buffer << ' b' if base64
|
|
308
|
-
buffer << " F#{bitflags}" if bitflags
|
|
309
|
-
buffer << " T#{ttl}" if ttl
|
|
310
|
-
buffer << ' MS q' << TERMINATOR << value << TERMINATOR
|
|
328
|
+
entries = pairs.map do |key, raw_value|
|
|
329
|
+
[key, @value_marshaller.store(key, raw_value, req_options)]
|
|
311
330
|
end
|
|
312
|
-
|
|
331
|
+
|
|
332
|
+
buffer = RequestFormatter.multi_meta_set(entries, ttl: ttl, **routing_token_kwargs(req_options))
|
|
313
333
|
flushed_write(buffer)
|
|
314
334
|
buffer.clear
|
|
315
335
|
response_processor.consume_all_responses_until_mn
|
|
316
336
|
end
|
|
317
|
-
# rubocop:enable Metrics/AbcSize
|
|
318
337
|
|
|
319
338
|
# Single-server fast path for delete_multi. Writes all quiet delete requests
|
|
320
339
|
# terminated by a noop, then consumes all responses.
|
|
321
|
-
def delete_multi_req(keys)
|
|
322
|
-
buffer =
|
|
323
|
-
|
|
324
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
325
|
-
# Inline format: "md <key> [b] q\r\n"
|
|
326
|
-
if base64
|
|
327
|
-
buffer << 'md ' << encoded_key << ' b q' << TERMINATOR
|
|
328
|
-
else
|
|
329
|
-
buffer << 'md ' << encoded_key << ' q' << TERMINATOR
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
buffer << RequestFormatter.meta_noop
|
|
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))
|
|
333
343
|
flushed_write(buffer)
|
|
334
344
|
buffer.clear
|
|
335
|
-
response_processor.
|
|
345
|
+
keys.size - response_processor.pipelined_delete_non_deletions
|
|
336
346
|
end
|
|
337
347
|
|
|
338
348
|
require_relative 'key_regularizer'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# frozen_string_literal:
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Dalli
|
|
4
4
|
module Protocol
|
|
@@ -7,14 +7,13 @@ module Dalli
|
|
|
7
7
|
# Class that encapsulates logic for formatting meta protocol requests
|
|
8
8
|
# to memcached.
|
|
9
9
|
##
|
|
10
|
-
|
|
10
|
+
module RequestFormatter
|
|
11
|
+
extend self
|
|
12
|
+
|
|
11
13
|
# Since these are string construction methods, we're going to disable these
|
|
12
14
|
# Rubocop directives. We really can't make this construction much simpler,
|
|
13
15
|
# and introducing an intermediate object seems like overkill.
|
|
14
16
|
#
|
|
15
|
-
# rubocop:disable Metrics/CyclomaticComplexity
|
|
16
|
-
# rubocop:disable Metrics/ParameterLists
|
|
17
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
|
18
17
|
#
|
|
19
18
|
# Meta get flags:
|
|
20
19
|
#
|
|
@@ -35,54 +34,142 @@ module Dalli
|
|
|
35
34
|
# - Z: Client lost the recache race (another client is already regenerating)
|
|
36
35
|
# - h0/h1: Hit status (0 = first access, 1 = previously accessed)
|
|
37
36
|
# - l<N>: Seconds since last access
|
|
38
|
-
def
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
cmd = "mg #{key}"
|
|
37
|
+
def meta_get(key:, value: true, return_cas: false, ttl: nil, quiet: false,
|
|
38
|
+
vivify_ttl: nil, recache_ttl: nil,
|
|
39
|
+
return_hit_status: false, return_last_access: false, return_ttl_remaining: false,
|
|
40
|
+
skip_lru_bump: false, skip_flags: false, p_token: nil, l_token: nil)
|
|
41
|
+
cmd = "mg #{encoded_key(key)}"
|
|
43
42
|
# In raw mode (skip_flags: true), we don't request bitflags since they're not used.
|
|
44
43
|
# This saves 2 bytes per request and skips parsing on response.
|
|
45
44
|
cmd << (skip_flags ? ' v' : ' v f') if value
|
|
46
45
|
cmd << ' c' if return_cas
|
|
47
|
-
cmd << ' b' if base64
|
|
48
46
|
cmd << " T#{ttl}" if ttl
|
|
47
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
49
48
|
cmd << ' k q s' if quiet # Return the key in the response if quiet
|
|
50
49
|
cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
|
|
51
50
|
cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
|
|
52
51
|
cmd << ' h' if return_hit_status # Return hit status (0 or 1)
|
|
53
52
|
cmd << ' l' if return_last_access # Return seconds since last access
|
|
53
|
+
cmd << ' t' if return_ttl_remaining # Return seconds of TTL remaining (-1 = no TTL)
|
|
54
54
|
cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
|
|
55
55
|
cmd << TERMINATOR
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
def
|
|
58
|
+
def multi_meta_get(keys, skip_flags: false, return_cas: false, p_token: nil, l_token: nil)
|
|
59
|
+
# In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
|
|
60
|
+
# Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
|
|
61
|
+
# With return_cas a "c" flag follows, which shifts those indexes --
|
|
62
|
+
# callers of that variant locate tokens by flag rather than position.
|
|
63
|
+
# Routing tokens apply to every key in the batch, so the suffix is
|
|
64
|
+
# built once rather than per key.
|
|
65
|
+
post_get = if return_cas
|
|
66
|
+
skip_flags ? ' v c' : ' v f c'
|
|
67
|
+
else
|
|
68
|
+
skip_flags ? ' v' : ' v f'
|
|
69
|
+
end
|
|
70
|
+
post_get += routing_tokens(p_token: p_token, l_token: l_token)
|
|
71
|
+
post_get += " k q s#{TERMINATOR}"
|
|
72
|
+
|
|
73
|
+
buffer = ''.b
|
|
74
|
+
keys.each do |key|
|
|
75
|
+
buffer << 'mg ' << encoded_key(key) << post_get
|
|
76
|
+
end
|
|
77
|
+
buffer << 'mn' << TERMINATOR
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false,
|
|
81
|
+
p_token: nil, l_token: nil)
|
|
82
|
+
base64 = KeyRegularizer.required?(key)
|
|
83
|
+
key = KeyRegularizer.encode(key) if base64
|
|
59
84
|
cmd = "ms #{key} #{value.bytesize}"
|
|
60
|
-
|
|
85
|
+
# Skip the cas-return flag in quiet mode: the response is suppressed,
|
|
86
|
+
# so requesting it only adds bytes to the request.
|
|
87
|
+
cmd << ' c' if !quiet && !%i[append prepend].include?(mode)
|
|
61
88
|
cmd << ' b' if base64
|
|
62
89
|
cmd << " F#{bitflags}" if bitflags
|
|
63
90
|
cmd << cas_string(cas)
|
|
64
91
|
cmd << " T#{ttl}" if ttl
|
|
65
92
|
cmd << " M#{mode_to_token(mode)}"
|
|
66
93
|
cmd << ' q' if quiet
|
|
94
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
67
95
|
cmd << TERMINATOR
|
|
68
96
|
end
|
|
69
97
|
|
|
98
|
+
def multi_meta_set(entries, ttl: nil, p_token: nil, l_token: nil)
|
|
99
|
+
# Routing tokens apply to every entry in the batch, so the suffix is
|
|
100
|
+
# built once rather than per entry.
|
|
101
|
+
token_suffix = routing_tokens(p_token: p_token, l_token: l_token)
|
|
102
|
+
|
|
103
|
+
buffer = ''.b
|
|
104
|
+
entries.each do |key, pair|
|
|
105
|
+
value, bitflags = pair
|
|
106
|
+
|
|
107
|
+
base64 = KeyRegularizer.required?(key)
|
|
108
|
+
key = KeyRegularizer.encode(key) if base64
|
|
109
|
+
|
|
110
|
+
# Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q [P/L]\r\n"
|
|
111
|
+
buffer << "ms #{key} #{value.bytesize} c"
|
|
112
|
+
buffer << ' b' if base64
|
|
113
|
+
buffer << " F#{bitflags}" if bitflags
|
|
114
|
+
buffer << " T#{ttl}" if ttl
|
|
115
|
+
buffer << ' MS q' << token_suffix << TERMINATOR << value << TERMINATOR
|
|
116
|
+
end
|
|
117
|
+
buffer << META_NOOP
|
|
118
|
+
end
|
|
119
|
+
|
|
70
120
|
# Thundering herd protection flag:
|
|
71
121
|
# - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
|
|
72
122
|
# using N/R flags will see the X flag and know the item is being regenerated.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
123
|
+
# Tombstone flags:
|
|
124
|
+
# - stale (I flag): mark the item stale instead of removing it. Readers
|
|
125
|
+
# using N/R flags, or get_with_metadata, see the X flag and know the
|
|
126
|
+
# item is being regenerated.
|
|
127
|
+
# - ttl (T flag): how long the stale marker lives. memcached only honors
|
|
128
|
+
# T on a delete when it is paired with I, so this raises rather than
|
|
129
|
+
# emitting a request the server would apply differently than intended.
|
|
130
|
+
# - drop_value (x flag): remove the item's value but leave the item, so a
|
|
131
|
+
# tombstone can be left without retaining the old payload.
|
|
132
|
+
def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false, drop_value: false,
|
|
133
|
+
p_token: nil, l_token: nil)
|
|
134
|
+
# Message uses this method's own parameter names (ttl/stale), not the
|
|
135
|
+
# client-facing tombstone_ttl/invalidate names Dalli::Client validates
|
|
136
|
+
# against -- this guard is also reachable by internal callers (tests,
|
|
137
|
+
# direct RequestFormatter use) that never go through the client.
|
|
138
|
+
raise ArgumentError, 'ttl requires stale: true' if ttl && !stale
|
|
139
|
+
|
|
140
|
+
cmd = "md #{encoded_key(key)}"
|
|
76
141
|
cmd << cas_string(cas)
|
|
77
|
-
cmd << " T#{ttl}" if ttl
|
|
78
142
|
cmd << ' I' if stale # Mark stale instead of deleting
|
|
143
|
+
cmd << " T#{Integer(ttl)}" if ttl
|
|
144
|
+
cmd << ' x' if drop_value # Drop the value but keep the item
|
|
79
145
|
cmd << ' q' if quiet
|
|
146
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
80
147
|
cmd << TERMINATOR
|
|
81
148
|
end
|
|
82
149
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
150
|
+
# Tombstone and routing-token flags apply to every key in the batch;
|
|
151
|
+
# see meta_delete.
|
|
152
|
+
def multi_meta_delete(keys, stale: false, ttl: nil, drop_value: false, p_token: nil, l_token: nil)
|
|
153
|
+
raise ArgumentError, 'tombstone_ttl requires invalidate: true' if ttl && !stale
|
|
154
|
+
|
|
155
|
+
suffix = +''
|
|
156
|
+
suffix << ' I' if stale
|
|
157
|
+
suffix << " T#{Integer(ttl)}" if ttl
|
|
158
|
+
suffix << ' x' if drop_value
|
|
159
|
+
suffix << ' q'
|
|
160
|
+
suffix << routing_tokens(p_token: p_token, l_token: l_token)
|
|
161
|
+
suffix << TERMINATOR
|
|
162
|
+
|
|
163
|
+
buffer = ''.b
|
|
164
|
+
keys.each do |key|
|
|
165
|
+
buffer << 'md ' << encoded_key(key) << suffix
|
|
166
|
+
end
|
|
167
|
+
buffer << META_NOOP
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false,
|
|
171
|
+
p_token: nil, l_token: nil)
|
|
172
|
+
cmd = "ma #{encoded_key(key)} v"
|
|
86
173
|
cmd << " D#{delta}" if delta
|
|
87
174
|
cmd << " J#{initial}" if initial
|
|
88
175
|
# Always set a TTL if an initial value is specified
|
|
@@ -90,22 +177,53 @@ module Dalli
|
|
|
90
177
|
cmd << cas_string(cas)
|
|
91
178
|
cmd << ' q' if quiet
|
|
92
179
|
cmd << " M#{incr ? 'I' : 'D'}"
|
|
180
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
93
181
|
cmd << TERMINATOR
|
|
94
182
|
end
|
|
183
|
+
|
|
184
|
+
# Builds the wire-format suffix for opaque routing tokens (P and L).
|
|
185
|
+
# memcached itself ignores these; they exist as hints for a proxy or
|
|
186
|
+
# router sitting between the client and memcached. See protocol.txt:
|
|
187
|
+
# "All commands accept tokens 'P' and 'L' which are completely ignored.
|
|
188
|
+
# The arguments to 'P' and 'L' can be used as hints or path
|
|
189
|
+
# specifications to a proxy or router inbetween a client and a
|
|
190
|
+
# memcached daemon."
|
|
191
|
+
#
|
|
192
|
+
# Empty / nil tokens are treated as no-ops. CRLF and null bytes are
|
|
193
|
+
# rejected with ArgumentError to prevent the token from being used as a
|
|
194
|
+
# wire-protocol injection vector (e.g. "foo\r\nflush_all\r\n" would
|
|
195
|
+
# otherwise be parsed as a second command by memcached or any
|
|
196
|
+
# intermediate proxy/LB).
|
|
197
|
+
def routing_tokens(p_token: nil, l_token: nil)
|
|
198
|
+
# Only an empty *String* is a no-op. Checking respond_to?(:empty?)
|
|
199
|
+
# instead would also swallow p_token: [] / {} before the type check
|
|
200
|
+
# below ever runs, silently dropping caller mistakes that should
|
|
201
|
+
# raise "must be a String".
|
|
202
|
+
p_token = nil if p_token.is_a?(String) && p_token.empty?
|
|
203
|
+
l_token = nil if l_token.is_a?(String) && l_token.empty?
|
|
204
|
+
validate_routing_token!('p_token', p_token)
|
|
205
|
+
validate_routing_token!('l_token', l_token)
|
|
206
|
+
return '' unless p_token || l_token
|
|
207
|
+
|
|
208
|
+
s = +''
|
|
209
|
+
s << " P#{p_token}" if p_token
|
|
210
|
+
s << " L#{l_token}" if l_token
|
|
211
|
+
s
|
|
212
|
+
end
|
|
95
213
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
96
214
|
# rubocop:enable Metrics/ParameterLists
|
|
97
215
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
98
216
|
|
|
99
217
|
META_NOOP = "mn#{TERMINATOR}".freeze
|
|
100
|
-
def
|
|
218
|
+
def meta_noop
|
|
101
219
|
META_NOOP
|
|
102
220
|
end
|
|
103
221
|
|
|
104
|
-
def
|
|
222
|
+
def version
|
|
105
223
|
"version#{TERMINATOR}"
|
|
106
224
|
end
|
|
107
225
|
|
|
108
|
-
def
|
|
226
|
+
def flush(delay: nil, quiet: false)
|
|
109
227
|
cmd = +'flush_all'
|
|
110
228
|
cmd << " #{parse_to_64_bit_int(delay, 0)}" if delay
|
|
111
229
|
cmd << ' noreply' if quiet
|
|
@@ -114,7 +232,7 @@ module Dalli
|
|
|
114
232
|
|
|
115
233
|
ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
|
|
116
234
|
|
|
117
|
-
def
|
|
235
|
+
def stats(arg = nil)
|
|
118
236
|
raise ArgumentError, "Invalid stats argument: #{arg.inspect}" unless ALLOWED_STATS_ARGS.include?(arg)
|
|
119
237
|
|
|
120
238
|
cmd = +'stats'
|
|
@@ -122,7 +240,28 @@ module Dalli
|
|
|
122
240
|
cmd << TERMINATOR
|
|
123
241
|
end
|
|
124
242
|
|
|
125
|
-
def
|
|
243
|
+
def encoded_key(key)
|
|
244
|
+
if KeyRegularizer.required?(key)
|
|
245
|
+
KeyRegularizer.encode(key) << ' b'
|
|
246
|
+
else
|
|
247
|
+
key
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
# Disallowed bytes: CR, LF, NUL. Any of these embedded in a routing
|
|
254
|
+
# token would let the caller inject a second wire-protocol command.
|
|
255
|
+
ROUTING_TOKEN_FORBIDDEN = /[\r\n\0]/
|
|
256
|
+
private_constant :ROUTING_TOKEN_FORBIDDEN
|
|
257
|
+
|
|
258
|
+
def validate_routing_token!(name, value)
|
|
259
|
+
return if value.nil?
|
|
260
|
+
raise ArgumentError, "#{name} must be a String, got #{value.class}" unless value.is_a?(String)
|
|
261
|
+
raise ArgumentError, "#{name} must not contain CRLF or null bytes" if value.match?(ROUTING_TOKEN_FORBIDDEN)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def mode_to_token(mode)
|
|
126
265
|
case mode
|
|
127
266
|
when :add
|
|
128
267
|
'E'
|
|
@@ -137,12 +276,12 @@ module Dalli
|
|
|
137
276
|
end
|
|
138
277
|
end
|
|
139
278
|
|
|
140
|
-
def
|
|
279
|
+
def cas_string(cas)
|
|
141
280
|
cas = parse_to_64_bit_int(cas, nil)
|
|
142
281
|
cas.nil? || cas.zero? ? '' : " C#{cas}"
|
|
143
282
|
end
|
|
144
283
|
|
|
145
|
-
def
|
|
284
|
+
def parse_to_64_bit_int(val, default)
|
|
146
285
|
val.nil? ? nil : Integer(val)
|
|
147
286
|
rescue ArgumentError
|
|
148
287
|
# Sanitize to default if it isn't parsable as an integer
|