dalli 5.0.5 → 5.0.7
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 +126 -0
- data/lib/dalli/client.rb +44 -7
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pipelined_deleter.rb +17 -10
- data/lib/dalli/protocol/base.rb +14 -1
- data/lib/dalli/protocol/connection_manager.rb +34 -10
- data/lib/dalli/protocol/key_regularizer.rb +8 -9
- data/lib/dalli/protocol/meta.rb +28 -71
- data/lib/dalli/protocol/request_formatter.rb +95 -34
- data/lib/dalli/protocol/response_buffer.rb +21 -22
- data/lib/dalli/protocol/response_processor.rb +41 -15
- data/lib/dalli/socket.rb +47 -32
- data/lib/dalli/version.rb +1 -1
- metadata +2 -3
- data/lib/dalli/pid_cache.rb +0 -40
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -24,34 +24,29 @@ 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)
|
|
31
30
|
flushed_write(req)
|
|
32
31
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
def quiet_get_request(key)
|
|
36
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
37
35
|
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
38
|
-
RequestFormatter.meta_get(key:
|
|
39
|
-
skip_flags: raw_mode?)
|
|
36
|
+
RequestFormatter.meta_get(key: key, return_cas: true, quiet: true, skip_flags: raw_mode?)
|
|
40
37
|
end
|
|
41
38
|
|
|
42
39
|
def gat(key, ttl, options = nil)
|
|
43
40
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
44
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
45
41
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
46
|
-
req = RequestFormatter.meta_get(key:
|
|
42
|
+
req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags)
|
|
47
43
|
flushed_write(req)
|
|
48
44
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
49
45
|
end
|
|
50
46
|
|
|
51
47
|
def touch(key, ttl)
|
|
52
48
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
53
|
-
|
|
54
|
-
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
|
|
49
|
+
req = RequestFormatter.meta_get(key: key, ttl: ttl, value: false)
|
|
55
50
|
flushed_write(req)
|
|
56
51
|
response_processor.meta_get_without_value
|
|
57
52
|
end
|
|
@@ -59,8 +54,7 @@ module Dalli
|
|
|
59
54
|
# TODO: This is confusing, as there's a cas command in memcached
|
|
60
55
|
# and this isn't it. Maybe rename? Maybe eliminate?
|
|
61
56
|
def cas(key)
|
|
62
|
-
|
|
63
|
-
req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
|
|
57
|
+
req = RequestFormatter.meta_get(key: key, value: true, return_cas: true)
|
|
64
58
|
flushed_write(req)
|
|
65
59
|
response_processor.meta_get_with_value_and_cas
|
|
66
60
|
end
|
|
@@ -90,9 +84,8 @@ module Dalli
|
|
|
90
84
|
# - :hit_before - true/false if previously accessed (only if return_hit_status: true)
|
|
91
85
|
# - :last_access - seconds since last access (only if return_last_access: true)
|
|
92
86
|
def meta_get(key, options = {})
|
|
93
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
94
87
|
req = RequestFormatter.meta_get(
|
|
95
|
-
key:
|
|
88
|
+
key: key, value: true, return_cas: true,
|
|
96
89
|
vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
|
|
97
90
|
return_hit_status: options[:return_hit_status],
|
|
98
91
|
return_last_access: options[:return_last_access], skip_lru_bump: options[:skip_lru_bump]
|
|
@@ -112,8 +105,7 @@ module Dalli
|
|
|
112
105
|
# @param cas [Integer] optional CAS value for compare-and-swap
|
|
113
106
|
# @return [Boolean] true if successful
|
|
114
107
|
def delete_stale(key, cas = nil)
|
|
115
|
-
|
|
116
|
-
req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
|
|
108
|
+
req = RequestFormatter.meta_delete(key: key, cas: cas, stale: true)
|
|
117
109
|
flushed_write(req)
|
|
118
110
|
response_processor.meta_delete
|
|
119
111
|
end
|
|
@@ -140,14 +132,12 @@ module Dalli
|
|
|
140
132
|
response_processor.meta_set_with_cas unless quiet?
|
|
141
133
|
end
|
|
142
134
|
|
|
143
|
-
# rubocop:disable Metrics/ParameterLists
|
|
144
135
|
def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
|
|
145
136
|
(value, bitflags) = @value_marshaller.store(key, raw_value, options)
|
|
146
137
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
147
|
-
|
|
148
|
-
req = RequestFormatter.meta_set(key: encoded_key, value: value,
|
|
138
|
+
req = RequestFormatter.meta_set(key: key, value: value,
|
|
149
139
|
bitflags: bitflags, cas: cas,
|
|
150
|
-
ttl: ttl, mode: mode, quiet: quiet
|
|
140
|
+
ttl: ttl, mode: mode, quiet: quiet)
|
|
151
141
|
write("#{req}#{value}#{TERMINATOR}")
|
|
152
142
|
@connection_manager.flush unless quiet
|
|
153
143
|
end
|
|
@@ -166,8 +156,7 @@ module Dalli
|
|
|
166
156
|
# rubocop:disable Metrics/ParameterLists
|
|
167
157
|
def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, _options = {})
|
|
168
158
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
169
|
-
|
|
170
|
-
req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
|
|
159
|
+
req = RequestFormatter.meta_set(key: key, value: value,
|
|
171
160
|
cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
|
|
172
161
|
write("#{req}#{value}#{TERMINATOR}")
|
|
173
162
|
@connection_manager.flush unless quiet?
|
|
@@ -176,9 +165,7 @@ module Dalli
|
|
|
176
165
|
|
|
177
166
|
# Delete Commands
|
|
178
167
|
def delete(key, cas)
|
|
179
|
-
|
|
180
|
-
req = RequestFormatter.meta_delete(key: encoded_key, cas: cas,
|
|
181
|
-
base64: base64, quiet: quiet?)
|
|
168
|
+
req = RequestFormatter.meta_delete(key: key, cas: cas, quiet: quiet?)
|
|
182
169
|
write(req)
|
|
183
170
|
@connection_manager.flush unless quiet?
|
|
184
171
|
response_processor.meta_delete unless quiet?
|
|
@@ -187,11 +174,15 @@ module Dalli
|
|
|
187
174
|
# Pipelined delete - writes a quiet delete request without reading response.
|
|
188
175
|
# Used by PipelinedDeleter for bulk operations.
|
|
189
176
|
def pipelined_delete(key)
|
|
190
|
-
|
|
191
|
-
req = RequestFormatter.meta_delete(key: encoded_key, base64: base64, quiet: true)
|
|
177
|
+
req = RequestFormatter.meta_delete(key: key, quiet: true)
|
|
192
178
|
write(req)
|
|
193
179
|
end
|
|
194
180
|
|
|
181
|
+
def finish_pipelined_delete(sent)
|
|
182
|
+
write_noop
|
|
183
|
+
sent - response_processor.pipelined_delete_non_deletions
|
|
184
|
+
end
|
|
185
|
+
|
|
195
186
|
# Arithmetic Commands
|
|
196
187
|
def decr(key, count, ttl, initial)
|
|
197
188
|
decr_incr false, key, count, ttl, initial
|
|
@@ -203,9 +194,8 @@ module Dalli
|
|
|
203
194
|
|
|
204
195
|
def decr_incr(incr, key, delta, ttl, initial)
|
|
205
196
|
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))
|
|
197
|
+
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
198
|
+
quiet: quiet?))
|
|
209
199
|
@connection_manager.flush unless quiet?
|
|
210
200
|
response_processor.decr_incr unless quiet?
|
|
211
201
|
end
|
|
@@ -248,23 +238,9 @@ module Dalli
|
|
|
248
238
|
# machinery (IO.select, response buffering, server grouping).
|
|
249
239
|
def read_multi_req(keys)
|
|
250
240
|
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
|
|
241
|
+
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw)
|
|
265
242
|
flushed_write(buffer)
|
|
266
243
|
buffer.clear
|
|
267
|
-
|
|
268
244
|
read_multi_get_responses(is_raw)
|
|
269
245
|
end
|
|
270
246
|
|
|
@@ -287,52 +263,33 @@ module Dalli
|
|
|
287
263
|
raw_key = tokens[key_index]
|
|
288
264
|
return unless raw_key
|
|
289
265
|
|
|
290
|
-
key =
|
|
266
|
+
key = raw_key[1..]
|
|
267
|
+
key = KeyRegularizer.decode(key) if tokens.include?('b')
|
|
291
268
|
bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
|
|
292
269
|
[key, @value_marshaller.retrieve(value, bitflags)]
|
|
293
270
|
end
|
|
294
271
|
|
|
295
|
-
# rubocop:disable Metrics/AbcSize
|
|
296
|
-
|
|
297
272
|
# Single-server fast path for set_multi. Inlines request formatting to
|
|
298
273
|
# minimize per-key overhead. Avoids PipelinedSetter server grouping.
|
|
299
274
|
def write_multi_req(pairs, ttl, req_options)
|
|
300
275
|
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
|
|
276
|
+
entries = pairs.map do |key, raw_value|
|
|
277
|
+
[key, @value_marshaller.store(key, raw_value, req_options)]
|
|
311
278
|
end
|
|
312
|
-
|
|
279
|
+
|
|
280
|
+
buffer = RequestFormatter.multi_meta_set(entries, ttl: ttl)
|
|
313
281
|
flushed_write(buffer)
|
|
314
282
|
buffer.clear
|
|
315
283
|
response_processor.consume_all_responses_until_mn
|
|
316
284
|
end
|
|
317
|
-
# rubocop:enable Metrics/AbcSize
|
|
318
285
|
|
|
319
286
|
# Single-server fast path for delete_multi. Writes all quiet delete requests
|
|
320
287
|
# terminated by a noop, then consumes all responses.
|
|
321
288
|
def delete_multi_req(keys)
|
|
322
|
-
buffer =
|
|
323
|
-
keys.each do |key|
|
|
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
|
|
289
|
+
buffer = RequestFormatter.multi_meta_delete(keys)
|
|
333
290
|
flushed_write(buffer)
|
|
334
291
|
buffer.clear
|
|
335
|
-
response_processor.
|
|
292
|
+
keys.size - response_processor.pipelined_delete_non_deletions
|
|
336
293
|
end
|
|
337
294
|
|
|
338
295
|
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,58 +34,98 @@ 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, skip_lru_bump: false,
|
|
40
|
+
skip_flags: false)
|
|
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 <<
|
|
48
|
-
cmd << " T#{ttl}" if ttl
|
|
46
|
+
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
49
47
|
cmd << ' k q s' if quiet # Return the key in the response if quiet
|
|
50
|
-
cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
|
|
51
|
-
|
|
48
|
+
cmd << " N#{integer_flag(:vivify_ttl, vivify_ttl)}" if vivify_ttl # Thundering herd: vivify on miss
|
|
49
|
+
# Thundering herd: win recache if TTL below threshold
|
|
50
|
+
cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
|
|
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
|
|
54
53
|
cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
|
|
55
54
|
cmd << TERMINATOR
|
|
56
55
|
end
|
|
57
56
|
|
|
58
|
-
def
|
|
57
|
+
def multi_meta_get(keys, skip_flags: false)
|
|
58
|
+
# In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
|
|
59
|
+
# Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
|
|
60
|
+
post_get = skip_flags ? " v k q s\r\n" : " v f k q s\r\n"
|
|
61
|
+
buffer = ''.b
|
|
62
|
+
keys.each do |key|
|
|
63
|
+
buffer << 'mg ' << encoded_key(key) << post_get
|
|
64
|
+
end
|
|
65
|
+
buffer << 'mn' << TERMINATOR
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false)
|
|
69
|
+
base64 = KeyRegularizer.required?(key)
|
|
70
|
+
key = KeyRegularizer.encode(key) if base64
|
|
59
71
|
cmd = "ms #{key} #{value.bytesize}"
|
|
60
|
-
|
|
72
|
+
# Skip the cas-return flag in quiet mode: the response is suppressed,
|
|
73
|
+
# so requesting it only adds bytes to the request.
|
|
74
|
+
cmd << ' c' if !quiet && !%i[append prepend].include?(mode)
|
|
61
75
|
cmd << ' b' if base64
|
|
62
76
|
cmd << " F#{bitflags}" if bitflags
|
|
63
77
|
cmd << cas_string(cas)
|
|
64
|
-
cmd << " T#{ttl}" if ttl
|
|
78
|
+
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
65
79
|
cmd << " M#{mode_to_token(mode)}"
|
|
66
80
|
cmd << ' q' if quiet
|
|
67
81
|
cmd << TERMINATOR
|
|
68
82
|
end
|
|
69
83
|
|
|
84
|
+
def multi_meta_set(entries, ttl: nil)
|
|
85
|
+
ttl = integer_flag(:ttl, ttl) if ttl
|
|
86
|
+
buffer = ''.b
|
|
87
|
+
entries.each do |key, pair|
|
|
88
|
+
value, bitflags = pair
|
|
89
|
+
|
|
90
|
+
base64 = KeyRegularizer.required?(key)
|
|
91
|
+
key = KeyRegularizer.encode(key) if base64
|
|
92
|
+
|
|
93
|
+
# Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
|
|
94
|
+
buffer << "ms #{key} #{value.bytesize} c"
|
|
95
|
+
buffer << ' b' if base64
|
|
96
|
+
buffer << " F#{bitflags}" if bitflags
|
|
97
|
+
buffer << " T#{ttl}" if ttl
|
|
98
|
+
buffer << ' MS q' << TERMINATOR << value << TERMINATOR
|
|
99
|
+
end
|
|
100
|
+
buffer << META_NOOP
|
|
101
|
+
end
|
|
102
|
+
|
|
70
103
|
# Thundering herd protection flag:
|
|
71
104
|
# - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
|
|
72
105
|
# using N/R flags will see the X flag and know the item is being regenerated.
|
|
73
|
-
def
|
|
74
|
-
cmd = "md #{key}"
|
|
75
|
-
cmd << ' b' if base64
|
|
106
|
+
def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false)
|
|
107
|
+
cmd = "md #{encoded_key(key)}"
|
|
76
108
|
cmd << cas_string(cas)
|
|
77
|
-
cmd << " T#{ttl}" if ttl
|
|
109
|
+
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
78
110
|
cmd << ' I' if stale # Mark stale instead of deleting
|
|
79
111
|
cmd << ' q' if quiet
|
|
80
112
|
cmd << TERMINATOR
|
|
81
113
|
end
|
|
82
114
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
def multi_meta_delete(keys)
|
|
116
|
+
buffer = ''.b
|
|
117
|
+
keys.each do |key|
|
|
118
|
+
buffer << 'md ' << encoded_key(key) << ' q' << TERMINATOR
|
|
119
|
+
end
|
|
120
|
+
buffer << META_NOOP
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false)
|
|
124
|
+
cmd = "ma #{encoded_key(key)} v"
|
|
125
|
+
cmd << " D#{integer_flag(:delta, delta)}" if delta
|
|
126
|
+
cmd << " J#{integer_flag(:initial, initial)}" if initial
|
|
88
127
|
# Always set a TTL if an initial value is specified
|
|
89
|
-
cmd << " N#{ttl
|
|
128
|
+
cmd << " N#{ttl ? integer_flag(:ttl, ttl) : 0}" if ttl || initial
|
|
90
129
|
cmd << cas_string(cas)
|
|
91
130
|
cmd << ' q' if quiet
|
|
92
131
|
cmd << " M#{incr ? 'I' : 'D'}"
|
|
@@ -97,15 +136,15 @@ module Dalli
|
|
|
97
136
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
98
137
|
|
|
99
138
|
META_NOOP = "mn#{TERMINATOR}".freeze
|
|
100
|
-
def
|
|
139
|
+
def meta_noop
|
|
101
140
|
META_NOOP
|
|
102
141
|
end
|
|
103
142
|
|
|
104
|
-
def
|
|
143
|
+
def version
|
|
105
144
|
"version#{TERMINATOR}"
|
|
106
145
|
end
|
|
107
146
|
|
|
108
|
-
def
|
|
147
|
+
def flush(delay: nil, quiet: false)
|
|
109
148
|
cmd = +'flush_all'
|
|
110
149
|
cmd << " #{parse_to_64_bit_int(delay, 0)}" if delay
|
|
111
150
|
cmd << ' noreply' if quiet
|
|
@@ -114,7 +153,7 @@ module Dalli
|
|
|
114
153
|
|
|
115
154
|
ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
|
|
116
155
|
|
|
117
|
-
def
|
|
156
|
+
def stats(arg = nil)
|
|
118
157
|
raise ArgumentError, "Invalid stats argument: #{arg.inspect}" unless ALLOWED_STATS_ARGS.include?(arg)
|
|
119
158
|
|
|
120
159
|
cmd = +'stats'
|
|
@@ -122,7 +161,29 @@ module Dalli
|
|
|
122
161
|
cmd << TERMINATOR
|
|
123
162
|
end
|
|
124
163
|
|
|
125
|
-
def
|
|
164
|
+
def encoded_key(key)
|
|
165
|
+
if KeyRegularizer.required?(key)
|
|
166
|
+
KeyRegularizer.encode(key) << ' b'
|
|
167
|
+
else
|
|
168
|
+
key
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
# Numeric flag values are written straight into the command line, so a
|
|
175
|
+
# value that isn't an integer -- e.g. a String carrying CRLF -- would let
|
|
176
|
+
# the caller inject further memcached commands (GHSA-6wmv-xq9m-fmp7).
|
|
177
|
+
# Converting to Integer means only digits ever reach the wire. Strings
|
|
178
|
+
# are parsed as base 10 so "010" means 10, as memcached would read it,
|
|
179
|
+
# rather than octal 8.
|
|
180
|
+
def integer_flag(name, value)
|
|
181
|
+
value.is_a?(String) ? Integer(value, 10) : Integer(value)
|
|
182
|
+
rescue ArgumentError, TypeError, FloatDomainError
|
|
183
|
+
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def mode_to_token(mode)
|
|
126
187
|
case mode
|
|
127
188
|
when :add
|
|
128
189
|
'E'
|
|
@@ -137,12 +198,12 @@ module Dalli
|
|
|
137
198
|
end
|
|
138
199
|
end
|
|
139
200
|
|
|
140
|
-
def
|
|
201
|
+
def cas_string(cas)
|
|
141
202
|
cas = parse_to_64_bit_int(cas, nil)
|
|
142
203
|
cas.nil? || cas.zero? ? '' : " C#{cas}"
|
|
143
204
|
end
|
|
144
205
|
|
|
145
|
-
def
|
|
206
|
+
def parse_to_64_bit_int(val, default)
|
|
146
207
|
val.nil? ? nil : Integer(val)
|
|
147
208
|
rescue ArgumentError
|
|
148
209
|
# Sanitize to default if it isn't parsable as an integer
|
|
@@ -5,16 +5,16 @@ require 'timeout'
|
|
|
5
5
|
|
|
6
6
|
module Dalli
|
|
7
7
|
module Protocol
|
|
8
|
+
# Compact the buffer when the consumed portion exceeds this
|
|
9
|
+
# threshold and represents more than half the buffer
|
|
10
|
+
COMPACT_THRESHOLD = 4096
|
|
11
|
+
|
|
8
12
|
##
|
|
9
13
|
# Manages the buffer for responses from memcached.
|
|
10
14
|
# Uses an offset-based approach to avoid string allocations
|
|
11
15
|
# when advancing through parsed responses.
|
|
12
16
|
##
|
|
13
17
|
class ResponseBuffer
|
|
14
|
-
# Compact the buffer when the consumed portion exceeds this
|
|
15
|
-
# threshold and represents more than half the buffer
|
|
16
|
-
COMPACT_THRESHOLD = 4096
|
|
17
|
-
|
|
18
18
|
def initialize(io_source, response_processor)
|
|
19
19
|
@io_source = io_source
|
|
20
20
|
@response_processor = response_processor
|
|
@@ -23,22 +23,32 @@ module Dalli
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def read
|
|
26
|
-
@buffer
|
|
26
|
+
remaining_bytes = @buffer.bytesize - @offset
|
|
27
|
+
if remaining_bytes.zero?
|
|
28
|
+
@offset = 0
|
|
29
|
+
@buffer = @io_source.read_available(@buffer)
|
|
30
|
+
else
|
|
31
|
+
if @offset > COMPACT_THRESHOLD && @offset > (@buffer.bytesize / 2)
|
|
32
|
+
@buffer.bytesplice(0, @offset, '')
|
|
33
|
+
@offset = 0
|
|
34
|
+
end
|
|
35
|
+
@buffer << @io_source.read_available
|
|
36
|
+
end
|
|
27
37
|
end
|
|
28
38
|
|
|
29
39
|
# Attempts to process a single response from the buffer,
|
|
30
40
|
# advancing the offset past the consumed bytes.
|
|
31
41
|
def process_single_getk_response
|
|
32
|
-
|
|
33
|
-
@offset +=
|
|
34
|
-
|
|
35
|
-
[status, cas, key, value]
|
|
42
|
+
response = @response_processor.getk_response_from_buffer(@buffer, @offset)
|
|
43
|
+
@offset += response.pop
|
|
44
|
+
response
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
# Resets the internal buffer to an empty state,
|
|
39
48
|
# so that we're ready to read pipelined responses
|
|
40
49
|
def reset
|
|
41
|
-
@buffer
|
|
50
|
+
@buffer&.clear
|
|
51
|
+
@buffer ||= ''.b
|
|
42
52
|
@offset = 0
|
|
43
53
|
end
|
|
44
54
|
|
|
@@ -54,6 +64,7 @@ module Dalli
|
|
|
54
64
|
|
|
55
65
|
# Clear the internal response buffer
|
|
56
66
|
def clear
|
|
67
|
+
@buffer&.clear
|
|
57
68
|
@buffer = nil
|
|
58
69
|
@offset = 0
|
|
59
70
|
end
|
|
@@ -61,18 +72,6 @@ module Dalli
|
|
|
61
72
|
def in_progress?
|
|
62
73
|
!@buffer.nil?
|
|
63
74
|
end
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
# Only compact when we've consumed a significant portion of the buffer.
|
|
68
|
-
# This avoids per-response string allocation while preventing unbounded
|
|
69
|
-
# memory growth for large pipelines.
|
|
70
|
-
def compact_if_needed
|
|
71
|
-
return unless @offset > COMPACT_THRESHOLD && @offset > @buffer.bytesize / 2
|
|
72
|
-
|
|
73
|
-
@buffer = @buffer.byteslice(@offset..)
|
|
74
|
-
@offset = 0
|
|
75
|
-
end
|
|
76
75
|
end
|
|
77
76
|
end
|
|
78
77
|
end
|
|
@@ -157,9 +157,26 @@ module Dalli
|
|
|
157
157
|
true
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
+
# Consumes the responses to a batch of quiet (pipelined) delete
|
|
161
|
+
# requests, which are terminated by a noop (MN). In quiet mode
|
|
162
|
+
# memcached suppresses the success response for each deleted key, so
|
|
163
|
+
# every line received before the terminator corresponds to a key that
|
|
164
|
+
# was NOT deleted -- a miss (NF) or an error. Returns that count so
|
|
165
|
+
# callers can derive the number of successful deletes as
|
|
166
|
+
# (keys_sent - non_deletions).
|
|
167
|
+
def pipelined_delete_non_deletions
|
|
168
|
+
non_deletions = 0
|
|
169
|
+
tokens = next_line_to_tokens
|
|
170
|
+
until tokens.first == MN
|
|
171
|
+
non_deletions += 1
|
|
172
|
+
tokens = next_line_to_tokens
|
|
173
|
+
end
|
|
174
|
+
non_deletions
|
|
175
|
+
end
|
|
176
|
+
|
|
160
177
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
161
178
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
162
|
-
[
|
|
179
|
+
[tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value, resp_size]
|
|
163
180
|
end
|
|
164
181
|
|
|
165
182
|
##
|
|
@@ -174,24 +191,26 @@ module Dalli
|
|
|
174
191
|
##
|
|
175
192
|
def getk_response_from_buffer(buf, offset = 0)
|
|
176
193
|
# Find the header terminator starting from offset
|
|
177
|
-
term_idx = buf.
|
|
178
|
-
return [0
|
|
194
|
+
term_idx = buf.byteindex(TERMINATOR, offset)
|
|
195
|
+
return [0] unless term_idx
|
|
179
196
|
|
|
180
197
|
header = buf.byteslice(offset, term_idx - offset)
|
|
181
198
|
tokens = header.split
|
|
182
199
|
header_len = header.bytesize + TERMINATOR.length
|
|
200
|
+
|
|
201
|
+
# The body len is removed from the tokens array
|
|
183
202
|
body_len = body_len_from_tokens(tokens)
|
|
184
203
|
|
|
185
204
|
# We have a complete response that has no body.
|
|
186
205
|
# This is either the response to the terminating
|
|
187
206
|
# noop or, if the status is not MN, an intermediate
|
|
188
207
|
# error response that needs to be discarded.
|
|
189
|
-
return [
|
|
208
|
+
return [true, header_len] if body_len.zero?
|
|
190
209
|
|
|
191
210
|
resp_size = header_len + body_len + TERMINATOR.length
|
|
192
211
|
# The header is in the buffer, but the body is not. As we don't have
|
|
193
212
|
# a complete response, don't advance the buffer
|
|
194
|
-
return [0
|
|
213
|
+
return [0] unless buf.bytesize >= offset + resp_size
|
|
195
214
|
|
|
196
215
|
# The full response is in our buffer, so parse it and return
|
|
197
216
|
# the values
|
|
@@ -210,17 +229,20 @@ module Dalli
|
|
|
210
229
|
end
|
|
211
230
|
|
|
212
231
|
def bitflags_from_tokens(tokens)
|
|
213
|
-
value_from_tokens(tokens, 'f')
|
|
232
|
+
value_from_tokens(tokens, 'f').to_i
|
|
214
233
|
end
|
|
215
234
|
|
|
216
235
|
def cas_from_tokens(tokens)
|
|
217
|
-
value_from_tokens(tokens, 'c')
|
|
236
|
+
value_from_tokens(tokens, 'c').to_i
|
|
218
237
|
end
|
|
219
238
|
|
|
220
239
|
def key_from_tokens(tokens)
|
|
221
240
|
encoded_key = value_from_tokens(tokens, 'k')
|
|
222
|
-
|
|
223
|
-
|
|
241
|
+
if tokens.delete('b')
|
|
242
|
+
KeyRegularizer.decode(encoded_key)
|
|
243
|
+
else
|
|
244
|
+
encoded_key
|
|
245
|
+
end
|
|
224
246
|
end
|
|
225
247
|
|
|
226
248
|
# Returns true if item was previously hit, false if first access, nil if not requested
|
|
@@ -235,18 +257,22 @@ module Dalli
|
|
|
235
257
|
# Returns seconds since last access, or nil if not requested
|
|
236
258
|
# The l flag returns l<seconds>
|
|
237
259
|
def last_access_from_tokens(tokens)
|
|
238
|
-
value_from_tokens(tokens, 'l')
|
|
260
|
+
value_from_tokens(tokens, 'l').to_i
|
|
239
261
|
end
|
|
240
262
|
|
|
241
263
|
def body_len_from_tokens(tokens)
|
|
242
|
-
value_from_tokens(tokens, 's')
|
|
264
|
+
value_from_tokens(tokens, 's').to_i
|
|
243
265
|
end
|
|
244
266
|
|
|
245
267
|
def value_from_tokens(tokens, flag)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
268
|
+
# NB: as an optimization, we're mutating the matching token in place
|
|
269
|
+
# so there is a baked assumption that we're only accessing each token at most once.
|
|
270
|
+
index = tokens.find_index { |t| t.start_with?(flag) }
|
|
271
|
+
if index
|
|
272
|
+
tokens.delete_at(index).delete_prefix!(flag)
|
|
273
|
+
else
|
|
274
|
+
0
|
|
275
|
+
end
|
|
250
276
|
end
|
|
251
277
|
|
|
252
278
|
def read_line
|