dalli 4.3.8 → 4.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +19 -0
- data/lib/dalli/client.rb +9 -3
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pipelined_getter.rb +36 -19
- data/lib/dalli/pipelined_setter.rb +17 -8
- data/lib/dalli/protocol/base.rb +58 -12
- data/lib/dalli/protocol/binary.rb +4 -1
- data/lib/dalli/protocol/connection_manager.rb +38 -3
- data/lib/dalli/protocol/meta.rb +4 -1
- 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
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41490f01a85678506bed8f5e234cbbea7dadee8d5369f8c9800206a63a82260c
|
|
4
|
+
data.tar.gz: 5176f61b9332988912f0216f516caedcfe0fb8a86f0eeea4d129e52ec7a6f183
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abccb3715898dcb8c1f6f3581f807253c91cf0190a2ff7ec44c70aea73d4312e5f39faebce3f510b47686cce02c8545806397cb8fb574801b36a6077a06582a2
|
|
7
|
+
data.tar.gz: 468fa248795f1e7c4905bf47c365e0807428ca2b05e5499d4686ea94b506584d818c5bd03b09d822182def1b3cb76ac97feab8977b8a906d5362d891f7120185
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,37 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
4.3.10
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
Bug fixes:
|
|
11
|
+
|
|
12
|
+
- `set_multi` across more than one server stored `nil` for a Symbol key or a key over 250 bytes, in place of the value given
|
|
13
|
+
|
|
14
|
+
4.3.9
|
|
15
|
+
==========
|
|
16
|
+
|
|
17
|
+
Security:
|
|
18
|
+
|
|
19
|
+
- Keep a connection shared by several threads or fibers in step during a `quiet` block (GHSA-5xvx-r8rg-5qm2)
|
|
20
|
+
- 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
|
|
21
|
+
- Don't let a `get_multi` that stopped early leave replies for a later request (GHSA-5xvx-r8rg-5qm2)
|
|
22
|
+
- 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
|
|
23
|
+
- Affects every supported line (the `get_multi` issue, 4.3 and later), with both protocols; with the binary protocol the connection stayed out of step after the block ended. Fixed in 5.2.4, 5.1.6, 5.0.12, 4.3.9 and 3.2.15
|
|
24
|
+
|
|
25
|
+
Bug fixes:
|
|
26
|
+
|
|
27
|
+
- A per-request `raw: true` write no longer compresses the value (backport of #1213)
|
|
28
|
+
- 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
|
|
29
|
+
- 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
|
|
30
|
+
- Handle failed TLS handshakes like other connection failures, and warn when TLS verification is off (backport of #1215)
|
|
31
|
+
- 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)
|
|
32
|
+
- 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
|
|
33
|
+
- 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)
|
|
34
|
+
- One value that can't be stored no longer discards other buffered writes (backport of #1218)
|
|
35
|
+
- 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`, and `set_multi` did this without raising
|
|
36
|
+
- `set_multi` now stores every other key, skips the bad one, and then raises
|
|
37
|
+
|
|
7
38
|
4.3.8
|
|
8
39
|
==========
|
|
9
40
|
|
data/README.md
CHANGED
|
@@ -86,6 +86,25 @@ See the [4.0-Upgrade.md](4.0-Upgrade.md) guide for more information.
|
|
|
86
86
|
|
|
87
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
88
|
|
|
89
|
+
## TLS
|
|
90
|
+
|
|
91
|
+
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:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
95
|
+
ssl_context.set_params(
|
|
96
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
|
97
|
+
verify_hostname: true,
|
|
98
|
+
ca_file: '/path/to/ca.crt' # omit to use the system's trusted certificates
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
Dalli::Client.new('memcached.example.com:11211', ssl_context: ssl_context)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
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.
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
89
108
|
## OpenTelemetry Tracing
|
|
90
109
|
|
|
91
110
|
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
|
@@ -42,9 +42,10 @@ module Dalli
|
|
|
42
42
|
# #fetch operations.
|
|
43
43
|
# - :raw - If set, disables serialization and compression entirely at the client level.
|
|
44
44
|
# Only String values are supported. This is useful when the caller handles its own
|
|
45
|
-
# serialization (e.g., Rails' ActiveSupport::Cache).
|
|
46
|
-
#
|
|
47
|
-
#
|
|
45
|
+
# serialization (e.g., Rails' ActiveSupport::Cache). The per-request :raw option
|
|
46
|
+
# (e.g. set(key, value, ttl, raw: true)) does the same for a single call: String
|
|
47
|
+
# values are stored as-is, uncompressed, and non-String values raise
|
|
48
|
+
# Dalli::MarshalError.
|
|
48
49
|
# - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
|
|
49
50
|
# useful for injecting a FIPS compliant hash object.
|
|
50
51
|
# - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
|
|
@@ -306,6 +307,11 @@ module Dalli
|
|
|
306
307
|
# Any error replies will be discarded at the end of the block, and
|
|
307
308
|
# Dalli client methods invoked inside the block will not
|
|
308
309
|
# have return values
|
|
310
|
+
#
|
|
311
|
+
# The block is quiet only for the thread or fiber that runs it. Another
|
|
312
|
+
# thread or fiber sharing this client can make requests meanwhile; a
|
|
313
|
+
# non-quiet one first sends the block's buffered requests and discards
|
|
314
|
+
# their replies, so it reads only its own.
|
|
309
315
|
def quiet
|
|
310
316
|
old = Thread.current[::Dalli::QUIET]
|
|
311
317
|
Thread.current[::Dalli::QUIET] = true
|
data/lib/dalli/options.rb
CHANGED
|
@@ -26,15 +26,21 @@ module Dalli
|
|
|
26
26
|
return {} if keys.empty?
|
|
27
27
|
|
|
28
28
|
@ring.lock do
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
# The servers with replies still to read, updated in place
|
|
30
|
+
servers = []
|
|
31
|
+
begin
|
|
32
|
+
# Stores partial results collected during interleaved send phase
|
|
33
|
+
@partial_results = {}
|
|
34
|
+
setup_requests(servers, keys)
|
|
35
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
# First yield any partial results collected during interleaved send
|
|
38
|
+
yield_partial_results(&block)
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
|
|
41
|
+
ensure
|
|
42
|
+
abort_unfinished(servers)
|
|
43
|
+
end
|
|
38
44
|
end
|
|
39
45
|
rescue Dalli::RetryableNetworkError => e
|
|
40
46
|
Dalli.logger.debug { e.inspect }
|
|
@@ -51,12 +57,15 @@ module Dalli
|
|
|
51
57
|
@partial_results.clear
|
|
52
58
|
end
|
|
53
59
|
|
|
54
|
-
|
|
60
|
+
# Adds the servers with a pending response to servers before writing
|
|
61
|
+
# their queries, so #process can abort them if it stops early.
|
|
62
|
+
def setup_requests(servers, keys)
|
|
55
63
|
groups = groups_for_keys(keys)
|
|
64
|
+
servers.concat(groups.keys)
|
|
56
65
|
make_getkq_requests(groups)
|
|
57
66
|
|
|
58
67
|
# TODO: How does this exit on a NetworkError
|
|
59
|
-
finish_queries(
|
|
68
|
+
finish_queries(servers)
|
|
60
69
|
end
|
|
61
70
|
|
|
62
71
|
##
|
|
@@ -118,29 +127,39 @@ module Dalli
|
|
|
118
127
|
raise
|
|
119
128
|
end
|
|
120
129
|
|
|
121
|
-
# Swallows Dalli::NetworkError
|
|
130
|
+
# Swallows Dalli::NetworkError. Empties servers, so they aren't aborted
|
|
131
|
+
# again.
|
|
122
132
|
def abort_without_timeout(servers)
|
|
123
133
|
servers.each(&:pipeline_abort)
|
|
134
|
+
servers.clear
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# When #process stops before reading every server's replies (the caller's
|
|
138
|
+
# block raises or breaks, a Timeout.timeout fires, a value can't be
|
|
139
|
+
# deserialized), the rest are still on the socket and some may be in the
|
|
140
|
+
# response buffer. Closing those connections keeps them from being read
|
|
141
|
+
# as a later request's replies. Aborts that #process already handled
|
|
142
|
+
# emptied servers.
|
|
143
|
+
def abort_unfinished(servers)
|
|
144
|
+
servers.each { |server| server.pipeline_abort(timed_out: false) }
|
|
145
|
+
servers.clear
|
|
124
146
|
end
|
|
125
147
|
|
|
148
|
+
# Reads the replies available, removing finished servers from servers
|
|
126
149
|
def fetch_responses(servers, start_time, timeout, &block)
|
|
127
150
|
# Remove any servers which are not connected
|
|
128
151
|
servers.select!(&:connected?)
|
|
129
|
-
return
|
|
152
|
+
return if servers.empty?
|
|
130
153
|
|
|
131
154
|
time_left = remaining_time(start_time, timeout)
|
|
132
155
|
readable_servers = servers_with_response(servers, time_left)
|
|
133
|
-
if readable_servers.empty?
|
|
134
|
-
abort_with_timeout(servers)
|
|
135
|
-
return []
|
|
136
|
-
end
|
|
156
|
+
return abort_with_timeout(servers) if readable_servers.empty?
|
|
137
157
|
|
|
138
158
|
# Loop through the servers with responses, and
|
|
139
159
|
# delete any from our list that are finished
|
|
140
160
|
readable_servers.each do |server|
|
|
141
161
|
servers.delete(server) if process_server(server, &block)
|
|
142
162
|
end
|
|
143
|
-
servers
|
|
144
163
|
rescue NetworkError
|
|
145
164
|
# Abort and raise if we encountered a network error. This triggers
|
|
146
165
|
# a retry at the top level on RetryableNetworkError.
|
|
@@ -157,12 +176,10 @@ module Dalli
|
|
|
157
176
|
|
|
158
177
|
# Swallows Dalli::NetworkError
|
|
159
178
|
def abort_with_timeout(servers)
|
|
160
|
-
abort_without_timeout(servers)
|
|
161
179
|
servers.each do |server|
|
|
162
180
|
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
|
163
181
|
end
|
|
164
|
-
|
|
165
|
-
true # Required to simplify caller
|
|
182
|
+
abort_without_timeout(servers)
|
|
166
183
|
end
|
|
167
184
|
|
|
168
185
|
# Processes responses from a server. Returns true if there are no
|
|
@@ -16,6 +16,9 @@ module Dalli
|
|
|
16
16
|
# Writes multiple key-value pairs to memcached.
|
|
17
17
|
# Raises an error if any server is unavailable.
|
|
18
18
|
#
|
|
19
|
+
# A value that can't be marshalled or is over value_max_bytes is skipped;
|
|
20
|
+
# the other keys are still stored, then the first such error is raised.
|
|
21
|
+
#
|
|
19
22
|
# @param hash [Hash] key-value pairs to set
|
|
20
23
|
# @param ttl [Integer] time-to-live in seconds
|
|
21
24
|
# @param req_options [Hash] options passed to each set operation
|
|
@@ -24,10 +27,12 @@ module Dalli
|
|
|
24
27
|
def process(hash, ttl, req_options)
|
|
25
28
|
return if hash.empty?
|
|
26
29
|
|
|
30
|
+
@value_error = nil
|
|
27
31
|
@ring.lock do
|
|
28
32
|
servers = setup_requests(hash, ttl, req_options)
|
|
29
33
|
finish_requests(servers)
|
|
30
34
|
end
|
|
35
|
+
raise @value_error if @value_error
|
|
31
36
|
rescue Dalli::RetryableNetworkError => e
|
|
32
37
|
Dalli.logger.debug { e.inspect }
|
|
33
38
|
Dalli.logger.debug { 'retrying pipelined sets because of network error' }
|
|
@@ -36,9 +41,12 @@ module Dalli
|
|
|
36
41
|
|
|
37
42
|
private
|
|
38
43
|
|
|
44
|
+
# Values are looked up by each key as sent. The caller's key can't be
|
|
45
|
+
# rebuilt from it: a long key is digested, and a Symbol becomes a String.
|
|
39
46
|
def setup_requests(hash, ttl, req_options)
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
pairs = hash.transform_keys { |k| @key_manager.validate_key(k.to_s) }
|
|
48
|
+
groups = groups_for_keys(pairs.keys)
|
|
49
|
+
make_set_requests(groups, pairs, ttl, req_options)
|
|
42
50
|
groups.keys
|
|
43
51
|
end
|
|
44
52
|
|
|
@@ -46,12 +54,14 @@ module Dalli
|
|
|
46
54
|
# Loop through the server-grouped sets of keys, writing
|
|
47
55
|
# the corresponding quiet set requests to the appropriate servers
|
|
48
56
|
##
|
|
49
|
-
def make_set_requests(groups,
|
|
57
|
+
def make_set_requests(groups, pairs, ttl, req_options)
|
|
50
58
|
groups.each do |server, keys_for_server|
|
|
51
59
|
keys_for_server.each do |key|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
server.request(:pipelined_set, key, pairs[key], ttl, req_options)
|
|
61
|
+
rescue Dalli::MarshalError, Dalli::ValueOverMaxSize => e
|
|
62
|
+
# Nothing was written for this key, so the server's other keys are
|
|
63
|
+
# unaffected; raised by #process once the batch is complete
|
|
64
|
+
@value_error ||= e
|
|
55
65
|
rescue DalliError, NetworkError => e
|
|
56
66
|
Dalli.logger.debug { e.inspect }
|
|
57
67
|
Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
|
|
@@ -72,8 +82,7 @@ module Dalli
|
|
|
72
82
|
end
|
|
73
83
|
|
|
74
84
|
def groups_for_keys(keys)
|
|
75
|
-
|
|
76
|
-
groups = @ring.keys_grouped_by_server(validated_keys)
|
|
85
|
+
groups = @ring.keys_grouped_by_server(keys)
|
|
77
86
|
|
|
78
87
|
if (unfound_keys = groups.delete(nil))
|
|
79
88
|
Dalli.logger.debug do
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -49,14 +49,23 @@ module Dalli
|
|
|
49
49
|
|
|
50
50
|
begin
|
|
51
51
|
@connection_manager.start_request!
|
|
52
|
+
drain_pending_quiet_responses(opkey)
|
|
52
53
|
response = send(opkey, *args)
|
|
54
|
+
# Quiet requests can leave replies on the socket; remember that so a
|
|
55
|
+
# later non-quiet request knows to read them first
|
|
56
|
+
@connection_manager.mark_quiet_responses_pending! if quiet? && opkey != :noop
|
|
53
57
|
|
|
54
58
|
# pipelined_get/pipelined_get_interleaved emit query but don't read the response(s)
|
|
55
59
|
@connection_manager.finish_request! unless %i[pipelined_get pipelined_get_interleaved].include?(opkey)
|
|
56
60
|
|
|
57
61
|
response
|
|
58
|
-
rescue Dalli::MarshalError => e
|
|
59
|
-
log_marshal_err(args.first, e)
|
|
62
|
+
rescue Dalli::MarshalError, Dalli::ValueOverMaxSize => e
|
|
63
|
+
log_marshal_err(args.first, e) if e.is_a?(Dalli::MarshalError)
|
|
64
|
+
# Raised only while the connection is clean: a value is marshalled
|
|
65
|
+
# before any of its request is written. Leaving the request in
|
|
66
|
+
# progress would close the connection on the next request, which
|
|
67
|
+
# discards earlier quiet writes still waiting in the write buffer.
|
|
68
|
+
@connection_manager.abort_request!
|
|
60
69
|
raise
|
|
61
70
|
rescue Dalli::DalliError
|
|
62
71
|
raise
|
|
@@ -91,7 +100,7 @@ module Dalli
|
|
|
91
100
|
verify_pipelined_state(:getkq)
|
|
92
101
|
write_noop
|
|
93
102
|
# Use ensure_ready instead of reset to preserve any data already buffered
|
|
94
|
-
#
|
|
103
|
+
# while this get_multi's interleaved requests were sent
|
|
95
104
|
response_buffer.ensure_ready
|
|
96
105
|
end
|
|
97
106
|
|
|
@@ -146,6 +155,10 @@ module Dalli
|
|
|
146
155
|
end
|
|
147
156
|
end
|
|
148
157
|
|
|
158
|
+
# A request to this server from inside the block closes the
|
|
159
|
+
# connection, and with it the rest of this get_multi's replies
|
|
160
|
+
raise_pipeline_closed unless response_buffer.in_progress?
|
|
161
|
+
|
|
149
162
|
# Get the next response from the buffer
|
|
150
163
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
151
164
|
end
|
|
@@ -154,21 +167,24 @@ module Dalli
|
|
|
154
167
|
end
|
|
155
168
|
|
|
156
169
|
# Abort current pipelined get. Generally used to signal an external
|
|
157
|
-
# timeout during pipelined get
|
|
170
|
+
# timeout during pipelined get, or that the get_multi stopped before
|
|
171
|
+
# reading all its replies (timed_out: false). The underlying socket is
|
|
158
172
|
# disconnected, and the exception is swallowed.
|
|
159
173
|
#
|
|
160
174
|
# Returns nothing.
|
|
161
|
-
def pipeline_abort
|
|
175
|
+
def pipeline_abort(timed_out: true)
|
|
162
176
|
response_buffer.clear
|
|
177
|
+
@pipeline_out_of_step = false
|
|
178
|
+
@expected_reply_keys = nil
|
|
163
179
|
@connection_manager.abort_request!
|
|
164
180
|
return true unless connected?
|
|
165
181
|
|
|
166
182
|
# Closes the connection, which ensures that our connection is in a
|
|
167
183
|
# clean state for future requests. The get_multi ran out of time
|
|
168
|
-
# overall rather than hitting a socket error, so it
|
|
169
|
-
# toward socket_max_failures: two slow get_multis in a
|
|
170
|
-
# otherwise mark a healthy server down.
|
|
171
|
-
|
|
184
|
+
# overall, or stopped early, rather than hitting a socket error, so it
|
|
185
|
+
# doesn't count toward socket_max_failures: two slow get_multis in a
|
|
186
|
+
# row would otherwise mark a healthy server down.
|
|
187
|
+
log_pipeline_abort(timed_out)
|
|
172
188
|
@connection_manager.close
|
|
173
189
|
true
|
|
174
190
|
end
|
|
@@ -202,6 +218,18 @@ module Dalli
|
|
|
202
218
|
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
203
219
|
private_constant :ALLOWED_QUIET_OPS
|
|
204
220
|
|
|
221
|
+
# Reads the replies still pending from quiet requests before any
|
|
222
|
+
# non-quiet request, which reads a reply of its own. A quiet block reads
|
|
223
|
+
# them at its end, but the connection is shared: another thread or fiber
|
|
224
|
+
# (quiet? is fiber-local) can make a request while the block is still
|
|
225
|
+
# open, and would otherwise read the block's replies as its own. The
|
|
226
|
+
# noop also sends any quiet requests still buffered.
|
|
227
|
+
def drain_pending_quiet_responses(opkey)
|
|
228
|
+
return unless @connection_manager.quiet_responses_pending? && opkey != :noop && !quiet?
|
|
229
|
+
|
|
230
|
+
noop
|
|
231
|
+
end
|
|
232
|
+
|
|
205
233
|
def verify_allowed_quiet!(opkey)
|
|
206
234
|
return if ALLOWED_QUIET_OPS.include?(opkey)
|
|
207
235
|
|
|
@@ -277,8 +305,9 @@ module Dalli
|
|
|
277
305
|
# This prevents socket buffer deadlock when sending many keys.
|
|
278
306
|
# Populates the provided results hash with any responses drained during send.
|
|
279
307
|
def pipelined_get_interleaved(keys, chunk_size, results)
|
|
280
|
-
#
|
|
281
|
-
|
|
308
|
+
# Start with an empty buffer for draining during the send phase. Bytes
|
|
309
|
+
# left by an earlier get_multi that stopped early belong to its replies.
|
|
310
|
+
response_buffer.reset
|
|
282
311
|
expect_reply_keys(keys)
|
|
283
312
|
|
|
284
313
|
keys.each_slice(chunk_size) do |chunk|
|
|
@@ -325,8 +354,12 @@ module Dalli
|
|
|
325
354
|
nil
|
|
326
355
|
end
|
|
327
356
|
|
|
357
|
+
# Registered with the connection manager, which clears it whenever the
|
|
358
|
+
# connection closes, so a new connection never parses an old one's bytes
|
|
328
359
|
def response_buffer
|
|
329
|
-
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
360
|
+
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor).tap do |buffer|
|
|
361
|
+
@connection_manager.response_buffer = buffer
|
|
362
|
+
end
|
|
330
363
|
end
|
|
331
364
|
|
|
332
365
|
# A multi-get only accepts replies for the keys it asked for. memcached
|
|
@@ -378,6 +411,19 @@ module Dalli
|
|
|
378
411
|
@connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
|
|
379
412
|
end
|
|
380
413
|
|
|
414
|
+
def raise_pipeline_closed
|
|
415
|
+
raise Dalli::DalliError, "The connection to #{name} closed while a get_multi was reading its replies " \
|
|
416
|
+
'(a request made from inside the get_multi block?)'
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def log_pipeline_abort(timed_out)
|
|
420
|
+
if timed_out
|
|
421
|
+
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
422
|
+
else
|
|
423
|
+
Dalli.logger.debug { "#{name} get_multi stopped before reading all replies; closing the connection" }
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
381
427
|
def log_marshal_err(key, err)
|
|
382
428
|
Dalli.logger.error "Marshalling error for key '#{key}': #{err.message}"
|
|
383
429
|
Dalli.logger.error 'You are trying to cache a Ruby object which cannot be serialized to memcached.'
|
|
@@ -161,7 +161,10 @@ module Dalli
|
|
|
161
161
|
# We need to read all the responses at once.
|
|
162
162
|
def noop
|
|
163
163
|
write_noop
|
|
164
|
-
response_processor.consume_all_responses_until_noop
|
|
164
|
+
result = response_processor.consume_all_responses_until_noop
|
|
165
|
+
# Everything up to and including the noop reply has been read
|
|
166
|
+
@connection_manager.clear_quiet_responses_pending!
|
|
167
|
+
result
|
|
165
168
|
end
|
|
166
169
|
|
|
167
170
|
def stats(info = '')
|
|
@@ -28,6 +28,9 @@ module Dalli
|
|
|
28
28
|
|
|
29
29
|
attr_accessor :hostname, :port, :socket_type, :options
|
|
30
30
|
attr_reader :sock
|
|
31
|
+
# The buffer replies are read into (see Base#response_buffer). It's
|
|
32
|
+
# cleared with the write buffer whenever the connection closes.
|
|
33
|
+
attr_writer :response_buffer
|
|
31
34
|
|
|
32
35
|
def initialize(hostname, port, socket_type, client_options)
|
|
33
36
|
@hostname = hostname
|
|
@@ -35,10 +38,12 @@ module Dalli
|
|
|
35
38
|
@socket_type = socket_type
|
|
36
39
|
@options = DEFAULTS.merge(client_options)
|
|
37
40
|
@request_in_progress = false
|
|
41
|
+
@quiet_responses_pending = false
|
|
38
42
|
@sock = nil
|
|
39
43
|
@pid = nil
|
|
40
44
|
@write_buffer = []
|
|
41
45
|
@write_buffer_bytes = 0
|
|
46
|
+
@response_buffer = nil
|
|
42
47
|
|
|
43
48
|
@fail_count = 0
|
|
44
49
|
reset_down_info
|
|
@@ -60,8 +65,9 @@ module Dalli
|
|
|
60
65
|
@sock.sync = true
|
|
61
66
|
@pid = Process.pid
|
|
62
67
|
@request_in_progress = false
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
@quiet_responses_pending = false
|
|
69
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError, SocketError => e
|
|
70
|
+
# SocketError = DNS resolution failure; SSL_ERRORS = failed TLS handshake
|
|
65
71
|
error_on_request!(e)
|
|
66
72
|
end
|
|
67
73
|
|
|
@@ -126,7 +132,9 @@ module Dalli
|
|
|
126
132
|
end
|
|
127
133
|
@sock = nil
|
|
128
134
|
@pid = nil
|
|
135
|
+
@quiet_responses_pending = false
|
|
129
136
|
discard_write_buffer
|
|
137
|
+
@response_buffer&.clear
|
|
130
138
|
abort_request!
|
|
131
139
|
end
|
|
132
140
|
|
|
@@ -141,6 +149,21 @@ module Dalli
|
|
|
141
149
|
!@sock.nil?
|
|
142
150
|
end
|
|
143
151
|
|
|
152
|
+
# True when a quiet request was written on this connection and its
|
|
153
|
+
# replies (memcached only suppresses some of them) haven't been
|
|
154
|
+
# consumed by a noop yet.
|
|
155
|
+
def quiet_responses_pending?
|
|
156
|
+
@quiet_responses_pending
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def mark_quiet_responses_pending!
|
|
160
|
+
@quiet_responses_pending = true
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def clear_quiet_responses_pending!
|
|
164
|
+
@quiet_responses_pending = false
|
|
165
|
+
end
|
|
166
|
+
|
|
144
167
|
def request_in_progress?
|
|
145
168
|
@request_in_progress
|
|
146
169
|
end
|
|
@@ -166,10 +189,22 @@ module Dalli
|
|
|
166
189
|
@request_in_progress = false
|
|
167
190
|
end
|
|
168
191
|
|
|
192
|
+
# The longest reply line Dalli reads, in bytes, CRLF included. A meta
|
|
193
|
+
# reply header is at most about 500 bytes: "VA <size>" plus its flags,
|
|
194
|
+
# the largest being a 250-byte key base64-encoded to 336 bytes, a
|
|
195
|
+
# 20-digit CAS, a 32-byte opaque and a few numeric fields. The longest
|
|
196
|
+
# stats line (stats settings) carries a file path such as ext_path, up
|
|
197
|
+
# to PATH_MAX (4096). 8 KiB leaves room for both, while a server that
|
|
198
|
+
# never sends CRLF can't grow the read buffer without bound.
|
|
199
|
+
MAX_LINE_BYTES = 8 * 1024
|
|
200
|
+
|
|
169
201
|
def read_line
|
|
170
202
|
flush_write_buffer
|
|
171
|
-
data = @sock.gets("\r\n")
|
|
203
|
+
data = @sock.gets("\r\n", MAX_LINE_BYTES)
|
|
172
204
|
error_on_request!('EOF in read_line') if data.nil?
|
|
205
|
+
# Without its CRLF the line either ran past MAX_LINE_BYTES or was cut
|
|
206
|
+
# short by EOF; either way the connection is out of step
|
|
207
|
+
error_on_request!("Reply line without CRLF in the first #{MAX_LINE_BYTES} bytes") unless data.end_with?("\r\n")
|
|
173
208
|
data
|
|
174
209
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
175
210
|
error_on_request!(e)
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -263,7 +263,10 @@ module Dalli
|
|
|
263
263
|
# We need to read all the responses at once.
|
|
264
264
|
def noop
|
|
265
265
|
write_noop
|
|
266
|
-
response_processor.consume_all_responses_until_mn
|
|
266
|
+
result = response_processor.consume_all_responses_until_mn
|
|
267
|
+
# Everything up to and including the MN has been read
|
|
268
|
+
@connection_manager.clear_quiet_responses_pending!
|
|
269
|
+
result
|
|
267
270
|
end
|
|
268
271
|
|
|
269
272
|
def stats(info = nil)
|
|
@@ -88,11 +88,13 @@ module Dalli
|
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
# Checks whether we should apply compression when serializing a value
|
|
91
|
-
# based on the specified options. Returns false
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
91
|
+
# based on the specified options. Returns false for a raw request
|
|
92
|
+
# (raw reads ignore bitflags, so the value must be stored as given) or
|
|
93
|
+
# unless the value is greater than the minimum compression size.
|
|
94
|
+
# Otherwise returns based on a method-level option if specified,
|
|
95
|
+
# falling back to the server default.
|
|
95
96
|
def compress_value?(value, req_options)
|
|
97
|
+
return false if req_options&.dig(:raw)
|
|
96
98
|
return false unless value.bytesize >= compression_min_size
|
|
97
99
|
return compress_by_default? unless req_options && !req_options[:compress].nil?
|
|
98
100
|
|
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
|
@@ -181,12 +181,41 @@ module Dalli
|
|
|
181
181
|
[seconds, microseconds].pack(timeval_pack_format(sock))
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
+
# Closes the TCP socket if the handshake fails or times out, so a failed
|
|
185
|
+
# attempt doesn't hold a file descriptor until GC.
|
|
184
186
|
def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
|
|
187
|
+
warn_if_unverified(ssl_context)
|
|
188
|
+
connected = false
|
|
185
189
|
ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
|
|
186
190
|
ssl_socket.hostname = host
|
|
187
191
|
ssl_socket.sync_close = true
|
|
188
192
|
ssl_socket.connect
|
|
193
|
+
connected = true
|
|
189
194
|
ssl_socket
|
|
195
|
+
ensure
|
|
196
|
+
tcp_socket.close unless connected
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Class variable to track whether the TLS verification warning has been logged
|
|
200
|
+
@@ssl_verification_warning_logged = false # rubocop:disable Style/ClassVars
|
|
201
|
+
|
|
202
|
+
# Verification comes entirely from the caller's ssl_context, and a bare
|
|
203
|
+
# SSLContext.new verifies nothing, so warn (once per process) when the
|
|
204
|
+
# certificate or the hostname won't be checked.
|
|
205
|
+
def self.warn_if_unverified(ssl_context)
|
|
206
|
+
return if @@ssl_verification_warning_logged
|
|
207
|
+
|
|
208
|
+
problem = if !ssl_context.verify_mode.to_i.anybits?(::OpenSSL::SSL::VERIFY_PEER)
|
|
209
|
+
'verify_mode is VERIFY_NONE, so the server certificate is not checked'
|
|
210
|
+
elsif !ssl_context.verify_hostname
|
|
211
|
+
'verify_hostname is false, so the certificate is not checked against the hostname'
|
|
212
|
+
end
|
|
213
|
+
return unless problem
|
|
214
|
+
|
|
215
|
+
Dalli.logger.warn "SECURITY WARNING: Dalli's ssl_context #{problem}. " \
|
|
216
|
+
'Use ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER, ' \
|
|
217
|
+
'verify_hostname: true) to verify the server.'
|
|
218
|
+
@@ssl_verification_warning_logged = true # rubocop:disable Style/ClassVars
|
|
190
219
|
end
|
|
191
220
|
end
|
|
192
221
|
|
data/lib/dalli/version.rb
CHANGED