dalli 5.1.4 → 5.1.6
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 +36 -0
- data/README.md +23 -0
- data/lib/dalli/client.rb +22 -3
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pipelined_getter.rb +36 -23
- data/lib/dalli/pipelined_setter.rb +9 -0
- data/lib/dalli/protocol/base.rb +116 -12
- data/lib/dalli/protocol/connection_manager.rb +38 -3
- data/lib/dalli/protocol/meta.rb +69 -10
- data/lib/dalli/protocol/value_compressor.rb +6 -4
- data/lib/dalli/protocol.rb +7 -9
- data/lib/dalli/socket.rb +29 -0
- 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: a14acb855194ffb54b0395ba7e9a4785a69e3d00068088e42aca53d91dcc3929
|
|
4
|
+
data.tar.gz: 3017450cd4da74f426885ae2684f562b42983ca83f85b13242611b7d32f76c02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b6f8bd702bb10c9f4d47ad981fbc702ac00dda484a84f82449fb0874aae98127b96c8518aad7768c7996f8c95c2772394771e84128d0c4d65278257a28540c4
|
|
7
|
+
data.tar.gz: 766ebee713280bc7ec98ec53becf3ece1a73c2734171f27379e13e2343f2dfce16649a77104254084cf9b948679646a8b6f4cbb0cb55529de354e1686d0e8b31
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.1.6
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
Security:
|
|
11
|
+
|
|
12
|
+
- Keep a connection shared by several threads or fibers in step during a `quiet` block (GHSA-5xvx-r8rg-5qm2)
|
|
13
|
+
- A `quiet` (or `multi`) block is quiet only for the thread or fiber that runs it. memcached still replies to a quiet request that fails (for example an `add` of a key that exists), and another thread or fiber using the same client while the block was open read that reply as its own, so its later requests could return another key's value. A non-quiet request now reads any pending quiet replies first
|
|
14
|
+
- Don't let a `get_multi` that stopped early leave replies for a later request (GHSA-5xvx-r8rg-5qm2)
|
|
15
|
+
- A pipelined `get_multi` stopped by a non-network exception (its block raising or breaking, `Timeout.timeout`, or a value that can't be deserialized) left its unread replies in the response buffer, and a later `get_multi` large enough to use the interleaved path returned them as its own. The buffer is now cleared when a connection closes or a pipelined get starts, and unfinished pipelines are aborted
|
|
16
|
+
- Affects every supported line (the `get_multi` issue, 4.3 and later); fixed in 5.2.4, 5.1.6, 5.0.12, 4.3.9 and 3.2.15
|
|
17
|
+
|
|
18
|
+
Bug fixes:
|
|
19
|
+
|
|
20
|
+
- A per-request `raw: true` write no longer compresses the value (backport of #1213)
|
|
21
|
+
- Values at or above the compression threshold (4 KB by default) were compressed and flagged even with `raw: true`. Raw reads ignore flags, so reading them back with `raw: true` returned the compressed bytes
|
|
22
|
+
- Raw values are now stored exactly as given. Callers that pass `raw: true` without `compress: false` store large values uncompressed; Rails' `MemCacheStore` passes `compress: false` and isn't affected
|
|
23
|
+
- Handle failed TLS handshakes like other connection failures, and warn when TLS verification is off (backport of #1215)
|
|
24
|
+
- A failed or timed-out handshake left the TCP socket open until GC and raised `OpenSSL::SSL::SSLError` directly, so the server was never marked down. The socket is now closed, and the error goes through the usual retry and `socket_max_failures` handling, raising `Dalli::NetworkError` (or `Dalli::RingError` once no server is available)
|
|
25
|
+
- Certificate and hostname checks come only from the `ssl_context` you pass, and a bare `OpenSSL::SSL::SSLContext.new` checks neither. Dalli now logs a warning once per process when `verify_mode` doesn't include `VERIFY_PEER` or `verify_hostname` is false; see the new TLS section in the README
|
|
26
|
+
- A reply line longer than 8 KiB without a CRLF, or one cut short by the connection closing, is now a network error that closes the connection. Before, the line had no length limit (backport of #1216)
|
|
27
|
+
- One value that can't be stored no longer discards other buffered writes (backport of #1218)
|
|
28
|
+
- A value that raised `Dalli::MarshalError` or `Dalli::ValueOverMaxSize` closed the connection, which dropped writes still buffered from the same `quiet` block or `set_multi`. Multi-server `set_multi` did this without raising
|
|
29
|
+
- Both `set_multi` paths now store every other key, skip the bad one, and then raise. Single-server `set_multi` used to store nothing
|
|
30
|
+
- When single-server `set_multi` failed to marshal a value, the error log included every value in the batch. It now lists only the keys
|
|
31
|
+
|
|
32
|
+
5.1.5
|
|
33
|
+
==========
|
|
34
|
+
|
|
35
|
+
Security:
|
|
36
|
+
|
|
37
|
+
- Don't rely on memcached's auto-create for keys that need base64 encoding (GHSA-r47x-6p6r-mvcp)
|
|
38
|
+
- 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
|
|
39
|
+
- `incr` and `decr` with an initial value now create such counters with an add instead. Return values are unchanged
|
|
40
|
+
- The new `verify_reply_keys` option makes `get_multi`, `get_multi_cas` and `get_multi_with_metadata` ignore replies for keys they didn't request, which guards against items created by older Dalli versions or other clients until memcached is upgraded. It's off by default, since it costs a multi-get about 15%
|
|
41
|
+
- 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
|
|
42
|
+
|
|
7
43
|
5.1.4
|
|
8
44
|
==========
|
|
9
45
|
|
data/README.md
CHANGED
|
@@ -63,6 +63,29 @@ Per-request `raw: true` returns the stored bytes without deserializing or decomp
|
|
|
63
63
|
|
|
64
64
|
See the [5.0-Upgrade.md](5.0-Upgrade.md) guide for upgrade information.
|
|
65
65
|
|
|
66
|
+
### Counters and verify_reply_keys
|
|
67
|
+
|
|
68
|
+
memcached names an item made by auto-create for a base64-encoded key (one with whitespace, control or non-ASCII characters) by its raw key bytes in later replies. Since 5.1.5, Dalli creates counters for such keys with an add rather than auto-create. Items created before then, or by other clients, can still be named that way until memcached releases a fix (1.6.45 and earlier behave this way). If keys passed to `incr` or `decr` with an initial value can contain untrusted input, set `verify_reply_keys: true` until then: multi-gets then ignore replies for keys they didn't request, at a cost of about 15% per multi-get.
|
|
69
|
+
|
|
70
|
+
## TLS
|
|
71
|
+
|
|
72
|
+
Pass an `OpenSSL::SSL::SSLContext` as `ssl_context:` to connect over TLS. Certificate and hostname verification come only from that context: Dalli adds no checks of its own. A bare `OpenSSL::SSL::SSLContext.new` verifies nothing, so it accepts any certificate from any server. Configure the context to verify both:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
76
|
+
ssl_context.set_params(
|
|
77
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
|
78
|
+
verify_hostname: true,
|
|
79
|
+
ca_file: '/path/to/ca.crt' # omit to use the system's trusted certificates
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
Dalli::Client.new('memcached.example.com:11211', ssl_context: ssl_context)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Dalli logs a warning once per process when the context's `verify_mode` is `VERIFY_NONE`, or when it is `VERIFY_PEER` but `verify_hostname` is false.
|
|
86
|
+
|
|
87
|
+
A failed TLS handshake counts as a failed connection attempt, like a refused connection: once `socket_max_failures` attempts have failed, the server is marked down and requests fail over to the other servers.
|
|
88
|
+
|
|
66
89
|
## OpenTelemetry Tracing
|
|
67
90
|
|
|
68
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
|
@@ -41,15 +41,21 @@ module Dalli
|
|
|
41
41
|
# #fetch operations.
|
|
42
42
|
# - :raw - If set, disables serialization and compression entirely at the client level.
|
|
43
43
|
# Only String values are supported. This is useful when the caller handles its own
|
|
44
|
-
# serialization (e.g., Rails' ActiveSupport::Cache).
|
|
45
|
-
#
|
|
46
|
-
#
|
|
44
|
+
# serialization (e.g., Rails' ActiveSupport::Cache). The per-request :raw option
|
|
45
|
+
# (e.g. set(key, value, ttl, raw: true)) does the same for a single call: String
|
|
46
|
+
# values are stored as-is, uncompressed, and non-String values raise
|
|
47
|
+
# Dalli::MarshalError.
|
|
47
48
|
# - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
|
|
48
49
|
# useful for injecting a FIPS compliant hash object.
|
|
49
50
|
# - :otel_db_statement - controls the +db.query.text+ span attribute when OpenTelemetry is loaded.
|
|
50
51
|
# +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
|
|
51
52
|
# +nil+ (default) omits the attribute entirely.
|
|
52
53
|
# - :otel_peer_service - when set, adds a +peer.service+ span attribute with this value for logical service naming.
|
|
54
|
+
# - :verify_reply_keys - defaults to false. When true, get_multi, get_multi_cas and get_multi_with_metadata only
|
|
55
|
+
# accept replies for the keys they requested; after a reply naming another key, the rest of
|
|
56
|
+
# that server's replies are treated as misses. memcached can name an item by its raw key
|
|
57
|
+
# bytes when the item was created by auto-create from a base64-encoded key, which Dalli no
|
|
58
|
+
# longer does. Costs a multi-get about 15%.
|
|
53
59
|
#
|
|
54
60
|
def initialize(servers = nil, options = {})
|
|
55
61
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
@@ -365,6 +371,11 @@ module Dalli
|
|
|
365
371
|
# Any error replies will be discarded at the end of the block, and
|
|
366
372
|
# Dalli client methods invoked inside the block will not
|
|
367
373
|
# have return values
|
|
374
|
+
#
|
|
375
|
+
# The block is quiet only for the thread or fiber that runs it. Another
|
|
376
|
+
# thread or fiber sharing this client can make requests meanwhile; a
|
|
377
|
+
# non-quiet one first sends the block's buffered requests and discards
|
|
378
|
+
# their replies, so it reads only its own.
|
|
368
379
|
def quiet
|
|
369
380
|
old = Thread.current[::Dalli::QUIET]
|
|
370
381
|
Thread.current[::Dalli::QUIET] = true
|
|
@@ -745,6 +756,11 @@ module Dalli
|
|
|
745
756
|
Dalli.logger.debug { e.inspect }
|
|
746
757
|
Dalli.logger.debug { 'retrying single-server get_multi because of network error' }
|
|
747
758
|
retry
|
|
759
|
+
rescue Dalli::UnexpectedReplyKeyError => e
|
|
760
|
+
# The connection is closed and the keys are treated as missing, as the
|
|
761
|
+
# pipelined get_multi does for a server whose replies are out of step
|
|
762
|
+
Dalli.logger.warn { e.message }
|
|
763
|
+
{}
|
|
748
764
|
end
|
|
749
765
|
|
|
750
766
|
def single_server_get_multi_with_metadata(keys, req_options = nil)
|
|
@@ -756,6 +772,9 @@ module Dalli
|
|
|
756
772
|
result
|
|
757
773
|
rescue Dalli::NetworkError
|
|
758
774
|
{}
|
|
775
|
+
rescue Dalli::UnexpectedReplyKeyError => e
|
|
776
|
+
Dalli.logger.warn { e.message }
|
|
777
|
+
{}
|
|
759
778
|
end
|
|
760
779
|
|
|
761
780
|
def single_server_set_multi(hash, ttl, req_options)
|
data/lib/dalli/options.rb
CHANGED
|
@@ -32,15 +32,21 @@ module Dalli
|
|
|
32
32
|
return {} if keys.empty?
|
|
33
33
|
|
|
34
34
|
@ring.lock do
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
# The servers with replies still to read, updated in place
|
|
36
|
+
servers = []
|
|
37
|
+
begin
|
|
38
|
+
# Stores partial results collected during interleaved send phase
|
|
39
|
+
@partial_results = {}
|
|
40
|
+
setup_requests(servers, keys, req_options, return_cas: return_cas)
|
|
41
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
42
|
+
|
|
43
|
+
# First yield any partial results collected during interleaved send
|
|
44
|
+
yield_partial_results(&block)
|
|
45
|
+
|
|
46
|
+
fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
|
|
47
|
+
ensure
|
|
48
|
+
abort_unfinished(servers)
|
|
49
|
+
end
|
|
44
50
|
end
|
|
45
51
|
rescue Dalli::RetryableNetworkError => e
|
|
46
52
|
Dalli.logger.debug { e.inspect }
|
|
@@ -93,9 +99,9 @@ module Dalli
|
|
|
93
99
|
|
|
94
100
|
# Sends each server's queries and its terminating noop before building
|
|
95
101
|
# the next server's, so memcached is already answering the first servers
|
|
96
|
-
# while the rest are prepared.
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
# while the rest are prepared. Adds the servers with a pending response
|
|
103
|
+
# to started.
|
|
104
|
+
def setup_requests(started, keys, req_options = nil, return_cas: true)
|
|
99
105
|
groups_for_keys(keys).each do |server, keys_for_server|
|
|
100
106
|
make_getkq_request(server, keys_for_server, req_options, return_cas: return_cas)
|
|
101
107
|
next unless server.connected?
|
|
@@ -108,7 +114,6 @@ module Dalli
|
|
|
108
114
|
rescue Dalli::DalliError
|
|
109
115
|
started.delete(server)
|
|
110
116
|
end
|
|
111
|
-
started
|
|
112
117
|
end
|
|
113
118
|
|
|
114
119
|
##
|
|
@@ -152,29 +157,39 @@ module Dalli
|
|
|
152
157
|
raise
|
|
153
158
|
end
|
|
154
159
|
|
|
155
|
-
# Swallows Dalli::NetworkError
|
|
160
|
+
# Swallows Dalli::NetworkError. Empties servers, so they aren't aborted
|
|
161
|
+
# again.
|
|
156
162
|
def abort_without_timeout(servers)
|
|
157
163
|
servers.each(&:pipeline_abort)
|
|
164
|
+
servers.clear
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# When #process stops before reading every server's replies (the caller's
|
|
168
|
+
# block raises or breaks, a Timeout.timeout fires, a value can't be
|
|
169
|
+
# deserialized), the rest are still on the socket and some may be in the
|
|
170
|
+
# response buffer. Closing those connections keeps them from being read
|
|
171
|
+
# as a later request's replies. Aborts that #process already handled
|
|
172
|
+
# emptied servers.
|
|
173
|
+
def abort_unfinished(servers)
|
|
174
|
+
servers.each { |server| server.pipeline_abort(timed_out: false) }
|
|
175
|
+
servers.clear
|
|
158
176
|
end
|
|
159
177
|
|
|
178
|
+
# Reads the replies available, removing finished servers from servers
|
|
160
179
|
def fetch_responses(servers, start_time, timeout, &block)
|
|
161
180
|
# Remove any servers which are not connected
|
|
162
181
|
servers.select!(&:connected?)
|
|
163
|
-
return
|
|
182
|
+
return if servers.empty?
|
|
164
183
|
|
|
165
184
|
time_left = remaining_time(start_time, timeout)
|
|
166
185
|
readable_servers = servers_with_response(servers, time_left)
|
|
167
|
-
if readable_servers.empty?
|
|
168
|
-
abort_with_timeout(servers)
|
|
169
|
-
return []
|
|
170
|
-
end
|
|
186
|
+
return abort_with_timeout(servers) if readable_servers.empty?
|
|
171
187
|
|
|
172
188
|
# Loop through the servers with responses, and
|
|
173
189
|
# delete any from our list that are finished
|
|
174
190
|
readable_servers.each do |server|
|
|
175
191
|
servers.delete(server) if process_server(server, &block)
|
|
176
192
|
end
|
|
177
|
-
servers
|
|
178
193
|
rescue NetworkError
|
|
179
194
|
# Abort and raise if we encountered a network error. This triggers
|
|
180
195
|
# a retry at the top level on RetryableNetworkError.
|
|
@@ -191,12 +206,10 @@ module Dalli
|
|
|
191
206
|
|
|
192
207
|
# Swallows Dalli::NetworkError
|
|
193
208
|
def abort_with_timeout(servers)
|
|
194
|
-
abort_without_timeout(servers)
|
|
195
209
|
servers.each do |server|
|
|
196
210
|
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
|
197
211
|
end
|
|
198
|
-
|
|
199
|
-
true # Required to simplify caller
|
|
212
|
+
abort_without_timeout(servers)
|
|
200
213
|
end
|
|
201
214
|
|
|
202
215
|
# Processes responses from a server. Returns true if there are no
|
|
@@ -18,6 +18,9 @@ module Dalli
|
|
|
18
18
|
# A transient network error is retried automatically. If a server remains
|
|
19
19
|
# unreachable after retrying, raises Dalli::NetworkError.
|
|
20
20
|
#
|
|
21
|
+
# A value that can't be marshalled or is over value_max_bytes is skipped;
|
|
22
|
+
# the other keys are still stored, then the first such error is raised.
|
|
23
|
+
#
|
|
21
24
|
# @param hash [Hash] key-value pairs to set
|
|
22
25
|
# @param ttl [Integer] time-to-live in seconds
|
|
23
26
|
# @param req_options [Hash] options passed to each set operation
|
|
@@ -26,10 +29,12 @@ module Dalli
|
|
|
26
29
|
def process(hash, ttl, req_options)
|
|
27
30
|
return if hash.empty?
|
|
28
31
|
|
|
32
|
+
@value_error = nil
|
|
29
33
|
@ring.lock do
|
|
30
34
|
servers = setup_requests(hash, ttl, req_options)
|
|
31
35
|
finish_requests(servers)
|
|
32
36
|
end
|
|
37
|
+
raise @value_error if @value_error
|
|
33
38
|
rescue Dalli::RetryableNetworkError => e
|
|
34
39
|
Dalli.logger.debug { e.inspect }
|
|
35
40
|
Dalli.logger.debug { 'retrying pipelined sets because of network error' }
|
|
@@ -62,6 +67,10 @@ module Dalli
|
|
|
62
67
|
server.request(:pipelined_set, key, value, ttl, req_options)
|
|
63
68
|
rescue Dalli::NetworkError
|
|
64
69
|
raise
|
|
70
|
+
rescue Dalli::MarshalError, Dalli::ValueOverMaxSize => e
|
|
71
|
+
# Nothing was written for this key, so the server's other keys are
|
|
72
|
+
# unaffected; raised by #process once the batch is complete
|
|
73
|
+
@value_error ||= e
|
|
65
74
|
rescue DalliError => e
|
|
66
75
|
Dalli.logger.debug { e.inspect }
|
|
67
76
|
Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -41,6 +41,7 @@ module Dalli
|
|
|
41
41
|
warn_uri_credentials(user_creds)
|
|
42
42
|
@options = client_options.merge(user_creds)
|
|
43
43
|
@raw_mode = client_options[:raw]
|
|
44
|
+
@verify_reply_keys = client_options[:verify_reply_keys] ? true : false
|
|
44
45
|
@value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
45
46
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
46
47
|
end
|
|
@@ -66,15 +67,25 @@ module Dalli
|
|
|
66
67
|
begin
|
|
67
68
|
request_completed = false
|
|
68
69
|
@connection_manager.start_request!
|
|
70
|
+
drain_pending_quiet_responses(opkey)
|
|
69
71
|
response = send(opkey, *args)
|
|
72
|
+
# Quiet requests can leave replies on the socket; remember that so a
|
|
73
|
+
# later non-quiet request knows to read them first
|
|
74
|
+
@connection_manager.mark_quiet_responses_pending! if quiet? && opkey != :noop
|
|
70
75
|
|
|
71
76
|
# pipelined_get/pipelined_get_interleaved emit query but don't read the response(s)
|
|
72
77
|
@connection_manager.finish_request! unless %i[pipelined_get pipelined_get_interleaved].include?(opkey)
|
|
73
78
|
|
|
74
79
|
request_completed = true
|
|
75
80
|
response
|
|
76
|
-
rescue Dalli::MarshalError => e
|
|
77
|
-
log_marshal_err(args.first, e)
|
|
81
|
+
rescue Dalli::MarshalError, Dalli::ValueOverMaxSize => e
|
|
82
|
+
log_marshal_err(args.first, e) if e.is_a?(Dalli::MarshalError)
|
|
83
|
+
# Raised only while the connection is clean: a value is marshalled
|
|
84
|
+
# before any of its request is written, and write_multi_req raises
|
|
85
|
+
# only after reading every reply. Closing the connection would
|
|
86
|
+
# discard earlier quiet writes still waiting in the write buffer.
|
|
87
|
+
@connection_manager.abort_request!
|
|
88
|
+
request_completed = true
|
|
78
89
|
raise
|
|
79
90
|
rescue Dalli::DalliError
|
|
80
91
|
raise
|
|
@@ -134,7 +145,7 @@ module Dalli
|
|
|
134
145
|
verify_pipelined_state(:getkq)
|
|
135
146
|
write_noop
|
|
136
147
|
# Use ensure_ready instead of reset to preserve any data already buffered
|
|
137
|
-
#
|
|
148
|
+
# while this get_multi's interleaved requests were sent
|
|
138
149
|
response_buffer.ensure_ready
|
|
139
150
|
end
|
|
140
151
|
|
|
@@ -147,6 +158,8 @@ module Dalli
|
|
|
147
158
|
# avoiding intermediate Hash allocation. Returns nil.
|
|
148
159
|
# Without a block, returns a Hash of { key => [value, cas] }.
|
|
149
160
|
def pipeline_next_responses(&block)
|
|
161
|
+
return abandon_pipeline if @pipeline_out_of_step
|
|
162
|
+
|
|
150
163
|
reconnect_on_pipeline_complete!
|
|
151
164
|
values = nil
|
|
152
165
|
|
|
@@ -168,6 +181,13 @@ module Dalli
|
|
|
168
181
|
# to the noop at the end of the pipeline
|
|
169
182
|
finish_pipeline && break if status && key.nil?
|
|
170
183
|
|
|
184
|
+
# A key that wasn't requested means the replies are out of step
|
|
185
|
+
# (see expect_reply_keys): keep what was read so far, drop the rest
|
|
186
|
+
if unexpected_reply_key?(key)
|
|
187
|
+
abandon_pipeline
|
|
188
|
+
break
|
|
189
|
+
end
|
|
190
|
+
|
|
171
191
|
# If the status is ok and the key is not nil, then this is a
|
|
172
192
|
# getkq response with a value that we want to set in the response hash
|
|
173
193
|
unless key.nil?
|
|
@@ -179,6 +199,10 @@ module Dalli
|
|
|
179
199
|
end
|
|
180
200
|
end
|
|
181
201
|
|
|
202
|
+
# A request to this server from inside the block closes the
|
|
203
|
+
# connection, and with it the rest of this get_multi's replies
|
|
204
|
+
raise_pipeline_closed unless response_buffer.in_progress?
|
|
205
|
+
|
|
182
206
|
# Get the next response from the buffer
|
|
183
207
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
184
208
|
end
|
|
@@ -188,21 +212,24 @@ module Dalli
|
|
|
188
212
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
189
213
|
|
|
190
214
|
# Abort current pipelined get. Generally used to signal an external
|
|
191
|
-
# timeout during pipelined get
|
|
215
|
+
# timeout during pipelined get, or that the get_multi stopped before
|
|
216
|
+
# reading all its replies (timed_out: false). The underlying socket is
|
|
192
217
|
# disconnected, and the exception is swallowed.
|
|
193
218
|
#
|
|
194
219
|
# Returns nothing.
|
|
195
|
-
def pipeline_abort
|
|
220
|
+
def pipeline_abort(timed_out: true)
|
|
196
221
|
response_buffer.clear
|
|
222
|
+
@pipeline_out_of_step = false
|
|
223
|
+
@expected_reply_keys = nil
|
|
197
224
|
@connection_manager.abort_request!
|
|
198
225
|
return true unless connected?
|
|
199
226
|
|
|
200
227
|
# Closes the connection, which ensures that our connection is in a
|
|
201
228
|
# clean state for future requests. The get_multi ran out of time
|
|
202
|
-
# overall rather than hitting a socket error, so it
|
|
203
|
-
# toward socket_max_failures: two slow get_multis in a
|
|
204
|
-
# otherwise mark a healthy server down.
|
|
205
|
-
|
|
229
|
+
# overall, or stopped early, rather than hitting a socket error, so it
|
|
230
|
+
# doesn't count toward socket_max_failures: two slow get_multis in a
|
|
231
|
+
# row would otherwise mark a healthy server down.
|
|
232
|
+
log_pipeline_abort(timed_out)
|
|
206
233
|
@connection_manager.close
|
|
207
234
|
true
|
|
208
235
|
end
|
|
@@ -234,6 +261,18 @@ module Dalli
|
|
|
234
261
|
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
235
262
|
private_constant :ALLOWED_QUIET_OPS
|
|
236
263
|
|
|
264
|
+
# Reads the replies still pending from quiet requests before any
|
|
265
|
+
# non-quiet request, which reads a reply of its own. A quiet block reads
|
|
266
|
+
# them at its end, but the connection is shared: another thread or fiber
|
|
267
|
+
# (quiet? is fiber-local) can make a request while the block is still
|
|
268
|
+
# open, and would otherwise read the block's replies as its own. The
|
|
269
|
+
# noop also sends any quiet requests still buffered.
|
|
270
|
+
def drain_pending_quiet_responses(opkey)
|
|
271
|
+
return unless @connection_manager.quiet_responses_pending? && opkey != :noop && !quiet?
|
|
272
|
+
|
|
273
|
+
noop
|
|
274
|
+
end
|
|
275
|
+
|
|
237
276
|
def verify_allowed_quiet!(opkey)
|
|
238
277
|
return if ALLOWED_QUIET_OPS.include?(opkey)
|
|
239
278
|
|
|
@@ -332,6 +371,7 @@ module Dalli
|
|
|
332
371
|
# the expected state before pipeline_response_setup is called.
|
|
333
372
|
response_buffer.clear
|
|
334
373
|
response_buffer.raw = raw_request?(options)
|
|
374
|
+
expect_reply_keys(keys)
|
|
335
375
|
|
|
336
376
|
# The terminating noop is sent by pipeline_response_setup
|
|
337
377
|
write(quiet_get_requests(keys, options, return_cas: return_cas))
|
|
@@ -341,9 +381,11 @@ module Dalli
|
|
|
341
381
|
# This prevents socket buffer deadlock when sending many keys.
|
|
342
382
|
# Populates the provided results hash with any responses drained during send.
|
|
343
383
|
def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
|
|
344
|
-
#
|
|
345
|
-
|
|
384
|
+
# Start with an empty buffer for draining during the send phase. Bytes
|
|
385
|
+
# left by an earlier get_multi that stopped early belong to its replies.
|
|
386
|
+
response_buffer.reset
|
|
346
387
|
response_buffer.raw = raw_request?(options)
|
|
388
|
+
expect_reply_keys(keys)
|
|
347
389
|
|
|
348
390
|
keys.each_slice(chunk_size) do |chunk|
|
|
349
391
|
# Build and write this chunk of requests
|
|
@@ -376,6 +418,12 @@ module Dalli
|
|
|
376
418
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
377
419
|
break if status.nil? # No complete response available
|
|
378
420
|
|
|
421
|
+
# Handled by the next pipeline_next_responses, once sending is done
|
|
422
|
+
if unexpected_reply_key?(key)
|
|
423
|
+
@pipeline_out_of_step = true
|
|
424
|
+
break
|
|
425
|
+
end
|
|
426
|
+
|
|
379
427
|
results[key] = [value, cas] unless key.nil?
|
|
380
428
|
end
|
|
381
429
|
rescue SystemCallError, Dalli::NetworkError
|
|
@@ -383,8 +431,48 @@ module Dalli
|
|
|
383
431
|
nil
|
|
384
432
|
end
|
|
385
433
|
|
|
434
|
+
# Registered with the connection manager, which clears it whenever the
|
|
435
|
+
# connection closes, so a new connection never parses an old one's bytes
|
|
386
436
|
def response_buffer
|
|
387
|
-
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
437
|
+
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor).tap do |buffer|
|
|
438
|
+
@connection_manager.response_buffer = buffer
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# A multi-get only accepts replies for the keys it asked for. memcached
|
|
443
|
+
# names an item created by auto-create (ma or mg with N) for a
|
|
444
|
+
# base64-encoded key by its raw bytes, without the b flag; such a key
|
|
445
|
+
# can contain spaces or CR/LF, which split the reply header and make the
|
|
446
|
+
# replies after it parse as different ones. A key that wasn't requested
|
|
447
|
+
# is how that shows up.
|
|
448
|
+
# Only with the verify_reply_keys option, since building the set costs a
|
|
449
|
+
# multi-get about 15%. Dalli itself no longer creates such items (see
|
|
450
|
+
# Meta#decr_incr); the check guards against ones that older versions or
|
|
451
|
+
# other clients created, on memcached versions with the behavior.
|
|
452
|
+
def expect_reply_keys(keys)
|
|
453
|
+
@pipeline_out_of_step = false
|
|
454
|
+
@expected_reply_keys = (keys.to_set { |k| reply_key_form(k) } if @verify_reply_keys)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def unexpected_reply_key?(key)
|
|
458
|
+
!key.nil? && @expected_reply_keys && !@expected_reply_keys.include?(reply_key_form(key))
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Keys compared as bytes; ASCII keys compare equal in any encoding as is
|
|
462
|
+
def reply_key_form(key)
|
|
463
|
+
key.ascii_only? ? key : key.b
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Keeps what was read, drops the rest of this server's replies, and
|
|
467
|
+
# closes the connection so nothing unread is left on it. Not a socket
|
|
468
|
+
# failure, so it doesn't count toward socket_max_failures; the keys not
|
|
469
|
+
# read are simply missing from the result.
|
|
470
|
+
def abandon_pipeline
|
|
471
|
+
Dalli.logger.warn { "#{name} replied to a get_multi with a key that wasn't requested; closing the connection" }
|
|
472
|
+
@pipeline_out_of_step = false
|
|
473
|
+
response_buffer.clear
|
|
474
|
+
@connection_manager.close
|
|
475
|
+
{}
|
|
388
476
|
end
|
|
389
477
|
|
|
390
478
|
# Called after the noop response is received at the end of a set
|
|
@@ -400,7 +488,23 @@ module Dalli
|
|
|
400
488
|
@connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
|
|
401
489
|
end
|
|
402
490
|
|
|
491
|
+
def raise_pipeline_closed
|
|
492
|
+
raise Dalli::DalliError, "The connection to #{name} closed while a get_multi was reading its replies " \
|
|
493
|
+
'(a request made from inside the get_multi block?)'
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def log_pipeline_abort(timed_out)
|
|
497
|
+
if timed_out
|
|
498
|
+
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
499
|
+
else
|
|
500
|
+
Dalli.logger.debug { "#{name} get_multi stopped before reading all replies; closing the connection" }
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# The single-server set_multi path passes its whole pairs hash as the
|
|
505
|
+
# first argument; log only its keys, never the values
|
|
403
506
|
def log_marshal_err(key, err)
|
|
507
|
+
key = key.keys.join("', '") if key.is_a?(Hash)
|
|
404
508
|
Dalli.logger.error "Marshalling error for key '#{key}': #{err.message}"
|
|
405
509
|
Dalli.logger.error 'You are trying to cache a Ruby object which cannot be serialized to memcached.'
|
|
406
510
|
end
|
|
@@ -26,6 +26,9 @@ module Dalli
|
|
|
26
26
|
|
|
27
27
|
attr_accessor :hostname, :port, :socket_type, :options
|
|
28
28
|
attr_reader :sock
|
|
29
|
+
# The buffer replies are read into (see Base#response_buffer). It's
|
|
30
|
+
# cleared with the write buffer whenever the connection closes.
|
|
31
|
+
attr_writer :response_buffer
|
|
29
32
|
|
|
30
33
|
def initialize(hostname, port, socket_type, client_options)
|
|
31
34
|
@hostname = hostname
|
|
@@ -33,10 +36,12 @@ module Dalli
|
|
|
33
36
|
@socket_type = socket_type
|
|
34
37
|
@options = DEFAULTS.merge(client_options)
|
|
35
38
|
@request_in_progress = false
|
|
39
|
+
@quiet_responses_pending = false
|
|
36
40
|
@sock = nil
|
|
37
41
|
@pid = nil
|
|
38
42
|
@write_buffer = []
|
|
39
43
|
@write_buffer_bytes = 0
|
|
44
|
+
@response_buffer = nil
|
|
40
45
|
|
|
41
46
|
@fail_count = 0
|
|
42
47
|
reset_down_info
|
|
@@ -58,8 +63,9 @@ module Dalli
|
|
|
58
63
|
@sock.sync = true
|
|
59
64
|
@pid = Process.pid
|
|
60
65
|
@request_in_progress = false
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
@quiet_responses_pending = false
|
|
67
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError, SocketError => e
|
|
68
|
+
# SocketError = DNS resolution failure; SSL_ERRORS = failed TLS handshake
|
|
63
69
|
error_on_request!(e)
|
|
64
70
|
end
|
|
65
71
|
|
|
@@ -129,7 +135,9 @@ module Dalli
|
|
|
129
135
|
# @request_in_progress == true.
|
|
130
136
|
@sock = nil
|
|
131
137
|
@pid = nil
|
|
138
|
+
@quiet_responses_pending = false
|
|
132
139
|
discard_write_buffer
|
|
140
|
+
@response_buffer&.clear
|
|
133
141
|
abort_request!
|
|
134
142
|
end
|
|
135
143
|
end
|
|
@@ -145,6 +153,21 @@ module Dalli
|
|
|
145
153
|
!@sock.nil?
|
|
146
154
|
end
|
|
147
155
|
|
|
156
|
+
# True when a quiet request was written on this connection and its
|
|
157
|
+
# replies (memcached only suppresses some of them) haven't been
|
|
158
|
+
# consumed by a noop yet.
|
|
159
|
+
def quiet_responses_pending?
|
|
160
|
+
@quiet_responses_pending
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def mark_quiet_responses_pending!
|
|
164
|
+
@quiet_responses_pending = true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def clear_quiet_responses_pending!
|
|
168
|
+
@quiet_responses_pending = false
|
|
169
|
+
end
|
|
170
|
+
|
|
148
171
|
def request_in_progress?
|
|
149
172
|
@request_in_progress
|
|
150
173
|
end
|
|
@@ -170,10 +193,22 @@ module Dalli
|
|
|
170
193
|
@request_in_progress = false
|
|
171
194
|
end
|
|
172
195
|
|
|
196
|
+
# The longest reply line Dalli reads, in bytes, CRLF included. A meta
|
|
197
|
+
# reply header is at most about 500 bytes: "VA <size>" plus its flags,
|
|
198
|
+
# the largest being a 250-byte key base64-encoded to 336 bytes, a
|
|
199
|
+
# 20-digit CAS, a 32-byte opaque and a few numeric fields. The longest
|
|
200
|
+
# stats line (stats settings) carries a file path such as ext_path, up
|
|
201
|
+
# to PATH_MAX (4096). 8 KiB leaves room for both, while a server that
|
|
202
|
+
# never sends CRLF can't grow the read buffer without bound.
|
|
203
|
+
MAX_LINE_BYTES = 8 * 1024
|
|
204
|
+
|
|
173
205
|
def read_line
|
|
174
206
|
flush_write_buffer
|
|
175
|
-
data = @sock.gets("\r\n")
|
|
207
|
+
data = @sock.gets("\r\n", MAX_LINE_BYTES)
|
|
176
208
|
error_on_request!('EOF in read_line') if data.nil?
|
|
209
|
+
# Without its CRLF the line either ran past MAX_LINE_BYTES or was cut
|
|
210
|
+
# short by EOF; either way the connection is out of step
|
|
211
|
+
error_on_request!("Reply line without CRLF in the first #{MAX_LINE_BYTES} bytes") unless data.end_with?("\r\n")
|
|
177
212
|
data
|
|
178
213
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
179
214
|
error_on_request!(e)
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -224,12 +224,45 @@ module Dalli
|
|
|
224
224
|
# rubocop:disable-next Metrics/ParameterLists
|
|
225
225
|
def decr_incr(incr, key, delta, ttl, initial, options = nil)
|
|
226
226
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
227
|
+
# memcached stores an item created by ma's auto-create (N/J) for a
|
|
228
|
+
# base64-encoded key without its binary-key flag, and later replies
|
|
229
|
+
# name it with the raw key bytes. Create such counters with an add.
|
|
230
|
+
return decr_incr_with_add(incr, key, delta, ttl, initial, options) if initial && KeyRegularizer.required?(key)
|
|
231
|
+
|
|
227
232
|
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
228
233
|
quiet: quiet?, **routing_token_kwargs(options)))
|
|
229
234
|
@connection_manager.flush unless quiet?
|
|
230
235
|
response_processor.decr_incr unless quiet?
|
|
231
236
|
end
|
|
232
237
|
|
|
238
|
+
# Like decr_incr with an initial value, without memcached's auto-create:
|
|
239
|
+
# on a miss the counter is added with the initial value (which is
|
|
240
|
+
# returned, as auto-create would), and the add losing a race to another
|
|
241
|
+
# client means the counter now exists, so the incr is tried again. In a
|
|
242
|
+
# quiet block both requests are sent and whichever applies takes effect.
|
|
243
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
244
|
+
def decr_incr_with_add(incr, key, delta, ttl, initial, options)
|
|
245
|
+
add_options = (options || {}).merge(raw: true) # counters are stored raw
|
|
246
|
+
return quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options) if quiet?
|
|
247
|
+
|
|
248
|
+
2.times do
|
|
249
|
+
value = decr_incr(incr, key, delta, nil, nil, options)
|
|
250
|
+
return value unless value.nil?
|
|
251
|
+
|
|
252
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: false)
|
|
253
|
+
return initial if response_processor.meta_set_with_cas
|
|
254
|
+
end
|
|
255
|
+
decr_incr(incr, key, delta, nil, nil, options)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
259
|
+
def quiet_decr_incr_with_add(incr, key, delta, ttl, initial, add_options)
|
|
260
|
+
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: nil, incr: incr, ttl: nil,
|
|
261
|
+
quiet: true, **routing_token_kwargs(add_options)))
|
|
262
|
+
# Sent with the rest of the quiet block, by its closing noop
|
|
263
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, add_options, quiet: true)
|
|
264
|
+
end
|
|
265
|
+
|
|
233
266
|
# Other Commands
|
|
234
267
|
def flush(delay = 0)
|
|
235
268
|
write(RequestFormatter.flush(delay: delay))
|
|
@@ -241,7 +274,10 @@ module Dalli
|
|
|
241
274
|
# We need to read all the responses at once.
|
|
242
275
|
def noop
|
|
243
276
|
write_noop
|
|
244
|
-
response_processor.consume_all_responses_until_mn
|
|
277
|
+
result = response_processor.consume_all_responses_until_mn
|
|
278
|
+
# Everything up to and including the MN has been read
|
|
279
|
+
@connection_manager.clear_quiet_responses_pending!
|
|
280
|
+
result
|
|
245
281
|
end
|
|
246
282
|
|
|
247
283
|
def stats(info = nil)
|
|
@@ -271,7 +307,7 @@ module Dalli
|
|
|
271
307
|
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw, **routing_token_kwargs(options))
|
|
272
308
|
flushed_write(buffer)
|
|
273
309
|
buffer.clear
|
|
274
|
-
read_multi_get_responses(is_raw)
|
|
310
|
+
read_multi_get_responses(is_raw, keys)
|
|
275
311
|
end
|
|
276
312
|
|
|
277
313
|
# Stale-aware bulk get. Returns { key => { value:, cas:, stale:, miss: } }
|
|
@@ -288,12 +324,13 @@ module Dalli
|
|
|
288
324
|
**routing_token_kwargs(options))
|
|
289
325
|
flushed_write(buffer)
|
|
290
326
|
buffer.clear
|
|
291
|
-
read_multi_metadata_responses(is_raw)
|
|
327
|
+
read_multi_metadata_responses(is_raw, keys)
|
|
292
328
|
end
|
|
293
329
|
|
|
294
330
|
# Unlike read_multi_get_responses this locates tokens by flag rather than
|
|
295
331
|
# by position, because the c flag shifts the key's index.
|
|
296
|
-
def read_multi_metadata_responses(is_raw)
|
|
332
|
+
def read_multi_metadata_responses(is_raw, keys)
|
|
333
|
+
expect_reply_keys(keys)
|
|
297
334
|
hash = {}
|
|
298
335
|
while (line = @connection_manager.read_line)
|
|
299
336
|
break if line.start_with?('MN')
|
|
@@ -309,12 +346,15 @@ module Dalli
|
|
|
309
346
|
key = response_processor.key_from_tokens(tokens)
|
|
310
347
|
next if key.nil?
|
|
311
348
|
|
|
349
|
+
raise_unexpected_reply_key(key) if unexpected_reply_key?(key)
|
|
350
|
+
|
|
312
351
|
hash[key] = { value: @value_marshaller.retrieve(value, bitflags), cas: cas, stale: stale, miss: false }
|
|
313
352
|
end
|
|
314
353
|
hash
|
|
315
354
|
end
|
|
316
355
|
|
|
317
|
-
def read_multi_get_responses(is_raw)
|
|
356
|
+
def read_multi_get_responses(is_raw, keys)
|
|
357
|
+
expect_reply_keys(keys)
|
|
318
358
|
hash = {}
|
|
319
359
|
key_index = is_raw ? 2 : 3
|
|
320
360
|
while (line = @connection_manager.read_line)
|
|
@@ -322,11 +362,19 @@ module Dalli
|
|
|
322
362
|
next unless line.start_with?('VA ')
|
|
323
363
|
|
|
324
364
|
key, value = parse_multi_get_value(line, key_index, is_raw)
|
|
365
|
+
raise_unexpected_reply_key(key) if unexpected_reply_key?(key)
|
|
325
366
|
hash[key] = value if key
|
|
326
367
|
end
|
|
327
368
|
hash
|
|
328
369
|
end
|
|
329
370
|
|
|
371
|
+
# The replies after a key that wasn't requested can't be trusted (see
|
|
372
|
+
# Base#expect_reply_keys). Raising ends the request, which closes the
|
|
373
|
+
# connection; the caller treats this server's keys as missing.
|
|
374
|
+
def raise_unexpected_reply_key(key)
|
|
375
|
+
raise Dalli::UnexpectedReplyKeyError, "#{name} replied with a key that wasn't requested: #{key.inspect}"
|
|
376
|
+
end
|
|
377
|
+
|
|
330
378
|
def parse_multi_get_value(line, key_index, is_raw)
|
|
331
379
|
tokens = line.chomp!(TERMINATOR).split
|
|
332
380
|
size = tokens[1].to_i
|
|
@@ -343,16 +391,27 @@ module Dalli
|
|
|
343
391
|
|
|
344
392
|
# Single-server fast path for set_multi. Inlines request formatting to
|
|
345
393
|
# minimize per-key overhead. Avoids PipelinedSetter server grouping.
|
|
394
|
+
#
|
|
395
|
+
# A value that can't be marshalled or is over value_max_bytes is skipped;
|
|
396
|
+
# the other keys are still stored, then the first such error is raised
|
|
397
|
+
# (as PipelinedSetter does on the multi-server path).
|
|
346
398
|
def write_multi_req(pairs, ttl, req_options)
|
|
347
399
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
348
|
-
|
|
400
|
+
value_error = nil
|
|
401
|
+
entries = pairs.filter_map do |key, raw_value|
|
|
349
402
|
[key, @value_marshaller.store(key, raw_value, req_options)]
|
|
403
|
+
rescue Dalli::MarshalError, Dalli::ValueOverMaxSize => e
|
|
404
|
+
value_error ||= e
|
|
405
|
+
nil
|
|
350
406
|
end
|
|
351
407
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
408
|
+
unless entries.empty?
|
|
409
|
+
buffer = RequestFormatter.multi_meta_set(entries, ttl: ttl, **routing_token_kwargs(req_options))
|
|
410
|
+
flushed_write(buffer)
|
|
411
|
+
buffer.clear
|
|
412
|
+
response_processor.consume_all_responses_until_mn
|
|
413
|
+
end
|
|
414
|
+
raise value_error if value_error
|
|
356
415
|
end
|
|
357
416
|
|
|
358
417
|
# Single-server fast path for delete_multi. Writes all quiet delete requests
|
|
@@ -84,11 +84,13 @@ module Dalli
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
# Checks whether we should apply compression when serializing a value
|
|
87
|
-
# based on the specified options. Returns false
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
87
|
+
# based on the specified options. Returns false for a raw request
|
|
88
|
+
# (raw reads ignore bitflags, so the value must be stored as given) or
|
|
89
|
+
# unless the value is greater than the minimum compression size.
|
|
90
|
+
# Otherwise returns based on a method-level option if specified,
|
|
91
|
+
# falling back to the server default.
|
|
91
92
|
def compress_value?(value, req_options)
|
|
93
|
+
return false if req_options&.dig(:raw)
|
|
92
94
|
return false unless value.bytesize >= compression_min_size
|
|
93
95
|
return compress_by_default? unless req_options && !req_options[:compress].nil?
|
|
94
96
|
|
data/lib/dalli/protocol.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'openssl'
|
|
3
4
|
require 'timeout'
|
|
4
5
|
|
|
5
6
|
module Dalli
|
|
@@ -16,14 +17,11 @@ module Dalli
|
|
|
16
17
|
[Timeout::Error]
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
# SSL errors
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
else
|
|
26
|
-
[]
|
|
27
|
-
end
|
|
20
|
+
# SSL errors during the handshake or during read/write operations count as
|
|
21
|
+
# a failed request: they trigger reconnection and, after
|
|
22
|
+
# socket_max_failures, mark the server down.
|
|
23
|
+
# openssl is required above: this file loads before dalli/socket, so a
|
|
24
|
+
# defined? check here would find OpenSSL missing and leave this empty.
|
|
25
|
+
SSL_ERRORS = [OpenSSL::SSL::SSLError].freeze
|
|
28
26
|
end
|
|
29
27
|
end
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -203,12 +203,41 @@ module Dalli
|
|
|
203
203
|
[seconds, microseconds].pack(timeval_pack_format(sock))
|
|
204
204
|
end
|
|
205
205
|
|
|
206
|
+
# Closes the TCP socket if the handshake fails or times out, so a failed
|
|
207
|
+
# attempt doesn't hold a file descriptor until GC.
|
|
206
208
|
def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
|
|
209
|
+
warn_if_unverified(ssl_context)
|
|
210
|
+
connected = false
|
|
207
211
|
ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
|
|
208
212
|
ssl_socket.hostname = host
|
|
209
213
|
ssl_socket.sync_close = true
|
|
210
214
|
ssl_socket.connect
|
|
215
|
+
connected = true
|
|
211
216
|
ssl_socket
|
|
217
|
+
ensure
|
|
218
|
+
tcp_socket.close unless connected
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Class variable to track whether the TLS verification warning has been logged
|
|
222
|
+
@@ssl_verification_warning_logged = false # rubocop:disable Style/ClassVars
|
|
223
|
+
|
|
224
|
+
# Verification comes entirely from the caller's ssl_context, and a bare
|
|
225
|
+
# SSLContext.new verifies nothing, so warn (once per process) when the
|
|
226
|
+
# certificate or the hostname won't be checked.
|
|
227
|
+
def self.warn_if_unverified(ssl_context)
|
|
228
|
+
return if @@ssl_verification_warning_logged
|
|
229
|
+
|
|
230
|
+
problem = if !ssl_context.verify_mode.to_i.anybits?(::OpenSSL::SSL::VERIFY_PEER)
|
|
231
|
+
'verify_mode is VERIFY_NONE, so the server certificate is not checked'
|
|
232
|
+
elsif !ssl_context.verify_hostname
|
|
233
|
+
'verify_hostname is false, so the certificate is not checked against the hostname'
|
|
234
|
+
end
|
|
235
|
+
return unless problem
|
|
236
|
+
|
|
237
|
+
Dalli.logger.warn "SECURITY WARNING: Dalli's ssl_context #{problem}. " \
|
|
238
|
+
'Use ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER, ' \
|
|
239
|
+
'verify_hostname: true) to verify the server.'
|
|
240
|
+
@@ssl_verification_warning_logged = true # rubocop:disable Style/ClassVars
|
|
212
241
|
end
|
|
213
242
|
end
|
|
214
243
|
|
data/lib/dalli/version.rb
CHANGED
data/lib/dalli.rb
CHANGED
|
@@ -31,6 +31,10 @@ module Dalli
|
|
|
31
31
|
# socket/server communication error that can be retried
|
|
32
32
|
class RetryableNetworkError < NetworkError; end
|
|
33
33
|
|
|
34
|
+
# a multi-get reply named a key that wasn't requested, so the rest of that
|
|
35
|
+
# server's replies can't be trusted
|
|
36
|
+
class UnexpectedReplyKeyError < DalliError; end
|
|
37
|
+
|
|
34
38
|
# Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
|
|
35
39
|
class NilObject; end # rubocop:disable Lint/EmptyClass
|
|
36
40
|
NOT_FOUND = NilObject.new
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dalli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.1.
|
|
4
|
+
version: 5.1.6
|
|
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: []
|