dalli 3.2.13 → 3.2.14
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 +9 -0
- data/README.md +4 -0
- data/lib/dalli/client.rb +5 -0
- data/lib/dalli/protocol/base.rb +48 -1
- data/lib/dalli/protocol/meta.rb +32 -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: 5b52d92d26a7277b6f65b402c082be2f99b98ca9cb7596af709ac70848167a53
|
|
4
|
+
data.tar.gz: 2cc59e69e536f07ec79ec6e37ffcae997c1e47231aaa213e32fdce02b3b3e123
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8db03f1265942f604b68bdded36f70aa00e859269950367df91c62ef1b2d1011ba6166269125f4ab377c4b4acd4acfd763c3f17a867d2e357479b259c0592ffc
|
|
7
|
+
data.tar.gz: 7f15a910062c45a776e3bc3964a3555b4a3c0c0a5897877dc170ee5a80d16a313bb4eb0da64821ca14d930ffbea1445855e2f1ca7fe2a068a33765aabc064636
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
3.2.14
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
- Don't rely on memcached's auto-create for keys that need base64 encoding (GHSA-r47x-6p6r-mvcp)
|
|
11
|
+
- 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
|
|
12
|
+
- `incr` and `decr` with an initial value now create such counters with an add instead. Return values are unchanged
|
|
13
|
+
- 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%
|
|
14
|
+
- 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
|
|
15
|
+
|
|
7
16
|
3.2.13
|
|
8
17
|
==========
|
|
9
18
|
|
data/README.md
CHANGED
|
@@ -37,6 +37,10 @@ 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
|
+
|
|
40
44
|
## Development
|
|
41
45
|
|
|
42
46
|
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)
|
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
|
|
@@ -105,7 +107,7 @@ module Dalli
|
|
|
105
107
|
|
|
106
108
|
# If the status is ok and the key is not nil, then this is a
|
|
107
109
|
# getkq response with a value that we want to set in the response hash
|
|
108
|
-
values
|
|
110
|
+
break unless keep_reading?(values, key, value, cas)
|
|
109
111
|
|
|
110
112
|
# Get the next response from the buffer
|
|
111
113
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
@@ -220,6 +222,7 @@ module Dalli
|
|
|
220
222
|
end
|
|
221
223
|
|
|
222
224
|
def pipelined_get(keys)
|
|
225
|
+
expect_reply_keys(keys)
|
|
223
226
|
req = +''
|
|
224
227
|
keys.each do |key|
|
|
225
228
|
req << quiet_get_request(key)
|
|
@@ -232,6 +235,50 @@ module Dalli
|
|
|
232
235
|
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
233
236
|
end
|
|
234
237
|
|
|
238
|
+
# A multi-get only accepts replies for the keys it asked for. With the
|
|
239
|
+
# meta protocol, memcached names an item created by auto-create (ma or mg
|
|
240
|
+
# with N) for a base64-encoded key by its raw bytes, without the b flag;
|
|
241
|
+
# such a key can contain spaces or CR/LF, which split the reply header and
|
|
242
|
+
# make the replies after it parse as different ones. A key that wasn't
|
|
243
|
+
# requested is how that shows up. Only with the verify_reply_keys option,
|
|
244
|
+
# since building the set costs a multi-get about 15%.
|
|
245
|
+
def expect_reply_keys(keys)
|
|
246
|
+
@expected_reply_keys = (keys.to_set { |k| reply_key_form(k) } if @verify_reply_keys)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Stores a getkq reply's value and returns true, unless its key wasn't
|
|
250
|
+
# requested: then the replies are out of step, so what was read so far is
|
|
251
|
+
# kept, the rest dropped, and false returned to stop reading
|
|
252
|
+
def keep_reading?(values, key, value, cas)
|
|
253
|
+
return true if key.nil?
|
|
254
|
+
|
|
255
|
+
if unexpected_reply_key?(key)
|
|
256
|
+
abandon_pipeline
|
|
257
|
+
return false
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
values[key] = [value, cas]
|
|
261
|
+
true
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def unexpected_reply_key?(key)
|
|
265
|
+
!key.nil? && @expected_reply_keys && !@expected_reply_keys.include?(reply_key_form(key))
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Keys compared as bytes; ASCII keys compare equal in any encoding as is
|
|
269
|
+
def reply_key_form(key)
|
|
270
|
+
key.ascii_only? ? key : key.b
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Drops the rest of this server's replies and closes the connection so
|
|
274
|
+
# nothing unread is left on it. Not a socket failure, so it doesn't count
|
|
275
|
+
# toward socket_max_failures; the keys not read are simply missing.
|
|
276
|
+
def abandon_pipeline
|
|
277
|
+
Dalli.logger.warn { "#{name} replied to a get_multi with a key that wasn't requested; closing the connection" }
|
|
278
|
+
response_buffer.clear
|
|
279
|
+
@connection_manager.close
|
|
280
|
+
end
|
|
281
|
+
|
|
235
282
|
# Called after the noop response is received at the end of a set
|
|
236
283
|
# of pipelined gets
|
|
237
284
|
def finish_pipeline
|
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))
|
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.14
|
|
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: []
|