dalli 5.2.1 → 5.2.3
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 +81 -0
- data/README.md +53 -0
- data/lib/dalli/client.rb +104 -36
- data/lib/dalli/compressor.rb +5 -0
- data/lib/dalli/key_manager.rb +6 -3
- data/lib/dalli/pipelined_getter.rb +5 -2
- data/lib/dalli/pipelined_setter.rb +9 -0
- data/lib/dalli/protocol/base.rb +74 -6
- data/lib/dalli/protocol/connection_manager.rb +35 -4
- data/lib/dalli/protocol/meta.rb +75 -15
- data/lib/dalli/protocol/response_buffer.rb +4 -1
- data/lib/dalli/protocol/response_processor.rb +45 -18
- data/lib/dalli/protocol/server_config_parser.rb +10 -5
- data/lib/dalli/protocol/string_marshaller.rb +3 -5
- data/lib/dalli/protocol/value_compressor.rb +6 -4
- data/lib/dalli/protocol/value_marshaller.rb +21 -5
- data/lib/dalli/protocol.rb +3 -3
- data/lib/dalli/socket.rb +40 -3
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +4 -0
- data/lib/rack/session/dalli.rb +37 -10
- metadata +2 -2
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -43,6 +43,7 @@ module Dalli
|
|
|
43
43
|
@options = client_options.merge(user_creds)
|
|
44
44
|
@raw_mode = client_options[:raw]
|
|
45
45
|
@defer_drain = client_options[:defer_drain] ? true : false
|
|
46
|
+
@verify_reply_keys = client_options[:verify_reply_keys] ? true : false
|
|
46
47
|
@value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
47
48
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
48
49
|
end
|
|
@@ -79,8 +80,14 @@ module Dalli
|
|
|
79
80
|
|
|
80
81
|
request_completed = true
|
|
81
82
|
response
|
|
82
|
-
rescue Dalli::MarshalError => e
|
|
83
|
-
log_marshal_err(args.first, e)
|
|
83
|
+
rescue Dalli::MarshalError, Dalli::ValueOverMaxSize => e
|
|
84
|
+
log_marshal_err(args.first, e) if e.is_a?(Dalli::MarshalError)
|
|
85
|
+
# Raised only while the connection is clean: a value is marshalled
|
|
86
|
+
# before any of its request is written, and write_multi_req raises
|
|
87
|
+
# only after reading every reply. Closing the connection would
|
|
88
|
+
# discard earlier quiet writes still waiting in the write buffer.
|
|
89
|
+
@connection_manager.abort_request!
|
|
90
|
+
request_completed = true
|
|
84
91
|
raise
|
|
85
92
|
rescue Dalli::DalliError
|
|
86
93
|
raise
|
|
@@ -153,6 +160,8 @@ module Dalli
|
|
|
153
160
|
# avoiding intermediate Hash allocation. Returns nil.
|
|
154
161
|
# Without a block, returns a Hash of { key => [value, cas] }.
|
|
155
162
|
def pipeline_next_responses(&block)
|
|
163
|
+
return abandon_pipeline if @pipeline_out_of_step
|
|
164
|
+
|
|
156
165
|
reconnect_on_pipeline_complete!
|
|
157
166
|
values = nil
|
|
158
167
|
|
|
@@ -174,6 +183,13 @@ module Dalli
|
|
|
174
183
|
# to the noop at the end of the pipeline
|
|
175
184
|
finish_pipeline && break if status && key.nil?
|
|
176
185
|
|
|
186
|
+
# A key that wasn't requested means the replies are out of step
|
|
187
|
+
# (see expect_reply_keys): keep what was read so far, drop the rest
|
|
188
|
+
if unexpected_reply_key?(key)
|
|
189
|
+
abandon_pipeline
|
|
190
|
+
break
|
|
191
|
+
end
|
|
192
|
+
|
|
177
193
|
# If the status is ok and the key is not nil, then this is a
|
|
178
194
|
# getkq response with a value that we want to set in the response hash
|
|
179
195
|
unless key.nil?
|
|
@@ -203,10 +219,13 @@ module Dalli
|
|
|
203
219
|
@connection_manager.abort_request!
|
|
204
220
|
return true unless connected?
|
|
205
221
|
|
|
206
|
-
# Closes the connection, which ensures that our connection
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
222
|
+
# Closes the connection, which ensures that our connection is in a
|
|
223
|
+
# clean state for future requests. The get_multi ran out of time
|
|
224
|
+
# overall rather than hitting a socket error, so it doesn't count
|
|
225
|
+
# toward socket_max_failures: two slow get_multis in a row would
|
|
226
|
+
# otherwise mark a healthy server down.
|
|
227
|
+
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
228
|
+
@connection_manager.close
|
|
210
229
|
true
|
|
211
230
|
end
|
|
212
231
|
|
|
@@ -342,6 +361,8 @@ module Dalli
|
|
|
342
361
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
343
362
|
# the expected state before pipeline_response_setup is called.
|
|
344
363
|
response_buffer.clear
|
|
364
|
+
response_buffer.raw = raw_request?(options)
|
|
365
|
+
expect_reply_keys(keys)
|
|
345
366
|
|
|
346
367
|
# The terminating noop is sent by pipeline_response_setup
|
|
347
368
|
write(quiet_get_requests(keys, options, return_cas: return_cas))
|
|
@@ -353,6 +374,8 @@ module Dalli
|
|
|
353
374
|
def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
|
|
354
375
|
# Initialize the response buffer for draining during send phase
|
|
355
376
|
response_buffer.ensure_ready
|
|
377
|
+
response_buffer.raw = raw_request?(options)
|
|
378
|
+
expect_reply_keys(keys)
|
|
356
379
|
|
|
357
380
|
keys.each_slice(chunk_size) do |chunk|
|
|
358
381
|
# Build and write this chunk of requests
|
|
@@ -385,6 +408,12 @@ module Dalli
|
|
|
385
408
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
386
409
|
break if status.nil? # No complete response available
|
|
387
410
|
|
|
411
|
+
# Handled by the next pipeline_next_responses, once sending is done
|
|
412
|
+
if unexpected_reply_key?(key)
|
|
413
|
+
@pipeline_out_of_step = true
|
|
414
|
+
break
|
|
415
|
+
end
|
|
416
|
+
|
|
388
417
|
results[key] = [value, cas] unless key.nil?
|
|
389
418
|
end
|
|
390
419
|
rescue SystemCallError, Dalli::NetworkError
|
|
@@ -396,6 +425,42 @@ module Dalli
|
|
|
396
425
|
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
397
426
|
end
|
|
398
427
|
|
|
428
|
+
# A multi-get only accepts replies for the keys it asked for. memcached
|
|
429
|
+
# names an item created by auto-create (ma or mg with N) for a
|
|
430
|
+
# base64-encoded key by its raw bytes, without the b flag; such a key
|
|
431
|
+
# can contain spaces or CR/LF, which split the reply header and make the
|
|
432
|
+
# replies after it parse as different ones. A key that wasn't requested
|
|
433
|
+
# is how that shows up.
|
|
434
|
+
# Only with the verify_reply_keys option, since building the set costs a
|
|
435
|
+
# multi-get about 15%. Dalli itself no longer creates such items (see
|
|
436
|
+
# Meta#decr_incr); the check guards against ones that older versions or
|
|
437
|
+
# other clients created, on memcached versions with the behavior.
|
|
438
|
+
def expect_reply_keys(keys)
|
|
439
|
+
@pipeline_out_of_step = false
|
|
440
|
+
@expected_reply_keys = (keys.to_set { |k| reply_key_form(k) } if @verify_reply_keys)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def unexpected_reply_key?(key)
|
|
444
|
+
!key.nil? && @expected_reply_keys && !@expected_reply_keys.include?(reply_key_form(key))
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Keys compared as bytes; ASCII keys compare equal in any encoding as is
|
|
448
|
+
def reply_key_form(key)
|
|
449
|
+
key.ascii_only? ? key : key.b
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Keeps what was read, drops the rest of this server's replies, and
|
|
453
|
+
# closes the connection so nothing unread is left on it. Not a socket
|
|
454
|
+
# failure, so it doesn't count toward socket_max_failures; the keys not
|
|
455
|
+
# read are simply missing from the result.
|
|
456
|
+
def abandon_pipeline
|
|
457
|
+
Dalli.logger.warn { "#{name} replied to a get_multi with a key that wasn't requested; closing the connection" }
|
|
458
|
+
@pipeline_out_of_step = false
|
|
459
|
+
response_buffer.clear
|
|
460
|
+
@connection_manager.close
|
|
461
|
+
{}
|
|
462
|
+
end
|
|
463
|
+
|
|
399
464
|
# Called after the noop response is received at the end of a set
|
|
400
465
|
# of pipelined gets
|
|
401
466
|
def finish_pipeline
|
|
@@ -409,7 +474,10 @@ module Dalli
|
|
|
409
474
|
@connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
|
|
410
475
|
end
|
|
411
476
|
|
|
477
|
+
# The single-server set_multi path passes its whole pairs hash as the
|
|
478
|
+
# first argument; log only its keys, never the values
|
|
412
479
|
def log_marshal_err(key, err)
|
|
480
|
+
key = key.keys.join("', '") if key.is_a?(Hash)
|
|
413
481
|
Dalli.logger.error "Marshalling error for key '#{key}': #{err.message}"
|
|
414
482
|
Dalli.logger.error 'You are trying to cache a Ruby object which cannot be serialized to memcached.'
|
|
415
483
|
end
|
|
@@ -60,8 +60,8 @@ module Dalli
|
|
|
60
60
|
@pid = Process.pid
|
|
61
61
|
@request_in_progress = false
|
|
62
62
|
@quiet_responses_pending = false
|
|
63
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
|
|
64
|
-
# SocketError = DNS resolution failure
|
|
63
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError, SocketError => e
|
|
64
|
+
# SocketError = DNS resolution failure; SSL_ERRORS = failed TLS handshake
|
|
65
65
|
error_on_request!(e)
|
|
66
66
|
end
|
|
67
67
|
|
|
@@ -188,10 +188,22 @@ module Dalli
|
|
|
188
188
|
@request_in_progress = false
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
+
# The longest reply line Dalli reads, in bytes, CRLF included. A meta
|
|
192
|
+
# reply header is at most about 500 bytes: "VA <size>" plus its flags,
|
|
193
|
+
# the largest being a 250-byte key base64-encoded to 336 bytes, a
|
|
194
|
+
# 20-digit CAS, a 32-byte opaque and a few numeric fields. The longest
|
|
195
|
+
# stats line (stats settings) carries a file path such as ext_path, up
|
|
196
|
+
# to PATH_MAX (4096). 8 KiB leaves room for both, while a server that
|
|
197
|
+
# never sends CRLF can't grow the read buffer without bound.
|
|
198
|
+
MAX_LINE_BYTES = 8 * 1024
|
|
199
|
+
|
|
191
200
|
def read_line
|
|
192
201
|
flush_write_buffer
|
|
193
|
-
data = @sock.gets("\r\n")
|
|
202
|
+
data = @sock.gets("\r\n", MAX_LINE_BYTES)
|
|
194
203
|
error_on_request!('EOF in read_line') if data.nil?
|
|
204
|
+
# Without its CRLF the line either ran past MAX_LINE_BYTES or was cut
|
|
205
|
+
# short by EOF; either way the connection is out of step
|
|
206
|
+
error_on_request!("Reply line without CRLF in the first #{MAX_LINE_BYTES} bytes") unless data.end_with?("\r\n")
|
|
195
207
|
data
|
|
196
208
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
197
209
|
error_on_request!(e)
|
|
@@ -352,13 +364,32 @@ module Dalli
|
|
|
352
364
|
|
|
353
365
|
private
|
|
354
366
|
|
|
367
|
+
# One write per flush. A TLS socket sends each argument of
|
|
368
|
+
# write(*parts) as its own record and system call, so the parts are
|
|
369
|
+
# joined first.
|
|
355
370
|
def flush_write_buffer
|
|
356
371
|
return if @write_buffer.empty?
|
|
357
372
|
|
|
358
|
-
@sock.write(
|
|
373
|
+
@sock.write(@write_buffer.size == 1 ? @write_buffer.first : joined_write_buffer)
|
|
359
374
|
discard_write_buffer
|
|
360
375
|
end
|
|
361
376
|
|
|
377
|
+
# Joined as bytes: requests can mix UTF-8 and binary strings, which
|
|
378
|
+
# String#<< refuses to combine when both hold non-ASCII bytes
|
|
379
|
+
if String.method_defined?(:append_as_bytes)
|
|
380
|
+
def joined_write_buffer
|
|
381
|
+
out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
|
|
382
|
+
@write_buffer.each { |part| out.append_as_bytes(part) }
|
|
383
|
+
out
|
|
384
|
+
end
|
|
385
|
+
else
|
|
386
|
+
def joined_write_buffer
|
|
387
|
+
out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
|
|
388
|
+
@write_buffer.each { |part| out << part.b }
|
|
389
|
+
out
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
362
393
|
def discard_write_buffer
|
|
363
394
|
@write_buffer.clear
|
|
364
395
|
@write_buffer_bytes = 0
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Dalli
|
|
|
38
38
|
RequestFormatter.plain_meta_get(key, skip_flags)
|
|
39
39
|
end
|
|
40
40
|
flushed_write(req)
|
|
41
|
-
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
41
|
+
response_processor.meta_get_with_value(cache_nils: cache_nils?(options), raw: raw_request?(options))
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def quiet_get_request(key, options = nil)
|
|
@@ -59,7 +59,7 @@ module Dalli
|
|
|
59
59
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
60
60
|
req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags, **routing_token_kwargs(options))
|
|
61
61
|
flushed_write(req)
|
|
62
|
-
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
62
|
+
response_processor.meta_get_with_value(cache_nils: cache_nils?(options), raw: raw_request?(options))
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def touch(key, ttl)
|
|
@@ -75,7 +75,7 @@ module Dalli
|
|
|
75
75
|
req = RequestFormatter.meta_get(key: key, value: true, return_cas: true, skip_flags: raw_request?(options),
|
|
76
76
|
**routing_token_kwargs(options))
|
|
77
77
|
flushed_write(req)
|
|
78
|
-
response_processor.meta_get_with_value_and_cas
|
|
78
|
+
response_processor.meta_get_with_value_and_cas(raw: raw_request?(options))
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# Comprehensive meta get with support for all metadata flags.
|
|
@@ -116,7 +116,7 @@ module Dalli
|
|
|
116
116
|
response_processor.meta_get_with_metadata(
|
|
117
117
|
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
118
118
|
return_last_access: options[:return_last_access],
|
|
119
|
-
return_ttl_remaining: options[:return_ttl_remaining]
|
|
119
|
+
return_ttl_remaining: options[:return_ttl_remaining], raw: raw_request?(options)
|
|
120
120
|
)
|
|
121
121
|
end
|
|
122
122
|
|
|
@@ -234,12 +234,45 @@ module Dalli
|
|
|
234
234
|
# rubocop:disable-next Metrics/ParameterLists
|
|
235
235
|
def decr_incr(incr, key, delta, ttl, initial, options = nil)
|
|
236
236
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
237
|
+
# memcached stores an item created by ma's auto-create (N/J) for a
|
|
238
|
+
# base64-encoded key without its binary-key flag, and later replies
|
|
239
|
+
# name it with the raw key bytes. Create such counters with an add.
|
|
240
|
+
return decr_incr_with_add(incr, key, delta, ttl, initial, options) if initial && KeyRegularizer.required?(key)
|
|
241
|
+
|
|
237
242
|
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
238
243
|
quiet: quiet?, **routing_token_kwargs(options)))
|
|
239
244
|
finish_write(quiet?)
|
|
240
245
|
response_processor.decr_incr unless quiet?
|
|
241
246
|
end
|
|
242
247
|
|
|
248
|
+
# Like decr_incr with an initial value, without memcached's auto-create:
|
|
249
|
+
# on a miss the counter is added with the initial value (which is
|
|
250
|
+
# returned, as auto-create would), and the add losing a race to another
|
|
251
|
+
# client means the counter now exists, so the incr is tried again. In a
|
|
252
|
+
# quiet block both requests are sent and whichever applies takes effect.
|
|
253
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
254
|
+
def decr_incr_with_add(incr, key, delta, ttl, initial, options)
|
|
255
|
+
add_options = (options || {}).merge(raw: true) # counters are stored raw
|
|
256
|
+
return quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options) if quiet?
|
|
257
|
+
|
|
258
|
+
2.times do
|
|
259
|
+
value = decr_incr(incr, key, delta, nil, nil, options)
|
|
260
|
+
return value unless value.nil?
|
|
261
|
+
|
|
262
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: false)
|
|
263
|
+
return initial if response_processor.meta_set_with_cas
|
|
264
|
+
end
|
|
265
|
+
decr_incr(incr, key, delta, nil, nil, options)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
269
|
+
def quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options)
|
|
270
|
+
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: nil, incr: incr, ttl: nil,
|
|
271
|
+
quiet: true, **routing_token_kwargs(add_options)))
|
|
272
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: true)
|
|
273
|
+
finish_write(true)
|
|
274
|
+
end
|
|
275
|
+
|
|
243
276
|
# Other Commands
|
|
244
277
|
def flush(delay = 0)
|
|
245
278
|
write(RequestFormatter.flush(delay: delay))
|
|
@@ -293,7 +326,7 @@ module Dalli
|
|
|
293
326
|
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw, **routing_token_kwargs(options))
|
|
294
327
|
flushed_write(buffer)
|
|
295
328
|
buffer.clear
|
|
296
|
-
read_multi_get_responses(is_raw)
|
|
329
|
+
read_multi_get_responses(is_raw, keys)
|
|
297
330
|
end
|
|
298
331
|
|
|
299
332
|
# Stale-aware bulk get. Returns { key => { value:, cas:, stale:, miss: } }
|
|
@@ -310,47 +343,63 @@ module Dalli
|
|
|
310
343
|
**routing_token_kwargs(options))
|
|
311
344
|
flushed_write(buffer)
|
|
312
345
|
buffer.clear
|
|
313
|
-
read_multi_metadata_responses(is_raw)
|
|
346
|
+
read_multi_metadata_responses(is_raw, keys)
|
|
314
347
|
end
|
|
315
348
|
|
|
316
349
|
# Unlike read_multi_get_responses this locates tokens by flag rather than
|
|
317
350
|
# by position, because the c flag shifts the key's index.
|
|
318
|
-
def read_multi_metadata_responses(is_raw)
|
|
351
|
+
def read_multi_metadata_responses(is_raw, keys)
|
|
352
|
+
expect_reply_keys(keys)
|
|
319
353
|
hash = {}
|
|
320
354
|
while (line = @connection_manager.read_line)
|
|
321
355
|
break if line.start_with?('MN')
|
|
322
356
|
next unless line.start_with?('VA ')
|
|
323
357
|
|
|
324
358
|
tokens = line.chomp!(TERMINATOR).split
|
|
325
|
-
|
|
359
|
+
size = tokens[1].to_i
|
|
360
|
+
response_processor.check_value_size!(size)
|
|
361
|
+
value = @connection_manager.read(size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
326
362
|
stale = response_processor.stale_from_tokens(tokens)
|
|
327
363
|
cas = response_processor.cas_from_tokens(tokens)
|
|
328
364
|
bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
|
|
329
365
|
key = response_processor.key_from_tokens(tokens)
|
|
330
366
|
next if key.nil?
|
|
331
367
|
|
|
368
|
+
raise_unexpected_reply_key(key) if unexpected_reply_key?(key)
|
|
369
|
+
|
|
332
370
|
hash[key] = { value: @value_marshaller.retrieve(value, bitflags), cas: cas, stale: stale, miss: false }
|
|
333
371
|
end
|
|
334
372
|
hash
|
|
335
373
|
end
|
|
336
374
|
|
|
337
|
-
def read_multi_get_responses(is_raw)
|
|
375
|
+
def read_multi_get_responses(is_raw, keys)
|
|
376
|
+
expect_reply_keys(keys)
|
|
338
377
|
hash = {}
|
|
339
378
|
while (line = @connection_manager.read_line)
|
|
340
379
|
break if line.start_with?('MN')
|
|
341
380
|
next unless line.start_with?('VA ')
|
|
342
381
|
|
|
343
382
|
key, value = parse_multi_get_value(line, is_raw)
|
|
383
|
+
raise_unexpected_reply_key(key) if unexpected_reply_key?(key)
|
|
344
384
|
hash[key] = value if key
|
|
345
385
|
end
|
|
346
386
|
hash
|
|
347
387
|
end
|
|
348
388
|
|
|
389
|
+
# The replies after a key that wasn't requested can't be trusted (see
|
|
390
|
+
# Base#expect_reply_keys). Raising ends the request, which closes the
|
|
391
|
+
# connection; the caller treats this server's keys as missing.
|
|
392
|
+
def raise_unexpected_reply_key(key)
|
|
393
|
+
raise Dalli::UnexpectedReplyKeyError, "#{name} replied with a key that wasn't requested: #{key.inspect}"
|
|
394
|
+
end
|
|
395
|
+
|
|
349
396
|
# Reads the "VA <size> [f<flags>] k<key> ..." line in place rather than
|
|
350
397
|
# splitting it into tokens
|
|
351
398
|
def parse_multi_get_value(line, is_raw)
|
|
352
399
|
processor = response_processor
|
|
353
|
-
|
|
400
|
+
size = processor.size_from_va_line(line)
|
|
401
|
+
processor.check_value_size!(size)
|
|
402
|
+
value = @connection_manager.read(size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
354
403
|
key = processor.key_from_va_line(line)
|
|
355
404
|
return unless key
|
|
356
405
|
|
|
@@ -360,16 +409,27 @@ module Dalli
|
|
|
360
409
|
|
|
361
410
|
# Single-server fast path for set_multi. Inlines request formatting to
|
|
362
411
|
# minimize per-key overhead. Avoids PipelinedSetter server grouping.
|
|
412
|
+
#
|
|
413
|
+
# A value that can't be marshalled or is over value_max_bytes is skipped;
|
|
414
|
+
# the other keys are still stored, then the first such error is raised
|
|
415
|
+
# (as PipelinedSetter does on the multi-server path).
|
|
363
416
|
def write_multi_req(pairs, ttl, req_options)
|
|
364
417
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
365
|
-
|
|
418
|
+
value_error = nil
|
|
419
|
+
entries = pairs.filter_map do |key, raw_value|
|
|
366
420
|
[key, @value_marshaller.store(key, raw_value, req_options)]
|
|
421
|
+
rescue Dalli::MarshalError, Dalli::ValueOverMaxSize => e
|
|
422
|
+
value_error ||= e
|
|
423
|
+
nil
|
|
367
424
|
end
|
|
368
425
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
426
|
+
unless entries.empty?
|
|
427
|
+
buffer = RequestFormatter.multi_meta_set(entries, ttl: ttl, **routing_token_kwargs(req_options))
|
|
428
|
+
flushed_write(buffer)
|
|
429
|
+
buffer.clear
|
|
430
|
+
response_processor.consume_all_responses_until_mn
|
|
431
|
+
end
|
|
432
|
+
raise value_error if value_error
|
|
373
433
|
end
|
|
374
434
|
|
|
375
435
|
# Single-server fast path for delete_multi. Writes all quiet delete requests
|
|
@@ -15,6 +15,9 @@ module Dalli
|
|
|
15
15
|
# when advancing through parsed responses.
|
|
16
16
|
##
|
|
17
17
|
class ResponseBuffer
|
|
18
|
+
# Whether the current pipeline was requested with raw: true
|
|
19
|
+
attr_writer :raw
|
|
20
|
+
|
|
18
21
|
def initialize(io_source, response_processor)
|
|
19
22
|
@io_source = io_source
|
|
20
23
|
@response_processor = response_processor
|
|
@@ -39,7 +42,7 @@ module Dalli
|
|
|
39
42
|
# Attempts to process a single response from the buffer,
|
|
40
43
|
# advancing the offset past the consumed bytes.
|
|
41
44
|
def process_single_getk_response
|
|
42
|
-
response = @response_processor.getk_response_from_buffer(@buffer, @offset)
|
|
45
|
+
response = @response_processor.getk_response_from_buffer(@buffer, @offset, raw: @raw || false)
|
|
43
46
|
@offset += response.pop
|
|
44
47
|
response
|
|
45
48
|
end
|
|
@@ -51,12 +51,15 @@ module Dalli
|
|
|
51
51
|
@value_marshaller = value_marshaller
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
# raw: true (a request made with raw: true) returns the value as stored,
|
|
55
|
+
# ignoring any flags in the reply. The request didn't ask for flags, so
|
|
56
|
+
# a reply carrying them anyway must not get the value deserialized.
|
|
57
|
+
def meta_get_with_value(cache_nils: false, raw: false)
|
|
55
58
|
line = read_line
|
|
56
59
|
# A hit ("VA <size> f<flags>") is parsed in place rather than split
|
|
57
60
|
# into tokens, which saves several allocations on the hottest path.
|
|
58
61
|
if line&.start_with?(VA_PREFIX)
|
|
59
|
-
return
|
|
62
|
+
return retrieve(read_data(size_from_va_line(line)), raw ? 0 : bitflags_from_va_line(line), raw)
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
tokens = line&.split || []
|
|
@@ -64,7 +67,7 @@ module Dalli
|
|
|
64
67
|
when EN
|
|
65
68
|
cache_nils ? ::Dalli::NOT_FOUND : nil
|
|
66
69
|
when VA # only reached for an unusually formatted hit line
|
|
67
|
-
|
|
70
|
+
retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
|
|
68
71
|
when HD
|
|
69
72
|
true
|
|
70
73
|
else
|
|
@@ -72,14 +75,14 @@ module Dalli
|
|
|
72
75
|
end
|
|
73
76
|
end
|
|
74
77
|
|
|
75
|
-
def meta_get_with_value_and_cas
|
|
78
|
+
def meta_get_with_value_and_cas(raw: false)
|
|
76
79
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
77
80
|
return [nil, 0] if tokens.first == EN
|
|
78
81
|
|
|
79
82
|
cas = cas_from_tokens(tokens)
|
|
80
83
|
return [nil, cas] unless tokens.first == VA
|
|
81
84
|
|
|
82
|
-
[
|
|
85
|
+
[retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw), cas]
|
|
83
86
|
end
|
|
84
87
|
|
|
85
88
|
def meta_get_without_value
|
|
@@ -99,13 +102,13 @@ module Dalli
|
|
|
99
102
|
# Used by meta_get for comprehensive metadata retrieval.
|
|
100
103
|
# Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
|
|
101
104
|
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
|
|
102
|
-
return_ttl_remaining: false)
|
|
105
|
+
return_ttl_remaining: false, raw: false)
|
|
103
106
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
104
107
|
result = build_metadata_result(tokens)
|
|
105
108
|
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
106
109
|
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
107
110
|
result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
|
|
108
|
-
result[:value] = parse_value_from_tokens(tokens, cache_nils)
|
|
111
|
+
result[:value] = parse_value_from_tokens(tokens, cache_nils, raw)
|
|
109
112
|
result
|
|
110
113
|
end
|
|
111
114
|
|
|
@@ -122,11 +125,11 @@ module Dalli
|
|
|
122
125
|
}
|
|
123
126
|
end
|
|
124
127
|
|
|
125
|
-
def parse_value_from_tokens(tokens, cache_nils)
|
|
128
|
+
def parse_value_from_tokens(tokens, cache_nils, raw = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
126
129
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
127
130
|
return unless tokens.first == VA
|
|
128
131
|
|
|
129
|
-
|
|
132
|
+
retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
|
|
130
133
|
end
|
|
131
134
|
|
|
132
135
|
def meta_set_with_cas
|
|
@@ -229,13 +232,15 @@ module Dalli
|
|
|
229
232
|
# The remaining three values in the array are the ResponseHeader,
|
|
230
233
|
# key, and value.
|
|
231
234
|
##
|
|
232
|
-
|
|
235
|
+
# raw: true returns values as stored, ignoring flags in the replies;
|
|
236
|
+
# see meta_get_with_value.
|
|
237
|
+
def getk_response_from_buffer(buf, offset = 0, raw: false)
|
|
233
238
|
# Find the header terminator starting from offset
|
|
234
239
|
term_idx = buf.byteindex(TERMINATOR, offset)
|
|
235
240
|
return [0] unless term_idx
|
|
236
241
|
|
|
237
242
|
if buf.byteslice(offset, VA_PREFIX.bytesize) == VA_PREFIX
|
|
238
|
-
response = va_response_from_buffer(buf, offset, term_idx)
|
|
243
|
+
response = va_response_from_buffer(buf, offset, term_idx, raw)
|
|
239
244
|
return response if response
|
|
240
245
|
end
|
|
241
246
|
|
|
@@ -256,7 +261,9 @@ module Dalli
|
|
|
256
261
|
when BYTE_B then base64 ||= token == 'b'
|
|
257
262
|
end
|
|
258
263
|
end
|
|
259
|
-
|
|
264
|
+
# memcached also gives the size positionally ("VA <size> ..."); use it
|
|
265
|
+
# when there's no s flag rather than reading the value as replies
|
|
266
|
+
body_len = size ? flag_int(size, 's') : positional_size(tokens)
|
|
260
267
|
|
|
261
268
|
# A complete response with no body. Only the terminating noop's MN
|
|
262
269
|
# ends the pipeline. Any other bodyless reply (CLIENT_ERROR,
|
|
@@ -276,8 +283,12 @@ module Dalli
|
|
|
276
283
|
# The full response is in our buffer, so parse it and return
|
|
277
284
|
# the values
|
|
278
285
|
body = buf.byteslice(offset + header_len, body_len)
|
|
279
|
-
value =
|
|
280
|
-
|
|
286
|
+
value = retrieve(body, raw ? 0 : flag_int(bitflags, 'f'), raw)
|
|
287
|
+
# A hit with no key can't be matched to a request: skip it (false
|
|
288
|
+
# status) rather than yield it, or take it for the end of the pipeline
|
|
289
|
+
return [false, resp_size] unless key
|
|
290
|
+
|
|
291
|
+
key = key.delete_prefix!('k')
|
|
281
292
|
key = KeyRegularizer.decode(key) if base64
|
|
282
293
|
[tokens.first == VA, flag_int(cas, 'c'), key, value, resp_size]
|
|
283
294
|
end
|
|
@@ -286,7 +297,7 @@ module Dalli
|
|
|
286
297
|
# without allocating the header, its tokens or the flag strings.
|
|
287
298
|
# Gives the same result as the token path. Returns nil to fall back to
|
|
288
299
|
# it when the header has no s flag or a zero size.
|
|
289
|
-
def va_response_from_buffer(buf, offset, term_idx)
|
|
300
|
+
def va_response_from_buffer(buf, offset, term_idx, raw = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
290
301
|
size = bitflags = cas = key = nil
|
|
291
302
|
base64 = false
|
|
292
303
|
pos = buf.byteindex(' ', offset + VA_PREFIX.bytesize)
|
|
@@ -309,9 +320,17 @@ module Dalli
|
|
|
309
320
|
resp_size = header_len + size + TERMINATOR.length
|
|
310
321
|
return [0] unless buf.bytesize >= offset + resp_size
|
|
311
322
|
|
|
312
|
-
value =
|
|
313
|
-
|
|
314
|
-
[
|
|
323
|
+
value = retrieve(buf.byteslice(offset + header_len, size), raw ? 0 : bitflags || 0, raw)
|
|
324
|
+
# Skipped like the token path does; see getk_response_from_buffer
|
|
325
|
+
return [false, resp_size] unless key
|
|
326
|
+
|
|
327
|
+
key = KeyRegularizer.decode(key) if base64
|
|
328
|
+
[true, cas || 0, key, value, resp_size]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Values from a raw request are returned as stored
|
|
332
|
+
def retrieve(value, bitflags, raw)
|
|
333
|
+
raw ? value : @value_marshaller.retrieve(value, bitflags)
|
|
315
334
|
end
|
|
316
335
|
|
|
317
336
|
# Integer value of a flag's token in buf, after its one-byte prefix,
|
|
@@ -431,6 +450,11 @@ module Dalli
|
|
|
431
450
|
|
|
432
451
|
# A pipelined reply claiming an impossible size would otherwise have the
|
|
433
452
|
# buffer wait for (and accumulate) that many bytes
|
|
453
|
+
# The size given right after "VA", or 0 when it isn't a number
|
|
454
|
+
def positional_size(tokens)
|
|
455
|
+
tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
|
|
456
|
+
end
|
|
457
|
+
|
|
434
458
|
def check_value_size!(size)
|
|
435
459
|
return if size.between?(0, MAX_VALUE_BYTES)
|
|
436
460
|
|
|
@@ -447,6 +471,9 @@ module Dalli
|
|
|
447
471
|
end
|
|
448
472
|
|
|
449
473
|
def read_data(data_size)
|
|
474
|
+
# Checked before the terminator is added: -1 or -2 would otherwise
|
|
475
|
+
# read 1 or 0 bytes and leave the connection out of step
|
|
476
|
+
check_value_size!(data_size)
|
|
450
477
|
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
451
478
|
end
|
|
452
479
|
end
|
|
@@ -33,7 +33,7 @@ module Dalli
|
|
|
33
33
|
username: uri.user,
|
|
34
34
|
password: uri.password
|
|
35
35
|
}
|
|
36
|
-
[uri.
|
|
36
|
+
[uri.hostname, normalize_port(uri.port), :tcp, DEFAULT_WEIGHT, auth_details]
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def self.parse_non_uri(str)
|
|
@@ -61,11 +61,11 @@ module Dalli
|
|
|
61
61
|
# in case of unix socket, allow only setting of weight, not port
|
|
62
62
|
raise Dalli::DalliError, "Could not parse hostname #{res[0]}" if res[4]
|
|
63
63
|
|
|
64
|
-
[nil, normalize_weight(res[3])]
|
|
64
|
+
[nil, normalize_weight(res[3], res[0])]
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def self.attributes_for_tcp_socket(res)
|
|
68
|
-
[normalize_port(res[3]), normalize_weight(res[4])]
|
|
68
|
+
[normalize_port(res[3]), normalize_weight(res[4], res[0])]
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def self.normalize_host_from_match(str, res)
|
|
@@ -78,8 +78,13 @@ module Dalli
|
|
|
78
78
|
Integer(port || DEFAULT_PORT)
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
# A weight below 1 gives the server no points on the ring, and if every
|
|
82
|
+
# server has one the ring divides by a zero total weight.
|
|
83
|
+
def self.normalize_weight(weight, str)
|
|
84
|
+
value = Integer(weight || DEFAULT_WEIGHT)
|
|
85
|
+
raise ArgumentError, "Server weight must be at least 1, got #{value} in #{str}" if value < 1
|
|
86
|
+
|
|
87
|
+
value
|
|
83
88
|
end
|
|
84
89
|
end
|
|
85
90
|
end
|
|
@@ -10,7 +10,8 @@ module Dalli
|
|
|
10
10
|
##
|
|
11
11
|
class StringMarshaller
|
|
12
12
|
DEFAULTS = {
|
|
13
|
-
# max
|
|
13
|
+
# max memcached item size in bytes: value + key + ITEM_OVERHEAD_BYTES. Set it to
|
|
14
|
+
# memcached's -I value (default 1 MB).
|
|
14
15
|
value_max_bytes: 1024 * 1024
|
|
15
16
|
}.freeze
|
|
16
17
|
|
|
@@ -55,10 +56,7 @@ module Dalli
|
|
|
55
56
|
private
|
|
56
57
|
|
|
57
58
|
def error_if_over_max_value_bytes(key, value)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
message = "Value for #{key} over max size: #{value_max_bytes} <= #{value.bytesize}"
|
|
61
|
-
raise Dalli::ValueOverMaxSize, message
|
|
59
|
+
ValueMarshaller.error_if_over_max_value_bytes(key, value, value_max_bytes)
|
|
62
60
|
end
|
|
63
61
|
end
|
|
64
62
|
end
|