dalli 5.1.4 → 5.2.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 +35 -60
- data/README.md +27 -5
- data/lib/dalli/cache_result.rb +65 -0
- data/lib/dalli/client.rb +84 -36
- data/lib/dalli/compressor.rb +5 -53
- data/lib/dalli/key_manager.rb +2 -23
- data/lib/dalli/protocol/base.rb +22 -27
- data/lib/dalli/protocol/connection_manager.rb +27 -90
- data/lib/dalli/protocol/meta.rb +55 -39
- data/lib/dalli/protocol/request_formatter.rb +27 -12
- data/lib/dalli/protocol/response_buffer.rb +1 -4
- data/lib/dalli/protocol/response_processor.rb +102 -60
- data/lib/dalli/protocol/value_compressor.rb +3 -30
- data/lib/dalli/protocol/value_serializer.rb +1 -1
- data/lib/dalli/ring.rb +44 -17
- data/lib/dalli/socket.rb +27 -2
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +1 -1
- metadata +2 -2
- data/lib/dalli/json_serializer.rb +0 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23a3890b1ca95b3668eea5b0f2f6eabfb3ad6062daebf5723e5b99c2c7a74854
|
|
4
|
+
data.tar.gz: 4f0c1775badbf133dab91d32a19f84ffafab7f1c0a6c9d7c195f968bed01c089
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1624d8d1024956e90ab86a5d5b7f56833d9d195b95d2e8f7ed92774775eebb5ae19baf798773e17ce8fdb7b8fa5abf728a5899d085ecd11335fe11daac52bdb
|
|
7
|
+
data.tar.gz: 76c4e96a6ac88ec7ce86d213d58bf4f709f6ebc52e747ffa05661343173c654696e6e2625090b14d31874f160f3778a750e9cd524cb4f8985ea86716f267793e
|
data/CHANGELOG.md
CHANGED
|
@@ -4,67 +4,46 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
-
5.
|
|
7
|
+
5.2.0
|
|
8
8
|
==========
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- Keep the caller's key when retrying a request (GHSA-m252-9cgf-vx2w)
|
|
13
|
-
- With a `namespace`, a request retried after a transient network error (a timeout, or a connection closed by memcached or a proxy) applied the namespace a second time, so a retried read could return a different key's value and a retried write could overwrite a different key
|
|
14
|
-
- Affects single-key operations since 5.0.3, single-server `get_multi` since 5.1.0, and `get_with_metadata` and `fetch_with_lock` since 4.1.0; fixed in 5.2.2, 5.1.4, 5.0.10 and 4.3.7. Clients without a namespace aren't affected
|
|
15
|
-
- Complete the fix for per-request `raw: true` on reads (GHSA-wr87-m4jw-29x5)
|
|
16
|
-
- A read made with `raw: true` doesn't ask for flags, but a reply carrying them anyway (from a proxy or a hostile server) still had its value deserialized. Raw reads now ignore flags in the reply
|
|
17
|
-
- Affects 5.1.3 and earlier; fixed in 5.2.2, 5.1.4, 5.0.10, 4.3.7 and 3.2.13
|
|
18
|
-
|
|
19
|
-
Bug fixes:
|
|
20
|
-
|
|
21
|
-
- `get_with_metadata` and `fetch_with_lock` retried the final error raised when a server is marked down, so with `down_retry_delay: 0` they retried an unresponsive server forever. They now retry only retryable errors, like other operations
|
|
22
|
-
- Fix a regression in 5.1.3: 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
|
|
23
|
-
- Fix a regression in 5.1.3: over TLS, every request buffered in a `quiet` block was sent as its own TLS record and system call (2.5 times slower for a block of 2000 deletes). Each flush is one write again
|
|
24
|
-
- Handle malformed replies from a broken or hostile server or proxy: a negative value size, a hit with no key, or a hit with no `s` flag could leave the connection out of step or raise a non-Dalli error
|
|
25
|
-
- 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
|
|
26
|
-
- With a `digest_class` whose digests aren't short hex strings, shortening a long key could loop forever. It now raises `ArgumentError`
|
|
27
|
-
|
|
28
|
-
Development:
|
|
29
|
-
|
|
30
|
-
- Run the Tests, RuboCop and Profiles workflows on pushes to `main` and the `*-stable` branches only, so a pull request's branch isn't tested twice (backport of #1198)
|
|
31
|
-
- 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)
|
|
32
|
-
|
|
33
|
-
5.1.3
|
|
34
|
-
==========
|
|
35
|
-
|
|
36
|
-
Security:
|
|
37
|
-
|
|
38
|
-
- Fix pipelined `get_multi` returning one key's value for another after an error reply (GHSA-p6pm-ch9v-44vx)
|
|
39
|
-
- 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
|
|
40
|
-
- Keys are now measured in bytes as sent, after base64 encoding when the key needs it. A key under 250 characters but over 250 bytes on the wire (for example, one with many non-ASCII characters) was the easiest way to cause that error reply. Such keys are now truncated like other long keys; keys that worked before are unchanged
|
|
41
|
-
- Affects the meta protocol since 3.2.0 (the default since 5.0.0); fixed in 5.2.1, 5.1.3, 5.0.9, 4.3.6 and 3.2.12
|
|
42
|
-
- Reject routing tokens that add meta flags, and stop retrying an unresponsive server forever (GHSA-4qp6-2jcr-596v)
|
|
43
|
-
- `p_token` and `l_token` now reject whitespace and control characters, not just CR, LF and NUL. A space let a token add meta flags to the request it was sent with: changing an item's TTL on a read, creating stub items on a miss, turning a delete into a stale tombstone, or reading a different key
|
|
44
|
-
- Affects 5.1.0 and later; fixed in 5.2.1 and 5.1.3
|
|
45
|
-
- 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`
|
|
46
|
-
- Affects all versions; fixed in 5.2.1, 5.1.3, 5.0.9, 4.3.6 and 3.2.12
|
|
47
|
-
- Limit decompressed and reply value sizes (GHSA-3553-vcg5-72jw)
|
|
48
|
-
- 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
|
|
49
|
-
- The value size in a reply 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
|
|
50
|
-
- Affects all versions; fixed in 5.2.1, 5.1.3, 5.0.9, 4.3.6 and 3.2.12
|
|
51
|
-
- Honor per-request `raw: true` on reads, and add `Dalli::JSONSerializer` (GHSA-wr87-m4jw-29x5)
|
|
52
|
-
- `get_multi`, `get_multi_cas`, `get_multi_with_metadata`, `get_cas` and `get_with_metadata` 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
|
|
53
|
-
- The `raw` part affects `get_with_metadata` since 4.2.0, and the other methods since they gained per-request options in 5.1.0
|
|
54
|
-
- `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 and the Marshal security warning now recommend it; `serializer: JSON` itself is unchanged
|
|
55
|
-
- The `serializer: JSON` part affects any version used with the json gem before 3.0
|
|
56
|
-
- Keep a forked child from writing to or closing the parent's connection (GHSA-w39f-xq2m-4g8x)
|
|
57
|
-
- A child forked while the parent had requests buffered but not yet sent, such as quiet writes inside a `quiet` block, sent them again when it closed the client or reconnected: quiet writes ran twice, and the parent's later replies could be read as the replies to other requests, returning one key's value for another. Dalli now buffers requests itself and a forked child discards them
|
|
58
|
-
- Affects 4.2.0 and later; fixed in 5.2.1, 5.1.3, 5.0.9 and 4.3.6
|
|
59
|
-
- 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
|
|
60
|
-
- Affects all versions with TLS; fixed in 5.2.1, 5.1.3, 5.0.9, 4.3.6 and 3.2.12
|
|
10
|
+
Features:
|
|
61
11
|
|
|
62
|
-
|
|
12
|
+
- Add `Dalli::CacheResult`, returned by the new `#get_with_metadata_result` and `#get_multi_with_metadata_result` (#1156)
|
|
13
|
+
- An opt-in, typed view over the Hash that `#get_with_metadata` and `#get_multi_with_metadata` already return; those methods are unchanged, and changing their return type is left for a future major version (#1151)
|
|
14
|
+
- `value`, `cas`, `hit_before`, `last_access` and `ttl_remaining` readers (the last three are `nil` unless requested), plus `miss?`, `hit?`, `stale?`, `won_recache?` and `lost_recache?` predicates
|
|
15
|
+
- Results are frozen, and a Hash that claims to be both a miss and stale raises `ArgumentError`
|
|
16
|
+
- `#get_multi_with_metadata_result` omits misses and accepts `req_options:`, like `#get_multi_with_metadata`; it has no block form
|
|
17
|
+
- Add the opt-in `defer_drain` client option (#1168)
|
|
18
|
+
- With `defer_drain: true`, a `quiet`/`multi` block no longer waits at its end for the replies to its requests. The requests are still sent right away; the replies are read with one noop per server just before the next non-quiet request to that server
|
|
19
|
+
- Adds `Client#drain_deferred_responses`, to drain at a boundary of the caller's choosing (the end of a web request or job)
|
|
20
|
+
- Tradeoff: an error reply to a quiet request surfaces (and is discarded) at that later point, not at the end of the block
|
|
21
|
+
- Off by default, so existing behavior is unchanged
|
|
22
|
+
- Replies memcached sends for quiet requests wait on the connection until they're drained, so a client that only makes quiet requests should call `drain_deferred_responses` periodically; see "Deferred Draining" in the README
|
|
23
|
+
- Thanks to Julian Richard Contreras and Matt Dick for this contribution
|
|
63
24
|
|
|
64
|
-
|
|
25
|
+
Performance:
|
|
65
26
|
|
|
66
|
-
|
|
67
|
-
|
|
27
|
+
- Skip the empty read at the end of each pipelined `read_available` call (#1164)
|
|
28
|
+
- A read shorter than the chunk size means the socket is drained, so the extra `read_nonblock` that only returned `:wait_readable` is gone; the `IO.select` loop still picks up anything that arrives later
|
|
29
|
+
- Saves one syscall per server per multi-server `get_multi`; on 4 servers a 3-key `get_multi` goes from 16% to 5% slower than 2.7.11 in the #930 benchmark
|
|
30
|
+
- TLS sockets also check `SSLSocket#pending`, since `IO.select` cannot see data OpenSSL has buffered
|
|
31
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
32
|
+
- Reduce Ruby overhead on plain `set` and `delete` (#1166)
|
|
33
|
+
- A `set` with no CAS, routing tokens or quiet mode builds its `ms` request with one string interpolation (`RequestFormatter.plain_meta_set`), and a `delete` with no options uses `plain_meta_delete`; both produce the same bytes as before
|
|
34
|
+
- `HD c<cas>`, `HD` and `NF` replies are parsed without splitting them into tokens
|
|
35
|
+
- Allocations per `set` drop from 22 to 18, and per `delete` from 12 to 7
|
|
36
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
37
|
+
- Only drain servers that were sent quiet requests when a `quiet`/`multi` block ends (#1167)
|
|
38
|
+
- Ending the block used to send a noop to every server in the ring and wait for each reply, one after another, so its cost grew with ring size even when the block touched one server (or none)
|
|
39
|
+
- Each connection now records when a quiet request is written, and the block's drain skips the others; servers that were never connected are no longer connected just to be drained
|
|
40
|
+
- With ~300us of network round trip, `multi { set }` on a 16-server ring goes from 4.9ms to 0.32ms; an empty block no longer touches the network
|
|
41
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
42
|
+
- Reduce Ruby overhead in `get_multi` reply parsing and key routing (#1169)
|
|
43
|
+
- `VA` reply headers are read in place instead of being split into tokens, on both the multi-server (pipelined) and single-server paths; results are unchanged, and unusual headers still take the token path
|
|
44
|
+
- A key's server is found through a bucket table over the ring's continuum instead of a binary search, and the key's own server is tried before the failover loop; the server chosen for every key is unchanged
|
|
45
|
+
- Allocations for a 100-key `get_multi` on 4 servers drop from 1,500 to 1,104; over loopback it goes from 301us to 243us
|
|
46
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
68
47
|
|
|
69
48
|
Bug fixes:
|
|
70
49
|
|
|
@@ -75,10 +54,6 @@ Bug fixes:
|
|
|
75
54
|
- Affects the meta protocol since 3.2.0 (the default since 5.0.0)
|
|
76
55
|
- Thanks to Julian Richard Contreras for this contribution
|
|
77
56
|
|
|
78
|
-
Development:
|
|
79
|
-
|
|
80
|
-
- Add property tests for request building and fuzz tests for the reply parsers
|
|
81
|
-
|
|
82
57
|
5.1.1
|
|
83
58
|
==========
|
|
84
59
|
|
data/README.md
CHANGED
|
@@ -49,17 +49,39 @@ Dalli::Client.new('localhost:11211', namespace: 'myapp', namespace_separator: '/
|
|
|
49
49
|
|
|
50
50
|
The separator must be a single non-alphanumeric character. Valid examples: `:`, `/`, `|`, `.`, `-`, `_`, `#`
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
### Deferred Draining
|
|
53
53
|
|
|
54
|
-
By default,
|
|
54
|
+
By default, a `quiet` (or `multi`) block ends by waiting for the replies memcached still sends for its quiet requests, one round trip for each server the block wrote to. With `defer_drain: true`, the block returns without waiting:
|
|
55
55
|
|
|
56
56
|
```ruby
|
|
57
|
-
Dalli::Client.new('localhost:11211',
|
|
57
|
+
dc = Dalli::Client.new('localhost:11211', defer_drain: true)
|
|
58
|
+
|
|
59
|
+
dc.quiet { dc.set('key', 'value') } # returns without a round trip
|
|
60
|
+
dc.get('key') # first drains that connection with a noop, then gets the value
|
|
58
61
|
```
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
The requests are still sent right away. Their replies stay unread on the connection until the next non-quiet request to that server, which reads them first with one `noop`. Error replies to quiet requests are discarded either way. With `defer_drain` they're discarded at that later point instead of at the end of the block.
|
|
64
|
+
|
|
65
|
+
**Replies accumulate until a non-quiet request drains them.** memcached still replies to some quiet requests: `NF` for a quiet `delete`, `incr` or `decr` of a missing key, `NS` for an `add` or `replace` that didn't store, and error replies. Those replies wait on the socket. A client that mixes quiet writes with reads (or other non-quiet requests) to the same servers drains them as it goes, and needs nothing more.
|
|
66
|
+
|
|
67
|
+
A client that only ever makes quiet requests, such as a worker that only issues quiet deletes, never sends that next request. Its unread replies keep growing, and if they fill the connection's socket buffers, the connection can stall until a socket timeout closes it and Dalli reconnects. Workloads like that should call `drain_deferred_responses` periodically, for example after each batch or job:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
jobs.each do |job|
|
|
71
|
+
job.keys.each { |key| dc.quiet { dc.delete(key) } }
|
|
72
|
+
dc.drain_deferred_responses # reads and discards the replies waiting on each server
|
|
73
|
+
end
|
|
74
|
+
```
|
|
61
75
|
|
|
62
|
-
|
|
76
|
+
`drain_deferred_responses` only sends a `noop` to servers that were sent quiet requests since they were last drained, so calling it when nothing is pending costs nothing.
|
|
77
|
+
|
|
78
|
+
## Security Note
|
|
79
|
+
|
|
80
|
+
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, consider using a safer serializer:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
Dalli::Client.new('localhost:11211', serializer: JSON)
|
|
84
|
+
```
|
|
63
85
|
|
|
64
86
|
See the [5.0-Upgrade.md](5.0-Upgrade.md) guide for upgrade information.
|
|
65
87
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# An immutable, typed view over the Hash returned by Client#get_with_metadata
|
|
6
|
+
# and Client#get_multi_with_metadata.
|
|
7
|
+
#
|
|
8
|
+
# This is an interim adapter (see #1130 / #1151): #get_with_metadata and
|
|
9
|
+
# #get_multi_with_metadata keep returning a Hash for now, since changing
|
|
10
|
+
# their return type is a breaking change reserved for a future major
|
|
11
|
+
# version. #get_with_metadata_result and #get_multi_with_metadata_result
|
|
12
|
+
# wrap that Hash in this type instead, so the structured shape can be
|
|
13
|
+
# validated by callers ahead of that change.
|
|
14
|
+
##
|
|
15
|
+
class CacheResult
|
|
16
|
+
attr_reader :value, :cas, :hit_before, :last_access, :ttl_remaining
|
|
17
|
+
|
|
18
|
+
# @param hash [Hash] a Hash as returned by Client#get_with_metadata, or a
|
|
19
|
+
# single value from the Hash returned by Client#get_multi_with_metadata
|
|
20
|
+
def initialize(hash)
|
|
21
|
+
@value = hash[:value]
|
|
22
|
+
@cas = hash[:cas]
|
|
23
|
+
@miss = hash.fetch(:miss, false)
|
|
24
|
+
@stale = hash.fetch(:stale, false)
|
|
25
|
+
@won_recache = hash.fetch(:won_recache, false)
|
|
26
|
+
@lost_recache = hash.fetch(:lost_recache, false)
|
|
27
|
+
@hit_before = hash[:hit_before]
|
|
28
|
+
@last_access = hash[:last_access]
|
|
29
|
+
@ttl_remaining = hash[:ttl_remaining]
|
|
30
|
+
|
|
31
|
+
raise ArgumentError, 'a result cannot be both a miss and stale' if @miss && @stale
|
|
32
|
+
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# True when the key did not exist. Prefer this over a nil #value, which
|
|
37
|
+
# cannot distinguish a miss from a stored nil under cache_nils.
|
|
38
|
+
def miss?
|
|
39
|
+
@miss
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The inverse of #miss?.
|
|
43
|
+
def hit?
|
|
44
|
+
!@miss
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# True when the item is a tombstone (marked stale via the meta protocol's
|
|
48
|
+
# invalidate flag) rather than removed. Not a miss: #value is still the
|
|
49
|
+
# last-known value unless the tombstone also dropped it.
|
|
50
|
+
def stale?
|
|
51
|
+
@stale
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# True if this call won the right to regenerate the value under
|
|
55
|
+
# thundering-herd protection (see Client#fetch_with_lock).
|
|
56
|
+
def won_recache?
|
|
57
|
+
@won_recache
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# True if another client already won the recache race for this key.
|
|
61
|
+
def lost_recache?
|
|
62
|
+
@lost_recache
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/dalli/client.rb
CHANGED
|
@@ -50,6 +50,14 @@ module Dalli
|
|
|
50
50
|
# +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
|
|
51
51
|
# +nil+ (default) omits the attribute entirely.
|
|
52
52
|
# - :otel_peer_service - when set, adds a +peer.service+ span attribute with this value for logical service naming.
|
|
53
|
+
# - :defer_drain - defaults to false. When true, a quiet/multi block no longer waits at its end for the replies
|
|
54
|
+
# to its requests. The requests are still sent right away; their replies are read (with one noop
|
|
55
|
+
# per server) just before the next non-quiet request to that server, or by
|
|
56
|
+
# #drain_deferred_responses. The tradeoff: an error reply to a quiet request surfaces, and is
|
|
57
|
+
# discarded, at that later point rather than when the block ends. Replies memcached sends for
|
|
58
|
+
# quiet requests (NF, NS, errors) wait on the connection until then, so a client that only makes
|
|
59
|
+
# quiet requests should call #drain_deferred_responses periodically. See "Deferred Draining" in
|
|
60
|
+
# the README.
|
|
53
61
|
#
|
|
54
62
|
def initialize(servers = nil, options = {})
|
|
55
63
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
@@ -136,23 +144,35 @@ module Dalli
|
|
|
136
144
|
#
|
|
137
145
|
def get_with_metadata(key, options = {})
|
|
138
146
|
validate_routing_tokens!(options)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
validated_key = @key_manager.validate_key(key.to_s)
|
|
147
|
+
key = key.to_s
|
|
148
|
+
key = @key_manager.validate_key(key)
|
|
142
149
|
|
|
143
|
-
server = ring.server_for_key(
|
|
144
|
-
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata',
|
|
145
|
-
server.request(:meta_get,
|
|
150
|
+
server = ring.server_for_key(key)
|
|
151
|
+
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', key, server)) do
|
|
152
|
+
server.request(:meta_get, key, options)
|
|
146
153
|
end
|
|
147
|
-
|
|
148
|
-
# is marked down is final, as in #perform; retrying it looped forever once
|
|
149
|
-
# down_retry_delay let the same server be tried again.
|
|
150
|
-
rescue RetryableNetworkError => e
|
|
154
|
+
rescue NetworkError => e
|
|
151
155
|
Dalli.logger.debug { e.inspect }
|
|
152
156
|
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
153
157
|
retry
|
|
154
158
|
end
|
|
155
159
|
|
|
160
|
+
##
|
|
161
|
+
# Like #get_with_metadata, but wraps the result in a Dalli::CacheResult
|
|
162
|
+
# instead of returning a Hash.
|
|
163
|
+
#
|
|
164
|
+
# Interim adapter (see #1130 / #1151): #get_with_metadata itself keeps
|
|
165
|
+
# returning a Hash, since changing that return type is a breaking change
|
|
166
|
+
# reserved for a future major version. This lets callers opt in to the
|
|
167
|
+
# structured shape today.
|
|
168
|
+
#
|
|
169
|
+
# @param key [String] the cache key
|
|
170
|
+
# @param options [Hash] see #get_with_metadata
|
|
171
|
+
# @return [Dalli::CacheResult]
|
|
172
|
+
def get_with_metadata_result(key, options = {})
|
|
173
|
+
CacheResult.new(get_with_metadata(key, options))
|
|
174
|
+
end
|
|
175
|
+
|
|
156
176
|
##
|
|
157
177
|
# Fetch multiple keys efficiently.
|
|
158
178
|
# If a block is given, yields key/value pairs one at a time.
|
|
@@ -232,6 +252,27 @@ module Dalli
|
|
|
232
252
|
results
|
|
233
253
|
end
|
|
234
254
|
|
|
255
|
+
##
|
|
256
|
+
# Like #get_multi_with_metadata, but wraps each entry's Hash in a
|
|
257
|
+
# Dalli::CacheResult instead. Missing keys are still omitted; no block
|
|
258
|
+
# form.
|
|
259
|
+
#
|
|
260
|
+
# Interim adapter (see #1130 / #1151): #get_multi_with_metadata itself
|
|
261
|
+
# keeps returning a Hash of Hashes, since changing that return type is a
|
|
262
|
+
# breaking change reserved for a future major version. This lets callers
|
|
263
|
+
# opt in to the structured shape today.
|
|
264
|
+
#
|
|
265
|
+
# Result key order follows #get_multi_with_metadata's own: request order
|
|
266
|
+
# only when every key lands on the same server, per-server response order
|
|
267
|
+
# otherwise.
|
|
268
|
+
#
|
|
269
|
+
# @param keys [Array<String>] the keys to fetch
|
|
270
|
+
# @param req_options [Hash, nil] see #get_multi_with_metadata
|
|
271
|
+
# @return [Hash] key => Dalli::CacheResult
|
|
272
|
+
def get_multi_with_metadata_result(*keys, req_options: nil)
|
|
273
|
+
get_multi_with_metadata(*keys, req_options: req_options).transform_values { |hash| CacheResult.new(hash) }
|
|
274
|
+
end
|
|
275
|
+
|
|
235
276
|
##
|
|
236
277
|
# Fetch multiple keys efficiently, including available metadata such as CAS.
|
|
237
278
|
# If a block is given, yields key/data pairs one a time. Data is an array:
|
|
@@ -306,17 +347,14 @@ module Dalli
|
|
|
306
347
|
validate_integer!(:lock_ttl, lock_ttl)
|
|
307
348
|
validate_integer!(:recache_threshold, recache_threshold)
|
|
308
349
|
validate_routing_tokens!(req_options)
|
|
309
|
-
|
|
310
|
-
|
|
350
|
+
key = key.to_s
|
|
351
|
+
key = @key_manager.validate_key(key)
|
|
311
352
|
|
|
312
|
-
server = ring.server_for_key(
|
|
313
|
-
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock',
|
|
314
|
-
fetch_with_lock_request(
|
|
353
|
+
server = ring.server_for_key(key)
|
|
354
|
+
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', key, server)) do
|
|
355
|
+
fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
315
356
|
end
|
|
316
|
-
|
|
317
|
-
# is marked down is final, as in #perform; retrying it looped forever once
|
|
318
|
-
# down_retry_delay let the same server be tried again.
|
|
319
|
-
rescue RetryableNetworkError => e
|
|
357
|
+
rescue NetworkError => e
|
|
320
358
|
Dalli.logger.debug { e.inspect }
|
|
321
359
|
Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
|
|
322
360
|
retry
|
|
@@ -362,7 +400,8 @@ module Dalli
|
|
|
362
400
|
# arithmetic (incr, decr), flush and delete operations. Use of
|
|
363
401
|
# unsupported operations inside a block will raise an error.
|
|
364
402
|
#
|
|
365
|
-
# Any error replies will be discarded at the end of the block,
|
|
403
|
+
# Any error replies will be discarded at the end of the block (or, with
|
|
404
|
+
# the defer_drain option, before the next non-quiet request), and
|
|
366
405
|
# Dalli client methods invoked inside the block will not
|
|
367
406
|
# have return values
|
|
368
407
|
def quiet
|
|
@@ -370,11 +409,25 @@ module Dalli
|
|
|
370
409
|
Thread.current[::Dalli::QUIET] = true
|
|
371
410
|
yield
|
|
372
411
|
ensure
|
|
373
|
-
@ring&.pipeline_consume_and_ignore_responses
|
|
412
|
+
@ring&.pipeline_consume_and_ignore_responses unless @options[:defer_drain]
|
|
374
413
|
Thread.current[::Dalli::QUIET] = old
|
|
375
414
|
end
|
|
376
415
|
alias multi quiet
|
|
377
416
|
|
|
417
|
+
##
|
|
418
|
+
# Reads and discards the replies still pending from quiet requests, on the
|
|
419
|
+
# servers that have any. Only needed with the defer_drain option.
|
|
420
|
+
#
|
|
421
|
+
# Normally the next non-quiet request to a server drains its pending
|
|
422
|
+
# replies first. A client that only makes quiet requests (for example, a
|
|
423
|
+
# worker that only issues quiet deletes) never does, so its replies keep
|
|
424
|
+
# accumulating on the socket; call this periodically, such as after each
|
|
425
|
+
# batch or job. It sends nothing to servers with no quiet requests pending.
|
|
426
|
+
def drain_deferred_responses
|
|
427
|
+
@ring&.pipeline_consume_and_ignore_responses
|
|
428
|
+
nil
|
|
429
|
+
end
|
|
430
|
+
|
|
378
431
|
def set(key, value, ttl = nil, req_options = nil)
|
|
379
432
|
set_cas(key, value, 0, ttl, req_options)
|
|
380
433
|
end
|
|
@@ -734,11 +787,10 @@ module Dalli
|
|
|
734
787
|
# to route to at all is still silent, matching Ring#keys_grouped_by_server
|
|
735
788
|
# dropping a key it can't route on both the single- and multi-server paths.
|
|
736
789
|
def single_server_get_multi(keys, req_options = nil)
|
|
737
|
-
|
|
738
|
-
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
790
|
+
keys.map! { |k| @key_manager.validate_key(k.to_s) }
|
|
739
791
|
return {} unless (server = single_server)
|
|
740
792
|
|
|
741
|
-
result = server.request(:read_multi_req,
|
|
793
|
+
result = server.request(:read_multi_req, keys, req_options)
|
|
742
794
|
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
743
795
|
result
|
|
744
796
|
rescue Dalli::RetryableNetworkError => e
|
|
@@ -829,7 +881,7 @@ module Dalli
|
|
|
829
881
|
# check means unwinding through Protocol::Base#request, which logs the
|
|
830
882
|
# failure as unexpected and closes the connection -- a caller passing a
|
|
831
883
|
# bad token should get a clean ArgumentError and keep its connection.
|
|
832
|
-
ROUTING_TOKEN_FORBIDDEN = /[\
|
|
884
|
+
ROUTING_TOKEN_FORBIDDEN = /[\r\n\0]/
|
|
833
885
|
private_constant :ROUTING_TOKEN_FORBIDDEN
|
|
834
886
|
|
|
835
887
|
def validate_routing_tokens!(req_options)
|
|
@@ -845,9 +897,7 @@ module Dalli
|
|
|
845
897
|
# here (it would also excuse [] / {} from the type check below).
|
|
846
898
|
return if value.nil? || (value.is_a?(String) && value.empty?)
|
|
847
899
|
raise ArgumentError, "#{name} must be a String, got #{value.class}" unless value.is_a?(String)
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
raise ArgumentError, "#{name} must not contain whitespace or control characters"
|
|
900
|
+
raise ArgumentError, "#{name} must not contain CRLF or null bytes" if value.match?(ROUTING_TOKEN_FORBIDDEN)
|
|
851
901
|
end
|
|
852
902
|
|
|
853
903
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
@@ -905,20 +955,18 @@ module Dalli
|
|
|
905
955
|
# rubocop:enable Naming/MethodParameterName
|
|
906
956
|
return yield if block_given?
|
|
907
957
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
# namespace a second time and send the retry to a different key.
|
|
911
|
-
validated_key = @key_manager.validate_key(key.to_s)
|
|
958
|
+
key = key.to_s
|
|
959
|
+
key = @key_manager.validate_key(key)
|
|
912
960
|
|
|
913
|
-
server = ring.server_for_key(
|
|
961
|
+
server = ring.server_for_key(key)
|
|
914
962
|
|
|
915
963
|
if Instrumentation.enabled?
|
|
916
964
|
op_name = op.name
|
|
917
|
-
Instrumentation.trace(op_name, trace_attrs(op_name,
|
|
918
|
-
server.request(op,
|
|
965
|
+
Instrumentation.trace(op_name, trace_attrs(op_name, key, server)) do
|
|
966
|
+
server.request(op, key, ...)
|
|
919
967
|
end
|
|
920
968
|
else
|
|
921
|
-
server.request(op,
|
|
969
|
+
server.request(op, key, ...)
|
|
922
970
|
end
|
|
923
971
|
rescue RetryableNetworkError => e
|
|
924
972
|
Dalli.logger.debug { e.inspect }
|
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
|
@@ -58,7 +58,7 @@ module Dalli
|
|
|
58
58
|
raise ArgumentError, 'key cannot be blank' unless key&.length&.positive?
|
|
59
59
|
|
|
60
60
|
key = key_with_namespace(key)
|
|
61
|
-
|
|
61
|
+
key.length > MAX_KEY_LENGTH ? truncated_key(key) : key
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
##
|
|
@@ -122,28 +122,7 @@ module Dalli
|
|
|
122
122
|
##
|
|
123
123
|
def truncated_key(key)
|
|
124
124
|
digest = digest_class.hexdigest(key)
|
|
125
|
-
|
|
126
|
-
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
127
|
-
# A prefix with whitespace or non-ASCII characters sends the whole key
|
|
128
|
-
# base64-encoded, which can still be too long on the wire; shorten it.
|
|
129
|
-
while wire_length(truncated) > MAX_KEY_LENGTH && !prefix.empty?
|
|
130
|
-
prefix = prefix[0...-1]
|
|
131
|
-
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
132
|
-
end
|
|
133
|
-
return truncated if wire_length(truncated) <= MAX_KEY_LENGTH
|
|
134
|
-
|
|
135
|
-
# Only possible with a digest_class whose digests aren't short hex strings
|
|
136
|
-
raise ArgumentError, "#{digest_class} digests are too long to fit in a truncated key"
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# memcached's key limit applies to the key as sent: in bytes, and after
|
|
140
|
-
# base64 encoding when the key needs it. A key can be within the limit in
|
|
141
|
-
# characters and still be rejected, and memcached's CLIENT_ERROR for it
|
|
142
|
-
# must never reach a pipelined get.
|
|
143
|
-
def wire_length(key)
|
|
144
|
-
return key.bytesize unless Dalli::Protocol::Meta::KeyRegularizer.required?(key)
|
|
145
|
-
|
|
146
|
-
((key.bytesize + 2) / 3) * 4
|
|
125
|
+
"#{key[0, prefix_length(digest)]}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
147
126
|
end
|
|
148
127
|
|
|
149
128
|
def prefix_length(digest)
|