dalli 5.2.6 → 5.3.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 +48 -0
- data/README.md +30 -0
- data/lib/dalli/client.rb +189 -76
- data/lib/dalli/key_grouping.rb +50 -0
- data/lib/dalli/key_manager.rb +30 -0
- data/lib/dalli/pipelined_deleter.rb +7 -16
- data/lib/dalli/pipelined_getter.rb +64 -58
- data/lib/dalli/pipelined_setter.rb +6 -15
- data/lib/dalli/protocol/base.rb +117 -41
- data/lib/dalli/protocol/connection_manager.rb +106 -2
- data/lib/dalli/protocol/key_regularizer.rb +5 -0
- data/lib/dalli/protocol/meta.rb +37 -9
- data/lib/dalli/protocol/request_formatter.rb +62 -33
- data/lib/dalli/protocol/response_processor.rb +49 -6
- data/lib/dalli/protocol/ttl_sanitizer.rb +13 -4
- data/lib/dalli/ring.rb +4 -0
- data/lib/dalli/servers_arg_normalizer.rb +19 -1
- data/lib/dalli/socket.rb +17 -2
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +14 -1
- data/lib/rack/session/dalli.rb +12 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2bf7ed15c1eb033940d2d1fb04d6577f9676d12bb30b547da704a56b282f0cdf
|
|
4
|
+
data.tar.gz: 65208448d43b257b0cd4f3af9b6f4d01e13c509f13482a2f641d6d01a1573bd7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ee59bbf0d4795a614a143f00594f0f90fcb17592ffb7439bf72a824dbb19d135718e18b5101706819f01039999b4d0c6c7f59c9aea98f09d78e7d6bfee4176ea
|
|
7
|
+
data.tar.gz: ffe4b0170ad6a31071ba8fe5275a4479b83efd31fe83bb97c790b19b86bf44d19ed6fc5bb67d2c2862a5e2d82068106d114e06a566c71c92d780e714762f5f26
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,54 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.3.0
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
New features:
|
|
11
|
+
|
|
12
|
+
- `enforce_read_timeout: true` makes JRuby and TruffleRuby give up waiting for a reply after `socket_timeout`, as CRuby does. Without it, a server that accepted a request but never replied blocked the caller indefinitely on those engines, including in `get` and `get_multi`. A server that stops partway through a reply can still block. It's off by default because it costs about 8% of single-key throughput on TruffleRuby and 20-29% on JRuby, and it has no effect on CRuby or TLS connections
|
|
13
|
+
|
|
14
|
+
Bug fixes:
|
|
15
|
+
|
|
16
|
+
- A `vivify_ttl` (or `fetch_with_lock`'s `lock_ttl`) over 30 days now protects against a thundering herd. memcached reads a TTL over 30 days as a Unix timestamp, so the stub for a miss expired at once and every caller won the recache race. `vivify_ttl` is now converted like other TTLs
|
|
17
|
+
- `set_cas`, `replace_cas` and `delete_cas` dropped a CAS that couldn't be parsed, such as `'abc'`, and sent an unconditional write, so the value was overwritten or deleted whatever its CAS. They now raise `ArgumentError`
|
|
18
|
+
- `flush('abc')` flushed immediately. It now raises `ArgumentError`
|
|
19
|
+
- A TTL String that isn't a whole number, such as `'abc'`, became 0, which means no expiry. It now raises `ArgumentError`, including for `expires_in` when the client is created
|
|
20
|
+
- A TTL, CAS, `incr`/`decr` amount or initial value outside the range memcached accepts got a `CLIENT_ERROR` reply, which a `quiet` block or `set_multi` discarded, so the write silently did nothing. These now raise `ArgumentError`
|
|
21
|
+
- An invalid `vivify_ttl` or `recache_ttl` raised from deep in the request and closed the connection
|
|
22
|
+
- `tombstone_ttl` read a String with a leading zero as octal (`'010'` was 8) and accepted hex such as `'0x10'`. It's now parsed in base 10 like every other TTL, and `Float::NAN` raises `ArgumentError` rather than `FloatDomainError`
|
|
23
|
+
- Multi-gets now raise `Dalli::UnmarshalError` for a value that can't be deserialized, as `get` does, and only after reading the rest of the replies
|
|
24
|
+
- `get_multi` and `get_multi_cas` raised partway through reading a server's replies, closing the connection, and `get_multi_with_metadata` on more than one server silently dropped every key on that server. A large `get_multi` could also drop that server's keys without raising when it read the bad value while still sending requests (seen mostly on JRuby and TruffleRuby)
|
|
25
|
+
- In block form the other values are still yielded, and the connections stay open. `get` and the other single-key reads also keep the connection open after this error now
|
|
26
|
+
- `get_multi`, `get_multi_cas` and `get_multi_with_metadata` now return each value under the key the caller passed. They rebuilt the key from the one memcached returned, so:
|
|
27
|
+
- With a namespace, a key that isn't valid UTF-8 (such as `"\xff".b`) raised `ArgumentError` for the whole call
|
|
28
|
+
- A key too long to send as is (over 250 bytes, or once base64-encoded) came back under its digested form
|
|
29
|
+
- A binary key with non-ASCII bytes came back tagged UTF-8, so `result.key?(key)` was false
|
|
30
|
+
- A key given more than once is now requested once, and a block is yielded it once
|
|
31
|
+
- On TruffleRuby, a process spawned while Dalli had a connection open (with `system`, `spawn` or backticks) inherited the socket and kept the connection open on memcached's side until it exited. Dalli's sockets are now close-on-exec, as CRuby makes them by default
|
|
32
|
+
- A request that failed after it was sent, such as one whose reply timed out, was sent again even though memcached may have applied it. `incr`, `decr`, `append` and `prepend` could be applied twice, and `add` and writes with a CAS could report failure for a write that had succeeded. Once some of a request has been sent, Dalli now retries only requests that are safe to apply twice (see Other changes)
|
|
33
|
+
- When a server stopped while connected, the multi-key operations (`get_multi`, `get_multi_cas`, `get_multi_with_metadata`, `set_multi` and `delete_multi`) returned its keys as misses, or left them unwritten, instead of raising `Dalli::NetworkError` as documented. The retry found the server down and skipped it as if it had been down from the start. Without `failover` they now raise. With `failover` the keys still go to another server, as they do for `get` and `set`, when the retry can't connect to the failed server. A server that accepts the retry's connection and fails again is marked down during the operation, which raises `Dalli::NetworkError` either way (#1263, @jaideeppyne)
|
|
34
|
+
- `Rack::Session::Dalli`: a stored session that couldn't be decoded (for example after changing the serializer, or removing a class that sessions stored) left the user on a session that was never saved, with no new cookie, so they couldn't log in until the cookie was cleared or the session expired. It's now treated as missing: the request gets a new session and cookie, and a warning is logged
|
|
35
|
+
- A reply value is now checked for the CRLF that should follow it. Values were read by their declared size alone, so a size that didn't match the value (a broken proxy, or an injected reply) was returned as a hit with a `nil` value, or the rest of the value was read as the next reply. These now raise `Dalli::DalliError` and close the connection, as a value size out of range does
|
|
36
|
+
- `get_multi_cas`, and `get_multi` on more than one server, returned a server's keys as misses when it sent nothing within `socket_timeout`, without retrying or counting a failure. They now treat it like any read timeout: the batch is retried, and `Dalli::NetworkError` is raised once the server is marked down, as `get`, `get_multi_with_metadata` and single-server `get_multi` already did. `socket_timeout` also used to cover the whole batch, so a large reply that was still arriving could be cut off, with its remaining keys returned as misses. The wait now starts again whenever data arrives (#1285)
|
|
37
|
+
- A reply key marked base64 that isn't valid base64 raised `ArgumentError` from `get_multi`, `get_multi_cas` and `get_multi_with_metadata`, which callers rescuing `Dalli::DalliError` didn't catch. It now raises `Dalli::DalliError`. Found by the new fuzz targets
|
|
38
|
+
- `set_multi` and `delete_multi` on more than one server retried forever, hanging the caller, when a server accepted their writes and then closed the connection before replying. Each quiet write reset the server's failure count, so it never reached `socket_max_failures`. Only a request whose reply is read resets it now, and the server is marked down and `Dalli::NetworkError` raised, as for `get`
|
|
39
|
+
- Since 5.2.0, on JRuby with TLS, Dalli could wait `socket_timeout` for part of a reply that had already arrived, because jruby-openssl can hold it where neither `IO.select` nor OpenSSL's `pending` reports it. With the multi-get change above (#1285), that wait would have made `get_multi` raise `Dalli::NetworkError`. Dalli now reads on JRuby until the TLS socket would block
|
|
40
|
+
|
|
41
|
+
Other changes:
|
|
42
|
+
|
|
43
|
+
- `Rack::Session::Dalli` docs: without `expire_after`, sessions are stored with no expiry and stay in memcached until deleted or evicted
|
|
44
|
+
- Numeric arguments are checked before anything is written, so a bad one raises `ArgumentError` without affecting the connection or earlier writes in a `quiet` block. Integers and Strings of decimal digits (such as `'300'`) are accepted throughout
|
|
45
|
+
- A TTL (`ttl`, `expires_in`, `vivify_ttl`, `lock_ttl`, `tombstone_ttl`) must fit memcached's signed 32-bit expiration time once a TTL over 30 days is converted to a Unix time. A negative TTL is still accepted and expires the item. A Float or `ActiveSupport::Duration` TTL is still converted with `to_i`
|
|
46
|
+
- A CAS, an `incr`/`decr` amount or initial value, and a `flush` delay must be from 0 to 2**64 - 1. A Float now raises; `incr(key, 0.9)` used to truncate the amount to 0. A String amount such as `'5'` now works
|
|
47
|
+
- `fetch`, `fetch_with_lock`, `cas` and `cas!` check `ttl` before running the block, so a bad one raises before any side effects. `fetch` checks it on a hit too
|
|
48
|
+
- `Dalli::Protocol::TtlSanitizer.sanitize` raises `ArgumentError` for a String that isn't a whole number instead of returning 0
|
|
49
|
+
- Usernames and passwords from `memcached://` URIs and the `:username`/`:password` options are no longer kept in the client's or server's options, so `#inspect` can't show them. Dalli 5 has never used them, and the warning is unchanged
|
|
50
|
+
- A TTL over 30 days is converted to a Unix time once per request, not twice, and the debug message for that conversion is built only when debug logging is on
|
|
51
|
+
- `incr`, `decr`, `append`, `prepend`, `add`, `set_cas`, `replace_cas`, `delete_cas`, `cas`, `cas!`, `fetch_with_lock`, and `get_with_metadata` with `vivify_ttl` or `recache_ttl` now raise `Dalli::NetworkError` when the connection fails after the request was sent, where they used to retry it. Reads, `touch`, `set`, `replace` and `delete` without a CAS, and the multi-key operations are still retried, as is any request that failed before it was sent
|
|
52
|
+
- `fetch` (which stores a missing value with `add`) and `Rack::Session::Dalli` (which creates sessions with `add`) can see this error too. `Rack::Session::Dalli` handles it like any other memcached error
|
|
53
|
+
- Before a request on a connection unused for over a second, Dalli checks whether the other end closed it (memcached's `idle_timeout`, a load balancer or a firewall), and if so reconnects before sending. Without this, the first `incr` or `add` after an idle spell raised this error, and any request counted a failure toward `socket_max_failures`
|
|
54
|
+
|
|
7
55
|
5.2.6
|
|
8
56
|
==========
|
|
9
57
|
|
data/README.md
CHANGED
|
@@ -96,6 +96,28 @@ end
|
|
|
96
96
|
|
|
97
97
|
`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.
|
|
98
98
|
|
|
99
|
+
## Network Errors and Retries
|
|
100
|
+
|
|
101
|
+
When a request fails with a network error (a refused connection, a reset, or a read that times out), Dalli reconnects and retries it. A server that fails `socket_max_failures` times in a row is marked down, and requests to it raise `Dalli::NetworkError` (or, with `failover`, go to another server).
|
|
102
|
+
|
|
103
|
+
On CRuby, a read that waits longer than `socket_timeout` raises a network error, which is retried and counted like any other. JRuby and TruffleRuby don't time out a blocked socket read, so on those engines a server that accepts a request and never replies blocks the caller indefinitely. Pass `enforce_read_timeout: true` to apply `socket_timeout` there too:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
Dalli::Client.new('localhost:11211', socket_timeout: 0.5, enforce_read_timeout: true)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
It limits how long Dalli waits for a reply to start, which covers a server that stops answering. A server that stops partway through a reply can still block. It's off by default because the check costs throughput, noticeably on JRuby. It has no effect on CRuby, which doesn't need it, or on TLS connections.
|
|
110
|
+
|
|
111
|
+
`get_multi` and `get_multi_cas` wait up to `socket_timeout` for any server to send more of its replies, on every engine. The wait starts again each time data arrives, so a large batch can take longer than `socket_timeout` in total. A server that sends nothing for `socket_timeout` counts as a failed read, as it would for `get`.
|
|
112
|
+
|
|
113
|
+
A request that failed after some of it was sent may have been applied by memcached even though its reply never arrived. Dalli only sends such a request again when applying it twice is harmless: reads, `touch`, `set`, `replace` and `delete` without a CAS, and the multi-key operations. For `incr`, `decr`, `append`, `prepend`, `add`, writes with a CAS (`set_cas`, `replace_cas`, `delete_cas`, `cas`, `cas!`), and `get_with_metadata` with `vivify_ttl` or `recache_ttl` (which `fetch_with_lock` uses), it raises `Dalli::NetworkError` instead, since the write may or may not have happened. A retried `delete` can return false if the first attempt deleted the key.
|
|
114
|
+
|
|
115
|
+
A connection that sat unused for over a second is checked before the next request. If memcached (with `idle_timeout`), a load balancer or a firewall closed it meanwhile, Dalli reconnects before sending, so the request isn't lost and doesn't count as a failure. A connection dropped without the other end closing it can't be detected this way, and a request on it fails as above.
|
|
116
|
+
|
|
117
|
+
The multi-key operations (`get_multi`, `get_multi_cas`, `get_multi_with_metadata`, `set_multi` and `delete_multi`) skip a server that is already down when they start: its keys are missing from the result, or go to another server with `failover`. A server that fails during the operation is retried. If the retry can't connect to it, then without `failover` the operation raises `Dalli::NetworkError`, rather than returning that server's keys as misses or leaving them unwritten, and with `failover` its keys go to another server, or are left out when no server is left. If the retry connects but the server fails again, it's marked down partway through the operation, which raises `Dalli::NetworkError` with or without `failover`.
|
|
118
|
+
|
|
119
|
+
A retried `get_multi` or `get_multi_cas` starts the batch again, so a block passed to either can be yielded a key it was already yielded before the error. Make the block safe to call twice for a key, or call the method without a block and use the Hash it returns. `get_multi_with_metadata` yields only after every server has replied, so its block sees each key once.
|
|
120
|
+
|
|
99
121
|
## Security Note
|
|
100
122
|
|
|
101
123
|
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:
|
|
@@ -152,6 +174,14 @@ A failed TLS handshake counts as a failed connection attempt, like a refused con
|
|
|
152
174
|
|
|
153
175
|
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
176
|
|
|
177
|
+
Set `expire_after` to give sessions an expiry. It sets both the cookie's expiry and the TTL of the session in memcached. Without it, sessions are stored with no expiry: a session stays in memcached until it's deleted (for example by a logout) or evicted, and a stolen session cookie keeps working until then.
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
use Rack::Session::Dalli, memcache_server: 'mc.example.com:11211', expire_after: 2 * 60 * 60 # two hours
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
A stored session that can't be decoded, for example after changing the serializer or removing a class that sessions stored, is treated as missing: the request gets a new, empty session and a new cookie, and Dalli logs a warning.
|
|
184
|
+
|
|
155
185
|
## OpenTelemetry Tracing
|
|
156
186
|
|
|
157
187
|
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
|
@@ -77,8 +77,8 @@ module Dalli
|
|
|
77
77
|
#
|
|
78
78
|
def initialize(servers = nil, options = {})
|
|
79
79
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
80
|
+
warn_removed_options(options)
|
|
80
81
|
@options = normalize_options(options)
|
|
81
|
-
warn_removed_options(@options)
|
|
82
82
|
@key_manager = ::Dalli::KeyManager.new(@options)
|
|
83
83
|
@ring = nil
|
|
84
84
|
end
|
|
@@ -134,6 +134,10 @@ module Dalli
|
|
|
134
134
|
# - :return_last_access [Boolean] return seconds since last access
|
|
135
135
|
# - :return_ttl_remaining [Boolean] return seconds of TTL remaining (-1 if no TTL)
|
|
136
136
|
# - :skip_lru_bump [Boolean] don't bump LRU or update access stats
|
|
137
|
+
# - :vivify_ttl [Integer] on a miss, create a stub that lives this long
|
|
138
|
+
# (sanitized like any other TTL) and report won_recache
|
|
139
|
+
# - :recache_ttl [Integer] report won_recache when the item's remaining
|
|
140
|
+
# TTL is below this
|
|
137
141
|
#
|
|
138
142
|
# @return [Hash] containing:
|
|
139
143
|
# - :value - the cached value (or nil on miss)
|
|
@@ -160,6 +164,8 @@ module Dalli
|
|
|
160
164
|
#
|
|
161
165
|
def get_with_metadata(key, options = {})
|
|
162
166
|
validate_routing_tokens!(options)
|
|
167
|
+
validate_ttl!(:vivify_ttl, options[:vivify_ttl])
|
|
168
|
+
validate_integer!(:recache_ttl, options[:recache_ttl])
|
|
163
169
|
# A retry re-runs this method with its original arguments, so `key` must
|
|
164
170
|
# stay as the caller gave it; reassigning it would add the namespace again.
|
|
165
171
|
validated_key = @key_manager.validate_key(key.to_s)
|
|
@@ -200,11 +206,17 @@ module Dalli
|
|
|
200
206
|
#
|
|
201
207
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
202
208
|
#
|
|
203
|
-
# A transient network error is retried automatically. If a server
|
|
204
|
-
# unreachable after retrying, raises
|
|
205
|
-
#
|
|
209
|
+
# A transient network error is retried automatically. If a server stops
|
|
210
|
+
# during the call and is still unreachable after retrying, raises
|
|
211
|
+
# Dalli::NetworkError only with `failover: false`; with failover (the
|
|
212
|
+
# default) its keys go to another server, or are left out when no server
|
|
213
|
+
# is left. See "Network Errors and Retries" in the README.
|
|
206
214
|
#
|
|
207
|
-
#
|
|
215
|
+
# A retry starts the batch again, so a block can be yielded a key it was
|
|
216
|
+
# already yielded before the error.
|
|
217
|
+
#
|
|
218
|
+
# @raise [Dalli::NetworkError] if a server stops during the call and is
|
|
219
|
+
# still unreachable after retrying, without failover
|
|
208
220
|
# rubocop:disable-next Style/ExplicitBlockArgument
|
|
209
221
|
def get_multi(*keys, req_options: nil)
|
|
210
222
|
keys.flatten!
|
|
@@ -242,14 +254,18 @@ module Dalli
|
|
|
242
254
|
#
|
|
243
255
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
244
256
|
#
|
|
245
|
-
# A transient network error is retried automatically. If a server
|
|
246
|
-
# unreachable after retrying, raises
|
|
247
|
-
#
|
|
257
|
+
# A transient network error is retried automatically. If a server stops
|
|
258
|
+
# during the call and is still unreachable after retrying, raises
|
|
259
|
+
# Dalli::NetworkError only with `failover: false`; with failover (the
|
|
260
|
+
# default) its keys go to another server, or are left out when no server
|
|
261
|
+
# is left. See "Network Errors and Retries" in the README. The block is
|
|
262
|
+
# called only once every server has replied, so never twice for a key.
|
|
248
263
|
#
|
|
249
264
|
# @param keys [Array<String>] the keys to fetch
|
|
250
265
|
# @param req_options [Hash, nil] routing-token options
|
|
251
266
|
# @return [Hash] key => { value:, cas:, stale:, miss: }
|
|
252
|
-
# @raise [Dalli::NetworkError] if a server
|
|
267
|
+
# @raise [Dalli::NetworkError] if a server stops during the call and is
|
|
268
|
+
# still unreachable after retrying, without failover
|
|
253
269
|
def get_multi_with_metadata(*keys, req_options: nil, &block)
|
|
254
270
|
keys.flatten!
|
|
255
271
|
keys.compact!
|
|
@@ -306,6 +322,9 @@ module Dalli
|
|
|
306
322
|
# { 'key' => [value, cas_id] }
|
|
307
323
|
#
|
|
308
324
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
325
|
+
#
|
|
326
|
+
# Network errors are handled as in #get_multi, and as there a retry can
|
|
327
|
+
# yield a key a second time.
|
|
309
328
|
def get_multi_cas(*keys, req_options: nil)
|
|
310
329
|
validate_routing_tokens!(req_options)
|
|
311
330
|
|
|
@@ -328,11 +347,12 @@ module Dalli
|
|
|
328
347
|
# written to the cache and returned.
|
|
329
348
|
def fetch(key, ttl = nil, req_options = nil)
|
|
330
349
|
req_options = req_options.nil? ? CACHE_NILS : req_options.merge(CACHE_NILS) if cache_nils
|
|
350
|
+
ttl = ttl_or_default(ttl)
|
|
331
351
|
val = get(key, req_options)
|
|
332
352
|
return val unless block_given? && not_found?(val)
|
|
333
353
|
|
|
334
354
|
new_val = yield
|
|
335
|
-
add(key, new_val,
|
|
355
|
+
add(key, new_val, ttl, req_options)
|
|
336
356
|
new_val
|
|
337
357
|
end
|
|
338
358
|
|
|
@@ -369,7 +389,10 @@ module Dalli
|
|
|
369
389
|
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
|
|
370
390
|
raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
|
|
371
391
|
|
|
372
|
-
|
|
392
|
+
# ttl is only used once the block has run; checked now so a bad one
|
|
393
|
+
# raises before the lock is taken
|
|
394
|
+
ttl_or_default(ttl)
|
|
395
|
+
validate_ttl!(:lock_ttl, lock_ttl)
|
|
373
396
|
validate_integer!(:recache_threshold, recache_threshold)
|
|
374
397
|
validate_routing_tokens!(req_options)
|
|
375
398
|
# Left unchanged for the retry below; see get_with_metadata
|
|
@@ -463,8 +486,10 @@ module Dalli
|
|
|
463
486
|
nil
|
|
464
487
|
end
|
|
465
488
|
|
|
489
|
+
# set_cas without its CAS check: the CAS here is always 0
|
|
466
490
|
def set(key, value, ttl = nil, req_options = nil)
|
|
467
|
-
|
|
491
|
+
validate_routing_tokens!(req_options)
|
|
492
|
+
perform(:set, key, value, ttl_or_default(ttl), 0, req_options)
|
|
468
493
|
end
|
|
469
494
|
|
|
470
495
|
##
|
|
@@ -472,16 +497,20 @@ module Dalli
|
|
|
472
497
|
# This method is more efficient than calling set() in a loop because
|
|
473
498
|
# it batches requests by server and uses quiet mode.
|
|
474
499
|
#
|
|
475
|
-
# A transient network error is retried automatically. If a server
|
|
476
|
-
# unreachable after retrying, raises
|
|
477
|
-
#
|
|
500
|
+
# A transient network error is retried automatically. If a server stops
|
|
501
|
+
# during the call and is still unreachable after retrying, raises
|
|
502
|
+
# Dalli::NetworkError only with `failover: false`; with failover (the
|
|
503
|
+
# default) its keys go to another server, or are left out when no server
|
|
504
|
+
# is left. See "Network Errors and Retries" in the README. Keys already
|
|
505
|
+
# written to other servers before an error are not rolled back.
|
|
478
506
|
#
|
|
479
507
|
# @param hash [Hash] key-value pairs to set
|
|
480
508
|
# @param ttl [Integer] time-to-live in seconds (optional, uses default if not provided)
|
|
481
509
|
# @param req_options [Hash] options passed to each set operation; accepts
|
|
482
510
|
# :p_token/:l_token, applied to every key in the batch
|
|
483
511
|
# @return [void]
|
|
484
|
-
# @raise [Dalli::NetworkError] if a server
|
|
512
|
+
# @raise [Dalli::NetworkError] if a server stops during the call and is
|
|
513
|
+
# still unreachable after retrying, without failover
|
|
485
514
|
#
|
|
486
515
|
# Example:
|
|
487
516
|
# client.set_multi({ 'key1' => 'value1', 'key2' => 'value2' }, 300)
|
|
@@ -502,7 +531,11 @@ module Dalli
|
|
|
502
531
|
##
|
|
503
532
|
# Set the key-value pair, verifying existing CAS.
|
|
504
533
|
# Returns the resulting CAS value if succeeded, and falsy otherwise.
|
|
534
|
+
# The CAS must be an Integer (or a String of decimal digits) between 0 and
|
|
535
|
+
# 2**64 - 1, or nil; anything else raises ArgumentError. A CAS of 0 or nil
|
|
536
|
+
# means an unconditional set.
|
|
505
537
|
def set_cas(key, value, cas, ttl = nil, req_options = nil)
|
|
538
|
+
validate_uint64!(:cas, cas)
|
|
506
539
|
validate_routing_tokens!(req_options)
|
|
507
540
|
perform(:set, key, value, ttl_or_default(ttl), cas, req_options)
|
|
508
541
|
end
|
|
@@ -525,18 +558,22 @@ module Dalli
|
|
|
525
558
|
##
|
|
526
559
|
# Conditionally add a key/value pair, verifying existing CAS, only if the
|
|
527
560
|
# key already exists on the server. Returns the new CAS value if the
|
|
528
|
-
# operation succeeded, or falsy otherwise.
|
|
561
|
+
# operation succeeded, or falsy otherwise. The CAS is validated as in
|
|
562
|
+
# #set_cas.
|
|
529
563
|
def replace_cas(key, value, cas, ttl = nil, req_options = nil)
|
|
564
|
+
validate_uint64!(:cas, cas)
|
|
530
565
|
validate_routing_tokens!(req_options)
|
|
531
566
|
perform(:replace, key, value, ttl_or_default(ttl), cas, req_options)
|
|
532
567
|
end
|
|
533
568
|
|
|
534
569
|
# Delete a key/value pair, verifying existing CAS.
|
|
535
570
|
# Returns true if succeeded, and falsy otherwise.
|
|
536
|
-
# Delete a key, optionally with a CAS check.
|
|
571
|
+
# Delete a key, optionally with a CAS check. The CAS is validated as in
|
|
572
|
+
# #set_cas.
|
|
537
573
|
#
|
|
538
574
|
# `req_options` accepts the same meta-delete options as #delete.
|
|
539
575
|
def delete_cas(key, cas = 0, req_options = nil)
|
|
576
|
+
validate_uint64!(:cas, cas)
|
|
540
577
|
validate_delete_options!(req_options)
|
|
541
578
|
validate_routing_tokens!(req_options)
|
|
542
579
|
perform(:delete, key, cas, req_options)
|
|
@@ -588,9 +625,10 @@ module Dalli
|
|
|
588
625
|
# is whichever one the caller asked for. This is best-effort: a transient
|
|
589
626
|
# network error is retried automatically, and keys handled before the
|
|
590
627
|
# error are not recounted, so the result may under-report when a retry
|
|
591
|
-
# occurs.
|
|
592
|
-
#
|
|
593
|
-
# @raise [Dalli::NetworkError] if a server
|
|
628
|
+
# occurs. A server that stops during the call is handled as in
|
|
629
|
+
# #set_multi.
|
|
630
|
+
# @raise [Dalli::NetworkError] if a server stops during the call and is
|
|
631
|
+
# still unreachable after retrying, without failover
|
|
594
632
|
#
|
|
595
633
|
# Example:
|
|
596
634
|
# client.delete_multi(['key1', 'key2', 'key3'])
|
|
@@ -628,9 +666,8 @@ module Dalli
|
|
|
628
666
|
|
|
629
667
|
##
|
|
630
668
|
# Incr adds the given amount to the counter on the memcached server.
|
|
631
|
-
# Amt must be
|
|
632
|
-
#
|
|
633
|
-
# anything else raises ArgumentError.
|
|
669
|
+
# Amt, and default if given, must be Integers (or Strings of decimal
|
|
670
|
+
# digits) between 0 and 2**64 - 1; anything else raises ArgumentError.
|
|
634
671
|
#
|
|
635
672
|
# If default is nil, the counter must already exist or the operation
|
|
636
673
|
# will fail and will return nil. Otherwise this method will return
|
|
@@ -642,18 +679,17 @@ module Dalli
|
|
|
642
679
|
#
|
|
643
680
|
# If the value already exists, it must have been set with raw: true
|
|
644
681
|
def incr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
645
|
-
|
|
646
|
-
|
|
682
|
+
amt = validate_uint64!(:amt, amt)
|
|
683
|
+
default = validate_uint64!(:default, default)
|
|
647
684
|
validate_routing_tokens!(req_options)
|
|
648
685
|
|
|
649
|
-
perform(:incr, key, amt
|
|
686
|
+
perform(:incr, key, amt, ttl_or_default(ttl), default, req_options)
|
|
650
687
|
end
|
|
651
688
|
|
|
652
689
|
##
|
|
653
690
|
# Decr subtracts the given amount from the counter on the memcached server.
|
|
654
|
-
# Amt must be
|
|
655
|
-
#
|
|
656
|
-
# anything else raises ArgumentError.
|
|
691
|
+
# Amt, and default if given, must be Integers (or Strings of decimal
|
|
692
|
+
# digits) between 0 and 2**64 - 1; anything else raises ArgumentError.
|
|
657
693
|
#
|
|
658
694
|
# memcached counters are unsigned and cannot hold negative values. Calling
|
|
659
695
|
# decr on a counter which is 0 will just return 0.
|
|
@@ -668,20 +704,23 @@ module Dalli
|
|
|
668
704
|
#
|
|
669
705
|
# If the value already exists, it must have been set with raw: true
|
|
670
706
|
def decr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
671
|
-
|
|
672
|
-
|
|
707
|
+
amt = validate_uint64!(:amt, amt)
|
|
708
|
+
default = validate_uint64!(:default, default)
|
|
673
709
|
validate_routing_tokens!(req_options)
|
|
674
710
|
|
|
675
|
-
perform(:decr, key, amt
|
|
711
|
+
perform(:decr, key, amt, ttl_or_default(ttl), default, req_options)
|
|
676
712
|
end
|
|
677
713
|
|
|
678
714
|
##
|
|
679
715
|
# Flush the memcached server, at 'delay' seconds in the future.
|
|
680
716
|
# Delay defaults to zero seconds, which means an immediate flush.
|
|
717
|
+
# Delay must be a non-negative Integer (or a String of decimal digits);
|
|
718
|
+
# anything else raises ArgumentError.
|
|
681
719
|
# Every server is attempted. If any of them fails, the first error is
|
|
682
720
|
# raised once all the others have been flushed.
|
|
683
721
|
##
|
|
684
722
|
def flush(delay = 0)
|
|
723
|
+
validate_uint64!(:delay, delay)
|
|
685
724
|
first_error = nil
|
|
686
725
|
results = ring.servers.map do |s|
|
|
687
726
|
s.request(:flush, delay)
|
|
@@ -774,15 +813,7 @@ module Dalli
|
|
|
774
813
|
|
|
775
814
|
raise ArgumentError, 'tombstone_ttl requires invalidate: true' unless req_options[:invalidate]
|
|
776
815
|
|
|
777
|
-
|
|
778
|
-
# path; validated here first so a bad value raises cleanly instead of
|
|
779
|
-
# unwinding through Protocol::Base#request, which would close the
|
|
780
|
-
# connection on the ArgumentError Integer() raises.
|
|
781
|
-
begin
|
|
782
|
-
Integer(tombstone_ttl)
|
|
783
|
-
rescue ArgumentError, TypeError
|
|
784
|
-
raise ArgumentError, "tombstone_ttl must be an integer, got #{tombstone_ttl.inspect}"
|
|
785
|
-
end
|
|
816
|
+
validate_ttl!(:tombstone_ttl, tombstone_ttl)
|
|
786
817
|
end
|
|
787
818
|
|
|
788
819
|
def record_hit_miss_metrics(span, key_count, hit_count)
|
|
@@ -818,9 +849,20 @@ module Dalli
|
|
|
818
849
|
end
|
|
819
850
|
end
|
|
820
851
|
|
|
821
|
-
|
|
852
|
+
# retried is true once a request to the server has failed with a
|
|
853
|
+
# RetryableNetworkError. Without failover, finding the server down then
|
|
854
|
+
# means that request failed, which raises, rather than the server being
|
|
855
|
+
# down before the operation began, which returns nil. With failover it
|
|
856
|
+
# returns nil either way, as the multi-server path leaves keys out when
|
|
857
|
+
# no server is left.
|
|
858
|
+
def single_server(retried: false)
|
|
822
859
|
server = ring.servers.first
|
|
823
|
-
server if server&.alive?
|
|
860
|
+
return server if server&.alive?
|
|
861
|
+
if retried && !ring.failover?
|
|
862
|
+
raise Dalli::NetworkError, "#{server&.name} failed during this request and is still unreachable"
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
nil
|
|
824
866
|
end
|
|
825
867
|
|
|
826
868
|
# The three single_server_* fast-path methods below share one contract,
|
|
@@ -829,20 +871,22 @@ module Dalli
|
|
|
829
871
|
# socket_max_failures, same as the pipelined path's retry), and a hard
|
|
830
872
|
# NetworkError -- the server genuinely unreachable, not just blipping --
|
|
831
873
|
# propagates to the caller rather than being swallowed into a silently
|
|
832
|
-
# wrong result
|
|
833
|
-
#
|
|
834
|
-
#
|
|
874
|
+
# wrong result, including (without failover) the server going down before
|
|
875
|
+
# the retry, which single_server raises for. Otherwise finding no live
|
|
876
|
+
# server is still silent, matching Ring#keys_grouped_by_server dropping a
|
|
877
|
+
# key it can't route on both the single- and multi-server paths. Each
|
|
878
|
+
# method sets retried before it retries; a local keeps its value across
|
|
879
|
+
# a retry.
|
|
835
880
|
def single_server_get_multi(keys, req_options = nil)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
return {} unless (server = single_server)
|
|
881
|
+
retried ||= false
|
|
882
|
+
caller_keys = @key_manager.caller_keys_by_wire_key(keys)
|
|
883
|
+
return {} unless (server = single_server(retried: retried))
|
|
839
884
|
|
|
840
|
-
|
|
841
|
-
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
842
|
-
result
|
|
885
|
+
results_for_caller_keys(server.request(:read_multi_req, caller_keys.keys, req_options), caller_keys)
|
|
843
886
|
rescue Dalli::RetryableNetworkError => e
|
|
844
887
|
Dalli.logger.debug { e.inspect }
|
|
845
888
|
Dalli.logger.debug { 'retrying single-server get_multi because of network error' }
|
|
889
|
+
retried = true
|
|
846
890
|
retry
|
|
847
891
|
rescue Dalli::UnexpectedReplyKeyError => e
|
|
848
892
|
# The connection is closed and the keys are treated as missing, as the
|
|
@@ -852,40 +896,54 @@ module Dalli
|
|
|
852
896
|
end
|
|
853
897
|
|
|
854
898
|
def single_server_get_multi_with_metadata(keys, req_options = nil)
|
|
855
|
-
|
|
856
|
-
|
|
899
|
+
retried ||= false
|
|
900
|
+
caller_keys = @key_manager.caller_keys_by_wire_key(keys)
|
|
901
|
+
return {} unless (server = single_server(retried: retried))
|
|
857
902
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
result
|
|
903
|
+
results_for_caller_keys(server.request(:read_multi_with_metadata_req, caller_keys.keys, req_options),
|
|
904
|
+
caller_keys)
|
|
861
905
|
rescue Dalli::RetryableNetworkError => e
|
|
862
906
|
Dalli.logger.debug { e.inspect }
|
|
863
907
|
Dalli.logger.debug { 'retrying single-server get_multi_with_metadata because of network error' }
|
|
908
|
+
retried = true
|
|
864
909
|
retry
|
|
865
910
|
rescue Dalli::UnexpectedReplyKeyError => e
|
|
866
911
|
Dalli.logger.warn { e.message }
|
|
867
912
|
{}
|
|
868
913
|
end
|
|
869
914
|
|
|
915
|
+
# Replies name the key as sent; returns them under the caller's keys,
|
|
916
|
+
# skipping any key that wasn't requested
|
|
917
|
+
def results_for_caller_keys(results, caller_keys)
|
|
918
|
+
results.each_with_object({}) do |(key, value), hash|
|
|
919
|
+
caller_key = @key_manager.caller_key(caller_keys, key)
|
|
920
|
+
hash[caller_key] = value if caller_key
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
|
|
870
924
|
def single_server_set_multi(hash, ttl, req_options)
|
|
925
|
+
retried ||= false
|
|
871
926
|
pairs = hash.transform_keys { |k| @key_manager.validate_key(k.to_s) }
|
|
872
|
-
return unless (server = single_server)
|
|
927
|
+
return unless (server = single_server(retried: retried))
|
|
873
928
|
|
|
874
929
|
server.request(:write_multi_req, pairs, ttl, req_options)
|
|
875
930
|
rescue Dalli::RetryableNetworkError => e
|
|
876
931
|
Dalli.logger.debug { e.inspect }
|
|
877
932
|
Dalli.logger.debug { 'retrying single-server set_multi because of network error' }
|
|
933
|
+
retried = true
|
|
878
934
|
retry
|
|
879
935
|
end
|
|
880
936
|
|
|
881
937
|
def single_server_delete_multi(keys, req_options = nil)
|
|
938
|
+
retried ||= false
|
|
882
939
|
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
883
|
-
return 0 unless (server = single_server)
|
|
940
|
+
return 0 unless (server = single_server(retried: retried))
|
|
884
941
|
|
|
885
942
|
server.request(:delete_multi_req, validated_keys, req_options)
|
|
886
943
|
rescue Dalli::RetryableNetworkError => e
|
|
887
944
|
Dalli.logger.debug { e.inspect }
|
|
888
945
|
Dalli.logger.debug { 'retrying single-server delete_multi because of network error' }
|
|
946
|
+
retried = true
|
|
889
947
|
retry
|
|
890
948
|
end
|
|
891
949
|
|
|
@@ -917,10 +975,6 @@ module Dalli
|
|
|
917
975
|
attrs
|
|
918
976
|
end
|
|
919
977
|
|
|
920
|
-
def check_positive!(amt)
|
|
921
|
-
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
|
922
|
-
end
|
|
923
|
-
|
|
924
978
|
# Numeric arguments that become meta protocol flags (GHSA-6wmv-xq9m-fmp7).
|
|
925
979
|
# RequestFormatter converts them to Integer as the wire-level backstop;
|
|
926
980
|
# checking here too gives the caller a clean ArgumentError for the same
|
|
@@ -933,6 +987,27 @@ module Dalli
|
|
|
933
987
|
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
934
988
|
end
|
|
935
989
|
|
|
990
|
+
MAX_UINT64 = (2**64) - 1
|
|
991
|
+
private_constant :MAX_UINT64
|
|
992
|
+
|
|
993
|
+
# CAS values, incr/decr amounts and initial values, and the flush delay
|
|
994
|
+
# are read by memcached as unsigned 64-bit integers. RequestFormatter
|
|
995
|
+
# raises on anything else as the wire-level backstop; checking here first
|
|
996
|
+
# keeps the connection open, for the reason given for routing tokens
|
|
997
|
+
# below. Accepts an Integer or a String of decimal digits, and returns it
|
|
998
|
+
# as an Integer.
|
|
999
|
+
def validate_uint64!(name, value)
|
|
1000
|
+
return if value.nil?
|
|
1001
|
+
|
|
1002
|
+
int = case value
|
|
1003
|
+
when Integer then value
|
|
1004
|
+
when String then Integer(value, 10, exception: false)
|
|
1005
|
+
end
|
|
1006
|
+
return int if int && int >= 0 && int <= MAX_UINT64
|
|
1007
|
+
|
|
1008
|
+
raise ArgumentError, "#{name} must be an Integer between 0 and #{MAX_UINT64}, got #{value.inspect}"
|
|
1009
|
+
end
|
|
1010
|
+
|
|
936
1011
|
# Validated here, before the request reaches Protocol::Base#request, rather
|
|
937
1012
|
# than only at the RequestFormatter level. Reaching only the formatter's
|
|
938
1013
|
# check means unwinding through Protocol::Base#request, which logs the
|
|
@@ -961,15 +1036,16 @@ module Dalli
|
|
|
961
1036
|
|
|
962
1037
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
963
1038
|
validate_routing_tokens!(req_options)
|
|
1039
|
+
ttl = ttl_or_default(ttl)
|
|
964
1040
|
(value, cas) = perform(:cas, key, req_options)
|
|
965
1041
|
return if !always_set && cas_miss?(value, cas)
|
|
966
1042
|
|
|
967
1043
|
newvalue = yield(value)
|
|
968
1044
|
# A CAS of 0 means the key was missing. A set with CAS 0 is unconditional,
|
|
969
1045
|
# so add instead: it fails if another client created the key meanwhile.
|
|
970
|
-
return perform(:add, key, newvalue,
|
|
1046
|
+
return perform(:add, key, newvalue, ttl, req_options) if cas.zero?
|
|
971
1047
|
|
|
972
|
-
perform(:set, key, newvalue,
|
|
1048
|
+
perform(:set, key, newvalue, ttl, cas, req_options)
|
|
973
1049
|
end
|
|
974
1050
|
|
|
975
1051
|
# The CAS lookup returns [nil, 0] on a miss rather than NOT_FOUND, and a
|
|
@@ -998,12 +1074,47 @@ module Dalli
|
|
|
998
1074
|
|
|
999
1075
|
##
|
|
1000
1076
|
# Uses the argument TTL or the client-wide default. Ensures
|
|
1001
|
-
# that the value is an integer
|
|
1077
|
+
# that the value is an integer memcached accepts as an expiration
|
|
1002
1078
|
##
|
|
1003
1079
|
def ttl_or_default(ttl)
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1080
|
+
ttl ||= @options[:expires_in] || 0
|
|
1081
|
+
# validate_ttl!'s fast path, without the call
|
|
1082
|
+
return ttl if ttl.is_a?(Integer) && ttl >= Protocol::TtlSanitizer::MIN_EXPIRATION_TIME &&
|
|
1083
|
+
ttl <= Protocol::TtlSanitizer::MAX_ACCEPTABLE_EXPIRATION_INTERVAL
|
|
1084
|
+
|
|
1085
|
+
validate_ttl!(:ttl, ttl)
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1088
|
+
# Returns the TTL as an Integer, sanitized (see TtlSanitizer), raising
|
|
1089
|
+
# ArgumentError unless memcached can use it. An interval over 30 days comes
|
|
1090
|
+
# back as the Unix time it becomes, so the request path doesn't convert it
|
|
1091
|
+
# again. A String must be a whole number in decimal, as for the other
|
|
1092
|
+
# numeric arguments; anything else is converted with to_i, so a Float or
|
|
1093
|
+
# an ActiveSupport::Duration still works. Once sanitized it must fit memcached's signed 32-bit expiration time. A
|
|
1094
|
+
# negative TTL is allowed: memcached stores the item as already expired.
|
|
1095
|
+
# memcached rejects a TTL outside that with CLIENT_ERROR, which a quiet
|
|
1096
|
+
# block or set_multi silently discards, so it is checked here, before the
|
|
1097
|
+
# request is written.
|
|
1098
|
+
def validate_ttl!(name, value)
|
|
1099
|
+
return if value.nil?
|
|
1100
|
+
# The usual case: an Integer TTL that TtlSanitizer leaves unchanged
|
|
1101
|
+
return value if value.is_a?(Integer) && value >= Protocol::TtlSanitizer::MIN_EXPIRATION_TIME &&
|
|
1102
|
+
value <= Protocol::TtlSanitizer::MAX_ACCEPTABLE_EXPIRATION_INTERVAL
|
|
1103
|
+
|
|
1104
|
+
ttl = ttl_to_i(name, value)
|
|
1105
|
+
min = Protocol::TtlSanitizer::MIN_EXPIRATION_TIME
|
|
1106
|
+
max = Protocol::TtlSanitizer::MAX_EXPIRATION_TIME
|
|
1107
|
+
ttl = Protocol::TtlSanitizer.sanitize(ttl)
|
|
1108
|
+
return ttl if ttl.between?(min, max)
|
|
1109
|
+
|
|
1110
|
+
raise ArgumentError, "#{name} must be a number of seconds no smaller than #{min} or a Unix time " \
|
|
1111
|
+
"no later than #{max}, got #{value.inspect}"
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
def ttl_to_i(name, value)
|
|
1115
|
+
value.is_a?(String) ? Integer(value, 10) : value.to_i
|
|
1116
|
+
rescue ArgumentError, NoMethodError, FloatDomainError
|
|
1117
|
+
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
1007
1118
|
end
|
|
1008
1119
|
|
|
1009
1120
|
def ring
|
|
@@ -1018,7 +1129,10 @@ module Dalli
|
|
|
1018
1129
|
#
|
|
1019
1130
|
# This method also forces retries on network errors - when
|
|
1020
1131
|
# a particular memcached instance becomes unreachable, or the
|
|
1021
|
-
# operation times out.
|
|
1132
|
+
# operation times out. Once some of the request has been sent, only
|
|
1133
|
+
# one that is safe to apply twice is retried; for the others
|
|
1134
|
+
# (incr, decr, append, prepend, add, replace and CAS writes)
|
|
1135
|
+
# Protocol::Base#request raises a NetworkError, which isn't retried.
|
|
1022
1136
|
##
|
|
1023
1137
|
# rubocop:disable Naming/MethodParameterName
|
|
1024
1138
|
def perform(op, key, ...)
|
|
@@ -1047,13 +1161,12 @@ module Dalli
|
|
|
1047
1161
|
end
|
|
1048
1162
|
|
|
1049
1163
|
# Works on a copy so the caller's hash is neither modified here nor able
|
|
1050
|
-
# to change this client's configuration later.
|
|
1164
|
+
# to change this client's configuration later. Credentials are dropped:
|
|
1165
|
+
# Dalli 5 doesn't use them, and kept they could show up in #inspect.
|
|
1051
1166
|
def normalize_options(options)
|
|
1052
|
-
opts = options.
|
|
1053
|
-
opts[:expires_in] = opts[:expires_in]
|
|
1167
|
+
opts = options.except(:username, :password)
|
|
1168
|
+
opts[:expires_in] = ttl_to_i(:expires_in, opts[:expires_in]) if opts[:expires_in]
|
|
1054
1169
|
opts
|
|
1055
|
-
rescue NoMethodError
|
|
1056
|
-
raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
|
|
1057
1170
|
end
|
|
1058
1171
|
|
|
1059
1172
|
REMOVED_OPTIONS = {
|