dalli 5.0.9 → 5.0.11
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 +37 -0
- data/README.md +4 -0
- data/lib/dalli/client.rb +43 -20
- data/lib/dalli/compressor.rb +5 -0
- data/lib/dalli/key_manager.rb +6 -3
- data/lib/dalli/protocol/base.rb +61 -4
- data/lib/dalli/protocol/connection_manager.rb +20 -1
- data/lib/dalli/protocol/meta.rb +52 -9
- data/lib/dalli/protocol/response_processor.rb +33 -10
- 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: 43164df9e3e36d747346b72c44ebe1a7a3be0dee501c83b2ef813a39d14d9bb9
|
|
4
|
+
data.tar.gz: e8e1ecbf3d019891da3935c2d0c805f09bfc8a9746df14d9a6954f8cdecfdc10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5b73ad3b6a075030a75f7f7903a097611cfd862f65a80a549d11f1c34c900205ac4ed4783392be4ee139c598b6de346ddaf782460c0ec11e8fc31847b4cb4aa
|
|
7
|
+
data.tar.gz: 5978a35d174e82c4cdc1dafdd439395f64035517f5a4592ee0ad1724e6b9b4241551c5b5990e6a6823d433e68604697bb43940e8bf3b82b15952325264e8b446
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.0.11
|
|
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` and `get_multi_cas` 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
|
+
|
|
18
|
+
5.0.10
|
|
19
|
+
==========
|
|
20
|
+
|
|
21
|
+
Security:
|
|
22
|
+
|
|
23
|
+
- Keep the caller's key when retrying a request (GHSA-m252-9cgf-vx2w)
|
|
24
|
+
- 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
|
|
25
|
+
- 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
|
|
26
|
+
- Complete the fix for per-request `raw: true` on reads (GHSA-wr87-m4jw-29x5)
|
|
27
|
+
- `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
|
|
28
|
+
- 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
|
|
29
|
+
- Affects 5.0.9 and earlier; fixed in 5.2.2, 5.1.4, 5.0.10, 4.3.7 and 3.2.13
|
|
30
|
+
|
|
31
|
+
Bug fixes:
|
|
32
|
+
|
|
33
|
+
- `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
|
|
34
|
+
- 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
|
|
35
|
+
- 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
|
|
36
|
+
- 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
|
|
37
|
+
- 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
|
|
38
|
+
- With a `digest_class` whose digests aren't short hex strings, shortening a long key could loop forever. It now raises `ArgumentError`
|
|
39
|
+
|
|
40
|
+
Development:
|
|
41
|
+
|
|
42
|
+
- 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)
|
|
43
|
+
|
|
7
44
|
5.0.9
|
|
8
45
|
==========
|
|
9
46
|
|
data/README.md
CHANGED
|
@@ -58,6 +58,10 @@ Per-request `raw: true` returns the stored bytes without deserializing or decomp
|
|
|
58
58
|
|
|
59
59
|
See the [5.0-Upgrade.md](5.0-Upgrade.md) guide for upgrade information.
|
|
60
60
|
|
|
61
|
+
### Counters and verify_reply_keys
|
|
62
|
+
|
|
63
|
+
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.0.11, 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.
|
|
64
|
+
|
|
61
65
|
## OpenTelemetry Tracing
|
|
62
66
|
|
|
63
67
|
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)
|
|
@@ -126,14 +131,18 @@ module Dalli
|
|
|
126
131
|
# # => { value: "data", cas: 123, hit_before: true, last_access: 42 }
|
|
127
132
|
#
|
|
128
133
|
def get_with_metadata(key, options = {})
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
# A retry re-runs this method with its original arguments, so `key` must
|
|
135
|
+
# stay as the caller gave it; reassigning it would add the namespace again.
|
|
136
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
131
137
|
|
|
132
|
-
server = ring.server_for_key(
|
|
133
|
-
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata',
|
|
134
|
-
server.request(:meta_get,
|
|
138
|
+
server = ring.server_for_key(validated_key)
|
|
139
|
+
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', validated_key, server)) do
|
|
140
|
+
server.request(:meta_get, validated_key, options)
|
|
135
141
|
end
|
|
136
|
-
|
|
142
|
+
# Only a retryable error is retried. The NetworkError raised when a server
|
|
143
|
+
# is marked down is final, as in #perform; retrying it looped forever once
|
|
144
|
+
# down_retry_delay let the same server be tried again.
|
|
145
|
+
rescue RetryableNetworkError => e
|
|
137
146
|
Dalli.logger.debug { e.inspect }
|
|
138
147
|
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
139
148
|
retry
|
|
@@ -225,14 +234,17 @@ module Dalli
|
|
|
225
234
|
|
|
226
235
|
validate_integer!(:lock_ttl, lock_ttl)
|
|
227
236
|
validate_integer!(:recache_threshold, recache_threshold)
|
|
228
|
-
|
|
229
|
-
|
|
237
|
+
# Left unchanged for the retry below; see get_with_metadata
|
|
238
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
230
239
|
|
|
231
|
-
server = ring.server_for_key(
|
|
232
|
-
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock',
|
|
233
|
-
fetch_with_lock_request(
|
|
240
|
+
server = ring.server_for_key(validated_key)
|
|
241
|
+
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', validated_key, server)) do
|
|
242
|
+
fetch_with_lock_request(validated_key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
234
243
|
end
|
|
235
|
-
|
|
244
|
+
# Only a retryable error is retried. The NetworkError raised when a server
|
|
245
|
+
# is marked down is final, as in #perform; retrying it looped forever once
|
|
246
|
+
# down_retry_delay let the same server be tried again.
|
|
247
|
+
rescue RetryableNetworkError => e
|
|
236
248
|
Dalli.logger.debug { e.inspect }
|
|
237
249
|
Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
|
|
238
250
|
retry
|
|
@@ -565,6 +577,9 @@ module Dalli
|
|
|
565
577
|
result
|
|
566
578
|
rescue Dalli::NetworkError
|
|
567
579
|
{}
|
|
580
|
+
rescue Dalli::UnexpectedReplyKeyError => e
|
|
581
|
+
Dalli.logger.warn { e.message }
|
|
582
|
+
{}
|
|
568
583
|
end
|
|
569
584
|
|
|
570
585
|
def single_server_set_multi(hash, ttl, req_options)
|
|
@@ -637,7 +652,8 @@ module Dalli
|
|
|
637
652
|
end
|
|
638
653
|
|
|
639
654
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
640
|
-
|
|
655
|
+
# req_options reaches the read too, so raw: true returns the stored bytes
|
|
656
|
+
(value, cas) = perform(:cas, key, req_options)
|
|
641
657
|
return if value.nil? && !always_set
|
|
642
658
|
|
|
643
659
|
newvalue = yield(value)
|
|
@@ -646,7 +662,12 @@ module Dalli
|
|
|
646
662
|
|
|
647
663
|
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
648
664
|
server = ring.server_for_key(key)
|
|
649
|
-
|
|
665
|
+
# req_options (e.g. raw: true) reaches the read too. It's the base, not
|
|
666
|
+
# the override: fetch_with_lock's own lock_ttl/recache_threshold win.
|
|
667
|
+
meta_options = req_options.is_a?(Hash) ? req_options.dup : {}
|
|
668
|
+
meta_options[:vivify_ttl] = lock_ttl
|
|
669
|
+
meta_options[:recache_ttl] = recache_threshold
|
|
670
|
+
result = server.request(:meta_get, key, meta_options)
|
|
650
671
|
|
|
651
672
|
return result[:value] unless result[:won_recache]
|
|
652
673
|
|
|
@@ -684,18 +705,20 @@ module Dalli
|
|
|
684
705
|
# rubocop:enable Naming/MethodParameterName
|
|
685
706
|
return yield if block_given?
|
|
686
707
|
|
|
687
|
-
|
|
688
|
-
|
|
708
|
+
# `retry` below re-runs this method with its original arguments, so `key`
|
|
709
|
+
# must stay as the caller gave it: reassigning it would apply the
|
|
710
|
+
# namespace a second time and send the retry to a different key.
|
|
711
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
689
712
|
|
|
690
|
-
server = ring.server_for_key(
|
|
713
|
+
server = ring.server_for_key(validated_key)
|
|
691
714
|
|
|
692
715
|
if Instrumentation.enabled?
|
|
693
716
|
op_name = op.name
|
|
694
|
-
Instrumentation.trace(op_name, trace_attrs(op_name,
|
|
695
|
-
server.request(op,
|
|
717
|
+
Instrumentation.trace(op_name, trace_attrs(op_name, validated_key, server)) do
|
|
718
|
+
server.request(op, validated_key, ...)
|
|
696
719
|
end
|
|
697
720
|
else
|
|
698
|
-
server.request(op,
|
|
721
|
+
server.request(op, validated_key, ...)
|
|
699
722
|
end
|
|
700
723
|
rescue RetryableNetworkError => e
|
|
701
724
|
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
|
@@ -25,6 +25,7 @@ module Dalli
|
|
|
25
25
|
warn_uri_credentials(user_creds)
|
|
26
26
|
@options = client_options.merge(user_creds)
|
|
27
27
|
@raw_mode = client_options[:raw]
|
|
28
|
+
@verify_reply_keys = client_options[:verify_reply_keys] ? true : false
|
|
28
29
|
@value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
29
30
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
30
31
|
end
|
|
@@ -118,6 +119,8 @@ module Dalli
|
|
|
118
119
|
# avoiding intermediate Hash allocation. Returns nil.
|
|
119
120
|
# Without a block, returns a Hash of { key => [value, cas] }.
|
|
120
121
|
def pipeline_next_responses(&block)
|
|
122
|
+
return abandon_pipeline if @pipeline_out_of_step
|
|
123
|
+
|
|
121
124
|
reconnect_on_pipeline_complete!
|
|
122
125
|
values = nil
|
|
123
126
|
|
|
@@ -139,6 +142,13 @@ module Dalli
|
|
|
139
142
|
# to the noop at the end of the pipeline
|
|
140
143
|
finish_pipeline && break if status && key.nil?
|
|
141
144
|
|
|
145
|
+
# A key that wasn't requested means the replies are out of step
|
|
146
|
+
# (see expect_reply_keys): keep what was read so far, drop the rest
|
|
147
|
+
if unexpected_reply_key?(key)
|
|
148
|
+
abandon_pipeline
|
|
149
|
+
break
|
|
150
|
+
end
|
|
151
|
+
|
|
142
152
|
# If the status is ok and the key is not nil, then this is a
|
|
143
153
|
# getkq response with a value that we want to set in the response hash
|
|
144
154
|
unless key.nil?
|
|
@@ -168,10 +178,13 @@ module Dalli
|
|
|
168
178
|
@connection_manager.abort_request!
|
|
169
179
|
return true unless connected?
|
|
170
180
|
|
|
171
|
-
# Closes the connection, which ensures that our connection
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
# Closes the connection, which ensures that our connection is in a
|
|
182
|
+
# clean state for future requests. The get_multi ran out of time
|
|
183
|
+
# overall rather than hitting a socket error, so it doesn't count
|
|
184
|
+
# toward socket_max_failures: two slow get_multis in a row would
|
|
185
|
+
# otherwise mark a healthy server down.
|
|
186
|
+
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
187
|
+
@connection_manager.close
|
|
175
188
|
true
|
|
176
189
|
end
|
|
177
190
|
|
|
@@ -262,6 +275,7 @@ module Dalli
|
|
|
262
275
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
263
276
|
# the expected state before pipeline_response_setup is called.
|
|
264
277
|
response_buffer.clear
|
|
278
|
+
expect_reply_keys(keys)
|
|
265
279
|
|
|
266
280
|
req = +''
|
|
267
281
|
keys.each do |key|
|
|
@@ -277,6 +291,7 @@ module Dalli
|
|
|
277
291
|
def pipelined_get_interleaved(keys, chunk_size, results)
|
|
278
292
|
# Initialize the response buffer for draining during send phase
|
|
279
293
|
response_buffer.ensure_ready
|
|
294
|
+
expect_reply_keys(keys)
|
|
280
295
|
|
|
281
296
|
keys.each_slice(chunk_size) do |chunk|
|
|
282
297
|
# Build and write this chunk of requests
|
|
@@ -309,6 +324,12 @@ module Dalli
|
|
|
309
324
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
310
325
|
break if status.nil? # No complete response available
|
|
311
326
|
|
|
327
|
+
# Handled by the next pipeline_next_responses, once sending is done
|
|
328
|
+
if unexpected_reply_key?(key)
|
|
329
|
+
@pipeline_out_of_step = true
|
|
330
|
+
break
|
|
331
|
+
end
|
|
332
|
+
|
|
312
333
|
results[key] = [value, cas] unless key.nil?
|
|
313
334
|
end
|
|
314
335
|
rescue SystemCallError, Dalli::NetworkError
|
|
@@ -320,6 +341,42 @@ module Dalli
|
|
|
320
341
|
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
321
342
|
end
|
|
322
343
|
|
|
344
|
+
# A multi-get only accepts replies for the keys it asked for. memcached
|
|
345
|
+
# names an item created by auto-create (ma or mg with N) for a
|
|
346
|
+
# base64-encoded key by its raw bytes, without the b flag; such a key
|
|
347
|
+
# can contain spaces or CR/LF, which split the reply header and make the
|
|
348
|
+
# replies after it parse as different ones. A key that wasn't requested
|
|
349
|
+
# is how that shows up.
|
|
350
|
+
# Only with the verify_reply_keys option, since building the set costs a
|
|
351
|
+
# multi-get about 15%. Dalli itself no longer creates such items (see
|
|
352
|
+
# Meta#decr_incr); the check guards against ones that older versions or
|
|
353
|
+
# other clients created, on memcached versions with the behavior.
|
|
354
|
+
def expect_reply_keys(keys)
|
|
355
|
+
@pipeline_out_of_step = false
|
|
356
|
+
@expected_reply_keys = (keys.to_set { |k| reply_key_form(k) } if @verify_reply_keys)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def unexpected_reply_key?(key)
|
|
360
|
+
!key.nil? && @expected_reply_keys && !@expected_reply_keys.include?(reply_key_form(key))
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Keys compared as bytes; ASCII keys compare equal in any encoding as is
|
|
364
|
+
def reply_key_form(key)
|
|
365
|
+
key.ascii_only? ? key : key.b
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Keeps what was read, drops the rest of this server's replies, and
|
|
369
|
+
# closes the connection so nothing unread is left on it. Not a socket
|
|
370
|
+
# failure, so it doesn't count toward socket_max_failures; the keys not
|
|
371
|
+
# read are simply missing from the result.
|
|
372
|
+
def abandon_pipeline
|
|
373
|
+
Dalli.logger.warn { "#{name} replied to a get_multi with a key that wasn't requested; closing the connection" }
|
|
374
|
+
@pipeline_out_of_step = false
|
|
375
|
+
response_buffer.clear
|
|
376
|
+
@connection_manager.close
|
|
377
|
+
{}
|
|
378
|
+
end
|
|
379
|
+
|
|
323
380
|
# Called after the noop response is received at the end of a set
|
|
324
381
|
# of pipelined gets
|
|
325
382
|
def finish_pipeline
|
|
@@ -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
|
|
|
@@ -193,12 +193,44 @@ module Dalli
|
|
|
193
193
|
|
|
194
194
|
def decr_incr(incr, key, delta, ttl, initial)
|
|
195
195
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
196
|
+
# memcached stores an item created by ma's auto-create (N/J) for a
|
|
197
|
+
# base64-encoded key without its binary-key flag, and later replies
|
|
198
|
+
# name it with the raw key bytes. Create such counters with an add.
|
|
199
|
+
return decr_incr_with_add(incr, key, delta, ttl, initial) if initial && KeyRegularizer.required?(key)
|
|
200
|
+
|
|
196
201
|
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
197
202
|
quiet: quiet?))
|
|
198
203
|
@connection_manager.flush unless quiet?
|
|
199
204
|
response_processor.decr_incr unless quiet?
|
|
200
205
|
end
|
|
201
206
|
|
|
207
|
+
# Like decr_incr with an initial value, without memcached's auto-create:
|
|
208
|
+
# on a miss the counter is added with the initial value (which is
|
|
209
|
+
# returned, as auto-create would), and the add losing a race to another
|
|
210
|
+
# client means the counter now exists, so the incr is tried again. In a
|
|
211
|
+
# quiet block both requests are sent and whichever applies takes effect.
|
|
212
|
+
def decr_incr_with_add(incr, key, delta, ttl, initial)
|
|
213
|
+
add_options = { raw: true } # counters are stored raw
|
|
214
|
+
return quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options) if quiet?
|
|
215
|
+
|
|
216
|
+
2.times do
|
|
217
|
+
value = decr_incr(incr, key, delta, nil, nil)
|
|
218
|
+
return value unless value.nil?
|
|
219
|
+
|
|
220
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: false)
|
|
221
|
+
return initial if response_processor.meta_set_with_cas
|
|
222
|
+
end
|
|
223
|
+
decr_incr(incr, key, delta, nil, nil)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
227
|
+
def quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options)
|
|
228
|
+
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: nil, incr: incr, ttl: nil,
|
|
229
|
+
quiet: true))
|
|
230
|
+
# Sent with the rest of the quiet block, by its closing noop
|
|
231
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: true)
|
|
232
|
+
end
|
|
233
|
+
|
|
202
234
|
# Other Commands
|
|
203
235
|
def flush(delay = 0)
|
|
204
236
|
write(RequestFormatter.flush(delay: delay))
|
|
@@ -240,10 +272,11 @@ module Dalli
|
|
|
240
272
|
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw)
|
|
241
273
|
flushed_write(buffer)
|
|
242
274
|
buffer.clear
|
|
243
|
-
read_multi_get_responses(is_raw)
|
|
275
|
+
read_multi_get_responses(is_raw, keys)
|
|
244
276
|
end
|
|
245
277
|
|
|
246
|
-
def read_multi_get_responses(is_raw)
|
|
278
|
+
def read_multi_get_responses(is_raw, keys)
|
|
279
|
+
expect_reply_keys(keys)
|
|
247
280
|
hash = {}
|
|
248
281
|
key_index = is_raw ? 2 : 3
|
|
249
282
|
while (line = @connection_manager.read_line)
|
|
@@ -251,14 +284,24 @@ module Dalli
|
|
|
251
284
|
next unless line.start_with?('VA ')
|
|
252
285
|
|
|
253
286
|
key, value = parse_multi_get_value(line, key_index, is_raw)
|
|
287
|
+
raise_unexpected_reply_key(key) if unexpected_reply_key?(key)
|
|
254
288
|
hash[key] = value if key
|
|
255
289
|
end
|
|
256
290
|
hash
|
|
257
291
|
end
|
|
258
292
|
|
|
293
|
+
# The replies after a key that wasn't requested can't be trusted (see
|
|
294
|
+
# Base#expect_reply_keys). Raising ends the request, which closes the
|
|
295
|
+
# connection; the caller treats this server's keys as missing.
|
|
296
|
+
def raise_unexpected_reply_key(key)
|
|
297
|
+
raise Dalli::UnexpectedReplyKeyError, "#{name} replied with a key that wasn't requested: #{key.inspect}"
|
|
298
|
+
end
|
|
299
|
+
|
|
259
300
|
def parse_multi_get_value(line, key_index, is_raw)
|
|
260
301
|
tokens = line.chomp!(TERMINATOR).split
|
|
261
|
-
|
|
302
|
+
size = tokens[1].to_i
|
|
303
|
+
response_processor.check_value_size!(size)
|
|
304
|
+
value = @connection_manager.read(size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
262
305
|
raw_key = tokens[key_index]
|
|
263
306
|
return unless raw_key
|
|
264
307
|
|
|
@@ -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
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.0.
|
|
4
|
+
version: 5.0.11
|
|
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: []
|