dalli 3.2.13 → 3.2.15
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 +26 -0
- data/README.md +23 -0
- data/lib/dalli/client.rb +10 -0
- data/lib/dalli/protocol/base.rb +64 -1
- data/lib/dalli/protocol/binary.rb +4 -1
- data/lib/dalli/protocol/connection_manager.rb +26 -3
- data/lib/dalli/protocol/meta/response_processor.rb +11 -2
- data/lib/dalli/protocol/meta.rb +36 -1
- data/lib/dalli/protocol/value_compressor.rb +6 -4
- data/lib/dalli/protocol.rb +7 -0
- data/lib/dalli/socket.rb +30 -0
- data/lib/dalli/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1282508d83c234919dfb2c8a05c08d7069186663c83d4e3aaf040e6f503a2e9f
|
|
4
|
+
data.tar.gz: cb10108ea1d22c25630b5cfa36b6e038515e3f053e2354e3921718050138535b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 614a4eebdaa9cee1d9dc6afdfd45c96ff1ec7eda12f471a12c6c1eca7866af971d907a7c288aeb5a72734b3f4f72bfb685ba58337da2ac50146ed74a018c0d0a
|
|
7
|
+
data.tar.gz: 8f577fef557f593d9cb6327a54ec1b5d3df8e554a5935ba7a7faadf86259248a60f29204b3ad5a295024b676ebc481e925f81f330095a73e6649e11d182b1127
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
3.2.15
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
- Keep a connection shared by several threads or fibers in step during a `quiet` block (GHSA-5xvx-r8rg-5qm2)
|
|
11
|
+
- 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
|
|
12
|
+
- Affects every supported line, 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
|
|
13
|
+
- With `protocol: :meta`, read values by their size instead of up to the first CRLF (GHSA-5xvx-r8rg-5qm2)
|
|
14
|
+
- `get`, `gat` and `cas` read a value as one line, so a value containing CR/LF was cut short and the rest was parsed as the next reply. A stored value containing a crafted reply was then returned for a later read of a different key, with flags that could cause it to be deserialized
|
|
15
|
+
- Affects 3.2.0 to 3.2.14 with `protocol: :meta`; fixed in 3.2.15. The binary protocol (3.2's default) and 4.0 and later aren't affected
|
|
16
|
+
- A per-request `raw: true` write no longer compresses the value (backport of #1213)
|
|
17
|
+
- 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
|
|
18
|
+
- 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
|
|
19
|
+
- Handle failed TLS handshakes like other connection failures, and warn when TLS verification is off (backport of #1215)
|
|
20
|
+
- 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)
|
|
21
|
+
- 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
|
|
22
|
+
- 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)
|
|
23
|
+
|
|
24
|
+
3.2.14
|
|
25
|
+
==========
|
|
26
|
+
|
|
27
|
+
- Don't rely on memcached's auto-create for keys that need base64 encoding (GHSA-r47x-6p6r-mvcp)
|
|
28
|
+
- 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
|
|
29
|
+
- `incr` and `decr` with an initial value now create such counters with an add instead. Return values are unchanged
|
|
30
|
+
- The new `verify_reply_keys` option makes `get_multi` 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%
|
|
31
|
+
- Affects `protocol: :meta` since 3.2.0; fixed in 5.2.3, 5.1.5, 5.0.11, 4.3.8 and 3.2.14. memcached 1.6.45 and earlier still name these items by their raw key bytes
|
|
32
|
+
|
|
7
33
|
3.2.13
|
|
8
34
|
==========
|
|
9
35
|
|
data/README.md
CHANGED
|
@@ -37,6 +37,29 @@ Dalli::Client.new('localhost:11211', serializer: Dalli::JSONSerializer)
|
|
|
37
37
|
|
|
38
38
|
Per-request `raw: true` returns the stored bytes without deserializing or decompressing them, on every read method that accepts it (`get` and `fetch`).
|
|
39
39
|
|
|
40
|
+
### Counters and verify_reply_keys
|
|
41
|
+
|
|
42
|
+
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 3.2.14, 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.
|
|
43
|
+
|
|
44
|
+
## TLS
|
|
45
|
+
|
|
46
|
+
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:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
50
|
+
ssl_context.set_params(
|
|
51
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
|
52
|
+
verify_hostname: true,
|
|
53
|
+
ca_file: '/path/to/ca.crt' # omit to use the system's trusted certificates
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
Dalli::Client.new('memcached.example.com:11211', ssl_context: ssl_context)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
|
|
40
63
|
## Development
|
|
41
64
|
|
|
42
65
|
After checking out the repo, run `bin/setup` to install dependencies. You can run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/lib/dalli/client.rb
CHANGED
|
@@ -45,6 +45,11 @@ module Dalli
|
|
|
45
45
|
# useful for injecting a FIPS compliant hash object.
|
|
46
46
|
# - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
|
|
47
47
|
# to communicate with memcached.
|
|
48
|
+
# - :verify_reply_keys - defaults to false. When true, get_multi only accepts replies for the keys it requested;
|
|
49
|
+
# after a reply naming another key, the rest of that server's replies are treated as misses.
|
|
50
|
+
# With the meta protocol, memcached can name an item by its raw key bytes when the item was
|
|
51
|
+
# created by auto-create from a base64-encoded key, which Dalli no longer does. Costs a
|
|
52
|
+
# multi-get about 15%.
|
|
48
53
|
#
|
|
49
54
|
def initialize(servers = nil, options = {})
|
|
50
55
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
@@ -187,6 +192,11 @@ module Dalli
|
|
|
187
192
|
# Any error replies will be discarded at the end of the block, and
|
|
188
193
|
# Dalli client methods invoked inside the block will not
|
|
189
194
|
# have return values
|
|
195
|
+
#
|
|
196
|
+
# The block is quiet only for the thread or fiber that runs it. Another
|
|
197
|
+
# thread or fiber sharing this client can make requests meanwhile; a
|
|
198
|
+
# non-quiet one first reads the block's pending replies with a noop and
|
|
199
|
+
# discards them, so it reads only its own.
|
|
190
200
|
def quiet
|
|
191
201
|
old = Thread.current[::Dalli::QUIET]
|
|
192
202
|
Thread.current[::Dalli::QUIET] = true
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'forwardable'
|
|
4
|
+
require 'set'
|
|
4
5
|
require 'socket'
|
|
5
6
|
require 'timeout'
|
|
6
7
|
|
|
@@ -23,6 +24,7 @@ module Dalli
|
|
|
23
24
|
def initialize(attribs, client_options = {})
|
|
24
25
|
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
|
25
26
|
@options = client_options.merge(user_creds)
|
|
27
|
+
@verify_reply_keys = client_options[:verify_reply_keys] ? true : false
|
|
26
28
|
@value_marshaller = ValueMarshaller.new(@options)
|
|
27
29
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
28
30
|
end
|
|
@@ -39,7 +41,11 @@ module Dalli
|
|
|
39
41
|
|
|
40
42
|
begin
|
|
41
43
|
@connection_manager.start_request!
|
|
44
|
+
drain_pending_quiet_responses(opkey)
|
|
42
45
|
response = send(opkey, *args)
|
|
46
|
+
# Quiet requests can leave replies on the socket; remember that so a
|
|
47
|
+
# later non-quiet request knows to read them first
|
|
48
|
+
@connection_manager.quiet_responses_pending = true if quiet? && opkey != :noop
|
|
43
49
|
|
|
44
50
|
# pipelined_get emit query but doesn't read the response(s)
|
|
45
51
|
@connection_manager.finish_request! unless opkey == :pipelined_get
|
|
@@ -105,7 +111,7 @@ module Dalli
|
|
|
105
111
|
|
|
106
112
|
# If the status is ok and the key is not nil, then this is a
|
|
107
113
|
# getkq response with a value that we want to set in the response hash
|
|
108
|
-
values
|
|
114
|
+
break unless keep_reading?(values, key, value, cas)
|
|
109
115
|
|
|
110
116
|
# Get the next response from the buffer
|
|
111
117
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
@@ -163,6 +169,18 @@ module Dalli
|
|
|
163
169
|
private
|
|
164
170
|
|
|
165
171
|
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
172
|
+
|
|
173
|
+
# Reads the replies still pending from quiet requests before any
|
|
174
|
+
# non-quiet request, which reads a reply of its own. A quiet block reads
|
|
175
|
+
# them at its end, but the connection is shared: another thread or fiber
|
|
176
|
+
# (quiet? is fiber-local) can make a request while the block is still
|
|
177
|
+
# open, and would otherwise read the block's replies as its own.
|
|
178
|
+
def drain_pending_quiet_responses(opkey)
|
|
179
|
+
return unless @connection_manager.quiet_responses_pending? && opkey != :noop && !quiet?
|
|
180
|
+
|
|
181
|
+
noop
|
|
182
|
+
end
|
|
183
|
+
|
|
166
184
|
def verify_allowed_quiet!(opkey)
|
|
167
185
|
return if ALLOWED_QUIET_OPS.include?(opkey)
|
|
168
186
|
|
|
@@ -220,6 +238,7 @@ module Dalli
|
|
|
220
238
|
end
|
|
221
239
|
|
|
222
240
|
def pipelined_get(keys)
|
|
241
|
+
expect_reply_keys(keys)
|
|
223
242
|
req = +''
|
|
224
243
|
keys.each do |key|
|
|
225
244
|
req << quiet_get_request(key)
|
|
@@ -232,6 +251,50 @@ module Dalli
|
|
|
232
251
|
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
233
252
|
end
|
|
234
253
|
|
|
254
|
+
# A multi-get only accepts replies for the keys it asked for. With the
|
|
255
|
+
# meta protocol, memcached names an item created by auto-create (ma or mg
|
|
256
|
+
# with N) for a base64-encoded key by its raw bytes, without the b flag;
|
|
257
|
+
# such a key can contain spaces or CR/LF, which split the reply header and
|
|
258
|
+
# make the replies after it parse as different ones. A key that wasn't
|
|
259
|
+
# requested is how that shows up. Only with the verify_reply_keys option,
|
|
260
|
+
# since building the set costs a multi-get about 15%.
|
|
261
|
+
def expect_reply_keys(keys)
|
|
262
|
+
@expected_reply_keys = (keys.to_set { |k| reply_key_form(k) } if @verify_reply_keys)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Stores a getkq reply's value and returns true, unless its key wasn't
|
|
266
|
+
# requested: then the replies are out of step, so what was read so far is
|
|
267
|
+
# kept, the rest dropped, and false returned to stop reading
|
|
268
|
+
def keep_reading?(values, key, value, cas)
|
|
269
|
+
return true if key.nil?
|
|
270
|
+
|
|
271
|
+
if unexpected_reply_key?(key)
|
|
272
|
+
abandon_pipeline
|
|
273
|
+
return false
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
values[key] = [value, cas]
|
|
277
|
+
true
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def unexpected_reply_key?(key)
|
|
281
|
+
!key.nil? && @expected_reply_keys && !@expected_reply_keys.include?(reply_key_form(key))
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Keys compared as bytes; ASCII keys compare equal in any encoding as is
|
|
285
|
+
def reply_key_form(key)
|
|
286
|
+
key.ascii_only? ? key : key.b
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Drops the rest of this server's replies and closes the connection so
|
|
290
|
+
# nothing unread is left on it. Not a socket failure, so it doesn't count
|
|
291
|
+
# toward socket_max_failures; the keys not read are simply missing.
|
|
292
|
+
def abandon_pipeline
|
|
293
|
+
Dalli.logger.warn { "#{name} replied to a get_multi with a key that wasn't requested; closing the connection" }
|
|
294
|
+
response_buffer.clear
|
|
295
|
+
@connection_manager.close
|
|
296
|
+
end
|
|
297
|
+
|
|
235
298
|
# Called after the noop response is received at the end of a set
|
|
236
299
|
# of pipelined gets
|
|
237
300
|
def finish_pipeline
|
|
@@ -139,7 +139,10 @@ module Dalli
|
|
|
139
139
|
# We need to read all the responses at once.
|
|
140
140
|
def noop
|
|
141
141
|
write_noop
|
|
142
|
-
response_processor.consume_all_responses_until_noop
|
|
142
|
+
result = response_processor.consume_all_responses_until_noop
|
|
143
|
+
# Everything up to and including the noop reply has been read
|
|
144
|
+
@connection_manager.quiet_responses_pending = false
|
|
145
|
+
result
|
|
143
146
|
end
|
|
144
147
|
|
|
145
148
|
def stats(info = '')
|
|
@@ -28,6 +28,7 @@ module Dalli
|
|
|
28
28
|
|
|
29
29
|
attr_accessor :hostname, :port, :socket_type, :options
|
|
30
30
|
attr_reader :sock
|
|
31
|
+
attr_writer :quiet_responses_pending
|
|
31
32
|
|
|
32
33
|
def initialize(hostname, port, socket_type, client_options)
|
|
33
34
|
@hostname = hostname
|
|
@@ -35,6 +36,7 @@ module Dalli
|
|
|
35
36
|
@socket_type = socket_type
|
|
36
37
|
@options = DEFAULTS.merge(client_options)
|
|
37
38
|
@request_in_progress = false
|
|
39
|
+
@quiet_responses_pending = false
|
|
38
40
|
@sock = nil
|
|
39
41
|
@pid = nil
|
|
40
42
|
|
|
@@ -56,8 +58,9 @@ module Dalli
|
|
|
56
58
|
@sock = memcached_socket
|
|
57
59
|
@pid = Process.pid
|
|
58
60
|
@request_in_progress = false
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
@quiet_responses_pending = false
|
|
62
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError, SocketError => e
|
|
63
|
+
# SocketError = DNS resolution failure; SSL_ERRORS = failed TLS handshake
|
|
61
64
|
error_on_request!(e)
|
|
62
65
|
end
|
|
63
66
|
|
|
@@ -122,6 +125,7 @@ module Dalli
|
|
|
122
125
|
end
|
|
123
126
|
@sock = nil
|
|
124
127
|
@pid = nil
|
|
128
|
+
@quiet_responses_pending = false
|
|
125
129
|
abort_request!
|
|
126
130
|
end
|
|
127
131
|
|
|
@@ -136,6 +140,13 @@ module Dalli
|
|
|
136
140
|
!@sock.nil?
|
|
137
141
|
end
|
|
138
142
|
|
|
143
|
+
# True when a quiet request was written on this connection and its
|
|
144
|
+
# replies (memcached only suppresses some of them) haven't been
|
|
145
|
+
# consumed by a noop yet.
|
|
146
|
+
def quiet_responses_pending?
|
|
147
|
+
@quiet_responses_pending
|
|
148
|
+
end
|
|
149
|
+
|
|
139
150
|
def request_in_progress?
|
|
140
151
|
@request_in_progress
|
|
141
152
|
end
|
|
@@ -161,9 +172,21 @@ module Dalli
|
|
|
161
172
|
@request_in_progress = false
|
|
162
173
|
end
|
|
163
174
|
|
|
175
|
+
# The longest reply line Dalli reads, in bytes, CRLF included. A meta
|
|
176
|
+
# reply header is at most about 500 bytes: "VA <size>" plus its flags,
|
|
177
|
+
# the largest being a 250-byte key base64-encoded to 336 bytes, a
|
|
178
|
+
# 20-digit CAS, a 32-byte opaque and a few numeric fields. The longest
|
|
179
|
+
# stats line (stats settings) carries a file path such as ext_path, up
|
|
180
|
+
# to PATH_MAX (4096). 8 KiB leaves room for both, while a server that
|
|
181
|
+
# never sends CRLF can't grow the read buffer without bound.
|
|
182
|
+
MAX_LINE_BYTES = 8 * 1024
|
|
183
|
+
|
|
164
184
|
def read_line
|
|
165
|
-
data = @sock.gets("\r\n")
|
|
185
|
+
data = @sock.gets("\r\n", MAX_LINE_BYTES)
|
|
166
186
|
error_on_request!('EOF in read_line') if data.nil?
|
|
187
|
+
# Without its CRLF the line either ran past MAX_LINE_BYTES or was cut
|
|
188
|
+
# short by EOF; either way the connection is out of step
|
|
189
|
+
error_on_request!("Reply line without CRLF in the first #{MAX_LINE_BYTES} bytes") unless data.end_with?("\r\n")
|
|
167
190
|
data
|
|
168
191
|
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
169
192
|
error_on_request!(e)
|
|
@@ -34,7 +34,7 @@ module Dalli
|
|
|
34
34
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
35
35
|
return true unless tokens.first == VA
|
|
36
36
|
|
|
37
|
-
value =
|
|
37
|
+
value = read_data(tokens[1].to_i)
|
|
38
38
|
raw ? value : @value_marshaller.retrieve(value, bitflags_from_tokens(tokens))
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -46,7 +46,7 @@ module Dalli
|
|
|
46
46
|
cas = cas_from_tokens(tokens)
|
|
47
47
|
return [nil, cas] unless tokens.first == VA
|
|
48
48
|
|
|
49
|
-
value =
|
|
49
|
+
value = read_data(tokens[1].to_i)
|
|
50
50
|
[raw ? value : @value_marshaller.retrieve(value, bitflags_from_tokens(tokens)), cas]
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -242,6 +242,15 @@ module Dalli
|
|
|
242
242
|
line = read_line
|
|
243
243
|
line&.split || []
|
|
244
244
|
end
|
|
245
|
+
|
|
246
|
+
# Values are read by the size in the VA line, not up to the next CRLF:
|
|
247
|
+
# a value can contain CRLF and can be longer than MAX_LINE_BYTES
|
|
248
|
+
def read_data(data_size)
|
|
249
|
+
# Checked before the terminator is added: -1 or -2 would otherwise
|
|
250
|
+
# read 1 or 0 bytes and leave the connection out of step
|
|
251
|
+
check_value_size!(data_size)
|
|
252
|
+
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
253
|
+
end
|
|
245
254
|
end
|
|
246
255
|
end
|
|
247
256
|
end
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -13,6 +13,7 @@ module Dalli
|
|
|
13
13
|
##
|
|
14
14
|
class Meta < Base
|
|
15
15
|
TERMINATOR = "\r\n"
|
|
16
|
+
COUNTER_ADD_OPTIONS = { raw: true }.freeze # counters are stored raw
|
|
16
17
|
|
|
17
18
|
def response_processor
|
|
18
19
|
@response_processor ||= ResponseProcessor.new(@connection_manager, @value_marshaller)
|
|
@@ -129,11 +130,42 @@ module Dalli
|
|
|
129
130
|
def decr_incr(incr, key, delta, ttl, initial)
|
|
130
131
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
131
132
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
133
|
+
# memcached stores an item created by ma's auto-create (N/J) for a
|
|
134
|
+
# base64-encoded key without its binary-key flag, and later replies
|
|
135
|
+
# name it with the raw key bytes. Create such counters with an add.
|
|
136
|
+
return decr_incr_with_add(incr, key, delta, ttl, initial) if initial && base64
|
|
137
|
+
|
|
132
138
|
write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
133
139
|
quiet: quiet?, base64: base64))
|
|
134
140
|
response_processor.decr_incr unless quiet?
|
|
135
141
|
end
|
|
136
142
|
|
|
143
|
+
# Like decr_incr with an initial value, without memcached's auto-create:
|
|
144
|
+
# on a miss the counter is added with the initial value (which is
|
|
145
|
+
# returned, as auto-create would), and the add losing a race to another
|
|
146
|
+
# client means the counter now exists, so the incr is tried again. In a
|
|
147
|
+
# quiet block both requests are sent and whichever applies takes effect.
|
|
148
|
+
def decr_incr_with_add(incr, key, delta, ttl, initial)
|
|
149
|
+
return quiet_decr_incr_with_add(incr, key, delta, ttl, initial) if quiet?
|
|
150
|
+
|
|
151
|
+
2.times do
|
|
152
|
+
value = decr_incr(incr, key, delta, nil, nil)
|
|
153
|
+
return value unless value.nil?
|
|
154
|
+
|
|
155
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, COUNTER_ADD_OPTIONS)
|
|
156
|
+
return initial if response_processor.meta_set_with_cas
|
|
157
|
+
end
|
|
158
|
+
decr_incr(incr, key, delta, nil, nil)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def quiet_decr_incr_with_add(incr, key, delta, ttl, initial)
|
|
162
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
163
|
+
write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: nil, incr: incr, ttl: nil,
|
|
164
|
+
quiet: true, base64: base64))
|
|
165
|
+
# Sent with the rest of the quiet block, by its closing noop
|
|
166
|
+
write_storage_req(:add, key, initial.to_s, ttl, nil, COUNTER_ADD_OPTIONS)
|
|
167
|
+
end
|
|
168
|
+
|
|
137
169
|
# Other Commands
|
|
138
170
|
def flush(delay = 0)
|
|
139
171
|
write(RequestFormatter.flush(delay: delay))
|
|
@@ -144,7 +176,10 @@ module Dalli
|
|
|
144
176
|
# We need to read all the responses at once.
|
|
145
177
|
def noop
|
|
146
178
|
write_noop
|
|
147
|
-
response_processor.consume_all_responses_until_mn
|
|
179
|
+
result = response_processor.consume_all_responses_until_mn
|
|
180
|
+
# Everything up to and including the MN has been read
|
|
181
|
+
@connection_manager.quiet_responses_pending = false
|
|
182
|
+
result
|
|
148
183
|
end
|
|
149
184
|
|
|
150
185
|
def stats(info = nil)
|
|
@@ -97,11 +97,13 @@ module Dalli
|
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
# Checks whether we should apply compression when serializing a value
|
|
100
|
-
# based on the specified options. Returns false
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
100
|
+
# based on the specified options. Returns false for a raw request
|
|
101
|
+
# (raw reads ignore bitflags, so the value must be stored as given) or
|
|
102
|
+
# unless the value is greater than the minimum compression size.
|
|
103
|
+
# Otherwise returns based on a method-level option if specified,
|
|
104
|
+
# falling back to the server default.
|
|
104
105
|
def compress_value?(value, req_options)
|
|
106
|
+
return false if req_options&.dig(:raw)
|
|
105
107
|
return false unless value.bytesize >= compression_min_size
|
|
106
108
|
return compress_by_default? unless req_options && !req_options[:compress].nil?
|
|
107
109
|
|
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
|
|
@@ -15,5 +16,11 @@ module Dalli
|
|
|
15
16
|
else
|
|
16
17
|
[Timeout::Error]
|
|
17
18
|
end
|
|
19
|
+
|
|
20
|
+
# SSL errors during the TLS handshake count as a failed request: they
|
|
21
|
+
# trigger reconnection and, after socket_max_failures, mark the server
|
|
22
|
+
# down. openssl is required above because this file loads before
|
|
23
|
+
# dalli/socket, so OpenSSL may not be defined yet.
|
|
24
|
+
SSL_ERRORS = [OpenSSL::SSL::SSLError].freeze
|
|
18
25
|
end
|
|
19
26
|
end
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -132,12 +132,42 @@ module Dalli
|
|
|
132
132
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
|
|
133
133
|
end
|
|
134
134
|
|
|
135
|
+
# Closes the TCP socket if the handshake fails or times out, so a failed
|
|
136
|
+
# attempt doesn't hold a file descriptor until GC.
|
|
135
137
|
def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
|
|
138
|
+
warn_if_unverified(ssl_context)
|
|
139
|
+
connected = false
|
|
136
140
|
ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
|
|
137
141
|
ssl_socket.hostname = host
|
|
138
142
|
ssl_socket.sync_close = true
|
|
139
143
|
ssl_socket.connect
|
|
144
|
+
connected = true
|
|
140
145
|
ssl_socket
|
|
146
|
+
ensure
|
|
147
|
+
tcp_socket.close unless connected
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Verification comes entirely from the caller's ssl_context, and a bare
|
|
151
|
+
# SSLContext.new verifies nothing, so warn (once per process) when the
|
|
152
|
+
# certificate or the hostname won't be checked.
|
|
153
|
+
def self.warn_if_unverified(ssl_context)
|
|
154
|
+
return if @ssl_verification_warning_logged
|
|
155
|
+
|
|
156
|
+
problem = unverified_problem(ssl_context)
|
|
157
|
+
return unless problem
|
|
158
|
+
|
|
159
|
+
Dalli.logger.warn "SECURITY WARNING: Dalli's ssl_context #{problem}. " \
|
|
160
|
+
'Use ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER, ' \
|
|
161
|
+
'verify_hostname: true) to verify the server.'
|
|
162
|
+
@ssl_verification_warning_logged = true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.unverified_problem(ssl_context)
|
|
166
|
+
if !ssl_context.verify_mode.to_i.anybits?(::OpenSSL::SSL::VERIFY_PEER)
|
|
167
|
+
'verify_mode is VERIFY_NONE, so the server certificate is not checked'
|
|
168
|
+
elsif !ssl_context.verify_hostname
|
|
169
|
+
'verify_hostname is false, so the certificate is not checked against the hostname'
|
|
170
|
+
end
|
|
141
171
|
end
|
|
142
172
|
end
|
|
143
173
|
|
data/lib/dalli/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dalli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.2.
|
|
4
|
+
version: 3.2.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -90,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
90
90
|
- !ruby/object:Gem::Version
|
|
91
91
|
version: '0'
|
|
92
92
|
requirements: []
|
|
93
|
-
rubygems_version: 4.0.
|
|
93
|
+
rubygems_version: 4.0.23
|
|
94
94
|
specification_version: 4
|
|
95
95
|
summary: High performance memcached client for Ruby
|
|
96
96
|
test_files: []
|