dalli 5.2.1 → 5.2.3
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 +81 -0
- data/README.md +53 -0
- data/lib/dalli/client.rb +104 -36
- data/lib/dalli/compressor.rb +5 -0
- data/lib/dalli/key_manager.rb +6 -3
- data/lib/dalli/pipelined_getter.rb +5 -2
- data/lib/dalli/pipelined_setter.rb +9 -0
- data/lib/dalli/protocol/base.rb +74 -6
- data/lib/dalli/protocol/connection_manager.rb +35 -4
- data/lib/dalli/protocol/meta.rb +75 -15
- data/lib/dalli/protocol/response_buffer.rb +4 -1
- data/lib/dalli/protocol/response_processor.rb +45 -18
- data/lib/dalli/protocol/server_config_parser.rb +10 -5
- data/lib/dalli/protocol/string_marshaller.rb +3 -5
- data/lib/dalli/protocol/value_compressor.rb +6 -4
- data/lib/dalli/protocol/value_marshaller.rb +21 -5
- data/lib/dalli/protocol.rb +3 -3
- data/lib/dalli/socket.rb +40 -3
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +4 -0
- data/lib/rack/session/dalli.rb +37 -10
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a31a4159ee5cd637dca6676f343595f653798864bef28b109fbc329debe2aff
|
|
4
|
+
data.tar.gz: efba0a1a4bb20eff2fd4f6b26c15359351484401502a798fa398b5bf1301d046
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38f6d3ba18790b94875f18bf6239a520f57144ba763826063471bea2047aa6def2cc528d37de35f29037a0c33ca9bb8b46d7ed0ddc5dccebf8914888c71dafd7
|
|
7
|
+
data.tar.gz: 9f091240e20313b7cbb9250e900cbf3aa02f9aedbffceabeedc7355bc780cd3895e231c77bb091ab97680dbdac85e093635e3cfbca904fd7f095512181e4a59d
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,87 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.2.3
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
Security:
|
|
11
|
+
|
|
12
|
+
- Don't rely on memcached's auto-create for keys that need base64 encoding (GHSA-r47x-6p6r-mvcp)
|
|
13
|
+
- 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
|
|
14
|
+
- `incr` and `decr` with an initial value now create such counters with an add instead. Return values are unchanged
|
|
15
|
+
- The new `verify_reply_keys` option makes `get_multi`, `get_multi_cas` and `get_multi_with_metadata` 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%
|
|
16
|
+
- Affects the meta protocol 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
|
|
17
|
+
|
|
18
|
+
Bug fixes:
|
|
19
|
+
|
|
20
|
+
- `get_multi_with_metadata` and its `CacheResult` adapter now retry transient network errors and raise `Dalli::NetworkError` on terminal failures instead of returning misses or partial results (#1222, @jaideeppyne)
|
|
21
|
+
- A per-request `raw: true` write no longer compresses the value (#1213)
|
|
22
|
+
- 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
|
|
23
|
+
- 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
|
|
24
|
+
- Fix `cas` and `cas!` edge cases, and make `flush` attempt every server (#1214)
|
|
25
|
+
- `cas!` on a missing key used an unconditional set, so a value another client created while the block ran was overwritten. It now adds the key and returns `false` if another client got there first
|
|
26
|
+
- With `cache_nils: true`, `cas` treated a stored `nil` as a miss and didn't call the block
|
|
27
|
+
- The docs now say what `cas` and `cas!` return on success: the new CAS value, not `true`. The return values are unchanged
|
|
28
|
+
- `flush` stopped at the first server that raised, so later servers weren't flushed. It now flushes every server it can reach, then raises the first error
|
|
29
|
+
- Handle failed TLS handshakes like other connection failures, and warn when TLS verification is off (#1215)
|
|
30
|
+
- 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)
|
|
31
|
+
- 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
|
|
32
|
+
- Connection and configuration fixes (#1216)
|
|
33
|
+
- `memcached://` URIs with a bracketed IPv6 host (`memcached://[::1]:11211`) now connect
|
|
34
|
+
- A server weight below 1 raises `ArgumentError`. Before, a ring where every weight was 0 raised `FloatDomainError`
|
|
35
|
+
- 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
|
|
36
|
+
- On CRuby, DNS resolution is now limited by `socket_timeout` (`resolv_timeout:`) as well as the connect
|
|
37
|
+
- `Dalli::Client.new` no longer modifies the options hash it's given, and accepts a frozen one (#1217)
|
|
38
|
+
- `value_max_bytes` now counts the key and memcached's per-item overhead (#1217)
|
|
39
|
+
- memcached's `-I` limit covers the whole item, not only the value. A value of exactly 1 MB passed Dalli's check and then failed in memcached with `Dalli::ServerError`, which `quiet` blocks and `set_multi` silently discarded
|
|
40
|
+
- Dalli now checks value + key + 63 bytes, the overhead measured on memcached 1.6, and raises `Dalli::ValueOverMaxSize` before sending. A custom `value_max_bytes` is now that much stricter
|
|
41
|
+
- A String `value_max_bytes` is converted to an Integer instead of raising on every set
|
|
42
|
+
- `Rack::Session::Dalli` fixes (#1217)
|
|
43
|
+
- It no longer requires the `connection_pool` gem unless `pool_size` or `pool_timeout` is given
|
|
44
|
+
- `pool_size` and `pool_timeout` are no longer passed to `Dalli::Client`, and the options hash isn't modified
|
|
45
|
+
- A failed session delete is logged as a warning, not only when `$VERBOSE` is set
|
|
46
|
+
- One value that can't be stored no longer discards other buffered writes (#1218)
|
|
47
|
+
- A value that raised `Dalli::MarshalError` or `Dalli::ValueOverMaxSize` closed the connection, which dropped writes still buffered from the same `quiet` block or `set_multi`. Multi-server `set_multi` did this without raising
|
|
48
|
+
- Both `set_multi` paths now store every other key, skip the bad one, and then raise. Single-server `set_multi` used to store nothing
|
|
49
|
+
- When single-server `set_multi` failed to marshal a value, the error log included every value in the batch. It now lists only the keys
|
|
50
|
+
- `Rack::Session::Dalli`'s 401 for a session deleted mid-request is now a valid Rack response (#1220)
|
|
51
|
+
- When a session is deleted while a request is using it (for example, by a logout in another request), the store still returns 401 instead of writing the session back. The 401 now has a `content-type` and expires the session cookie, so the browser stops sending the dead session ID
|
|
52
|
+
- The app's response body is now closed, as the Rack spec requires. Before, it was dropped without `close`
|
|
53
|
+
- The app has already run when the 401 is returned, so its side effects have happened. This is documented in the README
|
|
54
|
+
|
|
55
|
+
Improvements:
|
|
56
|
+
|
|
57
|
+
- Add a `connect_timeout` option to set a separate timeout for DNS resolution and TCP/UNIX socket connects (including reconnects); it defaults to `socket_timeout`. TLS handshakes continue to use `socket_timeout` (#1221, @underwoo16)
|
|
58
|
+
|
|
59
|
+
5.2.2
|
|
60
|
+
==========
|
|
61
|
+
|
|
62
|
+
Security:
|
|
63
|
+
|
|
64
|
+
- Keep the caller's key when retrying a request (GHSA-m252-9cgf-vx2w)
|
|
65
|
+
- 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
|
|
66
|
+
- 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
|
|
67
|
+
- Complete the fix for per-request `raw: true` on reads (GHSA-wr87-m4jw-29x5)
|
|
68
|
+
- 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
|
|
69
|
+
- Affects 5.2.1 and earlier; fixed in 5.2.2, 5.1.4, 5.0.10, 4.3.7 and 3.2.13
|
|
70
|
+
|
|
71
|
+
Bug fixes:
|
|
72
|
+
|
|
73
|
+
- `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
|
|
74
|
+
- Fix a regression in 5.2.1: 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
|
|
75
|
+
- Fix a regression in 5.2.1: 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
|
|
76
|
+
- 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
|
|
77
|
+
- 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
|
|
78
|
+
- With a `digest_class` whose digests aren't short hex strings, shortening a long key could loop forever. It now raises `ArgumentError`
|
|
79
|
+
|
|
80
|
+
Notes:
|
|
81
|
+
|
|
82
|
+
- Document how long keys are shortened, and that a short key written in the same form names the same item, in the README's security note (#1200)
|
|
83
|
+
|
|
84
|
+
Development:
|
|
85
|
+
|
|
86
|
+
- 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 (once for the push and once for the pull request) (#1198)
|
|
87
|
+
|
|
7
88
|
5.2.1
|
|
8
89
|
==========
|
|
9
90
|
|
data/README.md
CHANGED
|
@@ -59,6 +59,17 @@ Dalli::Client.new('localhost:11211', namespace: 'myapp', namespace_separator: '/
|
|
|
59
59
|
|
|
60
60
|
The separator must be a single non-alphanumeric character. Valid examples: `:`, `/`, `|`, `.`, `-`, `_`, `#`
|
|
61
61
|
|
|
62
|
+
### Maximum Item Size
|
|
63
|
+
|
|
64
|
+
memcached rejects items larger than its `-I` setting (1MB by default). Set `value_max_bytes` to the same value if you change it:
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
# memcached started with -I 2m
|
|
68
|
+
Dalli::Client.new('localhost:11211', value_max_bytes: 2 * 1024 * 1024)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
memcached's limit covers the whole item, so Dalli checks the stored (serialized and compressed) value plus the key plus 63 bytes of per-item overhead against `value_max_bytes`, and raises `Dalli::ValueOverMaxSize` before sending anything larger. With the defaults, the largest value you can store under a 10-byte key is 1,048,576 - 10 - 63 = 1,048,503 bytes.
|
|
72
|
+
|
|
62
73
|
### Deferred Draining
|
|
63
74
|
|
|
64
75
|
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:
|
|
@@ -97,8 +108,50 @@ Dalli::Client.new('localhost:11211', serializer: Dalli::JSONSerializer)
|
|
|
97
108
|
|
|
98
109
|
Per-request `raw: true` returns the stored bytes without deserializing or decompressing them, on every read method (`get`, `get_multi`, `get_cas`, `get_with_metadata` and `fetch`).
|
|
99
110
|
|
|
111
|
+
### Long keys
|
|
112
|
+
|
|
113
|
+
memcached limits keys to 250 bytes. Dalli shortens a longer key (counting its namespace) to the start of the key, followed by `:md5:` and a hex digest of the whole key:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
dc.set("report:#{'x' * 300}", 'data')
|
|
117
|
+
# stored under "report:xxx...xxx:md5:<32 hex characters>"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The shortened key is an ordinary memcached key, so a short key written in exactly that form names the same item, and reading or writing it reads or overwrites the long key's value. This only matters if untrusted input can choose an entire cache key. If it can, hash the untrusted part yourself so the key never needs shortening, or reject keys that contain `:md5:`.
|
|
121
|
+
|
|
122
|
+
The digest is MD5 by default. `digest_class:` takes any object that responds to `hexdigest`, such as `Digest::SHA256`. Changing it renames every shortened key, so existing entries for long keys are missed once.
|
|
123
|
+
|
|
100
124
|
See the [5.0-Upgrade.md](5.0-Upgrade.md) guide for upgrade information.
|
|
101
125
|
|
|
126
|
+
### Counters and verify_reply_keys
|
|
127
|
+
|
|
128
|
+
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. Since 5.2.3, 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.
|
|
129
|
+
|
|
130
|
+
## TLS
|
|
131
|
+
|
|
132
|
+
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:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
136
|
+
ssl_context.set_params(
|
|
137
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
|
138
|
+
verify_hostname: true,
|
|
139
|
+
ca_file: '/path/to/ca.crt' # omit to use the system's trusted certificates
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
Dalli::Client.new('memcached.example.com:11211', ssl_context: ssl_context)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
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.
|
|
146
|
+
|
|
147
|
+
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.
|
|
148
|
+
|
|
149
|
+
## Rack Sessions
|
|
150
|
+
|
|
151
|
+
`Rack::Session::Dalli` stores Rack sessions in memcached. If a session is deleted while a request is using it (for example, when the user logs out in another tab during a slow request), the store doesn't write the session back, so a logged-out session can't come back. It replaces the app's response with a `401 Wrong session ID` that expires the session cookie.
|
|
152
|
+
|
|
153
|
+
The app has already run by then, so whatever it did (database writes, emails, and so on) has happened, and only its response is lost. If a request needs a different outcome, check that the session still exists before making the changes.
|
|
154
|
+
|
|
102
155
|
## OpenTelemetry Tracing
|
|
103
156
|
|
|
104
157
|
Dalli automatically instruments operations with [OpenTelemetry](https://opentelemetry.io/) when the SDK is present. No configuration is required - just add the OpenTelemetry gems to your application:
|
data/lib/dalli/client.rb
CHANGED
|
@@ -35,15 +35,20 @@ module Dalli
|
|
|
35
35
|
# to memcached. Default: true.
|
|
36
36
|
# - :compression_min_size - the minimum size (in bytes) for which Dalli will compress values sent to Memcached.
|
|
37
37
|
# Defaults to 4K.
|
|
38
|
+
# - :value_max_bytes - the largest item memcached will accept, i.e. its -I setting. Defaults to 1MB. Like
|
|
39
|
+
# memcached, Dalli counts the key and 63 bytes of item overhead against this limit along
|
|
40
|
+
# with the (serialized, compressed) value, so the largest value that can be stored is
|
|
41
|
+
# value_max_bytes - key bytesize - 63. Larger values raise Dalli::ValueOverMaxSize.
|
|
38
42
|
# - :serializer - defaults to Marshal
|
|
39
43
|
# - :compressor - defaults to Dalli::Compressor, a Zlib-based implementation
|
|
40
44
|
# - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for
|
|
41
45
|
# #fetch operations.
|
|
42
46
|
# - :raw - If set, disables serialization and compression entirely at the client level.
|
|
43
47
|
# Only String values are supported. This is useful when the caller handles its own
|
|
44
|
-
# serialization (e.g., Rails' ActiveSupport::Cache).
|
|
45
|
-
#
|
|
46
|
-
#
|
|
48
|
+
# serialization (e.g., Rails' ActiveSupport::Cache). The per-request :raw option
|
|
49
|
+
# (e.g. set(key, value, ttl, raw: true)) does the same for a single call: String
|
|
50
|
+
# values are stored as-is, uncompressed, and non-String values raise
|
|
51
|
+
# Dalli::MarshalError.
|
|
47
52
|
# - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
|
|
48
53
|
# useful for injecting a FIPS compliant hash object.
|
|
49
54
|
# - :otel_db_statement - controls the +db.query.text+ span attribute when OpenTelemetry is loaded.
|
|
@@ -58,6 +63,17 @@ module Dalli
|
|
|
58
63
|
# quiet requests (NF, NS, errors) wait on the connection until then, so a client that only makes
|
|
59
64
|
# quiet requests should call #drain_deferred_responses periodically. See "Deferred Draining" in
|
|
60
65
|
# the README.
|
|
66
|
+
# - :socket_timeout - timeout in seconds for reads and writes on a connection, and for the TLS
|
|
67
|
+
# handshake. Also used for connecting unless :connect_timeout is set.
|
|
68
|
+
# Default: 1.
|
|
69
|
+
# - :connect_timeout - timeout in seconds for DNS resolution and TCP/UNIX socket connects,
|
|
70
|
+
# including reconnects after a failure. Defaults to :socket_timeout.
|
|
71
|
+
# TLS handshakes use :socket_timeout.
|
|
72
|
+
# - :verify_reply_keys - defaults to false. When true, get_multi, get_multi_cas and get_multi_with_metadata only
|
|
73
|
+
# accept replies for the keys they requested; after a reply naming another key, the rest of
|
|
74
|
+
# that server's replies are treated as misses. memcached can name an item by its raw key
|
|
75
|
+
# bytes when the item was created by auto-create from a base64-encoded key, which Dalli no
|
|
76
|
+
# longer does. Costs a multi-get about 15%.
|
|
61
77
|
#
|
|
62
78
|
def initialize(servers = nil, options = {})
|
|
63
79
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
@@ -144,14 +160,18 @@ module Dalli
|
|
|
144
160
|
#
|
|
145
161
|
def get_with_metadata(key, options = {})
|
|
146
162
|
validate_routing_tokens!(options)
|
|
147
|
-
|
|
148
|
-
|
|
163
|
+
# A retry re-runs this method with its original arguments, so `key` must
|
|
164
|
+
# stay as the caller gave it; reassigning it would add the namespace again.
|
|
165
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
149
166
|
|
|
150
|
-
server = ring.server_for_key(
|
|
151
|
-
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata',
|
|
152
|
-
server.request(:meta_get,
|
|
167
|
+
server = ring.server_for_key(validated_key)
|
|
168
|
+
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', validated_key, server)) do
|
|
169
|
+
server.request(:meta_get, validated_key, options)
|
|
153
170
|
end
|
|
154
|
-
|
|
171
|
+
# Only a retryable error is retried. The NetworkError raised when a server
|
|
172
|
+
# is marked down is final, as in #perform; retrying it looped forever once
|
|
173
|
+
# down_retry_delay let the same server be tried again.
|
|
174
|
+
rescue RetryableNetworkError => e
|
|
155
175
|
Dalli.logger.debug { e.inspect }
|
|
156
176
|
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
157
177
|
retry
|
|
@@ -222,9 +242,14 @@ module Dalli
|
|
|
222
242
|
#
|
|
223
243
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
224
244
|
#
|
|
245
|
+
# A transient network error is retried automatically. If a server remains
|
|
246
|
+
# unreachable after retrying, raises Dalli::NetworkError rather than
|
|
247
|
+
# returning its keys as misses or yielding a partial result.
|
|
248
|
+
#
|
|
225
249
|
# @param keys [Array<String>] the keys to fetch
|
|
226
250
|
# @param req_options [Hash, nil] routing-token options
|
|
227
251
|
# @return [Hash] key => { value:, cas:, stale:, miss: }
|
|
252
|
+
# @raise [Dalli::NetworkError] if a server is unreachable after retrying
|
|
228
253
|
def get_multi_with_metadata(*keys, req_options: nil, &block)
|
|
229
254
|
keys.flatten!
|
|
230
255
|
keys.compact!
|
|
@@ -347,14 +372,17 @@ module Dalli
|
|
|
347
372
|
validate_integer!(:lock_ttl, lock_ttl)
|
|
348
373
|
validate_integer!(:recache_threshold, recache_threshold)
|
|
349
374
|
validate_routing_tokens!(req_options)
|
|
350
|
-
|
|
351
|
-
|
|
375
|
+
# Left unchanged for the retry below; see get_with_metadata
|
|
376
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
352
377
|
|
|
353
|
-
server = ring.server_for_key(
|
|
354
|
-
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock',
|
|
355
|
-
fetch_with_lock_request(
|
|
378
|
+
server = ring.server_for_key(validated_key)
|
|
379
|
+
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', validated_key, server)) do
|
|
380
|
+
fetch_with_lock_request(validated_key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
356
381
|
end
|
|
357
|
-
|
|
382
|
+
# Only a retryable error is retried. The NetworkError raised when a server
|
|
383
|
+
# is marked down is final, as in #perform; retrying it looped forever once
|
|
384
|
+
# down_retry_delay let the same server be tried again.
|
|
385
|
+
rescue RetryableNetworkError => e
|
|
358
386
|
Dalli.logger.debug { e.inspect }
|
|
359
387
|
Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
|
|
360
388
|
retry
|
|
@@ -364,24 +392,26 @@ module Dalli
|
|
|
364
392
|
# compare and swap values using optimistic locking.
|
|
365
393
|
# Fetch the existing value for key.
|
|
366
394
|
# If it exists, yield the value to the block.
|
|
367
|
-
#
|
|
368
|
-
#
|
|
395
|
+
# Store the block's return value as the new value for the key.
|
|
396
|
+
# The store will fail if someone else changed the value.
|
|
369
397
|
#
|
|
370
398
|
# Returns:
|
|
371
|
-
# - nil if the key did not exist
|
|
399
|
+
# - nil if the key did not exist (or held nil and :cache_nils is false);
|
|
400
|
+
# the block is not called.
|
|
372
401
|
# - false if the value was changed by someone else.
|
|
373
|
-
# -
|
|
402
|
+
# - the new CAS value (a positive Integer) if the value was successfully updated.
|
|
374
403
|
def cas(key, ttl = nil, req_options = nil, &)
|
|
375
404
|
cas_core(key, false, ttl, req_options, &)
|
|
376
405
|
end
|
|
377
406
|
|
|
378
407
|
##
|
|
379
408
|
# like #cas, but will yield to the block whether or not the value
|
|
380
|
-
# already exists.
|
|
409
|
+
# already exists. If the key did not exist, the block receives nil.
|
|
381
410
|
#
|
|
382
411
|
# Returns:
|
|
383
|
-
# - false if the value was changed by someone else
|
|
384
|
-
#
|
|
412
|
+
# - false if the value was changed by someone else, including another
|
|
413
|
+
# client creating a missing key while the block ran.
|
|
414
|
+
# - the new CAS value (a positive Integer) if the value was successfully updated.
|
|
385
415
|
def cas!(key, ttl = nil, req_options = nil, &)
|
|
386
416
|
cas_core(key, true, ttl, req_options, &)
|
|
387
417
|
end
|
|
@@ -643,9 +673,20 @@ module Dalli
|
|
|
643
673
|
##
|
|
644
674
|
# Flush the memcached server, at 'delay' seconds in the future.
|
|
645
675
|
# Delay defaults to zero seconds, which means an immediate flush.
|
|
676
|
+
# Every server is attempted. If any of them fails, the first error is
|
|
677
|
+
# raised once all the others have been flushed.
|
|
646
678
|
##
|
|
647
679
|
def flush(delay = 0)
|
|
648
|
-
|
|
680
|
+
first_error = nil
|
|
681
|
+
results = ring.servers.map do |s|
|
|
682
|
+
s.request(:flush, delay)
|
|
683
|
+
rescue StandardError => e
|
|
684
|
+
first_error ||= e
|
|
685
|
+
nil
|
|
686
|
+
end
|
|
687
|
+
raise first_error if first_error
|
|
688
|
+
|
|
689
|
+
results
|
|
649
690
|
end
|
|
650
691
|
alias flush_all flush
|
|
651
692
|
|
|
@@ -787,26 +828,37 @@ module Dalli
|
|
|
787
828
|
# to route to at all is still silent, matching Ring#keys_grouped_by_server
|
|
788
829
|
# dropping a key it can't route on both the single- and multi-server paths.
|
|
789
830
|
def single_server_get_multi(keys, req_options = nil)
|
|
790
|
-
|
|
831
|
+
# A new array, so the retry below starts again from the caller's keys
|
|
832
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
791
833
|
return {} unless (server = single_server)
|
|
792
834
|
|
|
793
|
-
result = server.request(:read_multi_req,
|
|
835
|
+
result = server.request(:read_multi_req, validated_keys, req_options)
|
|
794
836
|
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
795
837
|
result
|
|
796
838
|
rescue Dalli::RetryableNetworkError => e
|
|
797
839
|
Dalli.logger.debug { e.inspect }
|
|
798
840
|
Dalli.logger.debug { 'retrying single-server get_multi because of network error' }
|
|
799
841
|
retry
|
|
842
|
+
rescue Dalli::UnexpectedReplyKeyError => e
|
|
843
|
+
# The connection is closed and the keys are treated as missing, as the
|
|
844
|
+
# pipelined get_multi does for a server whose replies are out of step
|
|
845
|
+
Dalli.logger.warn { e.message }
|
|
846
|
+
{}
|
|
800
847
|
end
|
|
801
848
|
|
|
802
849
|
def single_server_get_multi_with_metadata(keys, req_options = nil)
|
|
803
|
-
keys.map
|
|
850
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
804
851
|
return {} unless (server = single_server)
|
|
805
852
|
|
|
806
|
-
result = server.request(:read_multi_with_metadata_req,
|
|
853
|
+
result = server.request(:read_multi_with_metadata_req, validated_keys, req_options)
|
|
807
854
|
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
808
855
|
result
|
|
809
|
-
rescue Dalli::
|
|
856
|
+
rescue Dalli::RetryableNetworkError => e
|
|
857
|
+
Dalli.logger.debug { e.inspect }
|
|
858
|
+
Dalli.logger.debug { 'retrying single-server get_multi_with_metadata because of network error' }
|
|
859
|
+
retry
|
|
860
|
+
rescue Dalli::UnexpectedReplyKeyError => e
|
|
861
|
+
Dalli.logger.warn { e.message }
|
|
810
862
|
{}
|
|
811
863
|
end
|
|
812
864
|
|
|
@@ -905,12 +957,23 @@ module Dalli
|
|
|
905
957
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
906
958
|
validate_routing_tokens!(req_options)
|
|
907
959
|
(value, cas) = perform(:cas, key, req_options)
|
|
908
|
-
return if
|
|
960
|
+
return if !always_set && cas_miss?(value, cas)
|
|
909
961
|
|
|
910
962
|
newvalue = yield(value)
|
|
963
|
+
# A CAS of 0 means the key was missing. A set with CAS 0 is unconditional,
|
|
964
|
+
# so add instead: it fails if another client created the key meanwhile.
|
|
965
|
+
return perform(:add, key, newvalue, ttl_or_default(ttl), req_options) if cas.zero?
|
|
966
|
+
|
|
911
967
|
perform(:set, key, newvalue, ttl_or_default(ttl), cas, req_options)
|
|
912
968
|
end
|
|
913
969
|
|
|
970
|
+
# The CAS lookup returns [nil, 0] on a miss rather than NOT_FOUND, and a
|
|
971
|
+
# stored item always has a non-zero CAS. So under cache_nils a zero CAS
|
|
972
|
+
# marks the miss, and a stored nil is a hit, as in #not_found?.
|
|
973
|
+
def cas_miss?(value, cas)
|
|
974
|
+
cache_nils ? cas.zero? : value.nil?
|
|
975
|
+
end
|
|
976
|
+
|
|
914
977
|
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
915
978
|
server = ring.server_for_key(key)
|
|
916
979
|
# req_options is the base, not the override: fetch_with_lock's own
|
|
@@ -957,18 +1020,20 @@ module Dalli
|
|
|
957
1020
|
# rubocop:enable Naming/MethodParameterName
|
|
958
1021
|
return yield if block_given?
|
|
959
1022
|
|
|
960
|
-
|
|
961
|
-
|
|
1023
|
+
# `retry` below re-runs this method with its original arguments, so `key`
|
|
1024
|
+
# must stay as the caller gave it: reassigning it would apply the
|
|
1025
|
+
# namespace a second time and send the retry to a different key.
|
|
1026
|
+
validated_key = @key_manager.validate_key(key.to_s)
|
|
962
1027
|
|
|
963
|
-
server = ring.server_for_key(
|
|
1028
|
+
server = ring.server_for_key(validated_key)
|
|
964
1029
|
|
|
965
1030
|
if Instrumentation.enabled?
|
|
966
1031
|
op_name = op.name
|
|
967
|
-
Instrumentation.trace(op_name, trace_attrs(op_name,
|
|
968
|
-
server.request(op,
|
|
1032
|
+
Instrumentation.trace(op_name, trace_attrs(op_name, validated_key, server)) do
|
|
1033
|
+
server.request(op, validated_key, ...)
|
|
969
1034
|
end
|
|
970
1035
|
else
|
|
971
|
-
server.request(op,
|
|
1036
|
+
server.request(op, validated_key, ...)
|
|
972
1037
|
end
|
|
973
1038
|
rescue RetryableNetworkError => e
|
|
974
1039
|
Dalli.logger.debug { e.inspect }
|
|
@@ -976,7 +1041,10 @@ module Dalli
|
|
|
976
1041
|
retry
|
|
977
1042
|
end
|
|
978
1043
|
|
|
979
|
-
|
|
1044
|
+
# Works on a copy so the caller's hash is neither modified here nor able
|
|
1045
|
+
# to change this client's configuration later.
|
|
1046
|
+
def normalize_options(options)
|
|
1047
|
+
opts = options.dup
|
|
980
1048
|
opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
|
|
981
1049
|
opts
|
|
982
1050
|
rescue NoMethodError
|
data/lib/dalli/compressor.rb
CHANGED
|
@@ -39,7 +39,12 @@ module Dalli
|
|
|
39
39
|
out = String.new(encoding: Encoding::BINARY)
|
|
40
40
|
0.step(data.bytesize - 1, INFLATE_SLICE_BYTES) do |pos|
|
|
41
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?
|
|
42
45
|
end
|
|
46
|
+
return out if inflater.finished?
|
|
47
|
+
|
|
43
48
|
append_within_limit(out, inflater.finish, max_bytes)
|
|
44
49
|
ensure
|
|
45
50
|
inflater.close
|
data/lib/dalli/key_manager.rb
CHANGED
|
@@ -122,15 +122,18 @@ module Dalli
|
|
|
122
122
|
##
|
|
123
123
|
def truncated_key(key)
|
|
124
124
|
digest = digest_class.hexdigest(key)
|
|
125
|
-
prefix = key[0, prefix_length(digest)]
|
|
125
|
+
prefix = key[0, [prefix_length(digest), 0].max]
|
|
126
126
|
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
127
127
|
# A prefix with whitespace or non-ASCII characters sends the whole key
|
|
128
128
|
# base64-encoded, which can still be too long on the wire; shorten it.
|
|
129
|
-
while wire_length(truncated) > MAX_KEY_LENGTH
|
|
129
|
+
while wire_length(truncated) > MAX_KEY_LENGTH && !prefix.empty?
|
|
130
130
|
prefix = prefix[0...-1]
|
|
131
131
|
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
132
132
|
end
|
|
133
|
-
truncated
|
|
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"
|
|
134
137
|
end
|
|
135
138
|
|
|
136
139
|
# memcached's key limit applies to the key as sent: in bytes, and after
|
|
@@ -60,6 +60,9 @@ module Dalli
|
|
|
60
60
|
#
|
|
61
61
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
62
62
|
#
|
|
63
|
+
# A transient network error is retried automatically. A terminal
|
|
64
|
+
# Dalli::NetworkError propagates instead of returning partial metadata.
|
|
65
|
+
#
|
|
63
66
|
def process_with_metadata(keys, req_options = nil)
|
|
64
67
|
return {} if keys.empty?
|
|
65
68
|
|
|
@@ -67,9 +70,9 @@ module Dalli
|
|
|
67
70
|
results = {}
|
|
68
71
|
groups_for_keys(keys).each do |server, keys_for_server|
|
|
69
72
|
results.merge!(server.request(:read_multi_with_metadata_req, keys_for_server, req_options))
|
|
70
|
-
rescue Dalli::
|
|
73
|
+
rescue Dalli::NetworkError
|
|
71
74
|
raise
|
|
72
|
-
rescue DalliError
|
|
75
|
+
rescue DalliError => e
|
|
73
76
|
Dalli.logger.debug { e.inspect }
|
|
74
77
|
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
75
78
|
end
|
|
@@ -18,6 +18,9 @@ module Dalli
|
|
|
18
18
|
# A transient network error is retried automatically. If a server remains
|
|
19
19
|
# unreachable after retrying, raises Dalli::NetworkError.
|
|
20
20
|
#
|
|
21
|
+
# A value that can't be marshalled or is over value_max_bytes is skipped;
|
|
22
|
+
# the other keys are still stored, then the first such error is raised.
|
|
23
|
+
#
|
|
21
24
|
# @param hash [Hash] key-value pairs to set
|
|
22
25
|
# @param ttl [Integer] time-to-live in seconds
|
|
23
26
|
# @param req_options [Hash] options passed to each set operation
|
|
@@ -26,10 +29,12 @@ module Dalli
|
|
|
26
29
|
def process(hash, ttl, req_options)
|
|
27
30
|
return if hash.empty?
|
|
28
31
|
|
|
32
|
+
@value_error = nil
|
|
29
33
|
@ring.lock do
|
|
30
34
|
servers = setup_requests(hash, ttl, req_options)
|
|
31
35
|
finish_requests(servers)
|
|
32
36
|
end
|
|
37
|
+
raise @value_error if @value_error
|
|
33
38
|
rescue Dalli::RetryableNetworkError => e
|
|
34
39
|
Dalli.logger.debug { e.inspect }
|
|
35
40
|
Dalli.logger.debug { 'retrying pipelined sets because of network error' }
|
|
@@ -62,6 +67,10 @@ module Dalli
|
|
|
62
67
|
server.request(:pipelined_set, key, value, ttl, req_options)
|
|
63
68
|
rescue Dalli::NetworkError
|
|
64
69
|
raise
|
|
70
|
+
rescue Dalli::MarshalError, Dalli::ValueOverMaxSize => e
|
|
71
|
+
# Nothing was written for this key, so the server's other keys are
|
|
72
|
+
# unaffected; raised by #process once the batch is complete
|
|
73
|
+
@value_error ||= e
|
|
65
74
|
rescue DalliError => e
|
|
66
75
|
Dalli.logger.debug { e.inspect }
|
|
67
76
|
Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
|