dalli 3.2.15 → 4.0.0

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: 1282508d83c234919dfb2c8a05c08d7069186663c83d4e3aaf040e6f503a2e9f
4
- data.tar.gz: cb10108ea1d22c25630b5cfa36b6e038515e3f053e2354e3921718050138535b
3
+ metadata.gz: 86e39b2869b7c916472e88e6cbbfb6aa1f6533aa35c9e1f1c704cffcbc680f8a
4
+ data.tar.gz: bf2e56610c6bae187d561ccf09105e6f5b4a29eed1308d089745ef22cf5b673b
5
5
  SHA512:
6
- metadata.gz: 614a4eebdaa9cee1d9dc6afdfd45c96ff1ec7eda12f471a12c6c1eca7866af971d907a7c288aeb5a72734b3f4f72bfb685ba58337da2ac50146ed74a018c0d0a
7
- data.tar.gz: 8f577fef557f593d9cb6327a54ec1b5d3df8e554a5935ba7a7faadf86259248a60f29204b3ad5a295024b676ebc481e925f81f330095a73e6649e11d182b1127
6
+ metadata.gz: e2faab814abb4b25a53d87048f7e4e81e49c609bd64725cb18607670110da3d5b8038544598c44575831b9038500f9d8220a56ec28c16b9517a21d442212d12b
7
+ data.tar.gz: 3e5a7c326c908d72d2d8ce2211b27f67acebbbf97c279c5faf3ba2914ec7409df7f259a9082d82903cc9ae6b6716a137994aba849321546b8d3d3a35f8f7f5af
data/CHANGELOG.md CHANGED
@@ -1,108 +1,30 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
- Unreleased
5
- ==========
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
-
33
- 3.2.13
34
- ==========
35
-
36
- - Complete the fix for per-request `raw: true` on reads (GHSA-wr87-m4jw-29x5)
37
- - `cas` and `cas!` accepted `raw: true` but didn't pass it to the read, so the value was still deserialized. They now honor it, in both protocols
38
- - Affects 3.2.12 and earlier; fixed in 5.2.2, 5.1.4, 5.0.10, 4.3.7 and 3.2.13
39
- - Complete the fork-safety fix (GHSA-w39f-xq2m-4g8x)
40
- - Fork detection used a pid cache refreshed by a `Process._fork` hook. When another library's fork hook ran first in the child (connection_pool, loaded before Dalli as in Rails, closes its connections there), Dalli didn't know it was in a forked child and ended the parent's TLS session. It now checks `Process.pid`
41
- - Fixed in 4.3.7 and 3.2.13
42
-
43
- - Fix a regression in 3.2.12: a `get_multi` that didn't finish within `socket_timeout` counted toward `socket_max_failures`, so two slow `get_multi` calls in a row marked a healthy server down. It now just closes the connection
44
- - Handle malformed pipelined replies from a broken or hostile server or proxy: a hit with no key, or with no `s` flag, could leave the connection out of step or raise an error
45
- - With `decompressed_max_bytes` in effect, data after the end of a compressed value's stream was returned as part of the value. It's now ignored, as without the limit
46
- - With a `digest_class` whose digests aren't short hex strings, shortening a long key could loop forever. It now raises `ArgumentError`
47
-
48
- - Run the Tests and RuboCop workflows on pushes to `main` and the `*-stable` branches only, so a pull request's branch isn't tested twice (backport of #1198)
49
-
50
- 3.2.12
51
- ==========
52
-
53
- - Fix pipelined `get_multi` returning one key's value for another after an error reply (GHSA-p6pm-ch9v-44vx)
54
- - The pipelined reply parser took any reply without a body for the end of the batch. An error reply for one key (`CLIENT_ERROR`, `SERVER_ERROR`, or `EN` from a proxy) ended it early, and the replies still on the connection were read as the replies to later commands, so a `get` could return another key's value
55
- - Keys are now measured in bytes as sent (after base64 encoding when the meta protocol needs it). A key under 250 characters but over 250 bytes, for example one with many non-ASCII characters, passed validation:
56
- - with the meta protocol, memcached rejected it with the error reply above
57
- - with the binary protocol, memcached dropped the connection, and `get` or `get_multi` with that key retried forever
58
- - Such keys are now truncated like other long keys; keys that worked before are unchanged
59
- - The reply parser affects `protocol: :meta` since 3.2.0; the key length affects both protocols; fixed in 5.2.1, 5.1.3, 5.0.9, 4.3.6 and 3.2.12
60
- - Stop retrying an unresponsive server forever (GHSA-4qp6-2jcr-596v)
61
- - A server that accepted connections but never answered (or dropped the connection on a request) was retried forever, hanging the caller, because each successful reconnect reset the failure count. Requests now fail after `socket_max_failures` attempts and the server is marked down, as when it can't be reached at all; it gets a full set of attempts again after `down_retry_delay`
62
- - Affects all versions; fixed in 5.2.1, 5.1.3, 5.0.9, 4.3.6 and 3.2.12
63
- - Limit decompressed and reply value sizes (GHSA-3553-vcg5-72jw)
64
- - Values flagged as compressed were inflated without limit, so a small stored value could expand to gigabytes on read, whatever the client's `compress` or `serializer` settings. The new `decompressed_max_bytes` option (default 128 MiB; `nil` disables it) makes a read that would pass it raise `Dalli::UnmarshalError`. Custom compressors whose `decompress` takes only the data keep working, without the limit
65
- - The value size in a reply (a meta `VA` size or a binary body length) was used to read or buffer that many bytes, so a malicious server could make the client allocate gigabytes. Sizes over 1 GiB, memcached's largest item, now raise `Dalli::DalliError` before reading
66
- - Affects all versions; fixed in 5.2.1, 5.1.3, 5.0.9, 4.3.6 and 3.2.12
67
- - Honor per-request `raw: true` on reads, and add `Dalli::JSONSerializer` (GHSA-wr87-m4jw-29x5)
68
- - `get`, `gat` and `fetch` ignored a per-request `raw: true` and deserialized the value according to its stored flags, so a caller who asked for raw bytes could still have `Marshal.load` run on data someone else wrote to memcached. They now return the stored bytes
69
- - The `raw` part affects all versions
70
- - `serializer: JSON` reads values with `JSON.load`, which on json gem versions before 3.0 creates an object of the class named in a stored `json_class` key when the json additions are loaded. The new `Dalli::JSONSerializer` reads with `JSON.parse` and only returns plain JSON types. The README now recommends it; `serializer: JSON` itself is unchanged
71
- - The `serializer: JSON` part affects any version used with the json gem before 3.0
72
- - Keep a forked child from closing the parent's TLS session (GHSA-w39f-xq2m-4g8x)
73
- - With TLS, a forked child that closed the client (or reconnected after detecting the fork) sent a TLS close on the connection the parent was still using, ending the parent's session. A forked child now closes only its own copy of the socket
74
- - Affects all versions with TLS; fixed in 5.2.1, 5.1.3, 5.0.9, 4.3.6 and 3.2.12
75
-
76
- - Fix three flaky tests: the `KeyManager` namespace key-length test, the Rack session freshness test, and `MemcachedManager`'s start and stop handling (backport of #1141, #1183)
77
- - Fix flaky failover tests: move their ports out of Linux's ephemeral port range, and wait for memcached to accept connections after starting it (backport of #1184)
78
-
79
- 3.2.11
80
- ==========
81
-
82
- - Fix multi-server `get_multi` stopping at an empty value (#1170)
83
- - The pipelined reply parser took a hit on a zero-length value (`VA 0`) for the terminating `MN`, so that server's remaining keys were silently missing from the result
84
- - Its value terminator and any replies not yet parsed stayed on the connection and were read as the replies to later commands on it. Those commands could fail with `Dalli::DalliError: Response error`, or a single-key `get` could silently return another key's value
85
- - An empty value is now returned as `''`, like a single-key `get` and single-server `get_multi`
86
- - Affects only clients using `protocol: :meta`; the default binary protocol is not affected
87
- - Thanks to Julian Richard Contreras for this contribution
88
-
89
- 3.2.10
90
- ==========
91
-
92
- - Fix `Rack::Session::Dalli` with connection_pool 3.x, which only accepts keyword arguments (backport of #1051)
93
- - Add `logger` as a runtime dependency, since it is no longer a default gem as of Ruby 4.0 (backport of #1010, olleolleolle)
94
- - CI: patch memcached 1.5.22 so it builds on ubuntu-22.04, and run tests there (backport of #1033)
95
-
96
- 3.2.9
97
- ==========
98
-
99
- Security:
100
-
101
- - Fix memcached command injection through the `incr`/`decr` default with the meta protocol (GHSA-6wmv-xq9m-fmp7)
102
- - With `protocol: :meta`, the `default` argument of `incr`/`decr` was written into the command without conversion, so a String containing CRLF injected additional memcached commands (e.g. `set`, `flush_all`) on the connection. The default binary protocol is not affected
103
- - `default` must now be an Integer, or a String of decimal digits; anything else raises `ArgumentError` before a request is sent
104
- - As defense in depth, the meta `RequestFormatter` now converts every numeric flag it writes (`D`, `J`, `N`, `T`) to an Integer
105
- - Thanks to oss-security-shop for the report
4
+ 4.0.0
5
+ ==========
6
+
7
+ BREAKING CHANGES:
8
+
9
+ - Require Ruby 3.1+ (dropped support for Ruby 2.6, 2.7, and 3.0)
10
+ - Removed `Dalli::Server` deprecated alias - use `Dalli::Protocol::Binary` instead
11
+ - Removed `:compression` option - use `:compress` instead
12
+ - Removed `close_on_fork` method - use `reconnect_on_fork` instead
13
+
14
+ Other changes:
15
+
16
+ - Add security warning when using default Marshal serializer (silence with `silence_marshal_warning: true`)
17
+ - Add defense-in-depth input validation for stats command arguments
18
+ - Add `string_fastpath` option to skip serialization for simple strings (byroot)
19
+ - Meta protocol set performance improvement (danmayer)
20
+ - Fix connection_pool 3.0 compatibility for Rack session store
21
+ - Fix session recovery after deletion (stengineering0)
22
+ - Fix cannot read response data included terminator `\r\n` when use meta protocol (matsubara0507)
23
+ - Support SERVER_ERROR response from Memcached as per the [memcached spec](https://github.com/memcached/memcached/blob/e43364402195c8e822bb8f88755a60ab8bbed62a/doc/protocol.txt#L172) (grcooper)
24
+ - Update Socket timeout handling to use Socket#timeout= when available (nickamorim)
25
+ - Serializer: reraise all .load errors as UnmarshalError (olleolleolle)
26
+ - Reconnect gracefully when a fork is detected instead of crashing (PatrickTulskie)
27
+ - Update CI to test against memcached 1.6.40
106
28
 
107
29
  3.2.8
108
30
  ==========
data/Gemfile CHANGED
@@ -5,7 +5,9 @@ source 'https://rubygems.org'
5
5
  gemspec
6
6
 
7
7
  group :development, :test do
8
+ gem 'cgi'
8
9
  gem 'connection_pool'
10
+ gem 'debug' unless RUBY_PLATFORM == 'java'
9
11
  gem 'minitest', '~> 5'
10
12
  gem 'rack', '~> 2.0', '>= 2.2.0'
11
13
  gem 'rake', '~> 13.0'
data/README.md CHANGED
@@ -25,41 +25,6 @@ The name is a variant of Salvador Dali for his famous painting [The Persistence
25
25
  * [Forum](https://github.com/petergoldstein/dalli/discussions/categories/q-a) - If you have questions about Dalli, please post them here.
26
26
  * [Client API](https://www.rubydoc.info/gems/dalli) - Ruby documentation for the `Dalli::Client` API
27
27
 
28
- ## Security Note
29
-
30
- By default, Dalli uses Ruby's Marshal for serialization. Deserializing untrusted data with Marshal can lead to remote code execution. If you cache user-controlled data, or your memcached server could be written to by someone else, consider using a safer serializer:
31
-
32
- ```ruby
33
- Dalli::Client.new('localhost:11211', serializer: Dalli::JSONSerializer)
34
- ```
35
-
36
- `Dalli::JSONSerializer` reads values with `JSON.parse`, so it only ever returns plain hashes, arrays, strings, numbers, booleans and `nil`. Passing the `JSON` module itself (`serializer: JSON`) reads values with `JSON.load`, which can create objects of other classes from a stored `json_class` key and is not recommended for data an attacker might write.
37
-
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
-
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
-
63
28
  ## Development
64
29
 
65
30
  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,11 +45,6 @@ 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%.
53
48
  #
54
49
  def initialize(servers = nil, options = {})
55
50
  @normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
@@ -160,8 +155,8 @@ module Dalli
160
155
  # - nil if the key did not exist.
161
156
  # - false if the value was changed by someone else.
162
157
  # - true if the value was successfully updated.
163
- def cas(key, ttl = nil, req_options = nil, &block)
164
- cas_core(key, false, ttl, req_options, &block)
158
+ def cas(key, ttl = nil, req_options = nil, &)
159
+ cas_core(key, false, ttl, req_options, &)
165
160
  end
166
161
 
167
162
  ##
@@ -171,8 +166,8 @@ module Dalli
171
166
  # Returns:
172
167
  # - false if the value was changed by someone else.
173
168
  # - true if the value was successfully updated.
174
- def cas!(key, ttl = nil, req_options = nil, &block)
175
- cas_core(key, true, ttl, req_options, &block)
169
+ def cas!(key, ttl = nil, req_options = nil, &)
170
+ cas_core(key, true, ttl, req_options, &)
176
171
  end
177
172
 
178
173
  ##
@@ -192,11 +187,6 @@ module Dalli
192
187
  # Any error replies will be discarded at the end of the block, and
193
188
  # Dalli client methods invoked inside the block will not
194
189
  # 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.
200
190
  def quiet
201
191
  old = Thread.current[::Dalli::QUIET]
202
192
  Thread.current[::Dalli::QUIET] = true
@@ -267,8 +257,6 @@ module Dalli
267
257
  ##
268
258
  # Incr adds the given amount to the counter on the memcached server.
269
259
  # Amt must be a positive integer value.
270
- # Default, if given, must be an Integer (or a String of decimal digits);
271
- # anything else raises ArgumentError.
272
260
  #
273
261
  # If default is nil, the counter must already exist or the operation
274
262
  # will fail and will return nil. Otherwise this method will return
@@ -281,7 +269,6 @@ module Dalli
281
269
  # If the value already exists, it must have been set with raw: true
282
270
  def incr(key, amt = 1, ttl = nil, default = nil)
283
271
  check_positive!(amt)
284
- validate_integer!(:default, default)
285
272
 
286
273
  perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
287
274
  end
@@ -289,8 +276,6 @@ module Dalli
289
276
  ##
290
277
  # Decr subtracts the given amount from the counter on the memcached server.
291
278
  # Amt must be a positive integer value.
292
- # Default, if given, must be an Integer (or a String of decimal digits);
293
- # anything else raises ArgumentError.
294
279
  #
295
280
  # memcached counters are unsigned and cannot hold negative values. Calling
296
281
  # decr on a counter which is 0 will just return 0.
@@ -306,7 +291,6 @@ module Dalli
306
291
  # If the value already exists, it must have been set with raw: true
307
292
  def decr(key, amt = 1, ttl = nil, default = nil)
308
293
  check_positive!(amt)
309
- validate_integer!(:default, default)
310
294
 
311
295
  perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
312
296
  end
@@ -389,17 +373,8 @@ module Dalli
389
373
  raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
390
374
  end
391
375
 
392
- # Numeric arguments that become meta protocol flags (GHSA-6wmv-xq9m-fmp7).
393
- # Checked before the request starts so a bad value raises a clean
394
- # ArgumentError, instead of Protocol::Base#request logging it as
395
- # unexpected and closing the connection.
396
- def validate_integer!(name, value)
397
- Dalli::Protocol::Meta::RequestFormatter.integer_flag(name, value) unless value.nil?
398
- end
399
-
400
376
  def cas_core(key, always_set, ttl = nil, req_options = nil)
401
- # req_options reaches the read too, so raw: true returns the stored bytes
402
- (value, cas) = perform(:cas, key, req_options)
377
+ (value, cas) = perform(:cas, key)
403
378
  return if value.nil? && !always_set
404
379
 
405
380
  newvalue = yield(value)
@@ -13,54 +13,8 @@ module Dalli
13
13
  Zlib::Deflate.deflate(data)
14
14
  end
15
15
 
16
- # DEFLATE can't expand data by more than about 1032:1
17
- MAX_EXPANSION = 1032
18
- # Compressed input is fed to zlib this many bytes at a time. MRI's zlib
19
- # yields the output in small chunks; one that returns it instead still
20
- # produces at most MAX_EXPANSION times this per step.
21
- INFLATE_SLICE_BYTES = 64 * 1024
22
-
23
- # max_bytes caps the decompressed size; inflating stops and raises as soon
24
- # as the output passes it, so a small compressed value can't expand into
25
- # gigabytes of memory.
26
- def self.decompress(data, max_bytes: nil)
27
- return Zlib::Inflate.inflate(data) unless may_exceed?(data, max_bytes)
28
-
29
- inflate_within_limit(Zlib::Inflate.new, data, max_bytes)
30
- end
31
-
32
- # False when even the largest possible expansion stays within max_bytes,
33
- # so the value can be inflated in one step
34
- def self.may_exceed?(data, max_bytes)
35
- max_bytes ? data.bytesize * MAX_EXPANSION > max_bytes : false
36
- end
37
-
38
- def self.inflate_within_limit(inflater, data, max_bytes)
39
- out = String.new(encoding: Encoding::BINARY)
40
- 0.step(data.bytesize - 1, INFLATE_SLICE_BYTES) do |pos|
41
- inflate_slice(inflater, data.byteslice(pos, INFLATE_SLICE_BYTES), out, max_bytes)
42
- # Anything after the end of the stream isn't part of the value, as
43
- # with Zlib::Inflate.inflate; finish would return it as output
44
- break if inflater.finished?
45
- end
46
- return out if inflater.finished?
47
-
48
- append_within_limit(out, inflater.finish, max_bytes)
49
- ensure
50
- inflater.close
51
- end
52
-
53
- # Both yielded and returned output count toward the limit
54
- def self.inflate_slice(inflater, slice, out, max_bytes)
55
- rest = inflater.inflate(slice) { |chunk| append_within_limit(out, chunk, max_bytes) }
56
- append_within_limit(out, rest, max_bytes)
57
- end
58
-
59
- def self.append_within_limit(out, chunk, max_bytes)
60
- out << chunk if chunk
61
- return out if out.bytesize <= max_bytes
62
-
63
- raise Dalli::UnmarshalError, "Decompressed value exceeds #{max_bytes} bytes (decompressed_max_bytes)"
16
+ def self.decompress(data)
17
+ Zlib::Inflate.inflate(data)
64
18
  end
65
19
  end
66
20
 
@@ -78,11 +32,9 @@ module Dalli
78
32
  io.string
79
33
  end
80
34
 
81
- def self.decompress(data, max_bytes: nil)
82
- return Zlib::GzipReader.new(StringIO.new(data, 'rb')).read unless Compressor.may_exceed?(data, max_bytes)
83
-
84
- # MAX_WBITS + 16 makes zlib read the gzip format (and check its CRC)
85
- Compressor.inflate_within_limit(Zlib::Inflate.new(Zlib::MAX_WBITS + 16), data, max_bytes)
35
+ def self.decompress(data)
36
+ io = StringIO.new(data, 'rb')
37
+ Zlib::GzipReader.new(io).read
86
38
  end
87
39
  end
88
40
  end
@@ -30,13 +30,10 @@ module Dalli
30
30
 
31
31
  def initialize(client_options)
32
32
  @key_options =
33
- DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
33
+ DEFAULTS.merge(client_options.slice(*OPTIONS))
34
34
  validate_digest_class_option(@key_options)
35
35
 
36
36
  @namespace = namespace_from_options
37
- # The meta protocol base64-encodes keys with whitespace or non-ASCII
38
- # characters, so their length on the wire differs from their length here
39
- @meta_protocol = client_options[:protocol]&.to_s == 'meta'
40
37
  end
41
38
 
42
39
  ##
@@ -54,7 +51,7 @@ module Dalli
54
51
  raise ArgumentError, 'key cannot be blank' unless key&.length&.positive?
55
52
 
56
53
  key = key_with_namespace(key)
57
- wire_length(key) > MAX_KEY_LENGTH ? truncated_key(key) : key
54
+ key.length > MAX_KEY_LENGTH ? truncated_key(key) : key
58
55
  end
59
56
 
60
57
  ##
@@ -80,7 +77,7 @@ module Dalli
80
77
  def namespace_regexp
81
78
  return /\A#{Regexp.escape(evaluate_namespace)}:/ if namespace.is_a?(Proc)
82
79
 
83
- @namespace_regexp ||= /\A#{Regexp.escape(namespace)}:/.freeze unless namespace.nil?
80
+ @namespace_regexp ||= /\A#{Regexp.escape(namespace)}:/ unless namespace.nil?
84
81
  end
85
82
 
86
83
  def validate_digest_class_option(opts)
@@ -110,30 +107,7 @@ module Dalli
110
107
  ##
111
108
  def truncated_key(key)
112
109
  digest = digest_class.hexdigest(key)
113
- prefix = key[0, [prefix_length(digest), 0].max]
114
- truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
115
- # A prefix with whitespace or non-ASCII characters sends the whole key
116
- # base64-encoded, which can still be too long on the wire; shorten it.
117
- while wire_length(truncated) > MAX_KEY_LENGTH && !prefix.empty?
118
- prefix = prefix[0...-1]
119
- truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
120
- end
121
- return truncated if wire_length(truncated) <= MAX_KEY_LENGTH
122
-
123
- # Only possible with a digest_class whose digests aren't short hex strings
124
- raise ArgumentError, "#{digest_class} digests are too long to fit in a truncated key"
125
- end
126
-
127
- # memcached's key limit applies to the key as sent: in bytes, and, with
128
- # the meta protocol, after base64 encoding when the key needs it. A key
129
- # within the limit in characters can still be too long. With the meta
130
- # protocol memcached rejects it with a CLIENT_ERROR that must never reach
131
- # a pipelined get; with the binary protocol it drops the connection.
132
- def wire_length(key)
133
- return key.bytesize unless @meta_protocol
134
-
135
- encoded, = Dalli::Protocol::Meta::KeyRegularizer.encode(key)
136
- encoded.bytesize
110
+ "#{key[0, prefix_length(digest)]}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
137
111
  end
138
112
 
139
113
  def prefix_length(digest)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
- require 'set'
5
4
  require 'socket'
6
5
  require 'timeout'
7
6
 
@@ -24,28 +23,17 @@ module Dalli
24
23
  def initialize(attribs, client_options = {})
25
24
  hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
26
25
  @options = client_options.merge(user_creds)
27
- @verify_reply_keys = client_options[:verify_reply_keys] ? true : false
28
26
  @value_marshaller = ValueMarshaller.new(@options)
29
27
  @connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
30
28
  end
31
29
 
32
- # True when this request passed raw: true. A stored value is then
33
- # returned as stored, never deserialized or decompressed.
34
- def raw_request?(options)
35
- options.is_a?(Hash) && options[:raw] ? true : false
36
- end
37
-
38
30
  # Chokepoint method for error handling and ensuring liveness
39
31
  def request(opkey, *args)
40
32
  verify_state(opkey)
41
33
 
42
34
  begin
43
35
  @connection_manager.start_request!
44
- drain_pending_quiet_responses(opkey)
45
36
  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
49
37
 
50
38
  # pipelined_get emit query but doesn't read the response(s)
51
39
  @connection_manager.finish_request! unless opkey == :pipelined_get
@@ -111,7 +99,7 @@ module Dalli
111
99
 
112
100
  # If the status is ok and the key is not nil, then this is a
113
101
  # getkq response with a value that we want to set in the response hash
114
- break unless keep_reading?(values, key, value, cas)
102
+ values[key] = [value, cas] unless key.nil?
115
103
 
116
104
  # Get the next response from the buffer
117
105
  status, cas, key, value = response_buffer.process_single_getk_response
@@ -130,16 +118,13 @@ module Dalli
130
118
  def pipeline_abort
131
119
  response_buffer.clear
132
120
  @connection_manager.abort_request!
133
- return unless connected?
134
-
135
- # Closes the connection, which ensures that our connection is in a
136
- # clean state for future requests. The get_multi ran out of time
137
- # overall rather than hitting a socket error, so it doesn't count
138
- # toward socket_max_failures: two slow get_multis in a row would
139
- # otherwise mark a healthy server down.
140
- Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
141
- @connection_manager.close
142
- nil
121
+ return true unless connected?
122
+
123
+ # Closes the connection, which ensures that our connection
124
+ # is in a clean state for future requests
125
+ @connection_manager.error_on_request!('External timeout')
126
+ rescue NetworkError
127
+ true
143
128
  end
144
129
 
145
130
  # Did the last call to #pipeline_response_setup complete successfully?
@@ -169,17 +154,7 @@ module Dalli
169
154
  private
170
155
 
171
156
  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
157
+ private_constant :ALLOWED_QUIET_OPS
183
158
 
184
159
  def verify_allowed_quiet!(opkey)
185
160
  return if ALLOWED_QUIET_OPS.include?(opkey)
@@ -238,7 +213,6 @@ module Dalli
238
213
  end
239
214
 
240
215
  def pipelined_get(keys)
241
- expect_reply_keys(keys)
242
216
  req = +''
243
217
  keys.each do |key|
244
218
  req << quiet_get_request(key)
@@ -251,50 +225,6 @@ module Dalli
251
225
  @response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
252
226
  end
253
227
 
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
-
298
228
  # Called after the noop response is received at the end of a set
299
229
  # of pipelined gets
300
230
  def finish_pipeline
@@ -66,9 +66,7 @@ module Dalli
66
66
  raise Dalli::DalliError, "Response error #{resp_header.status}: #{RESPONSE_CODES[resp_header.status]}"
67
67
  end
68
68
 
69
- # raw: true returns the value as stored, without deserializing or
70
- # decompressing it, whatever flags it was stored with
71
- def get(cache_nils: false, raw: false)
69
+ def get(cache_nils: false)
72
70
  resp_header, body = read_response
73
71
 
74
72
  return false if resp_header.not_stored? # Not stored, normal status for add operation
@@ -77,7 +75,7 @@ module Dalli
77
75
  raise_on_not_ok!(resp_header)
78
76
  return true unless body
79
77
 
80
- unpack_response_body(resp_header, body, !raw).last
78
+ unpack_response_body(resp_header, body, true).last
81
79
  end
82
80
 
83
81
  ##
@@ -102,8 +100,7 @@ module Dalli
102
100
  true
103
101
  end
104
102
 
105
- # raw: true returns the value as stored; see #get
106
- def data_cas_response(raw: false)
103
+ def data_cas_response
107
104
  resp_header, body = read_response
108
105
  return [nil, resp_header.cas] if resp_header.not_found?
109
106
  return [nil, false] if resp_header.not_stored?
@@ -111,7 +108,7 @@ module Dalli
111
108
  raise_on_not_ok!(resp_header)
112
109
  return [nil, resp_header.cas] unless body
113
110
 
114
- [unpack_response_body(resp_header, body, !raw).last, resp_header.cas]
111
+ [unpack_response_body(resp_header, body, true).last, resp_header.cas]
115
112
  end
116
113
 
117
114
  # Returns the new value for the key, if found and updated
@@ -212,17 +209,6 @@ module Dalli
212
209
  # The remaining three values in the array are the ResponseHeader,
213
210
  # key, and value.
214
211
  ##
215
- # memcached can't store an item larger than 1 GiB (its -I maximum). A
216
- # pipelined reply claiming an impossible size would otherwise have the
217
- # buffer wait for (and accumulate) that many bytes.
218
- MAX_BODY_BYTES = 1024 * 1024 * 1024
219
-
220
- def check_body_size!(size)
221
- return if size <= MAX_BODY_BYTES
222
-
223
- raise Dalli::DalliError, "Reply body size #{size} is out of range"
224
- end
225
-
226
212
  def getk_response_from_buffer(buf)
227
213
  # There's no header in the buffer, so don't advance
228
214
  return [0, nil, nil, nil, nil] unless contains_header?(buf)
@@ -236,8 +222,6 @@ module Dalli
236
222
  # error response that needs to be discarded.
237
223
  return [ResponseHeader::SIZE, resp_header.ok?, resp_header.cas, nil, nil] if body_len.zero?
238
224
 
239
- check_body_size!(body_len)
240
-
241
225
  resp_size = ResponseHeader::SIZE + body_len
242
226
  # The header is in the buffer, but the body is not. As we don't have
243
227
  # a complete response, don't advance the buffer