dalli 5.2.3 → 5.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/dalli/client.rb +5 -0
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pipelined_getter.rb +36 -23
- data/lib/dalli/protocol/base.rb +45 -15
- data/lib/dalli/protocol/connection_manager.rb +5 -0
- data/lib/dalli/protocol/meta.rb +6 -4
- data/lib/dalli/protocol.rb +4 -6
- 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: 626c8dbb98798247423221395d6d518a87ad9ce64ce268488ecd4eb7d8d7f347
|
|
4
|
+
data.tar.gz: 999126be42f4c80be3eb566d3f10dfbb8e10b46446aaaf0da546523c0e734eef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 34aaa230697fd4ac27118ec8a90608d30bdc8dabaf9fc5cfc1b8c4e56a20c701708f1b6b7052fd76c7363f25e238905648296865c41b770f434457005af7aac8
|
|
7
|
+
data.tar.gz: 7bf1bbe810632bd9733a9cb122900ff9e1b283ade2ef071cec13d609cf4e0b2580de06f1116e9a71685fe98aa7804022c719ef3b921e41e9eb76f2be98b5ab1a
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.2.4
|
|
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. On 5.2, clients with `defer_drain: true` weren't affected by the `quiet` issue
|
|
17
|
+
|
|
18
|
+
Bug fixes:
|
|
19
|
+
|
|
20
|
+
- A failed TLS handshake is now handled as a network error even when openssl wasn't loaded before Dalli
|
|
21
|
+
- The fix in 5.2.3 (#1215) took effect only when the application had already required openssl: otherwise Dalli's list of TLS errors was empty, so a failed handshake still raised `OpenSSL::SSL::SSLError` and left the socket open
|
|
22
|
+
|
|
7
23
|
5.2.3
|
|
8
24
|
==========
|
|
9
25
|
|
data/lib/dalli/client.rb
CHANGED
|
@@ -434,6 +434,11 @@ module Dalli
|
|
|
434
434
|
# the defer_drain option, before the next non-quiet request), and
|
|
435
435
|
# Dalli client methods invoked inside the block will not
|
|
436
436
|
# have return values
|
|
437
|
+
#
|
|
438
|
+
# The block is quiet only for the thread or fiber that runs it. Another
|
|
439
|
+
# thread or fiber sharing this client can make requests meanwhile; a
|
|
440
|
+
# non-quiet one first sends the block's buffered requests and discards
|
|
441
|
+
# their replies, so it reads only its own.
|
|
437
442
|
def quiet
|
|
438
443
|
old = Thread.current[::Dalli::QUIET]
|
|
439
444
|
Thread.current[::Dalli::QUIET] = true
|
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 }
|
|
@@ -96,9 +102,9 @@ module Dalli
|
|
|
96
102
|
|
|
97
103
|
# Sends each server's queries and its terminating noop before building
|
|
98
104
|
# the next server's, so memcached is already answering the first servers
|
|
99
|
-
# while the rest are prepared.
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
# while the rest are prepared. Adds the servers with a pending response
|
|
106
|
+
# to started.
|
|
107
|
+
def setup_requests(started, keys, req_options = nil, return_cas: true)
|
|
102
108
|
groups_for_keys(keys).each do |server, keys_for_server|
|
|
103
109
|
make_getkq_request(server, keys_for_server, req_options, return_cas: return_cas)
|
|
104
110
|
next unless server.connected?
|
|
@@ -111,7 +117,6 @@ module Dalli
|
|
|
111
117
|
rescue Dalli::DalliError
|
|
112
118
|
started.delete(server)
|
|
113
119
|
end
|
|
114
|
-
started
|
|
115
120
|
end
|
|
116
121
|
|
|
117
122
|
##
|
|
@@ -155,29 +160,39 @@ module Dalli
|
|
|
155
160
|
raise
|
|
156
161
|
end
|
|
157
162
|
|
|
158
|
-
# Swallows Dalli::NetworkError
|
|
163
|
+
# Swallows Dalli::NetworkError. Empties servers, so they aren't aborted
|
|
164
|
+
# again.
|
|
159
165
|
def abort_without_timeout(servers)
|
|
160
166
|
servers.each(&:pipeline_abort)
|
|
167
|
+
servers.clear
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# When #process stops before reading every server's replies (the caller's
|
|
171
|
+
# block raises or breaks, a Timeout.timeout fires, a value can't be
|
|
172
|
+
# deserialized), the rest are still on the socket and some may be in the
|
|
173
|
+
# response buffer. Closing those connections keeps them from being read
|
|
174
|
+
# as a later request's replies. Aborts that #process already handled
|
|
175
|
+
# emptied servers.
|
|
176
|
+
def abort_unfinished(servers)
|
|
177
|
+
servers.each { |server| server.pipeline_abort(timed_out: false) }
|
|
178
|
+
servers.clear
|
|
161
179
|
end
|
|
162
180
|
|
|
181
|
+
# Reads the replies available, removing finished servers from servers
|
|
163
182
|
def fetch_responses(servers, start_time, timeout, &block)
|
|
164
183
|
# Remove any servers which are not connected
|
|
165
184
|
servers.select!(&:connected?)
|
|
166
|
-
return
|
|
185
|
+
return if servers.empty?
|
|
167
186
|
|
|
168
187
|
time_left = remaining_time(start_time, timeout)
|
|
169
188
|
readable_servers = servers_with_response(servers, time_left)
|
|
170
|
-
if readable_servers.empty?
|
|
171
|
-
abort_with_timeout(servers)
|
|
172
|
-
return []
|
|
173
|
-
end
|
|
189
|
+
return abort_with_timeout(servers) if readable_servers.empty?
|
|
174
190
|
|
|
175
191
|
# Loop through the servers with responses, and
|
|
176
192
|
# delete any from our list that are finished
|
|
177
193
|
readable_servers.each do |server|
|
|
178
194
|
servers.delete(server) if process_server(server, &block)
|
|
179
195
|
end
|
|
180
|
-
servers
|
|
181
196
|
rescue NetworkError
|
|
182
197
|
# Abort and raise if we encountered a network error. This triggers
|
|
183
198
|
# a retry at the top level on RetryableNetworkError.
|
|
@@ -194,12 +209,10 @@ module Dalli
|
|
|
194
209
|
|
|
195
210
|
# Swallows Dalli::NetworkError
|
|
196
211
|
def abort_with_timeout(servers)
|
|
197
|
-
abort_without_timeout(servers)
|
|
198
212
|
servers.each do |server|
|
|
199
213
|
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
|
200
214
|
end
|
|
201
|
-
|
|
202
|
-
true # Required to simplify caller
|
|
215
|
+
abort_without_timeout(servers)
|
|
203
216
|
end
|
|
204
217
|
|
|
205
218
|
# Processes responses from a server. Returns true if there are no
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -69,7 +69,7 @@ module Dalli
|
|
|
69
69
|
begin
|
|
70
70
|
request_completed = false
|
|
71
71
|
@connection_manager.start_request!
|
|
72
|
-
|
|
72
|
+
drain_pending_quiet_responses(opkey)
|
|
73
73
|
response = send(opkey, *args)
|
|
74
74
|
# Quiet requests can leave replies on the socket; remember that so the
|
|
75
75
|
# end of the quiet block knows this server needs draining
|
|
@@ -147,7 +147,7 @@ module Dalli
|
|
|
147
147
|
verify_pipelined_state(:getkq)
|
|
148
148
|
write_noop
|
|
149
149
|
# Use ensure_ready instead of reset to preserve any data already buffered
|
|
150
|
-
#
|
|
150
|
+
# while this get_multi's interleaved requests were sent
|
|
151
151
|
response_buffer.ensure_ready
|
|
152
152
|
end
|
|
153
153
|
|
|
@@ -201,6 +201,10 @@ module Dalli
|
|
|
201
201
|
end
|
|
202
202
|
end
|
|
203
203
|
|
|
204
|
+
# A request to this server from inside the block closes the
|
|
205
|
+
# connection, and with it the rest of this get_multi's replies
|
|
206
|
+
raise_pipeline_closed unless response_buffer.in_progress?
|
|
207
|
+
|
|
204
208
|
# Get the next response from the buffer
|
|
205
209
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
206
210
|
end
|
|
@@ -210,21 +214,24 @@ module Dalli
|
|
|
210
214
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
211
215
|
|
|
212
216
|
# Abort current pipelined get. Generally used to signal an external
|
|
213
|
-
# timeout during pipelined get
|
|
217
|
+
# timeout during pipelined get, or that the get_multi stopped before
|
|
218
|
+
# reading all its replies (timed_out: false). The underlying socket is
|
|
214
219
|
# disconnected, and the exception is swallowed.
|
|
215
220
|
#
|
|
216
221
|
# Returns nothing.
|
|
217
|
-
def pipeline_abort
|
|
222
|
+
def pipeline_abort(timed_out: true)
|
|
218
223
|
response_buffer.clear
|
|
224
|
+
@pipeline_out_of_step = false
|
|
225
|
+
@expected_reply_keys = nil
|
|
219
226
|
@connection_manager.abort_request!
|
|
220
227
|
return true unless connected?
|
|
221
228
|
|
|
222
229
|
# Closes the connection, which ensures that our connection is in a
|
|
223
230
|
# clean state for future requests. The get_multi ran out of time
|
|
224
|
-
# overall rather than hitting a socket error, so it
|
|
225
|
-
# toward socket_max_failures: two slow get_multis in a
|
|
226
|
-
# otherwise mark a healthy server down.
|
|
227
|
-
|
|
231
|
+
# overall, or stopped early, rather than hitting a socket error, so it
|
|
232
|
+
# doesn't count toward socket_max_failures: two slow get_multis in a
|
|
233
|
+
# row would otherwise mark a healthy server down.
|
|
234
|
+
log_pipeline_abort(timed_out)
|
|
228
235
|
@connection_manager.close
|
|
229
236
|
true
|
|
230
237
|
end
|
|
@@ -256,10 +263,15 @@ module Dalli
|
|
|
256
263
|
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
257
264
|
private_constant :ALLOWED_QUIET_OPS
|
|
258
265
|
|
|
259
|
-
#
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
|
|
266
|
+
# Reads the replies still pending from quiet requests before any
|
|
267
|
+
# non-quiet request, which reads a reply of its own. With defer_drain,
|
|
268
|
+
# quiet blocks leave their replies unread. Without it, a quiet block
|
|
269
|
+
# reads them at its end, but the connection is shared: another thread or
|
|
270
|
+
# fiber (quiet? is fiber-local) can make a request while the block is
|
|
271
|
+
# still open, and would otherwise read the block's replies as its own.
|
|
272
|
+
# The noop also sends any quiet requests still buffered.
|
|
273
|
+
def drain_pending_quiet_responses(opkey)
|
|
274
|
+
return unless @connection_manager.quiet_responses_pending? && opkey != :noop && !quiet?
|
|
263
275
|
|
|
264
276
|
noop
|
|
265
277
|
end
|
|
@@ -372,8 +384,9 @@ module Dalli
|
|
|
372
384
|
# This prevents socket buffer deadlock when sending many keys.
|
|
373
385
|
# Populates the provided results hash with any responses drained during send.
|
|
374
386
|
def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
|
|
375
|
-
#
|
|
376
|
-
|
|
387
|
+
# Start with an empty buffer for draining during the send phase. Bytes
|
|
388
|
+
# left by an earlier get_multi that stopped early belong to its replies.
|
|
389
|
+
response_buffer.reset
|
|
377
390
|
response_buffer.raw = raw_request?(options)
|
|
378
391
|
expect_reply_keys(keys)
|
|
379
392
|
|
|
@@ -421,8 +434,12 @@ module Dalli
|
|
|
421
434
|
nil
|
|
422
435
|
end
|
|
423
436
|
|
|
437
|
+
# Registered with the connection manager, which clears it whenever the
|
|
438
|
+
# connection closes, so a new connection never parses an old one's bytes
|
|
424
439
|
def response_buffer
|
|
425
|
-
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
440
|
+
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor).tap do |buffer|
|
|
441
|
+
@connection_manager.response_buffer = buffer
|
|
442
|
+
end
|
|
426
443
|
end
|
|
427
444
|
|
|
428
445
|
# A multi-get only accepts replies for the keys it asked for. memcached
|
|
@@ -474,6 +491,19 @@ module Dalli
|
|
|
474
491
|
@connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
|
|
475
492
|
end
|
|
476
493
|
|
|
494
|
+
def raise_pipeline_closed
|
|
495
|
+
raise Dalli::DalliError, "The connection to #{name} closed while a get_multi was reading its replies " \
|
|
496
|
+
'(a request made from inside the get_multi block?)'
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def log_pipeline_abort(timed_out)
|
|
500
|
+
if timed_out
|
|
501
|
+
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
502
|
+
else
|
|
503
|
+
Dalli.logger.debug { "#{name} get_multi stopped before reading all replies; closing the connection" }
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
477
507
|
# The single-server set_multi path passes its whole pairs hash as the
|
|
478
508
|
# first argument; log only its keys, never the values
|
|
479
509
|
def log_marshal_err(key, err)
|
|
@@ -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
|
|
@@ -38,6 +41,7 @@ module Dalli
|
|
|
38
41
|
@pid = nil
|
|
39
42
|
@write_buffer = []
|
|
40
43
|
@write_buffer_bytes = 0
|
|
44
|
+
@response_buffer = nil
|
|
41
45
|
|
|
42
46
|
@fail_count = 0
|
|
43
47
|
reset_down_info
|
|
@@ -133,6 +137,7 @@ module Dalli
|
|
|
133
137
|
@pid = nil
|
|
134
138
|
@quiet_responses_pending = false
|
|
135
139
|
discard_write_buffer
|
|
140
|
+
@response_buffer&.clear
|
|
136
141
|
abort_request!
|
|
137
142
|
end
|
|
138
143
|
end
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -283,10 +283,12 @@ module Dalli
|
|
|
283
283
|
# Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
|
|
284
284
|
# We need to read all the responses at once.
|
|
285
285
|
# A write that expects a reply is flushed so the reply can be read. A write
|
|
286
|
-
# in a quiet block stays buffered until the
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
# block
|
|
286
|
+
# in a quiet block stays buffered until a noop: the one at the end of the
|
|
287
|
+
# block, or the one another thread or fiber sends to drain the block's
|
|
288
|
+
# replies before its own request. With defer_drain no noop follows at the
|
|
289
|
+
# end of the block, so it is flushed right away. (pipelined_set passes
|
|
290
|
+
# quiet: true outside a quiet block; its caller flushes with a
|
|
291
|
+
# terminating noop.)
|
|
290
292
|
def finish_write(quiet)
|
|
291
293
|
@connection_manager.flush if !quiet || (@defer_drain && quiet?)
|
|
292
294
|
end
|
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
|
|
@@ -19,11 +20,8 @@ module Dalli
|
|
|
19
20
|
# SSL errors during the handshake or during read/write operations count as
|
|
20
21
|
# a failed request: they trigger reconnection and, after
|
|
21
22
|
# socket_max_failures, mark the server down.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
else
|
|
26
|
-
[]
|
|
27
|
-
end
|
|
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/version.rb
CHANGED