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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7f7bcbe37e631c3c733d1487e519501497702b873e81e6588d6dfad1ab96a65b
4
- data.tar.gz: d7a953d2c0052334d8edb7fa45306724ffa25cec64436dcceccc9847d057e8bd
3
+ metadata.gz: 5b52d92d26a7277b6f65b402c082be2f99b98ca9cb7596af709ac70848167a53
4
+ data.tar.gz: 2cc59e69e536f07ec79ec6e37ffcae997c1e47231aaa213e32fdce02b3b3e123
5
5
  SHA512:
6
- metadata.gz: 8d7b0ed94102d333f611736c48d944b0f1010760ae646003db60cd02c90b1a047a5fd10f7a5ef077527a88594f8844cbe08f9647259749e78ade74d09d2e851b
7
- data.tar.gz: bf86e4604fc18eaf2bdf57d6abad82258745238112a9866e5da9e05c0f45b44d62f82ebe8b5519ecc83d00ec6ee94e2d11bb931aa14171081a0b731745633e6d
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)
@@ -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[key] = [value, cas] unless key.nil?
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
@@ -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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '3.2.13'
4
+ VERSION = '3.2.14'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
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.13
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.21
93
+ rubygems_version: 4.0.23
94
94
  specification_version: 4
95
95
  summary: High performance memcached client for Ruby
96
96
  test_files: []