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 +4 -4
- data/CHANGELOG.md +24 -102
- data/Gemfile +2 -0
- data/README.md +0 -35
- data/lib/dalli/client.rb +5 -30
- data/lib/dalli/compressor.rb +5 -53
- data/lib/dalli/key_manager.rb +4 -30
- data/lib/dalli/protocol/base.rb +9 -79
- data/lib/dalli/protocol/binary/response_processor.rb +4 -20
- data/lib/dalli/protocol/binary.rb +6 -8
- data/lib/dalli/protocol/connection_manager.rb +12 -69
- data/lib/dalli/protocol/meta/key_regularizer.rb +1 -1
- data/lib/dalli/protocol/meta/request_formatter.rb +11 -25
- data/lib/dalli/protocol/meta/response_processor.rb +16 -54
- data/lib/dalli/protocol/meta.rb +9 -40
- data/lib/dalli/protocol/server_config_parser.rb +1 -1
- data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
- data/lib/dalli/protocol/value_compressor.rb +8 -46
- data/lib/dalli/protocol/value_marshaller.rb +1 -1
- data/lib/dalli/protocol/value_serializer.rb +49 -40
- data/lib/dalli/protocol.rb +0 -7
- data/lib/dalli/socket.rb +29 -36
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +3 -3
- data/lib/rack/session/dalli.rb +42 -7
- metadata +3 -5
- data/lib/dalli/json_serializer.rb +0 -28
- data/lib/dalli/server.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86e39b2869b7c916472e88e6cbbfb6aa1f6533aa35c9e1f1c704cffcbc680f8a
|
|
4
|
+
data.tar.gz: bf2e56610c6bae187d561ccf09105e6f5b4a29eed1308d089745ef22cf5b673b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
5
|
-
==========
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
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
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, &
|
|
164
|
-
cas_core(key, false, ttl, req_options, &
|
|
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, &
|
|
175
|
-
cas_core(key, true, ttl, req_options, &
|
|
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
|
-
|
|
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)
|
data/lib/dalli/compressor.rb
CHANGED
|
@@ -13,54 +13,8 @@ module Dalli
|
|
|
13
13
|
Zlib::Deflate.deflate(data)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
82
|
-
|
|
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
|
data/lib/dalli/key_manager.rb
CHANGED
|
@@ -30,13 +30,10 @@ module Dalli
|
|
|
30
30
|
|
|
31
31
|
def initialize(client_options)
|
|
32
32
|
@key_options =
|
|
33
|
-
DEFAULTS.merge(client_options.
|
|
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
|
-
|
|
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)}
|
|
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
|
-
|
|
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)
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
136
|
-
# clean state for future requests
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|