dalli 5.2.2 → 5.2.5
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 +75 -0
- data/README.md +40 -0
- data/lib/dalli/client.rb +79 -16
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pipelined_getter.rb +41 -25
- data/lib/dalli/pipelined_setter.rb +17 -8
- data/lib/dalli/protocol/base.rb +110 -17
- data/lib/dalli/protocol/connection_manager.rb +20 -3
- data/lib/dalli/protocol/meta.rb +71 -13
- 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 +7 -9
- 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: 64bd21546442078425398b10e3baa0b75000a4cc8388dcac08c834909bc29731
|
|
4
|
+
data.tar.gz: 67568b0a0d030ce81c657cb98a2957f0a6fc12d5ed98f485305e4a93573474f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b069ac0229a977d837ba26755ba279c9eda6b3295bb6c68fabc9baf07ae4c7367cc6acd6d1132f0a5e2913f4b000c8881df0094562b7dd773e34877fab8c21b3
|
|
7
|
+
data.tar.gz: 84fbaba58b48b46b7be72fbef38006259ce90268d8eb2ccfa3a1ceeee57a69e9f05472db76d2382d0a8d2e2965e63c363484f351a08bd99b9f9d65e7524d294e
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,81 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.2.5
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
Bug fixes:
|
|
11
|
+
|
|
12
|
+
- `set_multi` across more than one server stored `nil` for a Symbol key or a key over 250 bytes, in place of the value given
|
|
13
|
+
|
|
14
|
+
5.2.4
|
|
15
|
+
==========
|
|
16
|
+
|
|
17
|
+
Security:
|
|
18
|
+
|
|
19
|
+
- Keep a connection shared by several threads or fibers in step during a `quiet` block (GHSA-5xvx-r8rg-5qm2)
|
|
20
|
+
- A `quiet` (or `multi`) block is quiet only for the thread or fiber that runs it. memcached still replies to a quiet request that fails (for example an `add` of a key that exists), and another thread or fiber using the same client while the block was open read that reply as its own, so its later requests could return another key's value. A non-quiet request now reads any pending quiet replies first
|
|
21
|
+
- Don't let a `get_multi` that stopped early leave replies for a later request (GHSA-5xvx-r8rg-5qm2)
|
|
22
|
+
- A pipelined `get_multi` stopped by a non-network exception (its block raising or breaking, `Timeout.timeout`, or a value that can't be deserialized) left its unread replies in the response buffer, and a later `get_multi` large enough to use the interleaved path returned them as its own. The buffer is now cleared when a connection closes or a pipelined get starts, and unfinished pipelines are aborted
|
|
23
|
+
- Affects every supported line (the `get_multi` issue, 4.3 and later); fixed in 5.2.4, 5.1.6, 5.0.12, 4.3.9 and 3.2.15. On 5.2, clients with `defer_drain: true` weren't affected by the `quiet` issue
|
|
24
|
+
|
|
25
|
+
Bug fixes:
|
|
26
|
+
|
|
27
|
+
- A failed TLS handshake is now handled as a network error even when openssl wasn't loaded before Dalli
|
|
28
|
+
- The fix in 5.2.3 (#1215) took effect only when the application had already required openssl: otherwise Dalli's list of TLS errors was empty, so a failed handshake still raised `OpenSSL::SSL::SSLError` and left the socket open
|
|
29
|
+
|
|
30
|
+
5.2.3
|
|
31
|
+
==========
|
|
32
|
+
|
|
33
|
+
Security:
|
|
34
|
+
|
|
35
|
+
- Don't rely on memcached's auto-create for keys that need base64 encoding (GHSA-r47x-6p6r-mvcp)
|
|
36
|
+
- 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
|
|
37
|
+
- `incr` and `decr` with an initial value now create such counters with an add instead. Return values are unchanged
|
|
38
|
+
- 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%
|
|
39
|
+
- 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
|
|
40
|
+
|
|
41
|
+
Bug fixes:
|
|
42
|
+
|
|
43
|
+
- `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)
|
|
44
|
+
- A per-request `raw: true` write no longer compresses the value (#1213)
|
|
45
|
+
- 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
|
|
46
|
+
- 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
|
|
47
|
+
- Fix `cas` and `cas!` edge cases, and make `flush` attempt every server (#1214)
|
|
48
|
+
- `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
|
|
49
|
+
- With `cache_nils: true`, `cas` treated a stored `nil` as a miss and didn't call the block
|
|
50
|
+
- The docs now say what `cas` and `cas!` return on success: the new CAS value, not `true`. The return values are unchanged
|
|
51
|
+
- `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
|
|
52
|
+
- Handle failed TLS handshakes like other connection failures, and warn when TLS verification is off (#1215)
|
|
53
|
+
- 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)
|
|
54
|
+
- 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
|
|
55
|
+
- Connection and configuration fixes (#1216)
|
|
56
|
+
- `memcached://` URIs with a bracketed IPv6 host (`memcached://[::1]:11211`) now connect
|
|
57
|
+
- A server weight below 1 raises `ArgumentError`. Before, a ring where every weight was 0 raised `FloatDomainError`
|
|
58
|
+
- 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
|
|
59
|
+
- On CRuby, DNS resolution is now limited by `socket_timeout` (`resolv_timeout:`) as well as the connect
|
|
60
|
+
- `Dalli::Client.new` no longer modifies the options hash it's given, and accepts a frozen one (#1217)
|
|
61
|
+
- `value_max_bytes` now counts the key and memcached's per-item overhead (#1217)
|
|
62
|
+
- 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
|
|
63
|
+
- 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
|
|
64
|
+
- A String `value_max_bytes` is converted to an Integer instead of raising on every set
|
|
65
|
+
- `Rack::Session::Dalli` fixes (#1217)
|
|
66
|
+
- It no longer requires the `connection_pool` gem unless `pool_size` or `pool_timeout` is given
|
|
67
|
+
- `pool_size` and `pool_timeout` are no longer passed to `Dalli::Client`, and the options hash isn't modified
|
|
68
|
+
- A failed session delete is logged as a warning, not only when `$VERBOSE` is set
|
|
69
|
+
- One value that can't be stored no longer discards other buffered writes (#1218)
|
|
70
|
+
- 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
|
|
71
|
+
- Both `set_multi` paths now store every other key, skip the bad one, and then raise. Single-server `set_multi` used to store nothing
|
|
72
|
+
- 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
|
|
73
|
+
- `Rack::Session::Dalli`'s 401 for a session deleted mid-request is now a valid Rack response (#1220)
|
|
74
|
+
- 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
|
|
75
|
+
- The app's response body is now closed, as the Rack spec requires. Before, it was dropped without `close`
|
|
76
|
+
- The app has already run when the 401 is returned, so its side effects have happened. This is documented in the README
|
|
77
|
+
|
|
78
|
+
Improvements:
|
|
79
|
+
|
|
80
|
+
- 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)
|
|
81
|
+
|
|
7
82
|
5.2.2
|
|
8
83
|
==========
|
|
9
84
|
|
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:
|
|
@@ -112,6 +123,35 @@ The digest is MD5 by default. `digest_class:` takes any object that responds to
|
|
|
112
123
|
|
|
113
124
|
See the [5.0-Upgrade.md](5.0-Upgrade.md) guide for upgrade information.
|
|
114
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
|
+
|
|
115
155
|
## OpenTelemetry Tracing
|
|
116
156
|
|
|
117
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)
|
|
@@ -226,9 +242,14 @@ module Dalli
|
|
|
226
242
|
#
|
|
227
243
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
228
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
|
+
#
|
|
229
249
|
# @param keys [Array<String>] the keys to fetch
|
|
230
250
|
# @param req_options [Hash, nil] routing-token options
|
|
231
251
|
# @return [Hash] key => { value:, cas:, stale:, miss: }
|
|
252
|
+
# @raise [Dalli::NetworkError] if a server is unreachable after retrying
|
|
232
253
|
def get_multi_with_metadata(*keys, req_options: nil, &block)
|
|
233
254
|
keys.flatten!
|
|
234
255
|
keys.compact!
|
|
@@ -371,24 +392,26 @@ module Dalli
|
|
|
371
392
|
# compare and swap values using optimistic locking.
|
|
372
393
|
# Fetch the existing value for key.
|
|
373
394
|
# If it exists, yield the value to the block.
|
|
374
|
-
#
|
|
375
|
-
#
|
|
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.
|
|
376
397
|
#
|
|
377
398
|
# Returns:
|
|
378
|
-
# - 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.
|
|
379
401
|
# - false if the value was changed by someone else.
|
|
380
|
-
# -
|
|
402
|
+
# - the new CAS value (a positive Integer) if the value was successfully updated.
|
|
381
403
|
def cas(key, ttl = nil, req_options = nil, &)
|
|
382
404
|
cas_core(key, false, ttl, req_options, &)
|
|
383
405
|
end
|
|
384
406
|
|
|
385
407
|
##
|
|
386
408
|
# like #cas, but will yield to the block whether or not the value
|
|
387
|
-
# already exists.
|
|
409
|
+
# already exists. If the key did not exist, the block receives nil.
|
|
388
410
|
#
|
|
389
411
|
# Returns:
|
|
390
|
-
# - false if the value was changed by someone else
|
|
391
|
-
#
|
|
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.
|
|
392
415
|
def cas!(key, ttl = nil, req_options = nil, &)
|
|
393
416
|
cas_core(key, true, ttl, req_options, &)
|
|
394
417
|
end
|
|
@@ -411,6 +434,11 @@ module Dalli
|
|
|
411
434
|
# the defer_drain option, before the next non-quiet request), and
|
|
412
435
|
# Dalli client methods invoked inside the block will not
|
|
413
436
|
# have return values
|
|
437
|
+
#
|
|
438
|
+
# The block is quiet only for the thread or fiber that runs it. Another
|
|
439
|
+
# thread or fiber sharing this client can make requests meanwhile; a
|
|
440
|
+
# non-quiet one first sends the block's buffered requests and discards
|
|
441
|
+
# their replies, so it reads only its own.
|
|
414
442
|
def quiet
|
|
415
443
|
old = Thread.current[::Dalli::QUIET]
|
|
416
444
|
Thread.current[::Dalli::QUIET] = true
|
|
@@ -650,9 +678,20 @@ module Dalli
|
|
|
650
678
|
##
|
|
651
679
|
# Flush the memcached server, at 'delay' seconds in the future.
|
|
652
680
|
# Delay defaults to zero seconds, which means an immediate flush.
|
|
681
|
+
# Every server is attempted. If any of them fails, the first error is
|
|
682
|
+
# raised once all the others have been flushed.
|
|
653
683
|
##
|
|
654
684
|
def flush(delay = 0)
|
|
655
|
-
|
|
685
|
+
first_error = nil
|
|
686
|
+
results = ring.servers.map do |s|
|
|
687
|
+
s.request(:flush, delay)
|
|
688
|
+
rescue StandardError => e
|
|
689
|
+
first_error ||= e
|
|
690
|
+
nil
|
|
691
|
+
end
|
|
692
|
+
raise first_error if first_error
|
|
693
|
+
|
|
694
|
+
results
|
|
656
695
|
end
|
|
657
696
|
alias flush_all flush
|
|
658
697
|
|
|
@@ -805,16 +844,26 @@ module Dalli
|
|
|
805
844
|
Dalli.logger.debug { e.inspect }
|
|
806
845
|
Dalli.logger.debug { 'retrying single-server get_multi because of network error' }
|
|
807
846
|
retry
|
|
847
|
+
rescue Dalli::UnexpectedReplyKeyError => e
|
|
848
|
+
# The connection is closed and the keys are treated as missing, as the
|
|
849
|
+
# pipelined get_multi does for a server whose replies are out of step
|
|
850
|
+
Dalli.logger.warn { e.message }
|
|
851
|
+
{}
|
|
808
852
|
end
|
|
809
853
|
|
|
810
854
|
def single_server_get_multi_with_metadata(keys, req_options = nil)
|
|
811
|
-
keys.map
|
|
855
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
812
856
|
return {} unless (server = single_server)
|
|
813
857
|
|
|
814
|
-
result = server.request(:read_multi_with_metadata_req,
|
|
858
|
+
result = server.request(:read_multi_with_metadata_req, validated_keys, req_options)
|
|
815
859
|
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
816
860
|
result
|
|
817
|
-
rescue Dalli::
|
|
861
|
+
rescue Dalli::RetryableNetworkError => e
|
|
862
|
+
Dalli.logger.debug { e.inspect }
|
|
863
|
+
Dalli.logger.debug { 'retrying single-server get_multi_with_metadata because of network error' }
|
|
864
|
+
retry
|
|
865
|
+
rescue Dalli::UnexpectedReplyKeyError => e
|
|
866
|
+
Dalli.logger.warn { e.message }
|
|
818
867
|
{}
|
|
819
868
|
end
|
|
820
869
|
|
|
@@ -913,12 +962,23 @@ module Dalli
|
|
|
913
962
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
914
963
|
validate_routing_tokens!(req_options)
|
|
915
964
|
(value, cas) = perform(:cas, key, req_options)
|
|
916
|
-
return if
|
|
965
|
+
return if !always_set && cas_miss?(value, cas)
|
|
917
966
|
|
|
918
967
|
newvalue = yield(value)
|
|
968
|
+
# A CAS of 0 means the key was missing. A set with CAS 0 is unconditional,
|
|
969
|
+
# so add instead: it fails if another client created the key meanwhile.
|
|
970
|
+
return perform(:add, key, newvalue, ttl_or_default(ttl), req_options) if cas.zero?
|
|
971
|
+
|
|
919
972
|
perform(:set, key, newvalue, ttl_or_default(ttl), cas, req_options)
|
|
920
973
|
end
|
|
921
974
|
|
|
975
|
+
# The CAS lookup returns [nil, 0] on a miss rather than NOT_FOUND, and a
|
|
976
|
+
# stored item always has a non-zero CAS. So under cache_nils a zero CAS
|
|
977
|
+
# marks the miss, and a stored nil is a hit, as in #not_found?.
|
|
978
|
+
def cas_miss?(value, cas)
|
|
979
|
+
cache_nils ? cas.zero? : value.nil?
|
|
980
|
+
end
|
|
981
|
+
|
|
922
982
|
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
923
983
|
server = ring.server_for_key(key)
|
|
924
984
|
# req_options is the base, not the override: fetch_with_lock's own
|
|
@@ -986,7 +1046,10 @@ module Dalli
|
|
|
986
1046
|
retry
|
|
987
1047
|
end
|
|
988
1048
|
|
|
989
|
-
|
|
1049
|
+
# Works on a copy so the caller's hash is neither modified here nor able
|
|
1050
|
+
# to change this client's configuration later.
|
|
1051
|
+
def normalize_options(options)
|
|
1052
|
+
opts = options.dup
|
|
990
1053
|
opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
|
|
991
1054
|
opts
|
|
992
1055
|
rescue NoMethodError
|
data/lib/dalli/options.rb
CHANGED
|
@@ -32,15 +32,21 @@ module Dalli
|
|
|
32
32
|
return {} if keys.empty?
|
|
33
33
|
|
|
34
34
|
@ring.lock do
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
# The servers with replies still to read, updated in place
|
|
36
|
+
servers = []
|
|
37
|
+
begin
|
|
38
|
+
# Stores partial results collected during interleaved send phase
|
|
39
|
+
@partial_results = {}
|
|
40
|
+
setup_requests(servers, keys, req_options, return_cas: return_cas)
|
|
41
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
42
|
+
|
|
43
|
+
# First yield any partial results collected during interleaved send
|
|
44
|
+
yield_partial_results(&block)
|
|
45
|
+
|
|
46
|
+
fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
|
|
47
|
+
ensure
|
|
48
|
+
abort_unfinished(servers)
|
|
49
|
+
end
|
|
44
50
|
end
|
|
45
51
|
rescue Dalli::RetryableNetworkError => e
|
|
46
52
|
Dalli.logger.debug { e.inspect }
|
|
@@ -60,6 +66,9 @@ module Dalli
|
|
|
60
66
|
#
|
|
61
67
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
62
68
|
#
|
|
69
|
+
# A transient network error is retried automatically. A terminal
|
|
70
|
+
# Dalli::NetworkError propagates instead of returning partial metadata.
|
|
71
|
+
#
|
|
63
72
|
def process_with_metadata(keys, req_options = nil)
|
|
64
73
|
return {} if keys.empty?
|
|
65
74
|
|
|
@@ -67,9 +76,9 @@ module Dalli
|
|
|
67
76
|
results = {}
|
|
68
77
|
groups_for_keys(keys).each do |server, keys_for_server|
|
|
69
78
|
results.merge!(server.request(:read_multi_with_metadata_req, keys_for_server, req_options))
|
|
70
|
-
rescue Dalli::
|
|
79
|
+
rescue Dalli::NetworkError
|
|
71
80
|
raise
|
|
72
|
-
rescue DalliError
|
|
81
|
+
rescue DalliError => e
|
|
73
82
|
Dalli.logger.debug { e.inspect }
|
|
74
83
|
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
75
84
|
end
|
|
@@ -93,9 +102,9 @@ module Dalli
|
|
|
93
102
|
|
|
94
103
|
# Sends each server's queries and its terminating noop before building
|
|
95
104
|
# the next server's, so memcached is already answering the first servers
|
|
96
|
-
# while the rest are prepared.
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
# while the rest are prepared. Adds the servers with a pending response
|
|
106
|
+
# to started.
|
|
107
|
+
def setup_requests(started, keys, req_options = nil, return_cas: true)
|
|
99
108
|
groups_for_keys(keys).each do |server, keys_for_server|
|
|
100
109
|
make_getkq_request(server, keys_for_server, req_options, return_cas: return_cas)
|
|
101
110
|
next unless server.connected?
|
|
@@ -108,7 +117,6 @@ module Dalli
|
|
|
108
117
|
rescue Dalli::DalliError
|
|
109
118
|
started.delete(server)
|
|
110
119
|
end
|
|
111
|
-
started
|
|
112
120
|
end
|
|
113
121
|
|
|
114
122
|
##
|
|
@@ -152,29 +160,39 @@ module Dalli
|
|
|
152
160
|
raise
|
|
153
161
|
end
|
|
154
162
|
|
|
155
|
-
# Swallows Dalli::NetworkError
|
|
163
|
+
# Swallows Dalli::NetworkError. Empties servers, so they aren't aborted
|
|
164
|
+
# again.
|
|
156
165
|
def abort_without_timeout(servers)
|
|
157
166
|
servers.each(&:pipeline_abort)
|
|
167
|
+
servers.clear
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# When #process stops before reading every server's replies (the caller's
|
|
171
|
+
# block raises or breaks, a Timeout.timeout fires, a value can't be
|
|
172
|
+
# deserialized), the rest are still on the socket and some may be in the
|
|
173
|
+
# response buffer. Closing those connections keeps them from being read
|
|
174
|
+
# as a later request's replies. Aborts that #process already handled
|
|
175
|
+
# emptied servers.
|
|
176
|
+
def abort_unfinished(servers)
|
|
177
|
+
servers.each { |server| server.pipeline_abort(timed_out: false) }
|
|
178
|
+
servers.clear
|
|
158
179
|
end
|
|
159
180
|
|
|
181
|
+
# Reads the replies available, removing finished servers from servers
|
|
160
182
|
def fetch_responses(servers, start_time, timeout, &block)
|
|
161
183
|
# Remove any servers which are not connected
|
|
162
184
|
servers.select!(&:connected?)
|
|
163
|
-
return
|
|
185
|
+
return if servers.empty?
|
|
164
186
|
|
|
165
187
|
time_left = remaining_time(start_time, timeout)
|
|
166
188
|
readable_servers = servers_with_response(servers, time_left)
|
|
167
|
-
if readable_servers.empty?
|
|
168
|
-
abort_with_timeout(servers)
|
|
169
|
-
return []
|
|
170
|
-
end
|
|
189
|
+
return abort_with_timeout(servers) if readable_servers.empty?
|
|
171
190
|
|
|
172
191
|
# Loop through the servers with responses, and
|
|
173
192
|
# delete any from our list that are finished
|
|
174
193
|
readable_servers.each do |server|
|
|
175
194
|
servers.delete(server) if process_server(server, &block)
|
|
176
195
|
end
|
|
177
|
-
servers
|
|
178
196
|
rescue NetworkError
|
|
179
197
|
# Abort and raise if we encountered a network error. This triggers
|
|
180
198
|
# a retry at the top level on RetryableNetworkError.
|
|
@@ -191,12 +209,10 @@ module Dalli
|
|
|
191
209
|
|
|
192
210
|
# Swallows Dalli::NetworkError
|
|
193
211
|
def abort_with_timeout(servers)
|
|
194
|
-
abort_without_timeout(servers)
|
|
195
212
|
servers.each do |server|
|
|
196
213
|
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
|
197
214
|
end
|
|
198
|
-
|
|
199
|
-
true # Required to simplify caller
|
|
215
|
+
abort_without_timeout(servers)
|
|
200
216
|
end
|
|
201
217
|
|
|
202
218
|
# Processes responses from a server. Returns true if there are no
|
|
@@ -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' }
|
|
@@ -38,9 +43,12 @@ module Dalli
|
|
|
38
43
|
|
|
39
44
|
private
|
|
40
45
|
|
|
46
|
+
# Values are looked up by each key as sent. The caller's key can't be
|
|
47
|
+
# rebuilt from it: a long key is digested, and a Symbol becomes a String.
|
|
41
48
|
def setup_requests(hash, ttl, req_options)
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
pairs = hash.transform_keys { |k| @key_manager.validate_key(k.to_s) }
|
|
50
|
+
groups = groups_for_keys(pairs.keys)
|
|
51
|
+
make_set_requests(groups, pairs, ttl, req_options)
|
|
44
52
|
groups.keys
|
|
45
53
|
end
|
|
46
54
|
|
|
@@ -54,14 +62,16 @@ module Dalli
|
|
|
54
62
|
# RetryableNetworkError too -- since NetworkError < DalliError -- dropping
|
|
55
63
|
# this server's keys on a transient hiccup instead of retrying. Only a
|
|
56
64
|
# non-network DalliError should be swallowed here.
|
|
57
|
-
def make_set_requests(groups,
|
|
65
|
+
def make_set_requests(groups, pairs, ttl, req_options)
|
|
58
66
|
groups.each do |server, keys_for_server|
|
|
59
67
|
keys_for_server.each do |key|
|
|
60
|
-
|
|
61
|
-
value = hash[original_key]
|
|
62
|
-
server.request(:pipelined_set, key, value, ttl, req_options)
|
|
68
|
+
server.request(:pipelined_set, key, pairs[key], ttl, req_options)
|
|
63
69
|
rescue Dalli::NetworkError
|
|
64
70
|
raise
|
|
71
|
+
rescue Dalli::MarshalError, Dalli::ValueOverMaxSize => e
|
|
72
|
+
# Nothing was written for this key, so the server's other keys are
|
|
73
|
+
# unaffected; raised by #process once the batch is complete
|
|
74
|
+
@value_error ||= e
|
|
65
75
|
rescue DalliError => e
|
|
66
76
|
Dalli.logger.debug { e.inspect }
|
|
67
77
|
Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
|
|
@@ -84,8 +94,7 @@ module Dalli
|
|
|
84
94
|
end
|
|
85
95
|
|
|
86
96
|
def groups_for_keys(keys)
|
|
87
|
-
|
|
88
|
-
groups = @ring.keys_grouped_by_server(validated_keys)
|
|
97
|
+
groups = @ring.keys_grouped_by_server(keys)
|
|
89
98
|
|
|
90
99
|
if (unfound_keys = groups.delete(nil))
|
|
91
100
|
Dalli.logger.debug do
|