dalli 5.1.4 → 5.1.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 +11 -0
- data/README.md +4 -0
- data/lib/dalli/client.rb +13 -0
- data/lib/dalli/protocol/base.rb +54 -0
- data/lib/dalli/protocol/meta.rb +49 -4
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd6ca01dc58cadc97706b152f76a3bff1bb94a4a4727743cddac5870b6120332
|
|
4
|
+
data.tar.gz: ef6123de8d8267c5ded812f05fda7d4a84cb1b56c116afd3fe8bd00365881175
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16082586633b6f84d1486e82a603b4d7fdc1afb586bf67c0a9f6ede7d9b426a87ec0b2b59eef019c471568ad5f0a8bd081ddb164c08d608387c9093d3dba25d7
|
|
7
|
+
data.tar.gz: 15788269cf8910b1bee979b7859f57279f9723f57407c6c6b4dac79a60fe454d00e896e7514dc93de7a90366ce9b071d4308fabbad7e41f3f1b71593e376a6b2
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.1.5
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
Security:
|
|
11
|
+
|
|
12
|
+
- Don't rely on memcached's auto-create for keys that need base64 encoding (GHSA-r47x-6p6r-mvcp)
|
|
13
|
+
- memcached stores an item made by auto-create (`ma` with `N`/`J`, or `mg` with `N`) for a base64-encoded key without its binary-key flag, and later replies name the item by its raw key bytes. A key containing spaces or CR/LF could then make a multi-get read the replies after it as different ones
|
|
14
|
+
- `incr` and `decr` with an initial value now create such counters with an add instead. Return values are unchanged
|
|
15
|
+
- The new `verify_reply_keys` option makes `get_multi`, `get_multi_cas` and `get_multi_with_metadata` ignore replies for keys they didn't request, which guards against items created by older Dalli versions or other clients until memcached is upgraded. It's off by default, since it costs a multi-get about 15%
|
|
16
|
+
- Affects the meta protocol since 3.2.0; fixed in 5.2.3, 5.1.5, 5.0.11, 4.3.8 and 3.2.14. memcached 1.6.45 and earlier still name these items by their raw key bytes
|
|
17
|
+
|
|
7
18
|
5.1.4
|
|
8
19
|
==========
|
|
9
20
|
|
data/README.md
CHANGED
|
@@ -63,6 +63,10 @@ Per-request `raw: true` returns the stored bytes without deserializing or decomp
|
|
|
63
63
|
|
|
64
64
|
See the [5.0-Upgrade.md](5.0-Upgrade.md) guide for upgrade information.
|
|
65
65
|
|
|
66
|
+
### Counters and verify_reply_keys
|
|
67
|
+
|
|
68
|
+
memcached names an item made by auto-create for a base64-encoded key (one with whitespace, control or non-ASCII characters) by its raw key bytes in later replies. Since 5.1.5, Dalli creates counters for such keys with an add rather than auto-create. Items created before then, or by other clients, can still be named that way until memcached releases a fix (1.6.45 and earlier behave this way). If keys passed to `incr` or `decr` with an initial value can contain untrusted input, set `verify_reply_keys: true` until then: multi-gets then ignore replies for keys they didn't request, at a cost of about 15% per multi-get.
|
|
69
|
+
|
|
66
70
|
## OpenTelemetry Tracing
|
|
67
71
|
|
|
68
72
|
Dalli automatically instruments operations with [OpenTelemetry](https://opentelemetry.io/) when the SDK is present. No configuration is required - just add the OpenTelemetry gems to your application:
|
data/lib/dalli/client.rb
CHANGED
|
@@ -50,6 +50,11 @@ module Dalli
|
|
|
50
50
|
# +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
|
|
51
51
|
# +nil+ (default) omits the attribute entirely.
|
|
52
52
|
# - :otel_peer_service - when set, adds a +peer.service+ span attribute with this value for logical service naming.
|
|
53
|
+
# - :verify_reply_keys - defaults to false. When true, get_multi, get_multi_cas and get_multi_with_metadata only
|
|
54
|
+
# accept replies for the keys they requested; after a reply naming another key, the rest of
|
|
55
|
+
# that server's replies are treated as misses. memcached can name an item by its raw key
|
|
56
|
+
# bytes when the item was created by auto-create from a base64-encoded key, which Dalli no
|
|
57
|
+
# longer does. Costs a multi-get about 15%.
|
|
53
58
|
#
|
|
54
59
|
def initialize(servers = nil, options = {})
|
|
55
60
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
@@ -745,6 +750,11 @@ module Dalli
|
|
|
745
750
|
Dalli.logger.debug { e.inspect }
|
|
746
751
|
Dalli.logger.debug { 'retrying single-server get_multi because of network error' }
|
|
747
752
|
retry
|
|
753
|
+
rescue Dalli::UnexpectedReplyKeyError => e
|
|
754
|
+
# The connection is closed and the keys are treated as missing, as the
|
|
755
|
+
# pipelined get_multi does for a server whose replies are out of step
|
|
756
|
+
Dalli.logger.warn { e.message }
|
|
757
|
+
{}
|
|
748
758
|
end
|
|
749
759
|
|
|
750
760
|
def single_server_get_multi_with_metadata(keys, req_options = nil)
|
|
@@ -756,6 +766,9 @@ module Dalli
|
|
|
756
766
|
result
|
|
757
767
|
rescue Dalli::NetworkError
|
|
758
768
|
{}
|
|
769
|
+
rescue Dalli::UnexpectedReplyKeyError => e
|
|
770
|
+
Dalli.logger.warn { e.message }
|
|
771
|
+
{}
|
|
759
772
|
end
|
|
760
773
|
|
|
761
774
|
def single_server_set_multi(hash, ttl, req_options)
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -41,6 +41,7 @@ module Dalli
|
|
|
41
41
|
warn_uri_credentials(user_creds)
|
|
42
42
|
@options = client_options.merge(user_creds)
|
|
43
43
|
@raw_mode = client_options[:raw]
|
|
44
|
+
@verify_reply_keys = client_options[:verify_reply_keys] ? true : false
|
|
44
45
|
@value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
45
46
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
46
47
|
end
|
|
@@ -147,6 +148,8 @@ module Dalli
|
|
|
147
148
|
# avoiding intermediate Hash allocation. Returns nil.
|
|
148
149
|
# Without a block, returns a Hash of { key => [value, cas] }.
|
|
149
150
|
def pipeline_next_responses(&block)
|
|
151
|
+
return abandon_pipeline if @pipeline_out_of_step
|
|
152
|
+
|
|
150
153
|
reconnect_on_pipeline_complete!
|
|
151
154
|
values = nil
|
|
152
155
|
|
|
@@ -168,6 +171,13 @@ module Dalli
|
|
|
168
171
|
# to the noop at the end of the pipeline
|
|
169
172
|
finish_pipeline && break if status && key.nil?
|
|
170
173
|
|
|
174
|
+
# A key that wasn't requested means the replies are out of step
|
|
175
|
+
# (see expect_reply_keys): keep what was read so far, drop the rest
|
|
176
|
+
if unexpected_reply_key?(key)
|
|
177
|
+
abandon_pipeline
|
|
178
|
+
break
|
|
179
|
+
end
|
|
180
|
+
|
|
171
181
|
# If the status is ok and the key is not nil, then this is a
|
|
172
182
|
# getkq response with a value that we want to set in the response hash
|
|
173
183
|
unless key.nil?
|
|
@@ -332,6 +342,7 @@ module Dalli
|
|
|
332
342
|
# the expected state before pipeline_response_setup is called.
|
|
333
343
|
response_buffer.clear
|
|
334
344
|
response_buffer.raw = raw_request?(options)
|
|
345
|
+
expect_reply_keys(keys)
|
|
335
346
|
|
|
336
347
|
# The terminating noop is sent by pipeline_response_setup
|
|
337
348
|
write(quiet_get_requests(keys, options, return_cas: return_cas))
|
|
@@ -344,6 +355,7 @@ module Dalli
|
|
|
344
355
|
# Initialize the response buffer for draining during send phase
|
|
345
356
|
response_buffer.ensure_ready
|
|
346
357
|
response_buffer.raw = raw_request?(options)
|
|
358
|
+
expect_reply_keys(keys)
|
|
347
359
|
|
|
348
360
|
keys.each_slice(chunk_size) do |chunk|
|
|
349
361
|
# Build and write this chunk of requests
|
|
@@ -376,6 +388,12 @@ module Dalli
|
|
|
376
388
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
377
389
|
break if status.nil? # No complete response available
|
|
378
390
|
|
|
391
|
+
# Handled by the next pipeline_next_responses, once sending is done
|
|
392
|
+
if unexpected_reply_key?(key)
|
|
393
|
+
@pipeline_out_of_step = true
|
|
394
|
+
break
|
|
395
|
+
end
|
|
396
|
+
|
|
379
397
|
results[key] = [value, cas] unless key.nil?
|
|
380
398
|
end
|
|
381
399
|
rescue SystemCallError, Dalli::NetworkError
|
|
@@ -387,6 +405,42 @@ module Dalli
|
|
|
387
405
|
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
388
406
|
end
|
|
389
407
|
|
|
408
|
+
# A multi-get only accepts replies for the keys it asked for. memcached
|
|
409
|
+
# names an item created by auto-create (ma or mg with N) for a
|
|
410
|
+
# base64-encoded key by its raw bytes, without the b flag; such a key
|
|
411
|
+
# can contain spaces or CR/LF, which split the reply header and make the
|
|
412
|
+
# replies after it parse as different ones. A key that wasn't requested
|
|
413
|
+
# is how that shows up.
|
|
414
|
+
# Only with the verify_reply_keys option, since building the set costs a
|
|
415
|
+
# multi-get about 15%. Dalli itself no longer creates such items (see
|
|
416
|
+
# Meta#decr_incr); the check guards against ones that older versions or
|
|
417
|
+
# other clients created, on memcached versions with the behavior.
|
|
418
|
+
def expect_reply_keys(keys)
|
|
419
|
+
@pipeline_out_of_step = false
|
|
420
|
+
@expected_reply_keys = (keys.to_set { |k| reply_key_form(k) } if @verify_reply_keys)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def unexpected_reply_key?(key)
|
|
424
|
+
!key.nil? && @expected_reply_keys && !@expected_reply_keys.include?(reply_key_form(key))
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Keys compared as bytes; ASCII keys compare equal in any encoding as is
|
|
428
|
+
def reply_key_form(key)
|
|
429
|
+
key.ascii_only? ? key : key.b
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Keeps what was read, drops the rest of this server's replies, and
|
|
433
|
+
# closes the connection so nothing unread is left on it. Not a socket
|
|
434
|
+
# failure, so it doesn't count toward socket_max_failures; the keys not
|
|
435
|
+
# read are simply missing from the result.
|
|
436
|
+
def abandon_pipeline
|
|
437
|
+
Dalli.logger.warn { "#{name} replied to a get_multi with a key that wasn't requested; closing the connection" }
|
|
438
|
+
@pipeline_out_of_step = false
|
|
439
|
+
response_buffer.clear
|
|
440
|
+
@connection_manager.close
|
|
441
|
+
{}
|
|
442
|
+
end
|
|
443
|
+
|
|
390
444
|
# Called after the noop response is received at the end of a set
|
|
391
445
|
# of pipelined gets
|
|
392
446
|
def finish_pipeline
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -224,12 +224,45 @@ module Dalli
|
|
|
224
224
|
# rubocop:disable-next Metrics/ParameterLists
|
|
225
225
|
def decr_incr(incr, key, delta, ttl, initial, options = nil)
|
|
226
226
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
227
|
+
# memcached stores an item created by ma's auto-create (N/J) for a
|
|
228
|
+
# base64-encoded key without its binary-key flag, and later replies
|
|
229
|
+
# name it with the raw key bytes. Create such counters with an add.
|
|
230
|
+
return decr_incr_with_add(incr, key, delta, ttl, initial, options) if initial && KeyRegularizer.required?(key)
|
|
231
|
+
|
|
227
232
|
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
228
233
|
quiet: quiet?, **routing_token_kwargs(options)))
|
|
229
234
|
@connection_manager.flush unless quiet?
|
|
230
235
|
response_processor.decr_incr unless quiet?
|
|
231
236
|
end
|
|
232
237
|
|
|
238
|
+
# Like decr_incr with an initial value, without memcached's auto-create:
|
|
239
|
+
# on a miss the counter is added with the initial value (which is
|
|
240
|
+
# returned, as auto-create would), and the add losing a race to another
|
|
241
|
+
# client means the counter now exists, so the incr is tried again. In a
|
|
242
|
+
# quiet block both requests are sent and whichever applies takes effect.
|
|
243
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
244
|
+
def decr_incr_with_add(incr, key, delta, ttl, initial, options)
|
|
245
|
+
add_options = (options || {}).merge(raw: true) # counters are stored raw
|
|
246
|
+
return quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options) if quiet?
|
|
247
|
+
|
|
248
|
+
2.times do
|
|
249
|
+
value = decr_incr(incr, key, delta, nil, nil, options)
|
|
250
|
+
return value unless value.nil?
|
|
251
|
+
|
|
252
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: false)
|
|
253
|
+
return initial if response_processor.meta_set_with_cas
|
|
254
|
+
end
|
|
255
|
+
decr_incr(incr, key, delta, nil, nil, options)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
259
|
+
def quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options)
|
|
260
|
+
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: nil, incr: incr, ttl: nil,
|
|
261
|
+
quiet: true, **routing_token_kwargs(add_options)))
|
|
262
|
+
# Sent with the rest of the quiet block, by its closing noop
|
|
263
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: true)
|
|
264
|
+
end
|
|
265
|
+
|
|
233
266
|
# Other Commands
|
|
234
267
|
def flush(delay = 0)
|
|
235
268
|
write(RequestFormatter.flush(delay: delay))
|
|
@@ -271,7 +304,7 @@ module Dalli
|
|
|
271
304
|
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw, **routing_token_kwargs(options))
|
|
272
305
|
flushed_write(buffer)
|
|
273
306
|
buffer.clear
|
|
274
|
-
read_multi_get_responses(is_raw)
|
|
307
|
+
read_multi_get_responses(is_raw, keys)
|
|
275
308
|
end
|
|
276
309
|
|
|
277
310
|
# Stale-aware bulk get. Returns { key => { value:, cas:, stale:, miss: } }
|
|
@@ -288,12 +321,13 @@ module Dalli
|
|
|
288
321
|
**routing_token_kwargs(options))
|
|
289
322
|
flushed_write(buffer)
|
|
290
323
|
buffer.clear
|
|
291
|
-
read_multi_metadata_responses(is_raw)
|
|
324
|
+
read_multi_metadata_responses(is_raw, keys)
|
|
292
325
|
end
|
|
293
326
|
|
|
294
327
|
# Unlike read_multi_get_responses this locates tokens by flag rather than
|
|
295
328
|
# by position, because the c flag shifts the key's index.
|
|
296
|
-
def read_multi_metadata_responses(is_raw)
|
|
329
|
+
def read_multi_metadata_responses(is_raw, keys)
|
|
330
|
+
expect_reply_keys(keys)
|
|
297
331
|
hash = {}
|
|
298
332
|
while (line = @connection_manager.read_line)
|
|
299
333
|
break if line.start_with?('MN')
|
|
@@ -309,12 +343,15 @@ module Dalli
|
|
|
309
343
|
key = response_processor.key_from_tokens(tokens)
|
|
310
344
|
next if key.nil?
|
|
311
345
|
|
|
346
|
+
raise_unexpected_reply_key(key) if unexpected_reply_key?(key)
|
|
347
|
+
|
|
312
348
|
hash[key] = { value: @value_marshaller.retrieve(value, bitflags), cas: cas, stale: stale, miss: false }
|
|
313
349
|
end
|
|
314
350
|
hash
|
|
315
351
|
end
|
|
316
352
|
|
|
317
|
-
def read_multi_get_responses(is_raw)
|
|
353
|
+
def read_multi_get_responses(is_raw, keys)
|
|
354
|
+
expect_reply_keys(keys)
|
|
318
355
|
hash = {}
|
|
319
356
|
key_index = is_raw ? 2 : 3
|
|
320
357
|
while (line = @connection_manager.read_line)
|
|
@@ -322,11 +359,19 @@ module Dalli
|
|
|
322
359
|
next unless line.start_with?('VA ')
|
|
323
360
|
|
|
324
361
|
key, value = parse_multi_get_value(line, key_index, is_raw)
|
|
362
|
+
raise_unexpected_reply_key(key) if unexpected_reply_key?(key)
|
|
325
363
|
hash[key] = value if key
|
|
326
364
|
end
|
|
327
365
|
hash
|
|
328
366
|
end
|
|
329
367
|
|
|
368
|
+
# The replies after a key that wasn't requested can't be trusted (see
|
|
369
|
+
# Base#expect_reply_keys). Raising ends the request, which closes the
|
|
370
|
+
# connection; the caller treats this server's keys as missing.
|
|
371
|
+
def raise_unexpected_reply_key(key)
|
|
372
|
+
raise Dalli::UnexpectedReplyKeyError, "#{name} replied with a key that wasn't requested: #{key.inspect}"
|
|
373
|
+
end
|
|
374
|
+
|
|
330
375
|
def parse_multi_get_value(line, key_index, is_raw)
|
|
331
376
|
tokens = line.chomp!(TERMINATOR).split
|
|
332
377
|
size = tokens[1].to_i
|
data/lib/dalli/version.rb
CHANGED
data/lib/dalli.rb
CHANGED
|
@@ -31,6 +31,10 @@ module Dalli
|
|
|
31
31
|
# socket/server communication error that can be retried
|
|
32
32
|
class RetryableNetworkError < NetworkError; end
|
|
33
33
|
|
|
34
|
+
# a multi-get reply named a key that wasn't requested, so the rest of that
|
|
35
|
+
# server's replies can't be trusted
|
|
36
|
+
class UnexpectedReplyKeyError < DalliError; end
|
|
37
|
+
|
|
34
38
|
# Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
|
|
35
39
|
class NilObject; end # rubocop:disable Lint/EmptyClass
|
|
36
40
|
NOT_FOUND = NilObject.new
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dalli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.1.
|
|
4
|
+
version: 5.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -88,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
89
|
version: '0'
|
|
90
90
|
requirements: []
|
|
91
|
-
rubygems_version: 4.0.
|
|
91
|
+
rubygems_version: 4.0.23
|
|
92
92
|
specification_version: 4
|
|
93
93
|
summary: High performance memcached client for Ruby
|
|
94
94
|
test_files: []
|