dalli 4.3.6 → 4.3.8
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 +41 -0
- data/README.md +4 -0
- data/lib/dalli/client.rb +32 -14
- data/lib/dalli/compressor.rb +5 -0
- data/lib/dalli/key_manager.rb +6 -3
- data/lib/dalli/protocol/base.rb +62 -4
- data/lib/dalli/protocol/binary/response_processor.rb +3 -2
- data/lib/dalli/protocol/binary.rb +2 -2
- data/lib/dalli/protocol/connection_manager.rb +30 -4
- data/lib/dalli/protocol/meta/response_processor.rb +32 -9
- data/lib/dalli/protocol/meta.rb +40 -6
- data/lib/dalli/version.rb +1 -1
- 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: 7cddc5e9ac0bd88077676d94695d72804971c1b5a65522e3f8bff05b9b35f75a
|
|
4
|
+
data.tar.gz: 7c506e73dd7b0c71864aaa99bb35537642dd966ad692101556be733050891c1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db49da025cb40d2902dbd394d9c2ee1f3839e41b912f1e3d8a2f3f0f096e409e4ea564a8f9783677b00b5307aef4c64951818b385af66d8c958103a36e89f076
|
|
7
|
+
data.tar.gz: a8d55c26b215141a5af0f59782e1e8a3443f07b73a27175bc39cde8bda798c176e88c658b8cebad14c52198d974fd30ae6e859f321102cdf6d738b7157fc5bbe
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
4.3.8
|
|
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 `protocol: :meta` 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
|
+
4.3.7
|
|
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 `get_with_metadata` and `fetch_with_lock` since 4.1.0 (single-key operations on 4.x aren't affected); 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` and `cas!` (both protocols) and `fetch_with_lock` (meta protocol) 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. Meta protocol raw reads now ignore flags in the reply
|
|
29
|
+
- Affects 4.3.6 and earlier; fixed in 5.2.2, 5.1.4, 5.0.10, 4.3.7 and 3.2.13
|
|
30
|
+
- Complete the fork-safety fix (GHSA-w39f-xq2m-4g8x)
|
|
31
|
+
- Fork detection used a pid cache refreshed by a `Process._fork` hook. When another library's fork hook ran first in the child (connection_pool, loaded before Dalli as in Rails, closes its connections there), Dalli didn't know it was in a forked child and ended the parent's TLS session. It now checks `Process.pid`
|
|
32
|
+
- 4.3.6's write buffer kept a reference to the caller's value string with the meta protocol, so changing the string before the buffer was sent (for example, reusing one buffer inside a `quiet` block) changed the bytes sent, losing values or adding commands. The buffer now holds a copy
|
|
33
|
+
- Fixed in 4.3.7 and 3.2.13
|
|
34
|
+
|
|
35
|
+
Bug fixes:
|
|
36
|
+
|
|
37
|
+
- `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
|
|
38
|
+
- Fix a regression in 4.3.6: 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
|
|
39
|
+
- Fix a regression in 4.3.6: 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
|
|
40
|
+
- 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
|
|
41
|
+
- 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
|
|
42
|
+
- With a `digest_class` whose digests aren't short hex strings, shortening a long key could loop forever. It now raises `ArgumentError`
|
|
43
|
+
|
|
44
|
+
Development:
|
|
45
|
+
|
|
46
|
+
- 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)
|
|
47
|
+
|
|
7
48
|
4.3.6
|
|
8
49
|
==========
|
|
9
50
|
|
data/README.md
CHANGED
|
@@ -82,6 +82,10 @@ Per-request `raw: true` returns the stored bytes without deserializing or decomp
|
|
|
82
82
|
|
|
83
83
|
See the [4.0-Upgrade.md](4.0-Upgrade.md) guide for more information.
|
|
84
84
|
|
|
85
|
+
### Counters and verify_reply_keys
|
|
86
|
+
|
|
87
|
+
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 (with `protocol: :meta`). Since 4.3.8, 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.
|
|
88
|
+
|
|
85
89
|
## OpenTelemetry Tracing
|
|
86
90
|
|
|
87
91
|
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
|
@@ -53,6 +53,11 @@ module Dalli
|
|
|
53
53
|
# +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
|
|
54
54
|
# +nil+ (default) omits the attribute entirely.
|
|
55
55
|
# - :otel_peer_service - when set, adds a +peer.service+ span attribute with this value for logical service naming.
|
|
56
|
+
# - :verify_reply_keys - defaults to false. When true, get_multi and get_multi_cas only accept replies for the keys
|
|
57
|
+
# they requested; after a reply naming another key, the rest of that server's replies are
|
|
58
|
+
# treated as misses. With the meta protocol, memcached can name an item by its raw key bytes
|
|
59
|
+
# when the item was created by auto-create from a base64-encoded key, which Dalli no longer
|
|
60
|
+
# does. Costs a multi-get about 15%.
|
|
56
61
|
#
|
|
57
62
|
def initialize(servers = nil, options = {})
|
|
58
63
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
@@ -134,14 +139,18 @@ module Dalli
|
|
|
134
139
|
def get_with_metadata(key, options = {})
|
|
135
140
|
raise_unless_meta_protocol!
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
# A retry re-runs this method with its original arguments, so `key` must
|
|
143
|
+
# stay as the caller gave it; reassigning it would add the namespace again.
|
|
144
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
139
145
|
|
|
140
|
-
server = ring.server_for_key(
|
|
141
|
-
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata',
|
|
142
|
-
server.request(:meta_get,
|
|
146
|
+
server = ring.server_for_key(validated_key)
|
|
147
|
+
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', validated_key, server)) do
|
|
148
|
+
server.request(:meta_get, validated_key, options)
|
|
143
149
|
end
|
|
144
|
-
|
|
150
|
+
# Only a retryable error is retried. The NetworkError raised when a server
|
|
151
|
+
# is marked down is final, as in #perform; retrying it looped forever once
|
|
152
|
+
# down_retry_delay let the same server be tried again.
|
|
153
|
+
rescue RetryableNetworkError => e
|
|
145
154
|
Dalli.logger.debug { e.inspect }
|
|
146
155
|
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
147
156
|
retry
|
|
@@ -238,14 +247,17 @@ module Dalli
|
|
|
238
247
|
validate_integer!(:lock_ttl, lock_ttl)
|
|
239
248
|
validate_integer!(:recache_threshold, recache_threshold)
|
|
240
249
|
|
|
241
|
-
|
|
242
|
-
|
|
250
|
+
# Left unchanged for the retry below; see get_with_metadata
|
|
251
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
243
252
|
|
|
244
|
-
server = ring.server_for_key(
|
|
245
|
-
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock',
|
|
246
|
-
fetch_with_lock_request(
|
|
253
|
+
server = ring.server_for_key(validated_key)
|
|
254
|
+
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', validated_key, server)) do
|
|
255
|
+
fetch_with_lock_request(validated_key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
247
256
|
end
|
|
248
|
-
|
|
257
|
+
# Only a retryable error is retried. The NetworkError raised when a server
|
|
258
|
+
# is marked down is final, as in #perform; retrying it looped forever once
|
|
259
|
+
# down_retry_delay let the same server be tried again.
|
|
260
|
+
rescue RetryableNetworkError => e
|
|
249
261
|
Dalli.logger.debug { e.inspect }
|
|
250
262
|
Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
|
|
251
263
|
retry
|
|
@@ -592,7 +604,8 @@ module Dalli
|
|
|
592
604
|
end
|
|
593
605
|
|
|
594
606
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
595
|
-
|
|
607
|
+
# req_options reaches the read too, so raw: true returns the stored bytes
|
|
608
|
+
(value, cas) = perform(:cas, key, req_options)
|
|
596
609
|
return if value.nil? && !always_set
|
|
597
610
|
|
|
598
611
|
newvalue = yield(value)
|
|
@@ -601,7 +614,12 @@ module Dalli
|
|
|
601
614
|
|
|
602
615
|
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
603
616
|
server = ring.server_for_key(key)
|
|
604
|
-
|
|
617
|
+
# req_options (e.g. raw: true) reaches the read too. It's the base, not
|
|
618
|
+
# the override: fetch_with_lock's own lock_ttl/recache_threshold win.
|
|
619
|
+
meta_options = req_options.is_a?(Hash) ? req_options.dup : {}
|
|
620
|
+
meta_options[:vivify_ttl] = lock_ttl
|
|
621
|
+
meta_options[:recache_ttl] = recache_threshold
|
|
622
|
+
result = server.request(:meta_get, key, meta_options)
|
|
605
623
|
|
|
606
624
|
return result[:value] unless result[:won_recache]
|
|
607
625
|
|
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
|
@@ -125,15 +125,18 @@ module Dalli
|
|
|
125
125
|
##
|
|
126
126
|
def truncated_key(key)
|
|
127
127
|
digest = digest_class.hexdigest(key)
|
|
128
|
-
prefix = key[0, prefix_length(digest)]
|
|
128
|
+
prefix = key[0, [prefix_length(digest), 0].max]
|
|
129
129
|
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
130
130
|
# A prefix with whitespace or non-ASCII characters sends the whole key
|
|
131
131
|
# base64-encoded, which can still be too long on the wire; shorten it.
|
|
132
|
-
while wire_length(truncated) > MAX_KEY_LENGTH
|
|
132
|
+
while wire_length(truncated) > MAX_KEY_LENGTH && !prefix.empty?
|
|
133
133
|
prefix = prefix[0...-1]
|
|
134
134
|
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
135
135
|
end
|
|
136
|
-
truncated
|
|
136
|
+
return truncated if wire_length(truncated) <= MAX_KEY_LENGTH
|
|
137
|
+
|
|
138
|
+
# Only possible with a digest_class whose digests aren't short hex strings
|
|
139
|
+
raise ArgumentError, "#{digest_class} digests are too long to fit in a truncated key"
|
|
137
140
|
end
|
|
138
141
|
|
|
139
142
|
# memcached's key limit applies to the key as sent: in bytes, and, with
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'forwardable'
|
|
4
|
+
require 'set'
|
|
4
5
|
require 'socket'
|
|
5
6
|
require 'timeout'
|
|
6
7
|
|
|
@@ -24,6 +25,7 @@ module Dalli
|
|
|
24
25
|
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
|
25
26
|
@options = client_options.merge(user_creds)
|
|
26
27
|
@raw_mode = client_options[:raw]
|
|
28
|
+
@verify_reply_keys = client_options[:verify_reply_keys] ? true : false
|
|
27
29
|
@value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
28
30
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
29
31
|
end
|
|
@@ -103,6 +105,8 @@ module Dalli
|
|
|
103
105
|
# Without a block, returns a Hash of { key => [value, cas] }.
|
|
104
106
|
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
105
107
|
def pipeline_next_responses(&block)
|
|
108
|
+
return abandon_pipeline if @pipeline_out_of_step
|
|
109
|
+
|
|
106
110
|
reconnect_on_pipeline_complete!
|
|
107
111
|
values = nil
|
|
108
112
|
|
|
@@ -124,6 +128,13 @@ module Dalli
|
|
|
124
128
|
# to the noop at the end of the pipeline
|
|
125
129
|
finish_pipeline && break if status && key.nil?
|
|
126
130
|
|
|
131
|
+
# A key that wasn't requested means the replies are out of step
|
|
132
|
+
# (see expect_reply_keys): keep what was read so far, drop the rest
|
|
133
|
+
if unexpected_reply_key?(key)
|
|
134
|
+
abandon_pipeline
|
|
135
|
+
break
|
|
136
|
+
end
|
|
137
|
+
|
|
127
138
|
# If the status is ok and the key is not nil, then this is a
|
|
128
139
|
# getkq response with a value that we want to set in the response hash
|
|
129
140
|
unless key.nil?
|
|
@@ -152,10 +163,13 @@ module Dalli
|
|
|
152
163
|
@connection_manager.abort_request!
|
|
153
164
|
return true unless connected?
|
|
154
165
|
|
|
155
|
-
# Closes the connection, which ensures that our connection
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
# Closes the connection, which ensures that our connection is in a
|
|
167
|
+
# clean state for future requests. The get_multi ran out of time
|
|
168
|
+
# overall rather than hitting a socket error, so it doesn't count
|
|
169
|
+
# toward socket_max_failures: two slow get_multis in a row would
|
|
170
|
+
# otherwise mark a healthy server down.
|
|
171
|
+
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
172
|
+
@connection_manager.close
|
|
159
173
|
true
|
|
160
174
|
end
|
|
161
175
|
|
|
@@ -249,6 +263,7 @@ module Dalli
|
|
|
249
263
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
250
264
|
# the expected state before pipeline_response_setup is called.
|
|
251
265
|
response_buffer.clear
|
|
266
|
+
expect_reply_keys(keys)
|
|
252
267
|
|
|
253
268
|
req = +''
|
|
254
269
|
keys.each do |key|
|
|
@@ -264,6 +279,7 @@ module Dalli
|
|
|
264
279
|
def pipelined_get_interleaved(keys, chunk_size, results)
|
|
265
280
|
# Initialize the response buffer for draining during send phase
|
|
266
281
|
response_buffer.ensure_ready
|
|
282
|
+
expect_reply_keys(keys)
|
|
267
283
|
|
|
268
284
|
keys.each_slice(chunk_size) do |chunk|
|
|
269
285
|
# Build and write this chunk of requests
|
|
@@ -296,6 +312,12 @@ module Dalli
|
|
|
296
312
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
297
313
|
break if status.nil? # No complete response available
|
|
298
314
|
|
|
315
|
+
# Handled by the next pipeline_next_responses, once sending is done
|
|
316
|
+
if unexpected_reply_key?(key)
|
|
317
|
+
@pipeline_out_of_step = true
|
|
318
|
+
break
|
|
319
|
+
end
|
|
320
|
+
|
|
299
321
|
results[key] = [value, cas] unless key.nil?
|
|
300
322
|
end
|
|
301
323
|
rescue SystemCallError, Dalli::NetworkError
|
|
@@ -307,6 +329,42 @@ module Dalli
|
|
|
307
329
|
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
308
330
|
end
|
|
309
331
|
|
|
332
|
+
# A multi-get only accepts replies for the keys it asked for. memcached
|
|
333
|
+
# names an item created by auto-create (ma or mg with N) for a
|
|
334
|
+
# base64-encoded key by its raw bytes, without the b flag; such a key
|
|
335
|
+
# can contain spaces or CR/LF, which split the reply header and make the
|
|
336
|
+
# replies after it parse as different ones. A key that wasn't requested
|
|
337
|
+
# is how that shows up.
|
|
338
|
+
# Only with the verify_reply_keys option, since building the set costs a
|
|
339
|
+
# multi-get about 15%. Dalli itself no longer creates such items (see
|
|
340
|
+
# Meta#decr_incr); the check guards against ones that older versions or
|
|
341
|
+
# other clients created, on memcached versions with the behavior.
|
|
342
|
+
def expect_reply_keys(keys)
|
|
343
|
+
@pipeline_out_of_step = false
|
|
344
|
+
@expected_reply_keys = (keys.to_set { |k| reply_key_form(k) } if @verify_reply_keys)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def unexpected_reply_key?(key)
|
|
348
|
+
!key.nil? && @expected_reply_keys && !@expected_reply_keys.include?(reply_key_form(key))
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Keys compared as bytes; ASCII keys compare equal in any encoding as is
|
|
352
|
+
def reply_key_form(key)
|
|
353
|
+
key.ascii_only? ? key : key.b
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Keeps what was read, drops the rest of this server's replies, and
|
|
357
|
+
# closes the connection so nothing unread is left on it. Not a socket
|
|
358
|
+
# failure, so it doesn't count toward socket_max_failures; the keys not
|
|
359
|
+
# read are simply missing from the result.
|
|
360
|
+
def abandon_pipeline
|
|
361
|
+
Dalli.logger.warn { "#{name} replied to a get_multi with a key that wasn't requested; closing the connection" }
|
|
362
|
+
@pipeline_out_of_step = false
|
|
363
|
+
response_buffer.clear
|
|
364
|
+
@connection_manager.close
|
|
365
|
+
{}
|
|
366
|
+
end
|
|
367
|
+
|
|
310
368
|
# Called after the noop response is received at the end of a set
|
|
311
369
|
# of pipelined gets
|
|
312
370
|
def finish_pipeline
|
|
@@ -102,7 +102,8 @@ module Dalli
|
|
|
102
102
|
true
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
# raw: true returns the value as stored; see #get
|
|
106
|
+
def data_cas_response(raw: false)
|
|
106
107
|
resp_header, body = read_response
|
|
107
108
|
return [nil, resp_header.cas] if resp_header.not_found?
|
|
108
109
|
return [nil, false] if resp_header.not_stored?
|
|
@@ -110,7 +111,7 @@ module Dalli
|
|
|
110
111
|
raise_on_not_ok!(resp_header)
|
|
111
112
|
return [nil, resp_header.cas] unless body
|
|
112
113
|
|
|
113
|
-
[unpack_response_body(resp_header, body,
|
|
114
|
+
[unpack_response_body(resp_header, body, !raw).last, resp_header.cas]
|
|
114
115
|
end
|
|
115
116
|
|
|
116
117
|
# Returns the new value for the key, if found and updated
|
|
@@ -47,11 +47,11 @@ module Dalli
|
|
|
47
47
|
|
|
48
48
|
# TODO: This is confusing, as there's a cas command in memcached
|
|
49
49
|
# and this isn't it. Maybe rename? Maybe eliminate?
|
|
50
|
-
def cas(key)
|
|
50
|
+
def cas(key, options = nil)
|
|
51
51
|
req = RequestFormatter.standard_request(opkey: :get, key: key)
|
|
52
52
|
write(req)
|
|
53
53
|
@connection_manager.flush
|
|
54
|
-
response_processor.data_cas_response
|
|
54
|
+
response_processor.data_cas_response(raw: raw_request?(options))
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Storage Commands
|
|
@@ -58,7 +58,7 @@ module Dalli
|
|
|
58
58
|
@sock = memcached_socket
|
|
59
59
|
# Writes are buffered in @write_buffer instead; see WRITE_BUFFER_FLUSH_BYTES
|
|
60
60
|
@sock.sync = true
|
|
61
|
-
@pid =
|
|
61
|
+
@pid = Process.pid
|
|
62
62
|
@request_in_progress = false
|
|
63
63
|
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
|
|
64
64
|
# SocketError = DNS resolution failure
|
|
@@ -204,7 +204,10 @@ module Dalli
|
|
|
204
204
|
WRITE_BUFFER_FLUSH_BYTES = 64 * 1024
|
|
205
205
|
|
|
206
206
|
def write(bytes)
|
|
207
|
-
|
|
207
|
+
# A copy: the caller may change its string before the buffer is sent
|
|
208
|
+
# (the meta protocol writes a value string as its own part), as Ruby's
|
|
209
|
+
# IO buffer copied on write
|
|
210
|
+
@write_buffer << (bytes.frozen? ? bytes : bytes.dup)
|
|
208
211
|
@write_buffer_bytes += bytes.bytesize
|
|
209
212
|
flush_write_buffer if @write_buffer_bytes >= WRITE_BUFFER_FLUSH_BYTES
|
|
210
213
|
bytes.bytesize
|
|
@@ -282,7 +285,11 @@ module Dalli
|
|
|
282
285
|
end
|
|
283
286
|
|
|
284
287
|
def fork_detected?
|
|
285
|
-
|
|
288
|
+
# Process.pid rather than PIDCache: PIDCache is refreshed by a
|
|
289
|
+
# Process._fork hook, and another library's fork hook (such as
|
|
290
|
+
# connection_pool closing its connections) can run in the child before
|
|
291
|
+
# it, which would close the parent's TLS session as if it were ours.
|
|
292
|
+
@pid && @pid != Process.pid
|
|
286
293
|
end
|
|
287
294
|
|
|
288
295
|
def log_down_detected
|
|
@@ -306,13 +313,32 @@ module Dalli
|
|
|
306
313
|
|
|
307
314
|
private
|
|
308
315
|
|
|
316
|
+
# One write per flush. A TLS socket sends each argument of
|
|
317
|
+
# write(*parts) as its own record and system call, so the parts are
|
|
318
|
+
# joined first.
|
|
309
319
|
def flush_write_buffer
|
|
310
320
|
return if @write_buffer.empty?
|
|
311
321
|
|
|
312
|
-
@sock.write(
|
|
322
|
+
@sock.write(@write_buffer.size == 1 ? @write_buffer.first : joined_write_buffer)
|
|
313
323
|
discard_write_buffer
|
|
314
324
|
end
|
|
315
325
|
|
|
326
|
+
# Joined as bytes: requests can mix UTF-8 and binary strings, which
|
|
327
|
+
# String#<< refuses to combine when both hold non-ASCII bytes
|
|
328
|
+
if String.method_defined?(:append_as_bytes)
|
|
329
|
+
def joined_write_buffer
|
|
330
|
+
out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
|
|
331
|
+
@write_buffer.each { |part| out.append_as_bytes(part) }
|
|
332
|
+
out
|
|
333
|
+
end
|
|
334
|
+
else
|
|
335
|
+
def joined_write_buffer
|
|
336
|
+
out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
|
|
337
|
+
@write_buffer.each { |part| out << part.b }
|
|
338
|
+
out
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
316
342
|
def discard_write_buffer
|
|
317
343
|
@write_buffer.clear
|
|
318
344
|
@write_buffer_bytes = 0
|
|
@@ -28,22 +28,25 @@ module Dalli
|
|
|
28
28
|
@value_marshaller = value_marshaller
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# raw: true (a request made with raw: true) returns the value as stored,
|
|
32
|
+
# ignoring any flags in the reply. The request didn't ask for flags, so
|
|
33
|
+
# a reply carrying them anyway must not get the value deserialized.
|
|
34
|
+
def meta_get_with_value(cache_nils: false, raw: false)
|
|
32
35
|
tokens = error_on_unexpected!([VA, EN, HD])
|
|
33
36
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
34
37
|
return true unless tokens.first == VA
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
|
|
37
40
|
end
|
|
38
41
|
|
|
39
|
-
def meta_get_with_value_and_cas
|
|
42
|
+
def meta_get_with_value_and_cas(raw: false)
|
|
40
43
|
tokens = error_on_unexpected!([VA, EN, HD])
|
|
41
44
|
return [nil, 0] if tokens.first == EN
|
|
42
45
|
|
|
43
46
|
cas = cas_from_tokens(tokens)
|
|
44
47
|
return [nil, cas] unless tokens.first == VA
|
|
45
48
|
|
|
46
|
-
[
|
|
49
|
+
[retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw), cas]
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
def meta_get_without_value
|
|
@@ -62,12 +65,13 @@ module Dalli
|
|
|
62
65
|
#
|
|
63
66
|
# Used by meta_get for comprehensive metadata retrieval.
|
|
64
67
|
# Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
|
|
65
|
-
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false
|
|
68
|
+
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
|
|
69
|
+
raw: false)
|
|
66
70
|
tokens = error_on_unexpected!([VA, EN, HD])
|
|
67
71
|
result = build_metadata_result(tokens)
|
|
68
72
|
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
69
73
|
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
70
|
-
result[:value] = parse_value_from_tokens(tokens, cache_nils)
|
|
74
|
+
result[:value] = parse_value_from_tokens(tokens, cache_nils, raw)
|
|
71
75
|
result
|
|
72
76
|
end
|
|
73
77
|
|
|
@@ -79,11 +83,16 @@ module Dalli
|
|
|
79
83
|
}
|
|
80
84
|
end
|
|
81
85
|
|
|
82
|
-
def parse_value_from_tokens(tokens, cache_nils)
|
|
86
|
+
def parse_value_from_tokens(tokens, cache_nils, raw = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
83
87
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
84
88
|
return unless tokens.first == VA
|
|
85
89
|
|
|
86
|
-
|
|
90
|
+
retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Values from a raw request are returned as stored
|
|
94
|
+
def retrieve(value, bitflags, raw)
|
|
95
|
+
raw ? value : @value_marshaller.retrieve(value, bitflags)
|
|
87
96
|
end
|
|
88
97
|
|
|
89
98
|
def meta_set_with_cas
|
|
@@ -148,6 +157,10 @@ module Dalli
|
|
|
148
157
|
end
|
|
149
158
|
|
|
150
159
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
160
|
+
# A hit with no key can't be matched to a request: skip it (false
|
|
161
|
+
# status) rather than yield it, or take it for the end of the pipeline
|
|
162
|
+
return [resp_size, false, nil, nil, nil] unless tokens.any? { |t| t.start_with?('k') }
|
|
163
|
+
|
|
151
164
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
152
165
|
[resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
|
|
153
166
|
end
|
|
@@ -197,6 +210,11 @@ module Dalli
|
|
|
197
210
|
# buffer wait for (and accumulate) that many bytes.
|
|
198
211
|
MAX_VALUE_BYTES = 1024 * 1024 * 1024
|
|
199
212
|
|
|
213
|
+
# The size given right after "VA", or 0 when it isn't a number
|
|
214
|
+
def positional_size(tokens)
|
|
215
|
+
tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
|
|
216
|
+
end
|
|
217
|
+
|
|
200
218
|
def check_value_size!(size)
|
|
201
219
|
return if size.between?(0, MAX_VALUE_BYTES)
|
|
202
220
|
|
|
@@ -248,7 +266,9 @@ module Dalli
|
|
|
248
266
|
end
|
|
249
267
|
|
|
250
268
|
def body_len_from_tokens(tokens)
|
|
251
|
-
size
|
|
269
|
+
# memcached also gives the size positionally ("VA <size> ..."); use it
|
|
270
|
+
# when there's no s flag rather than reading the value as replies
|
|
271
|
+
size = tokens.any? { |t| t.start_with?('s') } ? value_from_tokens(tokens, 's')&.to_i : positional_size(tokens)
|
|
252
272
|
check_value_size!(size) if size
|
|
253
273
|
size
|
|
254
274
|
end
|
|
@@ -270,6 +290,9 @@ module Dalli
|
|
|
270
290
|
end
|
|
271
291
|
|
|
272
292
|
def read_data(data_size)
|
|
293
|
+
# Checked before the terminator is added: -1 or -2 would otherwise
|
|
294
|
+
# read 1 or 0 bytes and leave the connection out of step
|
|
295
|
+
check_value_size!(data_size)
|
|
273
296
|
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
274
297
|
end
|
|
275
298
|
end
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Dalli
|
|
|
30
30
|
req = RequestFormatter.meta_get(key: encoded_key, base64: base64, skip_flags: skip_flags)
|
|
31
31
|
write(req)
|
|
32
32
|
@connection_manager.flush
|
|
33
|
-
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
33
|
+
response_processor.meta_get_with_value(cache_nils: cache_nils?(options), raw: raw_request?(options))
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def quiet_get_request(key)
|
|
@@ -47,7 +47,7 @@ module Dalli
|
|
|
47
47
|
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64, skip_flags: skip_flags)
|
|
48
48
|
write(req)
|
|
49
49
|
@connection_manager.flush
|
|
50
|
-
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
50
|
+
response_processor.meta_get_with_value(cache_nils: cache_nils?(options), raw: raw_request?(options))
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def touch(key, ttl)
|
|
@@ -61,12 +61,13 @@ module Dalli
|
|
|
61
61
|
|
|
62
62
|
# TODO: This is confusing, as there's a cas command in memcached
|
|
63
63
|
# and this isn't it. Maybe rename? Maybe eliminate?
|
|
64
|
-
def cas(key)
|
|
64
|
+
def cas(key, options = nil)
|
|
65
65
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
66
|
-
req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64
|
|
66
|
+
req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64,
|
|
67
|
+
skip_flags: raw_request?(options))
|
|
67
68
|
write(req)
|
|
68
69
|
@connection_manager.flush
|
|
69
|
-
response_processor.meta_get_with_value_and_cas
|
|
70
|
+
response_processor.meta_get_with_value_and_cas(raw: raw_request?(options))
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
# Comprehensive meta get with support for all metadata flags.
|
|
@@ -105,7 +106,7 @@ module Dalli
|
|
|
105
106
|
@connection_manager.flush
|
|
106
107
|
response_processor.meta_get_with_metadata(
|
|
107
108
|
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
108
|
-
return_last_access: options[:return_last_access]
|
|
109
|
+
return_last_access: options[:return_last_access], raw: raw_request?(options)
|
|
109
110
|
)
|
|
110
111
|
end
|
|
111
112
|
|
|
@@ -212,12 +213,45 @@ module Dalli
|
|
|
212
213
|
def decr_incr(incr, key, delta, ttl, initial)
|
|
213
214
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
214
215
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
216
|
+
# memcached stores an item created by ma's auto-create (N/J) for a
|
|
217
|
+
# base64-encoded key without its binary-key flag, and later replies
|
|
218
|
+
# name it with the raw key bytes. Create such counters with an add.
|
|
219
|
+
return decr_incr_with_add(incr, key, delta, ttl, initial) if initial && base64
|
|
220
|
+
|
|
215
221
|
write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
216
222
|
quiet: quiet?, base64: base64))
|
|
217
223
|
@connection_manager.flush unless quiet?
|
|
218
224
|
response_processor.decr_incr unless quiet?
|
|
219
225
|
end
|
|
220
226
|
|
|
227
|
+
# Like decr_incr with an initial value, without memcached's auto-create:
|
|
228
|
+
# on a miss the counter is added with the initial value (which is
|
|
229
|
+
# returned, as auto-create would), and the add losing a race to another
|
|
230
|
+
# client means the counter now exists, so the incr is tried again. In a
|
|
231
|
+
# quiet block both requests are sent and whichever applies takes effect.
|
|
232
|
+
def decr_incr_with_add(incr, key, delta, ttl, initial)
|
|
233
|
+
add_options = { raw: true } # counters are stored raw
|
|
234
|
+
return quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options) if quiet?
|
|
235
|
+
|
|
236
|
+
2.times do
|
|
237
|
+
value = decr_incr(incr, key, delta, nil, nil)
|
|
238
|
+
return value unless value.nil?
|
|
239
|
+
|
|
240
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: false)
|
|
241
|
+
return initial if response_processor.meta_set_with_cas
|
|
242
|
+
end
|
|
243
|
+
decr_incr(incr, key, delta, nil, nil)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
247
|
+
def quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options)
|
|
248
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
249
|
+
write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: nil, incr: incr, ttl: nil,
|
|
250
|
+
quiet: true, base64: base64))
|
|
251
|
+
# Sent with the rest of the quiet block, by its closing noop
|
|
252
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: true)
|
|
253
|
+
end
|
|
254
|
+
|
|
221
255
|
# Other Commands
|
|
222
256
|
def flush(delay = 0)
|
|
223
257
|
write(RequestFormatter.flush(delay: delay))
|
data/lib/dalli/version.rb
CHANGED
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: 4.3.
|
|
4
|
+
version: 4.3.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -94,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: '0'
|
|
96
96
|
requirements: []
|
|
97
|
-
rubygems_version: 4.0.
|
|
97
|
+
rubygems_version: 4.0.23
|
|
98
98
|
specification_version: 4
|
|
99
99
|
summary: High performance memcached client for Ruby
|
|
100
100
|
test_files: []
|