dalli 5.1.2 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -1
- data/README.md +26 -0
- data/lib/dalli/cache_result.rb +65 -0
- data/lib/dalli/client.rb +62 -2
- data/lib/dalli/protocol/base.rb +15 -1
- data/lib/dalli/protocol/connection_manager.rb +18 -0
- data/lib/dalli/protocol/meta.rb +43 -22
- data/lib/dalli/protocol/request_formatter.rb +18 -0
- data/lib/dalli/protocol/response_processor.rb +82 -4
- data/lib/dalli/ring.rb +44 -17
- data/lib/dalli/socket.rb +27 -2
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +1 -0
- 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: 23a3890b1ca95b3668eea5b0f2f6eabfb3ad6062daebf5723e5b99c2c7a74854
|
|
4
|
+
data.tar.gz: 4f0c1775badbf133dab91d32a19f84ffafab7f1c0a6c9d7c195f968bed01c089
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1624d8d1024956e90ab86a5d5b7f56833d9d195b95d2e8f7ed92774775eebb5ae19baf798773e17ce8fdb7b8fa5abf728a5899d085ecd11335fe11daac52bdb
|
|
7
|
+
data.tar.gz: 76c4e96a6ac88ec7ce86d213d58bf4f709f6ebc52e747ffa05661343173c654696e6e2625090b14d31874f160f3778a750e9cd524cb4f8985ea86716f267793e
|
data/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,47 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
-
5.
|
|
7
|
+
5.2.0
|
|
8
8
|
==========
|
|
9
9
|
|
|
10
|
+
Features:
|
|
11
|
+
|
|
12
|
+
- Add `Dalli::CacheResult`, returned by the new `#get_with_metadata_result` and `#get_multi_with_metadata_result` (#1156)
|
|
13
|
+
- An opt-in, typed view over the Hash that `#get_with_metadata` and `#get_multi_with_metadata` already return; those methods are unchanged, and changing their return type is left for a future major version (#1151)
|
|
14
|
+
- `value`, `cas`, `hit_before`, `last_access` and `ttl_remaining` readers (the last three are `nil` unless requested), plus `miss?`, `hit?`, `stale?`, `won_recache?` and `lost_recache?` predicates
|
|
15
|
+
- Results are frozen, and a Hash that claims to be both a miss and stale raises `ArgumentError`
|
|
16
|
+
- `#get_multi_with_metadata_result` omits misses and accepts `req_options:`, like `#get_multi_with_metadata`; it has no block form
|
|
17
|
+
- Add the opt-in `defer_drain` client option (#1168)
|
|
18
|
+
- With `defer_drain: true`, a `quiet`/`multi` block no longer waits at its end for the replies to its requests. The requests are still sent right away; the replies are read with one noop per server just before the next non-quiet request to that server
|
|
19
|
+
- Adds `Client#drain_deferred_responses`, to drain at a boundary of the caller's choosing (the end of a web request or job)
|
|
20
|
+
- Tradeoff: an error reply to a quiet request surfaces (and is discarded) at that later point, not at the end of the block
|
|
21
|
+
- Off by default, so existing behavior is unchanged
|
|
22
|
+
- Replies memcached sends for quiet requests wait on the connection until they're drained, so a client that only makes quiet requests should call `drain_deferred_responses` periodically; see "Deferred Draining" in the README
|
|
23
|
+
- Thanks to Julian Richard Contreras and Matt Dick for this contribution
|
|
24
|
+
|
|
25
|
+
Performance:
|
|
26
|
+
|
|
27
|
+
- Skip the empty read at the end of each pipelined `read_available` call (#1164)
|
|
28
|
+
- A read shorter than the chunk size means the socket is drained, so the extra `read_nonblock` that only returned `:wait_readable` is gone; the `IO.select` loop still picks up anything that arrives later
|
|
29
|
+
- Saves one syscall per server per multi-server `get_multi`; on 4 servers a 3-key `get_multi` goes from 16% to 5% slower than 2.7.11 in the #930 benchmark
|
|
30
|
+
- TLS sockets also check `SSLSocket#pending`, since `IO.select` cannot see data OpenSSL has buffered
|
|
31
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
32
|
+
- Reduce Ruby overhead on plain `set` and `delete` (#1166)
|
|
33
|
+
- A `set` with no CAS, routing tokens or quiet mode builds its `ms` request with one string interpolation (`RequestFormatter.plain_meta_set`), and a `delete` with no options uses `plain_meta_delete`; both produce the same bytes as before
|
|
34
|
+
- `HD c<cas>`, `HD` and `NF` replies are parsed without splitting them into tokens
|
|
35
|
+
- Allocations per `set` drop from 22 to 18, and per `delete` from 12 to 7
|
|
36
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
37
|
+
- Only drain servers that were sent quiet requests when a `quiet`/`multi` block ends (#1167)
|
|
38
|
+
- Ending the block used to send a noop to every server in the ring and wait for each reply, one after another, so its cost grew with ring size even when the block touched one server (or none)
|
|
39
|
+
- Each connection now records when a quiet request is written, and the block's drain skips the others; servers that were never connected are no longer connected just to be drained
|
|
40
|
+
- With ~300us of network round trip, `multi { set }` on a 16-server ring goes from 4.9ms to 0.32ms; an empty block no longer touches the network
|
|
41
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
42
|
+
- Reduce Ruby overhead in `get_multi` reply parsing and key routing (#1169)
|
|
43
|
+
- `VA` reply headers are read in place instead of being split into tokens, on both the multi-server (pipelined) and single-server paths; results are unchanged, and unusual headers still take the token path
|
|
44
|
+
- A key's server is found through a bucket table over the ring's continuum instead of a binary search, and the key's own server is tried before the failover loop; the server chosen for every key is unchanged
|
|
45
|
+
- Allocations for a 100-key `get_multi` on 4 servers drop from 1,500 to 1,104; over loopback it goes from 301us to 243us
|
|
46
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
47
|
+
|
|
10
48
|
Bug fixes:
|
|
11
49
|
|
|
12
50
|
- Fix multi-server `get_multi` stopping at an empty value (#1170)
|
data/README.md
CHANGED
|
@@ -49,6 +49,32 @@ Dalli::Client.new('localhost:11211', namespace: 'myapp', namespace_separator: '/
|
|
|
49
49
|
|
|
50
50
|
The separator must be a single non-alphanumeric character. Valid examples: `:`, `/`, `|`, `.`, `-`, `_`, `#`
|
|
51
51
|
|
|
52
|
+
### Deferred Draining
|
|
53
|
+
|
|
54
|
+
By default, a `quiet` (or `multi`) block ends by waiting for the replies memcached still sends for its quiet requests, one round trip for each server the block wrote to. With `defer_drain: true`, the block returns without waiting:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
dc = Dalli::Client.new('localhost:11211', defer_drain: true)
|
|
58
|
+
|
|
59
|
+
dc.quiet { dc.set('key', 'value') } # returns without a round trip
|
|
60
|
+
dc.get('key') # first drains that connection with a noop, then gets the value
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The requests are still sent right away. Their replies stay unread on the connection until the next non-quiet request to that server, which reads them first with one `noop`. Error replies to quiet requests are discarded either way. With `defer_drain` they're discarded at that later point instead of at the end of the block.
|
|
64
|
+
|
|
65
|
+
**Replies accumulate until a non-quiet request drains them.** memcached still replies to some quiet requests: `NF` for a quiet `delete`, `incr` or `decr` of a missing key, `NS` for an `add` or `replace` that didn't store, and error replies. Those replies wait on the socket. A client that mixes quiet writes with reads (or other non-quiet requests) to the same servers drains them as it goes, and needs nothing more.
|
|
66
|
+
|
|
67
|
+
A client that only ever makes quiet requests, such as a worker that only issues quiet deletes, never sends that next request. Its unread replies keep growing, and if they fill the connection's socket buffers, the connection can stall until a socket timeout closes it and Dalli reconnects. Workloads like that should call `drain_deferred_responses` periodically, for example after each batch or job:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
jobs.each do |job|
|
|
71
|
+
job.keys.each { |key| dc.quiet { dc.delete(key) } }
|
|
72
|
+
dc.drain_deferred_responses # reads and discards the replies waiting on each server
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`drain_deferred_responses` only sends a `noop` to servers that were sent quiet requests since they were last drained, so calling it when nothing is pending costs nothing.
|
|
77
|
+
|
|
52
78
|
## Security Note
|
|
53
79
|
|
|
54
80
|
By default, Dalli uses Ruby's Marshal for serialization. Deserializing untrusted data with Marshal can lead to remote code execution. If you cache user-controlled data, consider using a safer serializer:
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# An immutable, typed view over the Hash returned by Client#get_with_metadata
|
|
6
|
+
# and Client#get_multi_with_metadata.
|
|
7
|
+
#
|
|
8
|
+
# This is an interim adapter (see #1130 / #1151): #get_with_metadata and
|
|
9
|
+
# #get_multi_with_metadata keep returning a Hash for now, since changing
|
|
10
|
+
# their return type is a breaking change reserved for a future major
|
|
11
|
+
# version. #get_with_metadata_result and #get_multi_with_metadata_result
|
|
12
|
+
# wrap that Hash in this type instead, so the structured shape can be
|
|
13
|
+
# validated by callers ahead of that change.
|
|
14
|
+
##
|
|
15
|
+
class CacheResult
|
|
16
|
+
attr_reader :value, :cas, :hit_before, :last_access, :ttl_remaining
|
|
17
|
+
|
|
18
|
+
# @param hash [Hash] a Hash as returned by Client#get_with_metadata, or a
|
|
19
|
+
# single value from the Hash returned by Client#get_multi_with_metadata
|
|
20
|
+
def initialize(hash)
|
|
21
|
+
@value = hash[:value]
|
|
22
|
+
@cas = hash[:cas]
|
|
23
|
+
@miss = hash.fetch(:miss, false)
|
|
24
|
+
@stale = hash.fetch(:stale, false)
|
|
25
|
+
@won_recache = hash.fetch(:won_recache, false)
|
|
26
|
+
@lost_recache = hash.fetch(:lost_recache, false)
|
|
27
|
+
@hit_before = hash[:hit_before]
|
|
28
|
+
@last_access = hash[:last_access]
|
|
29
|
+
@ttl_remaining = hash[:ttl_remaining]
|
|
30
|
+
|
|
31
|
+
raise ArgumentError, 'a result cannot be both a miss and stale' if @miss && @stale
|
|
32
|
+
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# True when the key did not exist. Prefer this over a nil #value, which
|
|
37
|
+
# cannot distinguish a miss from a stored nil under cache_nils.
|
|
38
|
+
def miss?
|
|
39
|
+
@miss
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The inverse of #miss?.
|
|
43
|
+
def hit?
|
|
44
|
+
!@miss
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# True when the item is a tombstone (marked stale via the meta protocol's
|
|
48
|
+
# invalidate flag) rather than removed. Not a miss: #value is still the
|
|
49
|
+
# last-known value unless the tombstone also dropped it.
|
|
50
|
+
def stale?
|
|
51
|
+
@stale
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# True if this call won the right to regenerate the value under
|
|
55
|
+
# thundering-herd protection (see Client#fetch_with_lock).
|
|
56
|
+
def won_recache?
|
|
57
|
+
@won_recache
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# True if another client already won the recache race for this key.
|
|
61
|
+
def lost_recache?
|
|
62
|
+
@lost_recache
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/dalli/client.rb
CHANGED
|
@@ -50,6 +50,14 @@ module Dalli
|
|
|
50
50
|
# +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
|
|
51
51
|
# +nil+ (default) omits the attribute entirely.
|
|
52
52
|
# - :otel_peer_service - when set, adds a +peer.service+ span attribute with this value for logical service naming.
|
|
53
|
+
# - :defer_drain - defaults to false. When true, a quiet/multi block no longer waits at its end for the replies
|
|
54
|
+
# to its requests. The requests are still sent right away; their replies are read (with one noop
|
|
55
|
+
# per server) just before the next non-quiet request to that server, or by
|
|
56
|
+
# #drain_deferred_responses. The tradeoff: an error reply to a quiet request surfaces, and is
|
|
57
|
+
# discarded, at that later point rather than when the block ends. Replies memcached sends for
|
|
58
|
+
# quiet requests (NF, NS, errors) wait on the connection until then, so a client that only makes
|
|
59
|
+
# quiet requests should call #drain_deferred_responses periodically. See "Deferred Draining" in
|
|
60
|
+
# the README.
|
|
53
61
|
#
|
|
54
62
|
def initialize(servers = nil, options = {})
|
|
55
63
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
@@ -149,6 +157,22 @@ module Dalli
|
|
|
149
157
|
retry
|
|
150
158
|
end
|
|
151
159
|
|
|
160
|
+
##
|
|
161
|
+
# Like #get_with_metadata, but wraps the result in a Dalli::CacheResult
|
|
162
|
+
# instead of returning a Hash.
|
|
163
|
+
#
|
|
164
|
+
# Interim adapter (see #1130 / #1151): #get_with_metadata itself keeps
|
|
165
|
+
# returning a Hash, since changing that return type is a breaking change
|
|
166
|
+
# reserved for a future major version. This lets callers opt in to the
|
|
167
|
+
# structured shape today.
|
|
168
|
+
#
|
|
169
|
+
# @param key [String] the cache key
|
|
170
|
+
# @param options [Hash] see #get_with_metadata
|
|
171
|
+
# @return [Dalli::CacheResult]
|
|
172
|
+
def get_with_metadata_result(key, options = {})
|
|
173
|
+
CacheResult.new(get_with_metadata(key, options))
|
|
174
|
+
end
|
|
175
|
+
|
|
152
176
|
##
|
|
153
177
|
# Fetch multiple keys efficiently.
|
|
154
178
|
# If a block is given, yields key/value pairs one at a time.
|
|
@@ -228,6 +252,27 @@ module Dalli
|
|
|
228
252
|
results
|
|
229
253
|
end
|
|
230
254
|
|
|
255
|
+
##
|
|
256
|
+
# Like #get_multi_with_metadata, but wraps each entry's Hash in a
|
|
257
|
+
# Dalli::CacheResult instead. Missing keys are still omitted; no block
|
|
258
|
+
# form.
|
|
259
|
+
#
|
|
260
|
+
# Interim adapter (see #1130 / #1151): #get_multi_with_metadata itself
|
|
261
|
+
# keeps returning a Hash of Hashes, since changing that return type is a
|
|
262
|
+
# breaking change reserved for a future major version. This lets callers
|
|
263
|
+
# opt in to the structured shape today.
|
|
264
|
+
#
|
|
265
|
+
# Result key order follows #get_multi_with_metadata's own: request order
|
|
266
|
+
# only when every key lands on the same server, per-server response order
|
|
267
|
+
# otherwise.
|
|
268
|
+
#
|
|
269
|
+
# @param keys [Array<String>] the keys to fetch
|
|
270
|
+
# @param req_options [Hash, nil] see #get_multi_with_metadata
|
|
271
|
+
# @return [Hash] key => Dalli::CacheResult
|
|
272
|
+
def get_multi_with_metadata_result(*keys, req_options: nil)
|
|
273
|
+
get_multi_with_metadata(*keys, req_options: req_options).transform_values { |hash| CacheResult.new(hash) }
|
|
274
|
+
end
|
|
275
|
+
|
|
231
276
|
##
|
|
232
277
|
# Fetch multiple keys efficiently, including available metadata such as CAS.
|
|
233
278
|
# If a block is given, yields key/data pairs one a time. Data is an array:
|
|
@@ -355,7 +400,8 @@ module Dalli
|
|
|
355
400
|
# arithmetic (incr, decr), flush and delete operations. Use of
|
|
356
401
|
# unsupported operations inside a block will raise an error.
|
|
357
402
|
#
|
|
358
|
-
# Any error replies will be discarded at the end of the block,
|
|
403
|
+
# Any error replies will be discarded at the end of the block (or, with
|
|
404
|
+
# the defer_drain option, before the next non-quiet request), and
|
|
359
405
|
# Dalli client methods invoked inside the block will not
|
|
360
406
|
# have return values
|
|
361
407
|
def quiet
|
|
@@ -363,11 +409,25 @@ module Dalli
|
|
|
363
409
|
Thread.current[::Dalli::QUIET] = true
|
|
364
410
|
yield
|
|
365
411
|
ensure
|
|
366
|
-
@ring&.pipeline_consume_and_ignore_responses
|
|
412
|
+
@ring&.pipeline_consume_and_ignore_responses unless @options[:defer_drain]
|
|
367
413
|
Thread.current[::Dalli::QUIET] = old
|
|
368
414
|
end
|
|
369
415
|
alias multi quiet
|
|
370
416
|
|
|
417
|
+
##
|
|
418
|
+
# Reads and discards the replies still pending from quiet requests, on the
|
|
419
|
+
# servers that have any. Only needed with the defer_drain option.
|
|
420
|
+
#
|
|
421
|
+
# Normally the next non-quiet request to a server drains its pending
|
|
422
|
+
# replies first. A client that only makes quiet requests (for example, a
|
|
423
|
+
# worker that only issues quiet deletes) never does, so its replies keep
|
|
424
|
+
# accumulating on the socket; call this periodically, such as after each
|
|
425
|
+
# batch or job. It sends nothing to servers with no quiet requests pending.
|
|
426
|
+
def drain_deferred_responses
|
|
427
|
+
@ring&.pipeline_consume_and_ignore_responses
|
|
428
|
+
nil
|
|
429
|
+
end
|
|
430
|
+
|
|
371
431
|
def set(key, value, ttl = nil, req_options = nil)
|
|
372
432
|
set_cas(key, value, 0, ttl, req_options)
|
|
373
433
|
end
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -18,7 +18,8 @@ module Dalli
|
|
|
18
18
|
|
|
19
19
|
def_delegators :@value_marshaller, :serializer, :compressor, :compression_min_size, :compress_by_default?
|
|
20
20
|
def_delegators :@connection_manager, :name, :hostname, :port, :close, :socket_timeout,
|
|
21
|
-
:socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
|
|
21
|
+
:socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error,
|
|
22
|
+
:quiet_responses_pending?
|
|
22
23
|
|
|
23
24
|
# Delegated by hand rather than with def_delegators because they run on
|
|
24
25
|
# every request, and a plain method call is cheaper than a Forwardable one.
|
|
@@ -41,6 +42,7 @@ module Dalli
|
|
|
41
42
|
warn_uri_credentials(user_creds)
|
|
42
43
|
@options = client_options.merge(user_creds)
|
|
43
44
|
@raw_mode = client_options[:raw]
|
|
45
|
+
@defer_drain = client_options[:defer_drain] ? true : false
|
|
44
46
|
@value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
45
47
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
46
48
|
end
|
|
@@ -58,7 +60,11 @@ module Dalli
|
|
|
58
60
|
begin
|
|
59
61
|
request_completed = false
|
|
60
62
|
@connection_manager.start_request!
|
|
63
|
+
drain_deferred_quiet_responses(opkey)
|
|
61
64
|
response = send(opkey, *args)
|
|
65
|
+
# Quiet requests can leave replies on the socket; remember that so the
|
|
66
|
+
# end of the quiet block knows this server needs draining
|
|
67
|
+
@connection_manager.mark_quiet_responses_pending! if quiet? && opkey != :noop
|
|
62
68
|
|
|
63
69
|
# pipelined_get/pipelined_get_interleaved emit query but don't read the response(s)
|
|
64
70
|
@connection_manager.finish_request! unless %i[pipelined_get pipelined_get_interleaved].include?(opkey)
|
|
@@ -217,6 +223,14 @@ module Dalli
|
|
|
217
223
|
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
218
224
|
private_constant :ALLOWED_QUIET_OPS
|
|
219
225
|
|
|
226
|
+
# With defer_drain, quiet blocks leave their replies unread. Read them
|
|
227
|
+
# before any non-quiet request, which reads a reply of its own.
|
|
228
|
+
def drain_deferred_quiet_responses(opkey)
|
|
229
|
+
return unless @defer_drain && !quiet? && opkey != :noop && @connection_manager.quiet_responses_pending?
|
|
230
|
+
|
|
231
|
+
noop
|
|
232
|
+
end
|
|
233
|
+
|
|
220
234
|
def verify_allowed_quiet!(opkey)
|
|
221
235
|
return if ALLOWED_QUIET_OPS.include?(opkey)
|
|
222
236
|
|
|
@@ -33,6 +33,7 @@ module Dalli
|
|
|
33
33
|
@socket_type = socket_type
|
|
34
34
|
@options = DEFAULTS.merge(client_options)
|
|
35
35
|
@request_in_progress = false
|
|
36
|
+
@quiet_responses_pending = false
|
|
36
37
|
@sock = nil
|
|
37
38
|
@pid = nil
|
|
38
39
|
|
|
@@ -54,6 +55,7 @@ module Dalli
|
|
|
54
55
|
@sock.sync = false # Enable buffered I/O for better performance
|
|
55
56
|
@pid = Process.pid
|
|
56
57
|
@request_in_progress = false
|
|
58
|
+
@quiet_responses_pending = false
|
|
57
59
|
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
|
|
58
60
|
# SocketError = DNS resolution failure
|
|
59
61
|
error_on_request!(e)
|
|
@@ -123,6 +125,7 @@ module Dalli
|
|
|
123
125
|
# @request_in_progress == true.
|
|
124
126
|
@sock = nil
|
|
125
127
|
@pid = nil
|
|
128
|
+
@quiet_responses_pending = false
|
|
126
129
|
abort_request!
|
|
127
130
|
end
|
|
128
131
|
end
|
|
@@ -131,6 +134,21 @@ module Dalli
|
|
|
131
134
|
!@sock.nil?
|
|
132
135
|
end
|
|
133
136
|
|
|
137
|
+
# True when a quiet request was written on this connection and its
|
|
138
|
+
# replies (memcached only suppresses some of them) haven't been
|
|
139
|
+
# consumed by a noop yet.
|
|
140
|
+
def quiet_responses_pending?
|
|
141
|
+
@quiet_responses_pending
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def mark_quiet_responses_pending!
|
|
145
|
+
@quiet_responses_pending = true
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def clear_quiet_responses_pending!
|
|
149
|
+
@quiet_responses_pending = false
|
|
150
|
+
end
|
|
151
|
+
|
|
134
152
|
def request_in_progress?
|
|
135
153
|
@request_in_progress
|
|
136
154
|
end
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -157,12 +157,17 @@ module Dalli
|
|
|
157
157
|
def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
|
|
158
158
|
(value, bitflags) = @value_marshaller.store(key, raw_value, options)
|
|
159
159
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
# eql?(0): only Integer 0 means "no CAS" here; anything else takes the general path
|
|
161
|
+
req = if mode == :set && !quiet && (cas.nil? || cas.eql?(0)) && !routing_tokens?(options)
|
|
162
|
+
RequestFormatter.plain_meta_set(key, value.bytesize, bitflags, ttl)
|
|
163
|
+
else
|
|
164
|
+
RequestFormatter.meta_set(key: key, value: value,
|
|
165
|
+
bitflags: bitflags, cas: cas,
|
|
166
|
+
ttl: ttl, mode: mode, quiet: quiet,
|
|
167
|
+
**routing_token_kwargs(options))
|
|
168
|
+
end
|
|
164
169
|
write("#{req}#{value}#{TERMINATOR}")
|
|
165
|
-
|
|
170
|
+
finish_write(quiet)
|
|
166
171
|
end
|
|
167
172
|
# rubocop:enable Metrics/ParameterLists
|
|
168
173
|
|
|
@@ -183,7 +188,7 @@ module Dalli
|
|
|
183
188
|
cas: cas, ttl: ttl, mode: mode, quiet: quiet?,
|
|
184
189
|
**routing_token_kwargs(options))
|
|
185
190
|
write("#{req}#{value}#{TERMINATOR}")
|
|
186
|
-
|
|
191
|
+
finish_write(quiet?)
|
|
187
192
|
end
|
|
188
193
|
|
|
189
194
|
# Delete Commands
|
|
@@ -191,10 +196,15 @@ module Dalli
|
|
|
191
196
|
# `options` supports the meta-delete keys :invalidate, :tombstone_ttl and
|
|
192
197
|
# :drop_value, plus :p_token/:l_token; see Dalli::Client#delete.
|
|
193
198
|
def delete(key, cas, options = nil)
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
# Fast path for a plain delete: no options, no CAS, not quiet
|
|
200
|
+
req = if options.nil? && (cas.nil? || cas.eql?(0)) && !quiet?
|
|
201
|
+
RequestFormatter.plain_meta_delete(key)
|
|
202
|
+
else
|
|
203
|
+
RequestFormatter.meta_delete(key: key, cas: cas, quiet: quiet?,
|
|
204
|
+
**tombstone_kwargs(options), **routing_token_kwargs(options))
|
|
205
|
+
end
|
|
196
206
|
write(req)
|
|
197
|
-
|
|
207
|
+
finish_write(quiet?)
|
|
198
208
|
response_processor.meta_delete unless quiet?
|
|
199
209
|
end
|
|
200
210
|
|
|
@@ -225,22 +235,34 @@ module Dalli
|
|
|
225
235
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
226
236
|
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
227
237
|
quiet: quiet?, **routing_token_kwargs(options)))
|
|
228
|
-
|
|
238
|
+
finish_write(quiet?)
|
|
229
239
|
response_processor.decr_incr unless quiet?
|
|
230
240
|
end
|
|
231
241
|
|
|
232
242
|
# Other Commands
|
|
233
243
|
def flush(delay = 0)
|
|
234
244
|
write(RequestFormatter.flush(delay: delay))
|
|
235
|
-
|
|
245
|
+
finish_write(quiet?)
|
|
236
246
|
response_processor.flush unless quiet?
|
|
237
247
|
end
|
|
238
248
|
|
|
239
249
|
# Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
|
|
240
250
|
# We need to read all the responses at once.
|
|
251
|
+
# A write that expects a reply is flushed so the reply can be read. A write
|
|
252
|
+
# in a quiet block stays buffered until the block's noop, unless
|
|
253
|
+
# defer_drain is on: then no noop follows at the end of the block, so it is
|
|
254
|
+
# flushed right away. (pipelined_set passes quiet: true outside a quiet
|
|
255
|
+
# block; its caller flushes with a terminating noop.)
|
|
256
|
+
def finish_write(quiet)
|
|
257
|
+
@connection_manager.flush if !quiet || (@defer_drain && quiet?)
|
|
258
|
+
end
|
|
259
|
+
|
|
241
260
|
def noop
|
|
242
261
|
write_noop
|
|
243
|
-
response_processor.consume_all_responses_until_mn
|
|
262
|
+
result = response_processor.consume_all_responses_until_mn
|
|
263
|
+
# Everything up to and including the MN has been read
|
|
264
|
+
@connection_manager.clear_quiet_responses_pending!
|
|
265
|
+
result
|
|
244
266
|
end
|
|
245
267
|
|
|
246
268
|
def stats(info = nil)
|
|
@@ -313,26 +335,25 @@ module Dalli
|
|
|
313
335
|
|
|
314
336
|
def read_multi_get_responses(is_raw)
|
|
315
337
|
hash = {}
|
|
316
|
-
key_index = is_raw ? 2 : 3
|
|
317
338
|
while (line = @connection_manager.read_line)
|
|
318
339
|
break if line.start_with?('MN')
|
|
319
340
|
next unless line.start_with?('VA ')
|
|
320
341
|
|
|
321
|
-
key, value = parse_multi_get_value(line,
|
|
342
|
+
key, value = parse_multi_get_value(line, is_raw)
|
|
322
343
|
hash[key] = value if key
|
|
323
344
|
end
|
|
324
345
|
hash
|
|
325
346
|
end
|
|
326
347
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
348
|
+
# Reads the "VA <size> [f<flags>] k<key> ..." line in place rather than
|
|
349
|
+
# splitting it into tokens
|
|
350
|
+
def parse_multi_get_value(line, is_raw)
|
|
351
|
+
processor = response_processor
|
|
352
|
+
value = @connection_manager.read(processor.size_from_va_line(line) + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
353
|
+
key = processor.key_from_va_line(line)
|
|
354
|
+
return unless key
|
|
332
355
|
|
|
333
|
-
|
|
334
|
-
key = KeyRegularizer.decode(key) if tokens.include?('b')
|
|
335
|
-
bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
|
|
356
|
+
bitflags = is_raw ? 0 : processor.bitflags_from_va_line(line)
|
|
336
357
|
[key, @value_marshaller.retrieve(value, bitflags)]
|
|
337
358
|
end
|
|
338
359
|
|
|
@@ -63,6 +63,19 @@ module Dalli
|
|
|
63
63
|
skip_flags ? "mg #{encoded_key(key)} v#{TERMINATOR}" : "mg #{encoded_key(key)} v f#{TERMINATOR}"
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Fast path for a plain set: mode :set, not quiet, no CAS and no routing
|
|
67
|
+
# tokens. Produces the same bytes as meta_set(key:, value:, bitflags:, ttl:)
|
|
68
|
+
# without the keyword-argument handling and incremental string building.
|
|
69
|
+
def plain_meta_set(key, value_bytesize, bitflags, ttl)
|
|
70
|
+
flags = bitflags ? " F#{bitflags}" : ''
|
|
71
|
+
flags += " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
72
|
+
if KeyRegularizer.required?(key)
|
|
73
|
+
"ms #{KeyRegularizer.encode(key)} #{value_bytesize} c b#{flags} MS#{TERMINATOR}"
|
|
74
|
+
else
|
|
75
|
+
"ms #{key} #{value_bytesize} c#{flags} MS#{TERMINATOR}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
66
79
|
# Pass terminate: false to leave off the trailing noop, for callers
|
|
67
80
|
# (the pipelined get) that send it separately.
|
|
68
81
|
def multi_meta_get(keys, skip_flags: false, return_cas: false, terminate: true, p_token: nil, l_token: nil)
|
|
@@ -158,6 +171,11 @@ module Dalli
|
|
|
158
171
|
cmd << TERMINATOR
|
|
159
172
|
end
|
|
160
173
|
|
|
174
|
+
# Same bytes as meta_delete(key: key), without the keyword-argument handling
|
|
175
|
+
def plain_meta_delete(key)
|
|
176
|
+
"md #{encoded_key(key)}#{TERMINATOR}"
|
|
177
|
+
end
|
|
178
|
+
|
|
161
179
|
# Tombstone and routing-token flags apply to every key in the batch;
|
|
162
180
|
# see meta_delete.
|
|
163
181
|
def multi_meta_delete(keys, stale: false, ttl: nil, drop_value: false, p_token: nil, l_token: nil)
|
|
@@ -24,7 +24,10 @@ module Dalli
|
|
|
24
24
|
SERVER_ERROR = 'SERVER_ERROR'
|
|
25
25
|
|
|
26
26
|
VA_PREFIX = 'VA '
|
|
27
|
+
HD_PREFIX = 'HD '
|
|
27
28
|
FLAGS_TOKEN_PREFIX = ' f'
|
|
29
|
+
CAS_TOKEN_PREFIX = ' c'
|
|
30
|
+
KEY_TOKEN_PREFIX = ' k'
|
|
28
31
|
BYTE_B = 'b'.ord
|
|
29
32
|
BYTE_C = 'c'.ord
|
|
30
33
|
BYTE_F = 'f'.ord
|
|
@@ -125,7 +128,13 @@ module Dalli
|
|
|
125
128
|
end
|
|
126
129
|
|
|
127
130
|
def meta_set_with_cas
|
|
128
|
-
|
|
131
|
+
line = read_line
|
|
132
|
+
# A stored item ("HD c<cas>") is parsed in place rather than split into tokens
|
|
133
|
+
# Search from just after "HD": the " c" token starts at the space that follows it
|
|
134
|
+
return flag_from_line(line, CAS_TOKEN_PREFIX, HD.bytesize) if line&.start_with?(HD_PREFIX)
|
|
135
|
+
|
|
136
|
+
tokens = line&.split || []
|
|
137
|
+
raise_unexpected!(tokens) unless T_HD_NS_NF_EX.include?(tokens.first)
|
|
129
138
|
return false unless tokens.first == HD
|
|
130
139
|
|
|
131
140
|
cas_from_tokens(tokens)
|
|
@@ -139,7 +148,13 @@ module Dalli
|
|
|
139
148
|
end
|
|
140
149
|
|
|
141
150
|
def meta_delete
|
|
142
|
-
|
|
151
|
+
line = read_line
|
|
152
|
+
# The two common replies, compared as whole lines instead of split into tokens
|
|
153
|
+
return true if line == HD
|
|
154
|
+
return false if line == NF
|
|
155
|
+
|
|
156
|
+
tokens = line&.split || []
|
|
157
|
+
raise_unexpected!(tokens) unless T_HD_NF_EX.include?(tokens.first)
|
|
143
158
|
tokens.first == HD
|
|
144
159
|
end
|
|
145
160
|
|
|
@@ -217,6 +232,11 @@ module Dalli
|
|
|
217
232
|
term_idx = buf.byteindex(TERMINATOR, offset)
|
|
218
233
|
return [0] unless term_idx
|
|
219
234
|
|
|
235
|
+
if buf.byteslice(offset, VA_PREFIX.bytesize) == VA_PREFIX
|
|
236
|
+
response = va_response_from_buffer(buf, offset, term_idx)
|
|
237
|
+
return response if response
|
|
238
|
+
end
|
|
239
|
+
|
|
220
240
|
header = buf.byteslice(offset, term_idx - offset)
|
|
221
241
|
tokens = header.split
|
|
222
242
|
header_len = header.bytesize + TERMINATOR.length
|
|
@@ -258,6 +278,43 @@ module Dalli
|
|
|
258
278
|
[tokens.first == VA, flag_int(cas, 'c'), key, value, resp_size]
|
|
259
279
|
end
|
|
260
280
|
|
|
281
|
+
# getk_response_from_buffer for a "VA ..." header, read in place
|
|
282
|
+
# without allocating the header, its tokens or the flag strings.
|
|
283
|
+
# Gives the same result as the token path. Returns nil to fall back to
|
|
284
|
+
# it when the header has no s flag or a zero size.
|
|
285
|
+
def va_response_from_buffer(buf, offset, term_idx)
|
|
286
|
+
size = bitflags = cas = key = nil
|
|
287
|
+
base64 = false
|
|
288
|
+
pos = buf.byteindex(' ', offset + VA_PREFIX.bytesize)
|
|
289
|
+
while pos && pos < term_idx
|
|
290
|
+
start = pos + 1
|
|
291
|
+
pos = buf.byteindex(' ', start)
|
|
292
|
+
stop = pos && pos < term_idx ? pos : term_idx
|
|
293
|
+
case buf.getbyte(start)
|
|
294
|
+
when BYTE_S then size ||= flag_int_at(buf, start, stop)
|
|
295
|
+
when BYTE_F then bitflags ||= flag_int_at(buf, start, stop)
|
|
296
|
+
when BYTE_C then cas ||= flag_int_at(buf, start, stop)
|
|
297
|
+
when BYTE_K then key ||= buf.byteslice(start + 1, stop - start - 1)
|
|
298
|
+
when BYTE_B then base64 ||= stop == start + 1
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
return nil if size.nil? || size.zero?
|
|
302
|
+
|
|
303
|
+
header_len = term_idx - offset + TERMINATOR.length
|
|
304
|
+
resp_size = header_len + size + TERMINATOR.length
|
|
305
|
+
return [0] unless buf.bytesize >= offset + resp_size
|
|
306
|
+
|
|
307
|
+
value = @value_marshaller.retrieve(buf.byteslice(offset + header_len, size), bitflags || 0)
|
|
308
|
+
key = KeyRegularizer.decode(key) if base64 && key
|
|
309
|
+
[true, cas || 0, key || 0, value, resp_size]
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Integer value of a flag's token in buf, after its one-byte prefix,
|
|
313
|
+
# as flag_int would read it
|
|
314
|
+
def flag_int_at(buf, start, stop)
|
|
315
|
+
buf.byteslice(start + 1, stop - start - 1).to_i
|
|
316
|
+
end
|
|
317
|
+
|
|
261
318
|
# Integer value of a flag token such as "f123", or 0 when absent.
|
|
262
319
|
# Strips the prefix in place, like value_from_tokens.
|
|
263
320
|
def flag_int(token, flag)
|
|
@@ -289,8 +346,29 @@ module Dalli
|
|
|
289
346
|
end
|
|
290
347
|
|
|
291
348
|
def bitflags_from_va_line(line)
|
|
292
|
-
|
|
293
|
-
|
|
349
|
+
flag_from_line(line, FLAGS_TOKEN_PREFIX, VA_PREFIX.bytesize)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# The key from a VA line's k flag, decoded when the line has the b flag,
|
|
353
|
+
# or nil when there is no k flag
|
|
354
|
+
def key_from_va_line(line)
|
|
355
|
+
idx = line.index(KEY_TOKEN_PREFIX, VA_PREFIX.bytesize)
|
|
356
|
+
return unless idx
|
|
357
|
+
|
|
358
|
+
start = idx + KEY_TOKEN_PREFIX.bytesize
|
|
359
|
+
stop = line.index(' ', start) || line.index(TERMINATOR, start) || line.bytesize
|
|
360
|
+
key = line.byteslice(start, stop - start)
|
|
361
|
+
base64_flag_in_line?(line) ? KeyRegularizer.decode(key) : key
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def base64_flag_in_line?(line)
|
|
365
|
+
line.include?(' b ') || line.end_with?(" b#{TERMINATOR}", ' b')
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Integer value of the first " <flag><digits>" token at or after start, or 0
|
|
369
|
+
def flag_from_line(line, token_prefix, start)
|
|
370
|
+
idx = line.index(token_prefix, start)
|
|
371
|
+
idx ? line.byteslice(idx + token_prefix.bytesize, line.bytesize).to_i : 0
|
|
294
372
|
end
|
|
295
373
|
|
|
296
374
|
def cas_from_tokens(tokens)
|
data/lib/dalli/ring.rb
CHANGED
|
@@ -36,9 +36,10 @@ module Dalli
|
|
|
36
36
|
|
|
37
37
|
def continuum=(entries)
|
|
38
38
|
@continuum = entries
|
|
39
|
-
# Plain integers, so the
|
|
40
|
-
#
|
|
39
|
+
# Plain integers, so the lookup in server_for_hash_key needn't call
|
|
40
|
+
# Entry#value at each step
|
|
41
41
|
@continuum_values = entries&.map(&:value)
|
|
42
|
+
build_buckets
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# alive_cache (optional) remembers each server's alive? result, so a
|
|
@@ -55,15 +56,17 @@ module Dalli
|
|
|
55
56
|
raise Dalli::RingError, 'No server available'
|
|
56
57
|
end
|
|
57
58
|
|
|
59
|
+
# Tries the key's own server first, then (with failover) up to 19
|
|
60
|
+
# rehashes of "<try><key>". The first try is outside the loop because it
|
|
61
|
+
# is almost always the one that's used.
|
|
58
62
|
def server_from_continuum(key, alive_cache = nil)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
server = server_for_hash_key(hash_for(key))
|
|
64
|
+
return server if server_alive?(server, alive_cache)
|
|
65
|
+
return nil unless @failover
|
|
62
66
|
|
|
67
|
+
19.times do |try|
|
|
68
|
+
server = server_for_hash_key(hash_for("#{try}#{key}"))
|
|
63
69
|
return server if server_alive?(server, alive_cache)
|
|
64
|
-
break unless @failover
|
|
65
|
-
|
|
66
|
-
hkey = hash_for("#{try}#{key}")
|
|
67
70
|
end
|
|
68
71
|
nil
|
|
69
72
|
end
|
|
@@ -87,8 +90,13 @@ module Dalli
|
|
|
87
90
|
end
|
|
88
91
|
end
|
|
89
92
|
|
|
93
|
+
# Drains the replies left by quiet requests. Only servers that were sent a
|
|
94
|
+
# quiet request need it, so the others (and servers never connected) are
|
|
95
|
+
# skipped rather than each costing a noop round trip.
|
|
90
96
|
def pipeline_consume_and_ignore_responses
|
|
91
97
|
@servers.each do |s|
|
|
98
|
+
next unless s.quiet_responses_pending?
|
|
99
|
+
|
|
92
100
|
s.request(:noop)
|
|
93
101
|
rescue Dalli::NetworkError
|
|
94
102
|
# Ignore this error, as it indicates the socket is unavailable
|
|
@@ -121,22 +129,41 @@ module Dalli
|
|
|
121
129
|
def server_alive?(server, alive_cache)
|
|
122
130
|
return server.alive? unless alive_cache
|
|
123
131
|
|
|
124
|
-
|
|
132
|
+
alive = alive_cache[server]
|
|
133
|
+
alive.nil? ? (alive_cache[server] = server.alive?) : alive
|
|
125
134
|
end
|
|
126
135
|
|
|
127
136
|
def entry_count_for(server, total_servers, total_weight)
|
|
128
137
|
((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
|
|
129
138
|
end
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
# Hash values are 32 bits. Splitting that range into about one bucket per
|
|
141
|
+
# continuum entry, and noting the first entry at or after each bucket's
|
|
142
|
+
# start, lets server_for_hash_key skip the binary search: it starts at the
|
|
143
|
+
# key's bucket and steps over the few entries before the key's hash.
|
|
144
|
+
def build_buckets
|
|
145
|
+
return unless @continuum_values
|
|
146
|
+
|
|
147
|
+
bits = [@continuum_values.size.bit_length, 1].max
|
|
148
|
+
@bucket_shift = 32 - bits
|
|
149
|
+
@bucket_first = Array.new(1 << bits)
|
|
150
|
+
idx = 0
|
|
151
|
+
size = @continuum_values.size
|
|
152
|
+
@bucket_first.each_index do |bucket|
|
|
153
|
+
floor = bucket << @bucket_shift
|
|
154
|
+
idx += 1 while idx < size && @continuum_values[idx] < floor
|
|
155
|
+
@bucket_first[bucket] = idx
|
|
138
156
|
end
|
|
139
|
-
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def server_for_hash_key(hash_key)
|
|
160
|
+
# Find the last entry with value <= hash_key, wrapping around to the
|
|
161
|
+
# last entry when every value is greater (idx - 1 is then -1)
|
|
162
|
+
idx = @bucket_first[hash_key >> @bucket_shift]
|
|
163
|
+
values = @continuum_values
|
|
164
|
+
size = values.size
|
|
165
|
+
idx += 1 while idx < size && values[idx] <= hash_key
|
|
166
|
+
@continuum[idx - 1].server
|
|
140
167
|
end
|
|
141
168
|
|
|
142
169
|
def build_continuum(servers)
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -12,22 +12,29 @@ module Dalli
|
|
|
12
12
|
# Common methods for all socket implementations.
|
|
13
13
|
##
|
|
14
14
|
module InstanceMethods
|
|
15
|
+
READ_CHUNK_SIZE = 8196
|
|
16
|
+
|
|
17
|
+
# Reads whatever is available without blocking. A read that returns less
|
|
18
|
+
# than a full chunk means the socket was drained, so we stop there rather
|
|
19
|
+
# than spend another read_nonblock call just to get :wait_readable back.
|
|
20
|
+
# Callers keep waiting on IO.select for anything that arrives later.
|
|
15
21
|
def read_available(reusable_buffer = nil)
|
|
16
22
|
if reusable_buffer
|
|
17
|
-
value = read_nonblock(
|
|
23
|
+
value = read_nonblock(READ_CHUNK_SIZE, reusable_buffer, exception: false)
|
|
18
24
|
case value
|
|
19
25
|
when :wait_writable, :wait_readable
|
|
20
26
|
return reusable_buffer.clear
|
|
21
27
|
when nil
|
|
22
28
|
raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}"
|
|
23
29
|
end
|
|
30
|
+
return value if drained_after?(value)
|
|
24
31
|
else
|
|
25
32
|
value = ''.b
|
|
26
33
|
end
|
|
27
34
|
|
|
28
35
|
buffer = ''.b
|
|
29
36
|
loop do
|
|
30
|
-
result = read_nonblock(
|
|
37
|
+
result = read_nonblock(READ_CHUNK_SIZE, buffer, exception: false)
|
|
31
38
|
case result
|
|
32
39
|
when :wait_writable, :wait_readable
|
|
33
40
|
buffer.clear
|
|
@@ -36,10 +43,21 @@ module Dalli
|
|
|
36
43
|
raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}"
|
|
37
44
|
else
|
|
38
45
|
value << result
|
|
46
|
+
return value if drained_after?(result)
|
|
39
47
|
end
|
|
40
48
|
end
|
|
41
49
|
end
|
|
42
50
|
|
|
51
|
+
# True when a read came back short and nothing is left buffered in-process.
|
|
52
|
+
def drained_after?(chunk)
|
|
53
|
+
chunk.bytesize < READ_CHUNK_SIZE && !buffered_data?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Plain sockets keep nothing buffered in-process. See SSLSocket.
|
|
57
|
+
def buffered_data?
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
43
61
|
FILTERED_OUT_OPTIONS = %i[username password].freeze
|
|
44
62
|
def logged_options
|
|
45
63
|
options.except(*FILTERED_OUT_OPTIONS)
|
|
@@ -83,6 +101,13 @@ module Dalli
|
|
|
83
101
|
io.options
|
|
84
102
|
end
|
|
85
103
|
|
|
104
|
+
# OpenSSL can hold decrypted bytes that IO.select won't report as readable.
|
|
105
|
+
# With OpenSSL's default (no read-ahead) a short read has already used up
|
|
106
|
+
# the current record, so this is normally 0; it guards read-ahead setups.
|
|
107
|
+
def buffered_data?
|
|
108
|
+
pending.positive?
|
|
109
|
+
end
|
|
110
|
+
|
|
86
111
|
unless method_defined?(:wait_readable)
|
|
87
112
|
def wait_readable(timeout = nil)
|
|
88
113
|
to_io.wait_readable(timeout)
|
data/lib/dalli/version.rb
CHANGED
data/lib/dalli.rb
CHANGED
|
@@ -63,6 +63,7 @@ require_relative 'dalli/instrumentation'
|
|
|
63
63
|
|
|
64
64
|
require_relative 'dalli/flags'
|
|
65
65
|
require_relative 'dalli/compressor'
|
|
66
|
+
require_relative 'dalli/cache_result'
|
|
66
67
|
require_relative 'dalli/client'
|
|
67
68
|
require_relative 'dalli/key_manager'
|
|
68
69
|
require_relative 'dalli/pipelined_getter'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dalli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -37,6 +37,7 @@ files:
|
|
|
37
37
|
- LICENSE
|
|
38
38
|
- README.md
|
|
39
39
|
- lib/dalli.rb
|
|
40
|
+
- lib/dalli/cache_result.rb
|
|
40
41
|
- lib/dalli/cas/client.rb
|
|
41
42
|
- lib/dalli/client.rb
|
|
42
43
|
- lib/dalli/compressor.rb
|