dalli 5.0.9 → 5.0.10
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 +26 -0
- data/lib/dalli/client.rb +35 -20
- data/lib/dalli/compressor.rb +5 -0
- data/lib/dalli/key_manager.rb +6 -3
- data/lib/dalli/protocol/base.rb +7 -4
- data/lib/dalli/protocol/connection_manager.rb +20 -1
- data/lib/dalli/protocol/meta.rb +9 -7
- data/lib/dalli/protocol/response_processor.rb +33 -10
- data/lib/dalli/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64bc18f470e5b97d87b6531a968a9252c4ff46ba715d927c74b66e52f7549683
|
|
4
|
+
data.tar.gz: 70fc9db7148b82932b9f3323ac74a2c59dd7ed8b72bd9543dc160c5026d33880
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a2c3def53ce7c5c4bf4d929bc45c5d1ff7f000db6b59abea3ac9ef3eb81e7f706ab8d0b01021b7002ff2b2153014b17c018c5060935e022f867b4981318afc06
|
|
7
|
+
data.tar.gz: f63a580cc08950f6da91213f0c4b9ecd2cb75961fe4467e06c11ecb434d2b13ff5950fceb31d43f8b8c42d8997fec3fd1bd40eebc1aa59dda41b2c1092a5e253
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.0.10
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
Security:
|
|
11
|
+
|
|
12
|
+
- Keep the caller's key when retrying a request (GHSA-m252-9cgf-vx2w)
|
|
13
|
+
- With a `namespace`, a request retried after a transient network error (a timeout, or a connection closed by memcached or a proxy) applied the namespace a second time, so a retried read could return a different key's value and a retried write could overwrite a different key
|
|
14
|
+
- Affects single-key operations since 5.0.3, and `get_with_metadata` and `fetch_with_lock` since 4.1.0; fixed in 5.2.2, 5.1.4, 5.0.10 and 4.3.7. Clients without a namespace aren't affected
|
|
15
|
+
- Complete the fix for per-request `raw: true` on reads (GHSA-wr87-m4jw-29x5)
|
|
16
|
+
- `cas`, `cas!` and `fetch_with_lock` accepted `raw: true` but didn't pass it to the read, so the value was still deserialized. They now honor it
|
|
17
|
+
- A read made with `raw: true` doesn't ask for flags, but a reply carrying them anyway (from a proxy or a hostile server) still had its value deserialized. Raw reads now ignore flags in the reply
|
|
18
|
+
- Affects 5.0.9 and earlier; fixed in 5.2.2, 5.1.4, 5.0.10, 4.3.7 and 3.2.13
|
|
19
|
+
|
|
20
|
+
Bug fixes:
|
|
21
|
+
|
|
22
|
+
- `get_with_metadata` and `fetch_with_lock` retried the final error raised when a server is marked down, so with `down_retry_delay: 0` they retried an unresponsive server forever. They now retry only retryable errors, like other operations
|
|
23
|
+
- Fix a regression in 5.0.9: a `get_multi` that didn't finish within `socket_timeout` counted toward `socket_max_failures`, so two slow `get_multi` calls in a row marked a healthy server down. It now just closes the connection
|
|
24
|
+
- Fix a regression in 5.0.9: over TLS, every request buffered in a `quiet` block was sent as its own TLS record and system call (2.5 times slower for a block of 2000 deletes). Each flush is one write again
|
|
25
|
+
- Handle malformed replies from a broken or hostile server or proxy: a negative value size, a hit with no key, or a hit with no `s` flag could leave the connection out of step or raise a non-Dalli error
|
|
26
|
+
- With `decompressed_max_bytes` in effect, data after the end of a compressed value's stream was returned as part of the value. It's now ignored, as without the limit
|
|
27
|
+
- With a `digest_class` whose digests aren't short hex strings, shortening a long key could loop forever. It now raises `ArgumentError`
|
|
28
|
+
|
|
29
|
+
Development:
|
|
30
|
+
|
|
31
|
+
- Run the Tests, RuboCop and Profiles workflows on pushes to `main` and the `*-stable` branches only, so a pull request's branch isn't tested twice (backport of #1198)
|
|
32
|
+
|
|
7
33
|
5.0.9
|
|
8
34
|
==========
|
|
9
35
|
|
data/lib/dalli/client.rb
CHANGED
|
@@ -126,14 +126,18 @@ module Dalli
|
|
|
126
126
|
# # => { value: "data", cas: 123, hit_before: true, last_access: 42 }
|
|
127
127
|
#
|
|
128
128
|
def get_with_metadata(key, options = {})
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
# A retry re-runs this method with its original arguments, so `key` must
|
|
130
|
+
# stay as the caller gave it; reassigning it would add the namespace again.
|
|
131
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
131
132
|
|
|
132
|
-
server = ring.server_for_key(
|
|
133
|
-
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata',
|
|
134
|
-
server.request(:meta_get,
|
|
133
|
+
server = ring.server_for_key(validated_key)
|
|
134
|
+
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', validated_key, server)) do
|
|
135
|
+
server.request(:meta_get, validated_key, options)
|
|
135
136
|
end
|
|
136
|
-
|
|
137
|
+
# Only a retryable error is retried. The NetworkError raised when a server
|
|
138
|
+
# is marked down is final, as in #perform; retrying it looped forever once
|
|
139
|
+
# down_retry_delay let the same server be tried again.
|
|
140
|
+
rescue RetryableNetworkError => e
|
|
137
141
|
Dalli.logger.debug { e.inspect }
|
|
138
142
|
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
139
143
|
retry
|
|
@@ -225,14 +229,17 @@ module Dalli
|
|
|
225
229
|
|
|
226
230
|
validate_integer!(:lock_ttl, lock_ttl)
|
|
227
231
|
validate_integer!(:recache_threshold, recache_threshold)
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
# Left unchanged for the retry below; see get_with_metadata
|
|
233
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
230
234
|
|
|
231
|
-
server = ring.server_for_key(
|
|
232
|
-
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock',
|
|
233
|
-
fetch_with_lock_request(
|
|
235
|
+
server = ring.server_for_key(validated_key)
|
|
236
|
+
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', validated_key, server)) do
|
|
237
|
+
fetch_with_lock_request(validated_key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
234
238
|
end
|
|
235
|
-
|
|
239
|
+
# Only a retryable error is retried. The NetworkError raised when a server
|
|
240
|
+
# is marked down is final, as in #perform; retrying it looped forever once
|
|
241
|
+
# down_retry_delay let the same server be tried again.
|
|
242
|
+
rescue RetryableNetworkError => e
|
|
236
243
|
Dalli.logger.debug { e.inspect }
|
|
237
244
|
Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
|
|
238
245
|
retry
|
|
@@ -637,7 +644,8 @@ module Dalli
|
|
|
637
644
|
end
|
|
638
645
|
|
|
639
646
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
640
|
-
|
|
647
|
+
# req_options reaches the read too, so raw: true returns the stored bytes
|
|
648
|
+
(value, cas) = perform(:cas, key, req_options)
|
|
641
649
|
return if value.nil? && !always_set
|
|
642
650
|
|
|
643
651
|
newvalue = yield(value)
|
|
@@ -646,7 +654,12 @@ module Dalli
|
|
|
646
654
|
|
|
647
655
|
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
648
656
|
server = ring.server_for_key(key)
|
|
649
|
-
|
|
657
|
+
# req_options (e.g. raw: true) reaches the read too. It's the base, not
|
|
658
|
+
# the override: fetch_with_lock's own lock_ttl/recache_threshold win.
|
|
659
|
+
meta_options = req_options.is_a?(Hash) ? req_options.dup : {}
|
|
660
|
+
meta_options[:vivify_ttl] = lock_ttl
|
|
661
|
+
meta_options[:recache_ttl] = recache_threshold
|
|
662
|
+
result = server.request(:meta_get, key, meta_options)
|
|
650
663
|
|
|
651
664
|
return result[:value] unless result[:won_recache]
|
|
652
665
|
|
|
@@ -684,18 +697,20 @@ module Dalli
|
|
|
684
697
|
# rubocop:enable Naming/MethodParameterName
|
|
685
698
|
return yield if block_given?
|
|
686
699
|
|
|
687
|
-
|
|
688
|
-
|
|
700
|
+
# `retry` below re-runs this method with its original arguments, so `key`
|
|
701
|
+
# must stay as the caller gave it: reassigning it would apply the
|
|
702
|
+
# namespace a second time and send the retry to a different key.
|
|
703
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
689
704
|
|
|
690
|
-
server = ring.server_for_key(
|
|
705
|
+
server = ring.server_for_key(validated_key)
|
|
691
706
|
|
|
692
707
|
if Instrumentation.enabled?
|
|
693
708
|
op_name = op.name
|
|
694
|
-
Instrumentation.trace(op_name, trace_attrs(op_name,
|
|
695
|
-
server.request(op,
|
|
709
|
+
Instrumentation.trace(op_name, trace_attrs(op_name, validated_key, server)) do
|
|
710
|
+
server.request(op, validated_key, ...)
|
|
696
711
|
end
|
|
697
712
|
else
|
|
698
|
-
server.request(op,
|
|
713
|
+
server.request(op, validated_key, ...)
|
|
699
714
|
end
|
|
700
715
|
rescue RetryableNetworkError => e
|
|
701
716
|
Dalli.logger.debug { e.inspect }
|
data/lib/dalli/compressor.rb
CHANGED
|
@@ -39,7 +39,12 @@ module Dalli
|
|
|
39
39
|
out = String.new(encoding: Encoding::BINARY)
|
|
40
40
|
0.step(data.bytesize - 1, INFLATE_SLICE_BYTES) do |pos|
|
|
41
41
|
inflate_slice(inflater, data.byteslice(pos, INFLATE_SLICE_BYTES), out, max_bytes)
|
|
42
|
+
# Anything after the end of the stream isn't part of the value, as
|
|
43
|
+
# with Zlib::Inflate.inflate; finish would return it as output
|
|
44
|
+
break if inflater.finished?
|
|
42
45
|
end
|
|
46
|
+
return out if inflater.finished?
|
|
47
|
+
|
|
43
48
|
append_within_limit(out, inflater.finish, max_bytes)
|
|
44
49
|
ensure
|
|
45
50
|
inflater.close
|
data/lib/dalli/key_manager.rb
CHANGED
|
@@ -122,15 +122,18 @@ module Dalli
|
|
|
122
122
|
##
|
|
123
123
|
def truncated_key(key)
|
|
124
124
|
digest = digest_class.hexdigest(key)
|
|
125
|
-
prefix = key[0, prefix_length(digest)]
|
|
125
|
+
prefix = key[0, [prefix_length(digest), 0].max]
|
|
126
126
|
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
127
127
|
# A prefix with whitespace or non-ASCII characters sends the whole key
|
|
128
128
|
# base64-encoded, which can still be too long on the wire; shorten it.
|
|
129
|
-
while wire_length(truncated) > MAX_KEY_LENGTH
|
|
129
|
+
while wire_length(truncated) > MAX_KEY_LENGTH && !prefix.empty?
|
|
130
130
|
prefix = prefix[0...-1]
|
|
131
131
|
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
132
132
|
end
|
|
133
|
-
truncated
|
|
133
|
+
return truncated if wire_length(truncated) <= MAX_KEY_LENGTH
|
|
134
|
+
|
|
135
|
+
# Only possible with a digest_class whose digests aren't short hex strings
|
|
136
|
+
raise ArgumentError, "#{digest_class} digests are too long to fit in a truncated key"
|
|
134
137
|
end
|
|
135
138
|
|
|
136
139
|
# memcached's key limit applies to the key as sent: in bytes, and after
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -168,10 +168,13 @@ module Dalli
|
|
|
168
168
|
@connection_manager.abort_request!
|
|
169
169
|
return true unless connected?
|
|
170
170
|
|
|
171
|
-
# Closes the connection, which ensures that our connection
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
# Closes the connection, which ensures that our connection is in a
|
|
172
|
+
# clean state for future requests. The get_multi ran out of time
|
|
173
|
+
# overall rather than hitting a socket error, so it doesn't count
|
|
174
|
+
# toward socket_max_failures: two slow get_multis in a row would
|
|
175
|
+
# otherwise mark a healthy server down.
|
|
176
|
+
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
177
|
+
@connection_manager.close
|
|
175
178
|
true
|
|
176
179
|
end
|
|
177
180
|
|
|
@@ -334,13 +334,32 @@ module Dalli
|
|
|
334
334
|
|
|
335
335
|
private
|
|
336
336
|
|
|
337
|
+
# One write per flush. A TLS socket sends each argument of
|
|
338
|
+
# write(*parts) as its own record and system call, so the parts are
|
|
339
|
+
# joined first.
|
|
337
340
|
def flush_write_buffer
|
|
338
341
|
return if @write_buffer.empty?
|
|
339
342
|
|
|
340
|
-
@sock.write(
|
|
343
|
+
@sock.write(@write_buffer.size == 1 ? @write_buffer.first : joined_write_buffer)
|
|
341
344
|
discard_write_buffer
|
|
342
345
|
end
|
|
343
346
|
|
|
347
|
+
# Joined as bytes: requests can mix UTF-8 and binary strings, which
|
|
348
|
+
# String#<< refuses to combine when both hold non-ASCII bytes
|
|
349
|
+
if String.method_defined?(:append_as_bytes)
|
|
350
|
+
def joined_write_buffer
|
|
351
|
+
out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
|
|
352
|
+
@write_buffer.each { |part| out.append_as_bytes(part) }
|
|
353
|
+
out
|
|
354
|
+
end
|
|
355
|
+
else
|
|
356
|
+
def joined_write_buffer
|
|
357
|
+
out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
|
|
358
|
+
@write_buffer.each { |part| out << part.b }
|
|
359
|
+
out
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
344
363
|
def discard_write_buffer
|
|
345
364
|
@write_buffer.clear
|
|
346
365
|
@write_buffer_bytes = 0
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Dalli
|
|
|
28
28
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
29
29
|
req = RequestFormatter.meta_get(key: key, skip_flags: skip_flags)
|
|
30
30
|
flushed_write(req)
|
|
31
|
-
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
31
|
+
response_processor.meta_get_with_value(cache_nils: cache_nils?(options), raw: raw_request?(options))
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def quiet_get_request(key)
|
|
@@ -41,7 +41,7 @@ module Dalli
|
|
|
41
41
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
42
42
|
req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags)
|
|
43
43
|
flushed_write(req)
|
|
44
|
-
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
44
|
+
response_processor.meta_get_with_value(cache_nils: cache_nils?(options), raw: raw_request?(options))
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def touch(key, ttl)
|
|
@@ -53,10 +53,10 @@ module Dalli
|
|
|
53
53
|
|
|
54
54
|
# TODO: This is confusing, as there's a cas command in memcached
|
|
55
55
|
# and this isn't it. Maybe rename? Maybe eliminate?
|
|
56
|
-
def cas(key)
|
|
57
|
-
req = RequestFormatter.meta_get(key: key, value: true, return_cas: true)
|
|
56
|
+
def cas(key, options = nil)
|
|
57
|
+
req = RequestFormatter.meta_get(key: key, value: true, return_cas: true, skip_flags: raw_request?(options))
|
|
58
58
|
flushed_write(req)
|
|
59
|
-
response_processor.meta_get_with_value_and_cas
|
|
59
|
+
response_processor.meta_get_with_value_and_cas(raw: raw_request?(options))
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# Comprehensive meta get with support for all metadata flags.
|
|
@@ -93,7 +93,7 @@ module Dalli
|
|
|
93
93
|
flushed_write(req)
|
|
94
94
|
response_processor.meta_get_with_metadata(
|
|
95
95
|
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
96
|
-
return_last_access: options[:return_last_access]
|
|
96
|
+
return_last_access: options[:return_last_access], raw: raw_request?(options)
|
|
97
97
|
)
|
|
98
98
|
end
|
|
99
99
|
|
|
@@ -258,7 +258,9 @@ module Dalli
|
|
|
258
258
|
|
|
259
259
|
def parse_multi_get_value(line, key_index, is_raw)
|
|
260
260
|
tokens = line.chomp!(TERMINATOR).split
|
|
261
|
-
|
|
261
|
+
size = tokens[1].to_i
|
|
262
|
+
response_processor.check_value_size!(size)
|
|
263
|
+
value = @connection_manager.read(size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
262
264
|
raw_key = tokens[key_index]
|
|
263
265
|
return unless raw_key
|
|
264
266
|
|
|
@@ -41,22 +41,25 @@ module Dalli
|
|
|
41
41
|
@value_marshaller = value_marshaller
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
# raw: true (a request made with raw: true) returns the value as stored,
|
|
45
|
+
# ignoring any flags in the reply. The request didn't ask for flags, so
|
|
46
|
+
# a reply carrying them anyway must not get the value deserialized.
|
|
47
|
+
def meta_get_with_value(cache_nils: false, raw: false)
|
|
45
48
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
46
49
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
47
50
|
return true unless tokens.first == VA
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
|
|
50
53
|
end
|
|
51
54
|
|
|
52
|
-
def meta_get_with_value_and_cas
|
|
55
|
+
def meta_get_with_value_and_cas(raw: false)
|
|
53
56
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
54
57
|
return [nil, 0] if tokens.first == EN
|
|
55
58
|
|
|
56
59
|
cas = cas_from_tokens(tokens)
|
|
57
60
|
return [nil, cas] unless tokens.first == VA
|
|
58
61
|
|
|
59
|
-
[
|
|
62
|
+
[retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw), cas]
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
def meta_get_without_value
|
|
@@ -75,12 +78,13 @@ module Dalli
|
|
|
75
78
|
#
|
|
76
79
|
# Used by meta_get for comprehensive metadata retrieval.
|
|
77
80
|
# Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
|
|
78
|
-
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false
|
|
81
|
+
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
|
|
82
|
+
raw: false)
|
|
79
83
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
80
84
|
result = build_metadata_result(tokens)
|
|
81
85
|
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
82
86
|
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
83
|
-
result[:value] = parse_value_from_tokens(tokens, cache_nils)
|
|
87
|
+
result[:value] = parse_value_from_tokens(tokens, cache_nils, raw)
|
|
84
88
|
result
|
|
85
89
|
end
|
|
86
90
|
|
|
@@ -92,11 +96,16 @@ module Dalli
|
|
|
92
96
|
}
|
|
93
97
|
end
|
|
94
98
|
|
|
95
|
-
def parse_value_from_tokens(tokens, cache_nils)
|
|
99
|
+
def parse_value_from_tokens(tokens, cache_nils, raw = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
96
100
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
97
101
|
return unless tokens.first == VA
|
|
98
102
|
|
|
99
|
-
|
|
103
|
+
retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Values from a raw request are returned as stored
|
|
107
|
+
def retrieve(value, bitflags, raw)
|
|
108
|
+
raw ? value : @value_marshaller.retrieve(value, bitflags)
|
|
100
109
|
end
|
|
101
110
|
|
|
102
111
|
def meta_set_with_cas
|
|
@@ -178,6 +187,10 @@ module Dalli
|
|
|
178
187
|
end
|
|
179
188
|
|
|
180
189
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
190
|
+
# A hit with no key can't be matched to a request: skip it (false
|
|
191
|
+
# status) rather than yield it, or take it for the end of the pipeline
|
|
192
|
+
return [false, resp_size] unless tokens.any? { |t| t.start_with?('k') }
|
|
193
|
+
|
|
181
194
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
182
195
|
[tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value, resp_size]
|
|
183
196
|
end
|
|
@@ -201,8 +214,10 @@ module Dalli
|
|
|
201
214
|
tokens = header.split
|
|
202
215
|
header_len = header.bytesize + TERMINATOR.length
|
|
203
216
|
|
|
204
|
-
# The body len is removed from the tokens array
|
|
205
|
-
|
|
217
|
+
# The body len is removed from the tokens array. memcached also gives
|
|
218
|
+
# the size positionally ("VA <size> ..."); use it when there's no s
|
|
219
|
+
# flag rather than reading the value as replies.
|
|
220
|
+
body_len = tokens.any? { |t| t.start_with?('s') } ? body_len_from_tokens(tokens) : positional_size(tokens)
|
|
206
221
|
|
|
207
222
|
# A complete response with no body. Only the terminating noop's MN
|
|
208
223
|
# ends the pipeline. Any other bodyless reply (CLIENT_ERROR,
|
|
@@ -284,6 +299,11 @@ module Dalli
|
|
|
284
299
|
|
|
285
300
|
# A pipelined reply claiming an impossible size would otherwise have the
|
|
286
301
|
# buffer wait for (and accumulate) that many bytes
|
|
302
|
+
# The size given right after "VA", or 0 when it isn't a number
|
|
303
|
+
def positional_size(tokens)
|
|
304
|
+
tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
|
|
305
|
+
end
|
|
306
|
+
|
|
287
307
|
def check_value_size!(size)
|
|
288
308
|
return if size.between?(0, MAX_VALUE_BYTES)
|
|
289
309
|
|
|
@@ -300,6 +320,9 @@ module Dalli
|
|
|
300
320
|
end
|
|
301
321
|
|
|
302
322
|
def read_data(data_size)
|
|
323
|
+
# Checked before the terminator is added: -1 or -2 would otherwise
|
|
324
|
+
# read 1 or 0 bytes and leave the connection out of step
|
|
325
|
+
check_value_size!(data_size)
|
|
303
326
|
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
304
327
|
end
|
|
305
328
|
end
|
data/lib/dalli/version.rb
CHANGED