dalli 3.2.8 → 5.0.5
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 +305 -0
- data/Gemfile +16 -2
- data/README.md +96 -2
- data/lib/dalli/client.rb +289 -28
- data/lib/dalli/flags.rb +15 -0
- data/lib/dalli/instrumentation.rb +153 -0
- data/lib/dalli/key_manager.rb +23 -8
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pid_cache.rb +1 -1
- data/lib/dalli/pipelined_deleter.rb +82 -0
- data/lib/dalli/pipelined_getter.rb +44 -20
- data/lib/dalli/pipelined_setter.rb +87 -0
- data/lib/dalli/protocol/base.rb +95 -25
- data/lib/dalli/protocol/connection_manager.rb +37 -14
- data/lib/dalli/protocol/{meta/key_regularizer.rb → key_regularizer.rb} +1 -1
- data/lib/dalli/protocol/meta.rb +185 -20
- data/lib/dalli/protocol/{meta/request_formatter.rb → request_formatter.rb} +49 -16
- data/lib/dalli/protocol/response_buffer.rb +36 -12
- data/lib/dalli/protocol/{meta/response_processor.rb → response_processor.rb} +93 -37
- data/lib/dalli/protocol/server_config_parser.rb +2 -2
- data/lib/dalli/protocol/string_marshaller.rb +65 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
- data/lib/dalli/protocol/value_compressor.rb +3 -16
- data/lib/dalli/protocol/value_marshaller.rb +1 -1
- data/lib/dalli/protocol/value_serializer.rb +61 -44
- data/lib/dalli/protocol.rb +10 -0
- data/lib/dalli/ring.rb +2 -2
- data/lib/dalli/servers_arg_normalizer.rb +1 -1
- data/lib/dalli/socket.rb +79 -14
- data/lib/dalli/version.rb +2 -2
- data/lib/dalli.rb +13 -5
- data/lib/rack/session/dalli.rb +43 -8
- metadata +27 -17
- data/lib/dalli/protocol/binary/request_formatter.rb +0 -117
- data/lib/dalli/protocol/binary/response_header.rb +0 -36
- data/lib/dalli/protocol/binary/response_processor.rb +0 -239
- data/lib/dalli/protocol/binary/sasl_authentication.rb +0 -60
- data/lib/dalli/protocol/binary.rb +0 -173
- data/lib/dalli/server.rb +0 -6
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -25,21 +25,26 @@ module Dalli
|
|
|
25
25
|
# Retrieval Commands
|
|
26
26
|
def get(key, options = nil)
|
|
27
27
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
29
|
+
skip_flags = raw_mode? || (options && options[:raw])
|
|
30
|
+
req = RequestFormatter.meta_get(key: encoded_key, base64: base64, skip_flags: skip_flags)
|
|
31
|
+
flushed_write(req)
|
|
30
32
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def quiet_get_request(key)
|
|
34
36
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
35
|
-
|
|
37
|
+
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
38
|
+
RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true,
|
|
39
|
+
skip_flags: raw_mode?)
|
|
36
40
|
end
|
|
37
41
|
|
|
38
42
|
def gat(key, ttl, options = nil)
|
|
39
43
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
40
44
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
skip_flags = raw_mode? || (options && options[:raw])
|
|
46
|
+
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64, skip_flags: skip_flags)
|
|
47
|
+
flushed_write(req)
|
|
43
48
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
44
49
|
end
|
|
45
50
|
|
|
@@ -47,7 +52,7 @@ module Dalli
|
|
|
47
52
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
48
53
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
49
54
|
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
|
|
50
|
-
|
|
55
|
+
flushed_write(req)
|
|
51
56
|
response_processor.meta_get_without_value
|
|
52
57
|
end
|
|
53
58
|
|
|
@@ -56,16 +61,75 @@ module Dalli
|
|
|
56
61
|
def cas(key)
|
|
57
62
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
58
63
|
req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
|
|
59
|
-
|
|
64
|
+
flushed_write(req)
|
|
60
65
|
response_processor.meta_get_with_value_and_cas
|
|
61
66
|
end
|
|
62
67
|
|
|
68
|
+
# Comprehensive meta get with support for all metadata flags.
|
|
69
|
+
# @note Requires memcached 1.6+ (meta protocol feature)
|
|
70
|
+
#
|
|
71
|
+
# This is the full-featured get method that supports:
|
|
72
|
+
# - Thundering herd protection (vivify_ttl, recache_ttl)
|
|
73
|
+
# - Item metadata (hit_status, last_access)
|
|
74
|
+
# - LRU control (skip_lru_bump)
|
|
75
|
+
#
|
|
76
|
+
# @param key [String] the key to retrieve
|
|
77
|
+
# @param options [Hash] options controlling what metadata to return
|
|
78
|
+
# - :vivify_ttl [Integer] creates a stub on miss with this TTL (N flag)
|
|
79
|
+
# - :recache_ttl [Integer] wins recache race if remaining TTL is below this (R flag)
|
|
80
|
+
# - :return_hit_status [Boolean] return whether item was previously accessed (h flag)
|
|
81
|
+
# - :return_last_access [Boolean] return seconds since last access (l flag)
|
|
82
|
+
# - :skip_lru_bump [Boolean] don't bump LRU or update access stats (u flag)
|
|
83
|
+
# - :cache_nils [Boolean] whether to cache nil values
|
|
84
|
+
# @return [Hash] containing:
|
|
85
|
+
# - :value - the cached value (or nil on miss)
|
|
86
|
+
# - :cas - the CAS value
|
|
87
|
+
# - :won_recache - true if client won recache race (W flag)
|
|
88
|
+
# - :stale - true if item is stale (X flag)
|
|
89
|
+
# - :lost_recache - true if another client is recaching (Z flag)
|
|
90
|
+
# - :hit_before - true/false if previously accessed (only if return_hit_status: true)
|
|
91
|
+
# - :last_access - seconds since last access (only if return_last_access: true)
|
|
92
|
+
def meta_get(key, options = {})
|
|
93
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
94
|
+
req = RequestFormatter.meta_get(
|
|
95
|
+
key: encoded_key, value: true, return_cas: true, base64: base64,
|
|
96
|
+
vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
|
|
97
|
+
return_hit_status: options[:return_hit_status],
|
|
98
|
+
return_last_access: options[:return_last_access], skip_lru_bump: options[:skip_lru_bump]
|
|
99
|
+
)
|
|
100
|
+
flushed_write(req)
|
|
101
|
+
response_processor.meta_get_with_metadata(
|
|
102
|
+
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
103
|
+
return_last_access: options[:return_last_access]
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Delete with stale invalidation instead of actual deletion.
|
|
108
|
+
# Used with thundering herd protection to mark items as stale rather than removing them.
|
|
109
|
+
# @note Requires memcached 1.6+ (meta protocol feature)
|
|
110
|
+
#
|
|
111
|
+
# @param key [String] the key to invalidate
|
|
112
|
+
# @param cas [Integer] optional CAS value for compare-and-swap
|
|
113
|
+
# @return [Boolean] true if successful
|
|
114
|
+
def delete_stale(key, cas = nil)
|
|
115
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
116
|
+
req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
|
|
117
|
+
flushed_write(req)
|
|
118
|
+
response_processor.meta_delete
|
|
119
|
+
end
|
|
120
|
+
|
|
63
121
|
# Storage Commands
|
|
64
122
|
def set(key, value, ttl, cas, options)
|
|
65
123
|
write_storage_req(:set, key, value, ttl, cas, options)
|
|
66
124
|
response_processor.meta_set_with_cas unless quiet?
|
|
67
125
|
end
|
|
68
126
|
|
|
127
|
+
# Pipelined set - writes a quiet set request without reading response.
|
|
128
|
+
# Used by PipelinedSetter for bulk operations.
|
|
129
|
+
def pipelined_set(key, value, ttl, options)
|
|
130
|
+
write_storage_req(:set, key, value, ttl, nil, options, quiet: true)
|
|
131
|
+
end
|
|
132
|
+
|
|
69
133
|
def add(key, value, ttl, options)
|
|
70
134
|
write_storage_req(:add, key, value, ttl, nil, options)
|
|
71
135
|
response_processor.meta_set_with_cas unless quiet?
|
|
@@ -77,14 +141,15 @@ module Dalli
|
|
|
77
141
|
end
|
|
78
142
|
|
|
79
143
|
# rubocop:disable Metrics/ParameterLists
|
|
80
|
-
def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {})
|
|
144
|
+
def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
|
|
81
145
|
(value, bitflags) = @value_marshaller.store(key, raw_value, options)
|
|
82
146
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
83
147
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
84
148
|
req = RequestFormatter.meta_set(key: encoded_key, value: value,
|
|
85
149
|
bitflags: bitflags, cas: cas,
|
|
86
|
-
ttl: ttl, mode: mode, quiet: quiet
|
|
87
|
-
write(req)
|
|
150
|
+
ttl: ttl, mode: mode, quiet: quiet, base64: base64)
|
|
151
|
+
write("#{req}#{value}#{TERMINATOR}")
|
|
152
|
+
@connection_manager.flush unless quiet
|
|
88
153
|
end
|
|
89
154
|
# rubocop:enable Metrics/ParameterLists
|
|
90
155
|
|
|
@@ -104,7 +169,8 @@ module Dalli
|
|
|
104
169
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
105
170
|
req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
|
|
106
171
|
cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
|
|
107
|
-
write(req)
|
|
172
|
+
write("#{req}#{value}#{TERMINATOR}")
|
|
173
|
+
@connection_manager.flush unless quiet?
|
|
108
174
|
end
|
|
109
175
|
# rubocop:enable Metrics/ParameterLists
|
|
110
176
|
|
|
@@ -114,9 +180,18 @@ module Dalli
|
|
|
114
180
|
req = RequestFormatter.meta_delete(key: encoded_key, cas: cas,
|
|
115
181
|
base64: base64, quiet: quiet?)
|
|
116
182
|
write(req)
|
|
183
|
+
@connection_manager.flush unless quiet?
|
|
117
184
|
response_processor.meta_delete unless quiet?
|
|
118
185
|
end
|
|
119
186
|
|
|
187
|
+
# Pipelined delete - writes a quiet delete request without reading response.
|
|
188
|
+
# Used by PipelinedDeleter for bulk operations.
|
|
189
|
+
def pipelined_delete(key)
|
|
190
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
191
|
+
req = RequestFormatter.meta_delete(key: encoded_key, base64: base64, quiet: true)
|
|
192
|
+
write(req)
|
|
193
|
+
end
|
|
194
|
+
|
|
120
195
|
# Arithmetic Commands
|
|
121
196
|
def decr(key, count, ttl, initial)
|
|
122
197
|
decr_incr false, key, count, ttl, initial
|
|
@@ -131,12 +206,14 @@ module Dalli
|
|
|
131
206
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
132
207
|
write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
133
208
|
quiet: quiet?, base64: base64))
|
|
209
|
+
@connection_manager.flush unless quiet?
|
|
134
210
|
response_processor.decr_incr unless quiet?
|
|
135
211
|
end
|
|
136
212
|
|
|
137
213
|
# Other Commands
|
|
138
214
|
def flush(delay = 0)
|
|
139
215
|
write(RequestFormatter.flush(delay: delay))
|
|
216
|
+
@connection_manager.flush unless quiet?
|
|
140
217
|
response_processor.flush unless quiet?
|
|
141
218
|
end
|
|
142
219
|
|
|
@@ -148,31 +225,119 @@ module Dalli
|
|
|
148
225
|
end
|
|
149
226
|
|
|
150
227
|
def stats(info = nil)
|
|
151
|
-
|
|
228
|
+
flushed_write(RequestFormatter.stats(info))
|
|
152
229
|
response_processor.stats
|
|
153
230
|
end
|
|
154
231
|
|
|
155
232
|
def reset_stats
|
|
156
|
-
|
|
233
|
+
flushed_write(RequestFormatter.stats('reset'))
|
|
157
234
|
response_processor.reset
|
|
158
235
|
end
|
|
159
236
|
|
|
160
237
|
def version
|
|
161
|
-
|
|
238
|
+
flushed_write(RequestFormatter.version)
|
|
162
239
|
response_processor.version
|
|
163
240
|
end
|
|
164
241
|
|
|
165
242
|
def write_noop
|
|
166
|
-
|
|
243
|
+
flushed_write(RequestFormatter.meta_noop)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Single-server fast path for get_multi. Inlines request formatting and
|
|
247
|
+
# response parsing to minimize per-key overhead. Avoids the PipelinedGetter
|
|
248
|
+
# machinery (IO.select, response buffering, server grouping).
|
|
249
|
+
def read_multi_req(keys)
|
|
250
|
+
is_raw = raw_mode?
|
|
251
|
+
# Inline request formatting — avoids RequestFormatter.meta_get overhead per key.
|
|
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
|
|
265
|
+
flushed_write(buffer)
|
|
266
|
+
buffer.clear
|
|
267
|
+
|
|
268
|
+
read_multi_get_responses(is_raw)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def read_multi_get_responses(is_raw)
|
|
272
|
+
hash = {}
|
|
273
|
+
key_index = is_raw ? 2 : 3
|
|
274
|
+
while (line = @connection_manager.read_line)
|
|
275
|
+
break if line.start_with?('MN')
|
|
276
|
+
next unless line.start_with?('VA ')
|
|
277
|
+
|
|
278
|
+
key, value = parse_multi_get_value(line, key_index, is_raw)
|
|
279
|
+
hash[key] = value if key
|
|
280
|
+
end
|
|
281
|
+
hash
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def parse_multi_get_value(line, key_index, is_raw)
|
|
285
|
+
tokens = line.chomp!(TERMINATOR).split
|
|
286
|
+
value = @connection_manager.read(tokens[1].to_i + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
287
|
+
raw_key = tokens[key_index]
|
|
288
|
+
return unless raw_key
|
|
289
|
+
|
|
290
|
+
key = KeyRegularizer.decode(raw_key[1..], tokens.include?('b'))
|
|
291
|
+
bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
|
|
292
|
+
[key, @value_marshaller.retrieve(value, bitflags)]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# rubocop:disable Metrics/AbcSize
|
|
296
|
+
|
|
297
|
+
# Single-server fast path for set_multi. Inlines request formatting to
|
|
298
|
+
# minimize per-key overhead. Avoids PipelinedSetter server grouping.
|
|
299
|
+
def write_multi_req(pairs, ttl, req_options)
|
|
300
|
+
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
301
|
+
buffer = ''.b
|
|
302
|
+
pairs.each do |key, raw_value|
|
|
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
|
|
311
|
+
end
|
|
312
|
+
buffer << RequestFormatter.meta_noop
|
|
313
|
+
flushed_write(buffer)
|
|
314
|
+
buffer.clear
|
|
315
|
+
response_processor.consume_all_responses_until_mn
|
|
167
316
|
end
|
|
317
|
+
# rubocop:enable Metrics/AbcSize
|
|
168
318
|
|
|
169
|
-
|
|
170
|
-
|
|
319
|
+
# Single-server fast path for delete_multi. Writes all quiet delete requests
|
|
320
|
+
# terminated by a noop, then consumes all responses.
|
|
321
|
+
def delete_multi_req(keys)
|
|
322
|
+
buffer = ''.b
|
|
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
|
|
333
|
+
flushed_write(buffer)
|
|
334
|
+
buffer.clear
|
|
335
|
+
response_processor.consume_all_responses_until_mn
|
|
171
336
|
end
|
|
172
337
|
|
|
173
|
-
require_relative '
|
|
174
|
-
require_relative '
|
|
175
|
-
require_relative '
|
|
338
|
+
require_relative 'key_regularizer'
|
|
339
|
+
require_relative 'request_formatter'
|
|
340
|
+
require_relative 'response_processor'
|
|
176
341
|
end
|
|
177
342
|
end
|
|
178
343
|
end
|
|
@@ -13,17 +13,46 @@ module Dalli
|
|
|
13
13
|
# and introducing an intermediate object seems like overkill.
|
|
14
14
|
#
|
|
15
15
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
16
|
-
# rubocop:disable Metrics/MethodLength
|
|
17
16
|
# rubocop:disable Metrics/ParameterLists
|
|
18
17
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
19
|
-
|
|
18
|
+
#
|
|
19
|
+
# Meta get flags:
|
|
20
|
+
#
|
|
21
|
+
# Thundering herd protection:
|
|
22
|
+
# - vivify_ttl (N flag): On miss, create a stub item and return W flag. The TTL
|
|
23
|
+
# specifies how long the stub lives. Other clients see X (stale) and Z (lost race).
|
|
24
|
+
# - recache_ttl (R flag): If item's remaining TTL is below this threshold, return W
|
|
25
|
+
# flag to indicate this client should recache. Other clients get Z (lost race).
|
|
26
|
+
#
|
|
27
|
+
# Metadata flags:
|
|
28
|
+
# - return_hit_status (h flag): Return whether item has been hit before (0 or 1)
|
|
29
|
+
# - return_last_access (l flag): Return seconds since item was last accessed
|
|
30
|
+
# - skip_lru_bump (u flag): Don't bump item in LRU, don't update hit status or last access
|
|
31
|
+
#
|
|
32
|
+
# Response flags (parsed by response processor):
|
|
33
|
+
# - W: Client won the right to recache this item
|
|
34
|
+
# - X: Item is stale (another client is regenerating)
|
|
35
|
+
# - Z: Client lost the recache race (another client is already regenerating)
|
|
36
|
+
# - h0/h1: Hit status (0 = first access, 1 = previously accessed)
|
|
37
|
+
# - l<N>: Seconds since last access
|
|
38
|
+
def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false,
|
|
39
|
+
vivify_ttl: nil, recache_ttl: nil,
|
|
40
|
+
return_hit_status: false, return_last_access: false, skip_lru_bump: false,
|
|
41
|
+
skip_flags: false)
|
|
20
42
|
cmd = "mg #{key}"
|
|
21
|
-
|
|
43
|
+
# In raw mode (skip_flags: true), we don't request bitflags since they're not used.
|
|
44
|
+
# This saves 2 bytes per request and skips parsing on response.
|
|
45
|
+
cmd << (skip_flags ? ' v' : ' v f') if value
|
|
22
46
|
cmd << ' c' if return_cas
|
|
23
47
|
cmd << ' b' if base64
|
|
24
48
|
cmd << " T#{ttl}" if ttl
|
|
25
49
|
cmd << ' k q s' if quiet # Return the key in the response if quiet
|
|
26
|
-
cmd
|
|
50
|
+
cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
|
|
51
|
+
cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
|
|
52
|
+
cmd << ' h' if return_hit_status # Return hit status (0 or 1)
|
|
53
|
+
cmd << ' l' if return_last_access # Return seconds since last access
|
|
54
|
+
cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
|
|
55
|
+
cmd << TERMINATOR
|
|
27
56
|
end
|
|
28
57
|
|
|
29
58
|
def self.meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, base64: false, quiet: false)
|
|
@@ -36,17 +65,19 @@ module Dalli
|
|
|
36
65
|
cmd << " M#{mode_to_token(mode)}"
|
|
37
66
|
cmd << ' q' if quiet
|
|
38
67
|
cmd << TERMINATOR
|
|
39
|
-
cmd << value
|
|
40
|
-
cmd + TERMINATOR
|
|
41
68
|
end
|
|
42
69
|
|
|
43
|
-
|
|
70
|
+
# Thundering herd protection flag:
|
|
71
|
+
# - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
|
|
72
|
+
# using N/R flags will see the X flag and know the item is being regenerated.
|
|
73
|
+
def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false, stale: false)
|
|
44
74
|
cmd = "md #{key}"
|
|
45
75
|
cmd << ' b' if base64
|
|
46
76
|
cmd << cas_string(cas)
|
|
47
77
|
cmd << " T#{ttl}" if ttl
|
|
78
|
+
cmd << ' I' if stale # Mark stale instead of deleting
|
|
48
79
|
cmd << ' q' if quiet
|
|
49
|
-
cmd
|
|
80
|
+
cmd << TERMINATOR
|
|
50
81
|
end
|
|
51
82
|
|
|
52
83
|
def self.meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, base64: false, quiet: false)
|
|
@@ -59,15 +90,15 @@ module Dalli
|
|
|
59
90
|
cmd << cas_string(cas)
|
|
60
91
|
cmd << ' q' if quiet
|
|
61
92
|
cmd << " M#{incr ? 'I' : 'D'}"
|
|
62
|
-
cmd
|
|
93
|
+
cmd << TERMINATOR
|
|
63
94
|
end
|
|
64
95
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
65
|
-
# rubocop:enable Metrics/MethodLength
|
|
66
96
|
# rubocop:enable Metrics/ParameterLists
|
|
67
97
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
68
98
|
|
|
99
|
+
META_NOOP = "mn#{TERMINATOR}".freeze
|
|
69
100
|
def self.meta_noop
|
|
70
|
-
|
|
101
|
+
META_NOOP
|
|
71
102
|
end
|
|
72
103
|
|
|
73
104
|
def self.version
|
|
@@ -78,16 +109,19 @@ module Dalli
|
|
|
78
109
|
cmd = +'flush_all'
|
|
79
110
|
cmd << " #{parse_to_64_bit_int(delay, 0)}" if delay
|
|
80
111
|
cmd << ' noreply' if quiet
|
|
81
|
-
cmd
|
|
112
|
+
cmd << TERMINATOR
|
|
82
113
|
end
|
|
83
114
|
|
|
115
|
+
ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
|
|
116
|
+
|
|
84
117
|
def self.stats(arg = nil)
|
|
118
|
+
raise ArgumentError, "Invalid stats argument: #{arg.inspect}" unless ALLOWED_STATS_ARGS.include?(arg)
|
|
119
|
+
|
|
85
120
|
cmd = +'stats'
|
|
86
|
-
cmd << " #{arg}" if arg
|
|
87
|
-
cmd
|
|
121
|
+
cmd << " #{arg}" if arg && !arg.empty?
|
|
122
|
+
cmd << TERMINATOR
|
|
88
123
|
end
|
|
89
124
|
|
|
90
|
-
# rubocop:disable Metrics/MethodLength
|
|
91
125
|
def self.mode_to_token(mode)
|
|
92
126
|
case mode
|
|
93
127
|
when :add
|
|
@@ -102,7 +136,6 @@ module Dalli
|
|
|
102
136
|
'S'
|
|
103
137
|
end
|
|
104
138
|
end
|
|
105
|
-
# rubocop:enable Metrics/MethodLength
|
|
106
139
|
|
|
107
140
|
def self.cas_string(cas)
|
|
108
141
|
cas = parse_to_64_bit_int(cas, nil)
|
|
@@ -7,48 +7,72 @@ module Dalli
|
|
|
7
7
|
module Protocol
|
|
8
8
|
##
|
|
9
9
|
# Manages the buffer for responses from memcached.
|
|
10
|
+
# Uses an offset-based approach to avoid string allocations
|
|
11
|
+
# when advancing through parsed responses.
|
|
10
12
|
##
|
|
11
13
|
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
|
+
|
|
12
18
|
def initialize(io_source, response_processor)
|
|
13
19
|
@io_source = io_source
|
|
14
20
|
@response_processor = response_processor
|
|
15
21
|
@buffer = nil
|
|
22
|
+
@offset = 0
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
def read
|
|
19
26
|
@buffer << @io_source.read_nonblock
|
|
20
27
|
end
|
|
21
28
|
|
|
22
|
-
# Attempts to process a single response from the buffer
|
|
23
|
-
#
|
|
29
|
+
# Attempts to process a single response from the buffer,
|
|
30
|
+
# advancing the offset past the consumed bytes.
|
|
24
31
|
def process_single_getk_response
|
|
25
|
-
bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer)
|
|
26
|
-
|
|
32
|
+
bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer, @offset)
|
|
33
|
+
@offset += bytes
|
|
34
|
+
compact_if_needed
|
|
27
35
|
[status, cas, key, value]
|
|
28
36
|
end
|
|
29
37
|
|
|
30
|
-
# Advances the internal response buffer by bytes_to_advance
|
|
31
|
-
# bytes. The
|
|
32
|
-
def advance(bytes_to_advance)
|
|
33
|
-
return unless bytes_to_advance.positive?
|
|
34
|
-
|
|
35
|
-
@buffer = @buffer.byteslice(bytes_to_advance..-1)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
38
|
# Resets the internal buffer to an empty state,
|
|
39
39
|
# so that we're ready to read pipelined responses
|
|
40
40
|
def reset
|
|
41
41
|
@buffer = ''.b
|
|
42
|
+
@offset = 0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Ensures the buffer is initialized for reading without discarding
|
|
46
|
+
# existing data. Used by interleaved pipelined get which may have
|
|
47
|
+
# already buffered partial responses during the send phase.
|
|
48
|
+
def ensure_ready
|
|
49
|
+
return if in_progress?
|
|
50
|
+
|
|
51
|
+
@buffer = ''.b
|
|
52
|
+
@offset = 0
|
|
42
53
|
end
|
|
43
54
|
|
|
44
55
|
# Clear the internal response buffer
|
|
45
56
|
def clear
|
|
46
57
|
@buffer = nil
|
|
58
|
+
@offset = 0
|
|
47
59
|
end
|
|
48
60
|
|
|
49
61
|
def in_progress?
|
|
50
62
|
!@buffer.nil?
|
|
51
63
|
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
|
|
52
76
|
end
|
|
53
77
|
end
|
|
54
78
|
end
|