dalli 5.1.3 → 5.1.4
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 +30 -20
- data/lib/dalli/compressor.rb +5 -0
- data/lib/dalli/key_manager.rb +6 -3
- data/lib/dalli/protocol/base.rb +9 -4
- data/lib/dalli/protocol/connection_manager.rb +20 -1
- data/lib/dalli/protocol/meta.rb +10 -6
- data/lib/dalli/protocol/response_buffer.rb +4 -1
- data/lib/dalli/protocol/response_processor.rb +37 -13
- 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: 70f17667b9c623f9233be342d920de385bab797b44a4bc9f1e85c233f766404f
|
|
4
|
+
data.tar.gz: 527b9ff3dd4699ad5d307107668b31d3a739675825ad4d0b2cfb35ac09581a81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e54f454a1bca3b79f229a219b458533d2778c1521043c1ee7c2d203ae0d25e1449fc375b59754aaadac79de6ea484170ba52c10008da6abd40451821300a60c
|
|
7
|
+
data.tar.gz: 1dea61992e48d8786dbb3ef929e30c53d3544bafed895334cf4316ad0710791c4ebb0887fb4fa63b79358c0029be11b3495a2875a24956a296246fe9d5e4eaa0
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.1.4
|
|
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, single-server `get_multi` since 5.1.0, 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
|
+
- 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
|
|
17
|
+
- Affects 5.1.3 and earlier; fixed in 5.2.2, 5.1.4, 5.0.10, 4.3.7 and 3.2.13
|
|
18
|
+
|
|
19
|
+
Bug fixes:
|
|
20
|
+
|
|
21
|
+
- `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
|
|
22
|
+
- Fix a regression in 5.1.3: 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
|
|
23
|
+
- Fix a regression in 5.1.3: 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
|
|
24
|
+
- 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
|
|
25
|
+
- 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
|
|
26
|
+
- With a `digest_class` whose digests aren't short hex strings, shortening a long key could loop forever. It now raises `ArgumentError`
|
|
27
|
+
|
|
28
|
+
Development:
|
|
29
|
+
|
|
30
|
+
- 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)
|
|
31
|
+
- Fix flaky failover tests: move their ports out of Linux's ephemeral port range, and wait for memcached to accept connections after starting it (backport of #1184)
|
|
32
|
+
|
|
7
33
|
5.1.3
|
|
8
34
|
==========
|
|
9
35
|
|
data/lib/dalli/client.rb
CHANGED
|
@@ -136,14 +136,18 @@ module Dalli
|
|
|
136
136
|
#
|
|
137
137
|
def get_with_metadata(key, options = {})
|
|
138
138
|
validate_routing_tokens!(options)
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
# A retry re-runs this method with its original arguments, so `key` must
|
|
140
|
+
# stay as the caller gave it; reassigning it would add the namespace again.
|
|
141
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
141
142
|
|
|
142
|
-
server = ring.server_for_key(
|
|
143
|
-
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata',
|
|
144
|
-
server.request(:meta_get,
|
|
143
|
+
server = ring.server_for_key(validated_key)
|
|
144
|
+
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', validated_key, server)) do
|
|
145
|
+
server.request(:meta_get, validated_key, options)
|
|
145
146
|
end
|
|
146
|
-
|
|
147
|
+
# Only a retryable error is retried. The NetworkError raised when a server
|
|
148
|
+
# is marked down is final, as in #perform; retrying it looped forever once
|
|
149
|
+
# down_retry_delay let the same server be tried again.
|
|
150
|
+
rescue RetryableNetworkError => e
|
|
147
151
|
Dalli.logger.debug { e.inspect }
|
|
148
152
|
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
149
153
|
retry
|
|
@@ -302,14 +306,17 @@ module Dalli
|
|
|
302
306
|
validate_integer!(:lock_ttl, lock_ttl)
|
|
303
307
|
validate_integer!(:recache_threshold, recache_threshold)
|
|
304
308
|
validate_routing_tokens!(req_options)
|
|
305
|
-
|
|
306
|
-
|
|
309
|
+
# Left unchanged for the retry below; see get_with_metadata
|
|
310
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
307
311
|
|
|
308
|
-
server = ring.server_for_key(
|
|
309
|
-
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock',
|
|
310
|
-
fetch_with_lock_request(
|
|
312
|
+
server = ring.server_for_key(validated_key)
|
|
313
|
+
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', validated_key, server)) do
|
|
314
|
+
fetch_with_lock_request(validated_key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
311
315
|
end
|
|
312
|
-
|
|
316
|
+
# Only a retryable error is retried. The NetworkError raised when a server
|
|
317
|
+
# is marked down is final, as in #perform; retrying it looped forever once
|
|
318
|
+
# down_retry_delay let the same server be tried again.
|
|
319
|
+
rescue RetryableNetworkError => e
|
|
313
320
|
Dalli.logger.debug { e.inspect }
|
|
314
321
|
Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
|
|
315
322
|
retry
|
|
@@ -727,10 +734,11 @@ module Dalli
|
|
|
727
734
|
# to route to at all is still silent, matching Ring#keys_grouped_by_server
|
|
728
735
|
# dropping a key it can't route on both the single- and multi-server paths.
|
|
729
736
|
def single_server_get_multi(keys, req_options = nil)
|
|
730
|
-
|
|
737
|
+
# A new array, so the retry below starts again from the caller's keys
|
|
738
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
731
739
|
return {} unless (server = single_server)
|
|
732
740
|
|
|
733
|
-
result = server.request(:read_multi_req,
|
|
741
|
+
result = server.request(:read_multi_req, validated_keys, req_options)
|
|
734
742
|
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
735
743
|
result
|
|
736
744
|
rescue Dalli::RetryableNetworkError => e
|
|
@@ -897,18 +905,20 @@ module Dalli
|
|
|
897
905
|
# rubocop:enable Naming/MethodParameterName
|
|
898
906
|
return yield if block_given?
|
|
899
907
|
|
|
900
|
-
|
|
901
|
-
|
|
908
|
+
# `retry` below re-runs this method with its original arguments, so `key`
|
|
909
|
+
# must stay as the caller gave it: reassigning it would apply the
|
|
910
|
+
# namespace a second time and send the retry to a different key.
|
|
911
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
902
912
|
|
|
903
|
-
server = ring.server_for_key(
|
|
913
|
+
server = ring.server_for_key(validated_key)
|
|
904
914
|
|
|
905
915
|
if Instrumentation.enabled?
|
|
906
916
|
op_name = op.name
|
|
907
|
-
Instrumentation.trace(op_name, trace_attrs(op_name,
|
|
908
|
-
server.request(op,
|
|
917
|
+
Instrumentation.trace(op_name, trace_attrs(op_name, validated_key, server)) do
|
|
918
|
+
server.request(op, validated_key, ...)
|
|
909
919
|
end
|
|
910
920
|
else
|
|
911
|
-
server.request(op,
|
|
921
|
+
server.request(op, validated_key, ...)
|
|
912
922
|
end
|
|
913
923
|
rescue RetryableNetworkError => e
|
|
914
924
|
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
|
@@ -197,10 +197,13 @@ module Dalli
|
|
|
197
197
|
@connection_manager.abort_request!
|
|
198
198
|
return true unless connected?
|
|
199
199
|
|
|
200
|
-
# Closes the connection, which ensures that our connection
|
|
201
|
-
#
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
# Closes the connection, which ensures that our connection is in a
|
|
201
|
+
# clean state for future requests. The get_multi ran out of time
|
|
202
|
+
# overall rather than hitting a socket error, so it doesn't count
|
|
203
|
+
# toward socket_max_failures: two slow get_multis in a row would
|
|
204
|
+
# otherwise mark a healthy server down.
|
|
205
|
+
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
206
|
+
@connection_manager.close
|
|
204
207
|
true
|
|
205
208
|
end
|
|
206
209
|
|
|
@@ -328,6 +331,7 @@ module Dalli
|
|
|
328
331
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
329
332
|
# the expected state before pipeline_response_setup is called.
|
|
330
333
|
response_buffer.clear
|
|
334
|
+
response_buffer.raw = raw_request?(options)
|
|
331
335
|
|
|
332
336
|
# The terminating noop is sent by pipeline_response_setup
|
|
333
337
|
write(quiet_get_requests(keys, options, return_cas: return_cas))
|
|
@@ -339,6 +343,7 @@ module Dalli
|
|
|
339
343
|
def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
|
|
340
344
|
# Initialize the response buffer for draining during send phase
|
|
341
345
|
response_buffer.ensure_ready
|
|
346
|
+
response_buffer.raw = raw_request?(options)
|
|
342
347
|
|
|
343
348
|
keys.each_slice(chunk_size) do |chunk|
|
|
344
349
|
# Build and write this chunk of requests
|
|
@@ -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
|
@@ -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
|
|
|
@@ -300,7 +300,9 @@ module Dalli
|
|
|
300
300
|
next unless line.start_with?('VA ')
|
|
301
301
|
|
|
302
302
|
tokens = line.chomp!(TERMINATOR).split
|
|
303
|
-
|
|
303
|
+
size = tokens[1].to_i
|
|
304
|
+
response_processor.check_value_size!(size)
|
|
305
|
+
value = @connection_manager.read(size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
304
306
|
stale = response_processor.stale_from_tokens(tokens)
|
|
305
307
|
cas = response_processor.cas_from_tokens(tokens)
|
|
306
308
|
bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
|
|
@@ -327,7 +329,9 @@ module Dalli
|
|
|
327
329
|
|
|
328
330
|
def parse_multi_get_value(line, key_index, is_raw)
|
|
329
331
|
tokens = line.chomp!(TERMINATOR).split
|
|
330
|
-
|
|
332
|
+
size = tokens[1].to_i
|
|
333
|
+
response_processor.check_value_size!(size)
|
|
334
|
+
value = @connection_manager.read(size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
331
335
|
raw_key = tokens[key_index]
|
|
332
336
|
return unless raw_key
|
|
333
337
|
|
|
@@ -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
|
|
@@ -48,12 +48,15 @@ module Dalli
|
|
|
48
48
|
@value_marshaller = value_marshaller
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
# raw: true (a request made with raw: true) returns the value as stored,
|
|
52
|
+
# ignoring any flags in the reply. The request didn't ask for flags, so
|
|
53
|
+
# a reply carrying them anyway must not get the value deserialized.
|
|
54
|
+
def meta_get_with_value(cache_nils: false, raw: false)
|
|
52
55
|
line = read_line
|
|
53
56
|
# A hit ("VA <size> f<flags>") is parsed in place rather than split
|
|
54
57
|
# into tokens, which saves several allocations on the hottest path.
|
|
55
58
|
if line&.start_with?(VA_PREFIX)
|
|
56
|
-
return
|
|
59
|
+
return retrieve(read_data(size_from_va_line(line)), raw ? 0 : bitflags_from_va_line(line), raw)
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
tokens = line&.split || []
|
|
@@ -61,7 +64,7 @@ module Dalli
|
|
|
61
64
|
when EN
|
|
62
65
|
cache_nils ? ::Dalli::NOT_FOUND : nil
|
|
63
66
|
when VA # only reached for an unusually formatted hit line
|
|
64
|
-
|
|
67
|
+
retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
|
|
65
68
|
when HD
|
|
66
69
|
true
|
|
67
70
|
else
|
|
@@ -69,14 +72,14 @@ module Dalli
|
|
|
69
72
|
end
|
|
70
73
|
end
|
|
71
74
|
|
|
72
|
-
def meta_get_with_value_and_cas
|
|
75
|
+
def meta_get_with_value_and_cas(raw: false)
|
|
73
76
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
74
77
|
return [nil, 0] if tokens.first == EN
|
|
75
78
|
|
|
76
79
|
cas = cas_from_tokens(tokens)
|
|
77
80
|
return [nil, cas] unless tokens.first == VA
|
|
78
81
|
|
|
79
|
-
[
|
|
82
|
+
[retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw), cas]
|
|
80
83
|
end
|
|
81
84
|
|
|
82
85
|
def meta_get_without_value
|
|
@@ -96,13 +99,13 @@ module Dalli
|
|
|
96
99
|
# Used by meta_get for comprehensive metadata retrieval.
|
|
97
100
|
# Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
|
|
98
101
|
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
|
|
99
|
-
return_ttl_remaining: false)
|
|
102
|
+
return_ttl_remaining: false, raw: false)
|
|
100
103
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
101
104
|
result = build_metadata_result(tokens)
|
|
102
105
|
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
103
106
|
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
104
107
|
result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
|
|
105
|
-
result[:value] = parse_value_from_tokens(tokens, cache_nils)
|
|
108
|
+
result[:value] = parse_value_from_tokens(tokens, cache_nils, raw)
|
|
106
109
|
result
|
|
107
110
|
end
|
|
108
111
|
|
|
@@ -119,11 +122,11 @@ module Dalli
|
|
|
119
122
|
}
|
|
120
123
|
end
|
|
121
124
|
|
|
122
|
-
def parse_value_from_tokens(tokens, cache_nils)
|
|
125
|
+
def parse_value_from_tokens(tokens, cache_nils, raw = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
123
126
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
124
127
|
return unless tokens.first == VA
|
|
125
128
|
|
|
126
|
-
|
|
129
|
+
retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
|
|
127
130
|
end
|
|
128
131
|
|
|
129
132
|
def meta_set_with_cas
|
|
@@ -214,7 +217,9 @@ module Dalli
|
|
|
214
217
|
# The remaining three values in the array are the ResponseHeader,
|
|
215
218
|
# key, and value.
|
|
216
219
|
##
|
|
217
|
-
|
|
220
|
+
# raw: true returns values as stored, ignoring flags in the replies;
|
|
221
|
+
# see meta_get_with_value.
|
|
222
|
+
def getk_response_from_buffer(buf, offset = 0, raw: false)
|
|
218
223
|
# Find the header terminator starting from offset
|
|
219
224
|
term_idx = buf.byteindex(TERMINATOR, offset)
|
|
220
225
|
return [0] unless term_idx
|
|
@@ -236,7 +241,9 @@ module Dalli
|
|
|
236
241
|
when BYTE_B then base64 ||= token == 'b'
|
|
237
242
|
end
|
|
238
243
|
end
|
|
239
|
-
|
|
244
|
+
# memcached also gives the size positionally ("VA <size> ..."); use it
|
|
245
|
+
# when there's no s flag rather than reading the value as replies
|
|
246
|
+
body_len = size ? flag_int(size, 's') : positional_size(tokens)
|
|
240
247
|
|
|
241
248
|
# A complete response with no body. Only the terminating noop's MN
|
|
242
249
|
# ends the pipeline. Any other bodyless reply (CLIENT_ERROR,
|
|
@@ -256,12 +263,21 @@ module Dalli
|
|
|
256
263
|
# The full response is in our buffer, so parse it and return
|
|
257
264
|
# the values
|
|
258
265
|
body = buf.byteslice(offset + header_len, body_len)
|
|
259
|
-
value =
|
|
260
|
-
|
|
266
|
+
value = retrieve(body, raw ? 0 : flag_int(bitflags, 'f'), raw)
|
|
267
|
+
# A hit with no key can't be matched to a request: skip it (false
|
|
268
|
+
# status) rather than yield it, or take it for the end of the pipeline
|
|
269
|
+
return [false, resp_size] unless key
|
|
270
|
+
|
|
271
|
+
key = key.delete_prefix!('k')
|
|
261
272
|
key = KeyRegularizer.decode(key) if base64
|
|
262
273
|
[tokens.first == VA, flag_int(cas, 'c'), key, value, resp_size]
|
|
263
274
|
end
|
|
264
275
|
|
|
276
|
+
# Values from a raw request are returned as stored
|
|
277
|
+
def retrieve(value, bitflags, raw)
|
|
278
|
+
raw ? value : @value_marshaller.retrieve(value, bitflags)
|
|
279
|
+
end
|
|
280
|
+
|
|
265
281
|
# Integer value of a flag token such as "f123", or 0 when absent.
|
|
266
282
|
# Strips the prefix in place, like value_from_tokens.
|
|
267
283
|
def flag_int(token, flag)
|
|
@@ -352,6 +368,11 @@ module Dalli
|
|
|
352
368
|
|
|
353
369
|
# A pipelined reply claiming an impossible size would otherwise have the
|
|
354
370
|
# buffer wait for (and accumulate) that many bytes
|
|
371
|
+
# The size given right after "VA", or 0 when it isn't a number
|
|
372
|
+
def positional_size(tokens)
|
|
373
|
+
tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
|
|
374
|
+
end
|
|
375
|
+
|
|
355
376
|
def check_value_size!(size)
|
|
356
377
|
return if size.between?(0, MAX_VALUE_BYTES)
|
|
357
378
|
|
|
@@ -368,6 +389,9 @@ module Dalli
|
|
|
368
389
|
end
|
|
369
390
|
|
|
370
391
|
def read_data(data_size)
|
|
392
|
+
# Checked before the terminator is added: -1 or -2 would otherwise
|
|
393
|
+
# read 1 or 0 bytes and leave the connection out of step
|
|
394
|
+
check_value_size!(data_size)
|
|
371
395
|
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
372
396
|
end
|
|
373
397
|
end
|
data/lib/dalli/version.rb
CHANGED