dalli 5.1.0 → 5.1.1
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 +33 -0
- data/Gemfile +1 -1
- data/lib/dalli/client.rb +26 -6
- data/lib/dalli/pipelined_getter.rb +45 -57
- data/lib/dalli/protocol/base.rb +29 -11
- data/lib/dalli/protocol/key_regularizer.rb +8 -1
- data/lib/dalli/protocol/meta.rb +21 -6
- data/lib/dalli/protocol/request_formatter.rb +33 -11
- data/lib/dalli/protocol/response_processor.rb +64 -16
- data/lib/dalli/ring.rb +31 -17
- data/lib/dalli/socket.rb +2 -4
- data/lib/dalli/version.rb +1 -1
- data/lib/rack/session/dalli.rb +1 -2
- 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: 1cb62e15948a7d649e178bcfc9aa03de998be618f4fa7d08c988f874006c941f
|
|
4
|
+
data.tar.gz: 0b2d3d9d1a7a73276a6fa22e2015805a306bab7c0e53616a9531eccff7979c33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eaab609ed368a7f8f2fa129eab6177894cfc7548ea25002b30bae4ee13d22daaa6470241f97ba6e01a5b457820587e5c08e4426b948f889f6921c556dbdd8438
|
|
7
|
+
data.tar.gz: 78d2faea9492b9f18bf7a0f5a0459a785319d0261c81f32c9fd218bb837b73550b79af8bd94373abc17f6e94db8b049a515308de2475905fda004a66de4cacc6
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,39 @@ Dalli Changelog
|
|
|
4
4
|
Unreleased
|
|
5
5
|
==========
|
|
6
6
|
|
|
7
|
+
5.1.1
|
|
8
|
+
==========
|
|
9
|
+
|
|
10
|
+
Security:
|
|
11
|
+
|
|
12
|
+
- Fix memcached command injection through numeric arguments (GHSA-6wmv-xq9m-fmp7)
|
|
13
|
+
- The `default` argument of `incr`/`decr`, and `fetch_with_lock`'s `lock_ttl` and `recache_threshold`, were written into the meta protocol command without conversion, so a String containing CRLF injected additional memcached commands (e.g. `set`, `flush_all`) on the connection
|
|
14
|
+
- These arguments must now be Integers, or Strings of decimal digits; anything else raises `ArgumentError` before a request is sent
|
|
15
|
+
- As defense in depth, `RequestFormatter` now converts every numeric flag it writes (`D`, `J`, `N`, `R`, `T`) to an Integer
|
|
16
|
+
- Affects 3.2.0 and later (3.2.x only with `protocol: :meta`); fixed in 5.1.1 and 4.3.4
|
|
17
|
+
- Thanks to oss-security-shop for the report
|
|
18
|
+
|
|
19
|
+
Performance:
|
|
20
|
+
|
|
21
|
+
- Reduce Ruby overhead on the single-key `get` path by about 28% (#1160)
|
|
22
|
+
- A plain `get` builds its `mg` request with one string interpolation instead of going through `meta_get`'s keyword arguments, and skips option handling when called without options
|
|
23
|
+
- A `VA <size> f<flags>` hit line is parsed in place instead of being split into tokens
|
|
24
|
+
- The key check for control characters and whitespace uses a byte class that matches the same ASCII bytes as `[\p{Cntrl}\s]`, about 5x faster; this applies to every operation that sends a key
|
|
25
|
+
- Allocations per `get` hit drop from 23 to 16
|
|
26
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
27
|
+
- Speed up multi-server `get_multi` by about 30% (4 servers, 100 keys), and bring small batches in line with 2.7.11 (#1161)
|
|
28
|
+
- Each server's queries and terminating noop are sent before the next server's are built, so memcached answers earlier servers while later ones are prepared
|
|
29
|
+
- A server's queries are built in one pass with `RequestFormatter.multi_meta_get`, and plain `get_multi` no longer requests the CAS value it discards (`get_multi_cas` still does)
|
|
30
|
+
- Pipelined replies are parsed in one pass over the returned flags
|
|
31
|
+
- Routing many keys checks each server's `alive?` once per call instead of twice per key, and the ring's binary search runs over plain integers
|
|
32
|
+
- Thanks to Julian Richard Contreras for this contribution
|
|
33
|
+
|
|
34
|
+
Development:
|
|
35
|
+
|
|
36
|
+
- Fix offenses reported by RuboCop 1.91 and require `rubocop >= 1.91` (#1162)
|
|
37
|
+
- RuboCop 1.91 adds `Style/DirectiveScope`; single-statement `disable`/`enable` pairs become `disable-next` directives, which older RuboCop versions do not recognize
|
|
38
|
+
- Removes a misplaced `# encoding: ascii` comment in `client.rb` that Ruby had always ignored
|
|
39
|
+
|
|
7
40
|
5.1.0
|
|
8
41
|
==========
|
|
9
42
|
|
data/Gemfile
CHANGED
data/lib/dalli/client.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'digest/md5'
|
|
4
4
|
|
|
5
|
-
# encoding: ascii
|
|
6
5
|
module Dalli
|
|
7
6
|
##
|
|
8
7
|
# Dalli::Client is the main class which developers will use to interact with
|
|
@@ -162,7 +161,7 @@ module Dalli
|
|
|
162
161
|
# silently omitting that server's keys from the result.
|
|
163
162
|
#
|
|
164
163
|
# @raise [Dalli::NetworkError] if a server is unreachable after retrying
|
|
165
|
-
# rubocop:disable Style/ExplicitBlockArgument
|
|
164
|
+
# rubocop:disable-next Style/ExplicitBlockArgument
|
|
166
165
|
def get_multi(*keys, req_options: nil)
|
|
167
166
|
keys.flatten!
|
|
168
167
|
keys.compact!
|
|
@@ -176,7 +175,6 @@ module Dalli
|
|
|
176
175
|
get_multi_hash(keys, req_options)
|
|
177
176
|
end
|
|
178
177
|
end
|
|
179
|
-
# rubocop:enable Style/ExplicitBlockArgument
|
|
180
178
|
|
|
181
179
|
##
|
|
182
180
|
# Fetch multiple keys efficiently, returning a stale-aware metadata Hash per
|
|
@@ -301,6 +299,8 @@ module Dalli
|
|
|
301
299
|
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
|
|
302
300
|
raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
|
|
303
301
|
|
|
302
|
+
validate_integer!(:lock_ttl, lock_ttl)
|
|
303
|
+
validate_integer!(:recache_threshold, recache_threshold)
|
|
304
304
|
validate_routing_tokens!(req_options)
|
|
305
305
|
key = key.to_s
|
|
306
306
|
key = @key_manager.validate_key(key)
|
|
@@ -534,6 +534,8 @@ module Dalli
|
|
|
534
534
|
##
|
|
535
535
|
# Incr adds the given amount to the counter on the memcached server.
|
|
536
536
|
# Amt must be a positive integer value.
|
|
537
|
+
# Default, if given, must be an Integer (or a String of decimal digits);
|
|
538
|
+
# anything else raises ArgumentError.
|
|
537
539
|
#
|
|
538
540
|
# If default is nil, the counter must already exist or the operation
|
|
539
541
|
# will fail and will return nil. Otherwise this method will return
|
|
@@ -546,6 +548,7 @@ module Dalli
|
|
|
546
548
|
# If the value already exists, it must have been set with raw: true
|
|
547
549
|
def incr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
548
550
|
check_positive!(amt)
|
|
551
|
+
validate_integer!(:default, default)
|
|
549
552
|
validate_routing_tokens!(req_options)
|
|
550
553
|
|
|
551
554
|
perform(:incr, key, amt.to_i, ttl_or_default(ttl), default, req_options)
|
|
@@ -554,6 +557,8 @@ module Dalli
|
|
|
554
557
|
##
|
|
555
558
|
# Decr subtracts the given amount from the counter on the memcached server.
|
|
556
559
|
# Amt must be a positive integer value.
|
|
560
|
+
# Default, if given, must be an Integer (or a String of decimal digits);
|
|
561
|
+
# anything else raises ArgumentError.
|
|
557
562
|
#
|
|
558
563
|
# memcached counters are unsigned and cannot hold negative values. Calling
|
|
559
564
|
# decr on a counter which is 0 will just return 0.
|
|
@@ -569,6 +574,7 @@ module Dalli
|
|
|
569
574
|
# If the value already exists, it must have been set with raw: true
|
|
570
575
|
def decr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
571
576
|
check_positive!(amt)
|
|
577
|
+
validate_integer!(:default, default)
|
|
572
578
|
validate_routing_tokens!(req_options)
|
|
573
579
|
|
|
574
580
|
perform(:decr, key, amt.to_i, ttl_or_default(ttl), default, req_options)
|
|
@@ -632,6 +638,7 @@ module Dalli
|
|
|
632
638
|
alias reset close
|
|
633
639
|
|
|
634
640
|
CACHE_NILS = { cache_nils: true }.freeze
|
|
641
|
+
EMPTY_ATTRIBUTES = {}.freeze
|
|
635
642
|
|
|
636
643
|
def not_found?(val)
|
|
637
644
|
cache_nils ? val == ::Dalli::NOT_FOUND : val.nil?
|
|
@@ -682,7 +689,7 @@ module Dalli
|
|
|
682
689
|
def get_multi_yielding(keys, req_options = nil)
|
|
683
690
|
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
684
691
|
hit_count = 0
|
|
685
|
-
pipelined_getter.process(keys, req_options) do |k, data|
|
|
692
|
+
pipelined_getter.process(keys, req_options, return_cas: false) do |k, data|
|
|
686
693
|
hit_count += 1
|
|
687
694
|
yield k, data.first
|
|
688
695
|
end
|
|
@@ -697,7 +704,7 @@ module Dalli
|
|
|
697
704
|
single_server_get_multi(keys, req_options)
|
|
698
705
|
else
|
|
699
706
|
{}.tap do |h|
|
|
700
|
-
pipelined_getter.process(keys, req_options) { |k, data| h[k] = data.first }
|
|
707
|
+
pipelined_getter.process(keys, req_options, return_cas: false) { |k, data| h[k] = data.first }
|
|
701
708
|
end
|
|
702
709
|
end
|
|
703
710
|
record_hit_miss_metrics(span, keys.size, hash.size)
|
|
@@ -765,8 +772,9 @@ module Dalli
|
|
|
765
772
|
retry
|
|
766
773
|
end
|
|
767
774
|
|
|
775
|
+
# Only built when tracing is on, since the Hash is thrown away otherwise
|
|
768
776
|
def get_multi_attributes(keys)
|
|
769
|
-
multi_trace_attrs('get_multi', keys.size, keys)
|
|
777
|
+
Instrumentation.enabled? ? multi_trace_attrs('get_multi', keys.size, keys) : EMPTY_ATTRIBUTES
|
|
770
778
|
end
|
|
771
779
|
|
|
772
780
|
def trace_attrs(operation, key, server)
|
|
@@ -796,6 +804,18 @@ module Dalli
|
|
|
796
804
|
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
|
797
805
|
end
|
|
798
806
|
|
|
807
|
+
# Numeric arguments that become meta protocol flags (GHSA-6wmv-xq9m-fmp7).
|
|
808
|
+
# RequestFormatter converts them to Integer as the wire-level backstop;
|
|
809
|
+
# checking here too gives the caller a clean ArgumentError for the same
|
|
810
|
+
# reason as routing tokens, described below.
|
|
811
|
+
def validate_integer!(name, value)
|
|
812
|
+
return if value.nil?
|
|
813
|
+
|
|
814
|
+
value.is_a?(String) ? Integer(value, 10) : Integer(value)
|
|
815
|
+
rescue ArgumentError, TypeError, FloatDomainError
|
|
816
|
+
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
817
|
+
end
|
|
818
|
+
|
|
799
819
|
# Validated here, before the request reaches Protocol::Base#request, rather
|
|
800
820
|
# than only at the RequestFormatter level. Reaching only the formatter's
|
|
801
821
|
# check means unwinding through Protocol::Base#request, which logs the
|
|
@@ -22,16 +22,19 @@ module Dalli
|
|
|
22
22
|
#
|
|
23
23
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
24
24
|
#
|
|
25
|
+
# With return_cas: false the CAS value is not requested, and each yielded
|
|
26
|
+
# CAS is 0.
|
|
27
|
+
#
|
|
25
28
|
# A transient network error is retried automatically. If a server remains
|
|
26
29
|
# unreachable after retrying, raises Dalli::NetworkError.
|
|
27
30
|
#
|
|
28
|
-
def process(keys, req_options = nil, &block)
|
|
31
|
+
def process(keys, req_options = nil, return_cas: true, &block)
|
|
29
32
|
return {} if keys.empty?
|
|
30
33
|
|
|
31
34
|
@ring.lock do
|
|
32
35
|
# Stores partial results collected during interleaved send phase
|
|
33
36
|
@partial_results = {}
|
|
34
|
-
servers = setup_requests(keys, req_options)
|
|
37
|
+
servers = setup_requests(keys, req_options, return_cas: return_cas)
|
|
35
38
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
36
39
|
|
|
37
40
|
# First yield any partial results collected during interleaved send
|
|
@@ -88,71 +91,55 @@ module Dalli
|
|
|
88
91
|
@partial_results.clear
|
|
89
92
|
end
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
# Sends each server's queries and its terminating noop before building
|
|
95
|
+
# the next server's, so memcached is already answering the first servers
|
|
96
|
+
# while the rest are prepared. Returns the servers with a pending response.
|
|
97
|
+
def setup_requests(keys, req_options = nil, return_cas: true)
|
|
98
|
+
started = []
|
|
99
|
+
groups_for_keys(keys).each do |server, keys_for_server|
|
|
100
|
+
make_getkq_request(server, keys_for_server, req_options, return_cas: return_cas)
|
|
101
|
+
next unless server.connected?
|
|
94
102
|
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
started << server
|
|
104
|
+
finish_query_for_server(server)
|
|
105
|
+
rescue Dalli::NetworkError
|
|
106
|
+
abort_without_timeout(started)
|
|
107
|
+
raise
|
|
108
|
+
rescue Dalli::DalliError
|
|
109
|
+
started.delete(server)
|
|
110
|
+
end
|
|
111
|
+
started
|
|
97
112
|
end
|
|
98
113
|
|
|
99
114
|
##
|
|
100
|
-
#
|
|
101
|
-
# the corresponding getkq requests to the appropriate servers
|
|
115
|
+
# Writes the getkq requests for one server's keys
|
|
102
116
|
#
|
|
103
117
|
# It's worth noting that we could potentially reduce bytes
|
|
104
118
|
# on the wire by switching from getkq to getq, and using
|
|
105
119
|
# the opaque value to match requests to responses.
|
|
106
120
|
##
|
|
107
|
-
def
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results, req_options)
|
|
116
|
-
end
|
|
117
|
-
# NetworkError (which RetryableNetworkError subclasses) must propagate:
|
|
118
|
-
# #process's top-level rescue retries the whole pipelined get on it. This
|
|
119
|
-
# rescue used to catch DalliError and NetworkError together -- since
|
|
120
|
-
# NetworkError < DalliError, that silently swallowed RetryableNetworkError
|
|
121
|
-
# too, dropping this server's keys from the result on a transient hiccup
|
|
122
|
-
# instead of retrying, with nothing surfaced above debug-level logging.
|
|
123
|
-
# Only a non-network DalliError (this server genuinely can't serve these
|
|
124
|
-
# keys) should be swallowed here.
|
|
125
|
-
rescue Dalli::NetworkError
|
|
126
|
-
raise
|
|
127
|
-
rescue DalliError => e
|
|
128
|
-
Dalli.logger.debug { e.inspect }
|
|
129
|
-
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
##
|
|
134
|
-
# This loops through the servers that have keys in
|
|
135
|
-
# our set, sending the noop to terminate the set of queries.
|
|
136
|
-
##
|
|
137
|
-
def finish_queries(servers)
|
|
138
|
-
deleted = Set.new
|
|
139
|
-
|
|
140
|
-
servers.each do |server|
|
|
141
|
-
next unless server.connected?
|
|
142
|
-
|
|
143
|
-
begin
|
|
144
|
-
finish_query_for_server(server)
|
|
145
|
-
rescue Dalli::NetworkError
|
|
146
|
-
raise
|
|
147
|
-
rescue Dalli::DalliError
|
|
148
|
-
deleted << server
|
|
149
|
-
end
|
|
121
|
+
def make_getkq_request(server, keys_for_server, req_options = nil, return_cas: true)
|
|
122
|
+
if keys_for_server.size <= INTERLEAVE_THRESHOLD
|
|
123
|
+
# Small batch - send all at once (existing behavior)
|
|
124
|
+
server.request(:pipelined_get, keys_for_server, req_options, return_cas)
|
|
125
|
+
else
|
|
126
|
+
# Large batch - interleave sends with response draining
|
|
127
|
+
# Pass @partial_results directly to avoid hash allocation/merge overhead
|
|
128
|
+
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results, req_options)
|
|
150
129
|
end
|
|
151
|
-
|
|
152
|
-
|
|
130
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate:
|
|
131
|
+
# #process's top-level rescue retries the whole pipelined get on it. This
|
|
132
|
+
# rescue used to catch DalliError and NetworkError together -- since
|
|
133
|
+
# NetworkError < DalliError, that silently swallowed RetryableNetworkError
|
|
134
|
+
# too, dropping this server's keys from the result on a transient hiccup
|
|
135
|
+
# instead of retrying, with nothing surfaced above debug-level logging.
|
|
136
|
+
# Only a non-network DalliError (this server genuinely can't serve these
|
|
137
|
+
# keys) should be swallowed here.
|
|
153
138
|
rescue Dalli::NetworkError
|
|
154
|
-
abort_without_timeout(servers)
|
|
155
139
|
raise
|
|
140
|
+
rescue DalliError => e
|
|
141
|
+
Dalli.logger.debug { e.inspect }
|
|
142
|
+
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
156
143
|
end
|
|
157
144
|
|
|
158
145
|
def finish_query_for_server(server)
|
|
@@ -230,8 +217,9 @@ module Dalli
|
|
|
230
217
|
return [] if readable.nil?
|
|
231
218
|
|
|
232
219
|
# For typical server counts (1-5), linear scan is faster than
|
|
233
|
-
# building and looking up a hash map
|
|
234
|
-
|
|
220
|
+
# building and looking up a hash map. Array#index scans in C and
|
|
221
|
+
# reuses the sockets already fetched above.
|
|
222
|
+
readable.filter_map { |sock| (idx = sockets.index(sock)) && servers[idx] }
|
|
235
223
|
end
|
|
236
224
|
|
|
237
225
|
def groups_for_keys(*keys)
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -17,8 +17,24 @@ module Dalli
|
|
|
17
17
|
attr_accessor :weight, :options
|
|
18
18
|
|
|
19
19
|
def_delegators :@value_marshaller, :serializer, :compressor, :compression_min_size, :compress_by_default?
|
|
20
|
-
def_delegators :@connection_manager, :name, :
|
|
21
|
-
:socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
|
|
20
|
+
def_delegators :@connection_manager, :name, :hostname, :port, :close, :socket_timeout,
|
|
21
|
+
:socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
|
|
22
|
+
|
|
23
|
+
# Delegated by hand rather than with def_delegators because they run on
|
|
24
|
+
# every request, and a plain method call is cheaper than a Forwardable one.
|
|
25
|
+
def connected?
|
|
26
|
+
@connection_manager.connected?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def flushed_write(bytes)
|
|
30
|
+
@connection_manager.flushed_write(bytes)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Written out rather than delegated: a multi-server get_multi calls it for
|
|
34
|
+
# every server, and a plain method call is cheaper than a Forwardable one.
|
|
35
|
+
def sock
|
|
36
|
+
@connection_manager.sock
|
|
37
|
+
end
|
|
22
38
|
|
|
23
39
|
def initialize(attribs, client_options = {})
|
|
24
40
|
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
|
@@ -257,12 +273,15 @@ module Dalli
|
|
|
257
273
|
# wire-formatter level, where it can raise uniformly regardless of how
|
|
258
274
|
# the token reached the formatter.
|
|
259
275
|
def routing_token_kwargs(opts)
|
|
260
|
-
return {} unless
|
|
261
|
-
return {} unless opts[:p_token] || opts[:l_token]
|
|
276
|
+
return {} unless routing_tokens?(opts)
|
|
262
277
|
|
|
263
278
|
{ p_token: opts[:p_token], l_token: opts[:l_token] }
|
|
264
279
|
end
|
|
265
280
|
|
|
281
|
+
def routing_tokens?(opts)
|
|
282
|
+
opts.is_a?(Hash) && (opts[:p_token] || opts[:l_token]) ? true : false
|
|
283
|
+
end
|
|
284
|
+
|
|
266
285
|
# Maps the client-facing meta-delete options onto RequestFormatter's flag
|
|
267
286
|
# names, so they can be splatted into a meta_delete call. Returns {} when
|
|
268
287
|
# none are set, keeping the splat a no-op on the common path.
|
|
@@ -287,18 +306,17 @@ module Dalli
|
|
|
287
306
|
up!
|
|
288
307
|
end
|
|
289
308
|
|
|
290
|
-
|
|
309
|
+
# return_cas = false leaves the c flag off each request, for callers
|
|
310
|
+
# (plain get_multi) that discard the CAS value. It is positional because
|
|
311
|
+
# #request forwards only positional arguments.
|
|
312
|
+
def pipelined_get(keys, options = nil, return_cas = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
291
313
|
# Clear buffer to remove any stale data from interrupted operations.
|
|
292
314
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
293
315
|
# the expected state before pipeline_response_setup is called.
|
|
294
316
|
response_buffer.clear
|
|
295
317
|
|
|
296
|
-
|
|
297
|
-
keys
|
|
298
|
-
req << quiet_get_request(key, options)
|
|
299
|
-
end
|
|
300
|
-
# Could send noop here instead of in pipeline_response_setup
|
|
301
|
-
write(req)
|
|
318
|
+
# The terminating noop is sent by pipeline_response_setup
|
|
319
|
+
write(quiet_get_requests(keys, options, return_cas: return_cas))
|
|
302
320
|
end
|
|
303
321
|
|
|
304
322
|
# For large batches, interleave writing requests with draining responses.
|
|
@@ -29,8 +29,15 @@ module Dalli
|
|
|
29
29
|
# in one -- fatal here, since this suite's -w run treats warnings as
|
|
30
30
|
# errors (see test_strict_warnings.rb). \p{Cntrl} matches the same
|
|
31
31
|
# bytes without the overlap warning.
|
|
32
|
+
#
|
|
33
|
+
# The ascii_only? check runs first, so the regexp only ever sees ASCII
|
|
34
|
+
# keys. Over ASCII, [\p{Cntrl}\s] is exactly 0x00-0x20 plus 0x7F, and
|
|
35
|
+
# the plain byte class below is about 5x faster to match.
|
|
36
|
+
ASCII_CNTRL_OR_SPACE = /[\x00-\x20\x7F]/
|
|
37
|
+
private_constant :ASCII_CNTRL_OR_SPACE
|
|
38
|
+
|
|
32
39
|
def required?(key)
|
|
33
|
-
!key.ascii_only? ||
|
|
40
|
+
!key.ascii_only? || ASCII_CNTRL_OR_SPACE.match?(key)
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
def encode(key)
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -24,9 +24,19 @@ module Dalli
|
|
|
24
24
|
|
|
25
25
|
# Retrieval Commands
|
|
26
26
|
def get(key, options = nil)
|
|
27
|
+
# Fast path for the common case of no per-request options (nil or false)
|
|
28
|
+
unless options
|
|
29
|
+
flushed_write(RequestFormatter.plain_meta_get(key, raw_mode?))
|
|
30
|
+
return response_processor.meta_get_with_value
|
|
31
|
+
end
|
|
32
|
+
|
|
27
33
|
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
28
|
-
skip_flags = raw_mode? ||
|
|
29
|
-
req =
|
|
34
|
+
skip_flags = raw_mode? || options[:raw]
|
|
35
|
+
req = if routing_tokens?(options)
|
|
36
|
+
RequestFormatter.meta_get(key: key, skip_flags: skip_flags, **routing_token_kwargs(options))
|
|
37
|
+
else
|
|
38
|
+
RequestFormatter.plain_meta_get(key, skip_flags)
|
|
39
|
+
end
|
|
30
40
|
flushed_write(req)
|
|
31
41
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
32
42
|
end
|
|
@@ -37,6 +47,13 @@ module Dalli
|
|
|
37
47
|
**routing_token_kwargs(options))
|
|
38
48
|
end
|
|
39
49
|
|
|
50
|
+
# Same requests as quiet_get_request for each key, built in one pass
|
|
51
|
+
# and without the trailing noop.
|
|
52
|
+
def quiet_get_requests(keys, options = nil, return_cas: true)
|
|
53
|
+
RequestFormatter.multi_meta_get(keys, skip_flags: raw_mode?, return_cas: return_cas, terminate: false,
|
|
54
|
+
**routing_token_kwargs(options))
|
|
55
|
+
end
|
|
56
|
+
|
|
40
57
|
def gat(key, ttl, options = nil)
|
|
41
58
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
42
59
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
@@ -159,7 +176,7 @@ module Dalli
|
|
|
159
176
|
response_processor.meta_set_append_prepend unless quiet?
|
|
160
177
|
end
|
|
161
178
|
|
|
162
|
-
# rubocop:disable Metrics/ParameterLists
|
|
179
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
163
180
|
def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, options = nil)
|
|
164
181
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
165
182
|
req = RequestFormatter.meta_set(key: key, value: value,
|
|
@@ -168,7 +185,6 @@ module Dalli
|
|
|
168
185
|
write("#{req}#{value}#{TERMINATOR}")
|
|
169
186
|
@connection_manager.flush unless quiet?
|
|
170
187
|
end
|
|
171
|
-
# rubocop:enable Metrics/ParameterLists
|
|
172
188
|
|
|
173
189
|
# Delete Commands
|
|
174
190
|
#
|
|
@@ -204,7 +220,7 @@ module Dalli
|
|
|
204
220
|
decr_incr true, key, count, ttl, initial, options
|
|
205
221
|
end
|
|
206
222
|
|
|
207
|
-
# rubocop:disable Metrics/ParameterLists
|
|
223
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
208
224
|
def decr_incr(incr, key, delta, ttl, initial, options = nil)
|
|
209
225
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
210
226
|
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
@@ -212,7 +228,6 @@ module Dalli
|
|
|
212
228
|
@connection_manager.flush unless quiet?
|
|
213
229
|
response_processor.decr_incr unless quiet?
|
|
214
230
|
end
|
|
215
|
-
# rubocop:enable Metrics/ParameterLists
|
|
216
231
|
|
|
217
232
|
# Other Commands
|
|
218
233
|
def flush(delay = 0)
|
|
@@ -43,11 +43,12 @@ module Dalli
|
|
|
43
43
|
# This saves 2 bytes per request and skips parsing on response.
|
|
44
44
|
cmd << (skip_flags ? ' v' : ' v f') if value
|
|
45
45
|
cmd << ' c' if return_cas
|
|
46
|
-
cmd << " T#{ttl}" if ttl
|
|
46
|
+
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
47
47
|
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
48
48
|
cmd << ' k q s' if quiet # Return the key in the response if quiet
|
|
49
|
-
cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
|
|
50
|
-
|
|
49
|
+
cmd << " N#{integer_flag(:vivify_ttl, vivify_ttl)}" if vivify_ttl # Thundering herd: vivify on miss
|
|
50
|
+
# Thundering herd: win recache if TTL below threshold
|
|
51
|
+
cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
|
|
51
52
|
cmd << ' h' if return_hit_status # Return hit status (0 or 1)
|
|
52
53
|
cmd << ' l' if return_last_access # Return seconds since last access
|
|
53
54
|
cmd << ' t' if return_ttl_remaining # Return seconds of TTL remaining (-1 = no TTL)
|
|
@@ -55,7 +56,16 @@ module Dalli
|
|
|
55
56
|
cmd << TERMINATOR
|
|
56
57
|
end
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
# Fast path for the common single-key get with no optional flags.
|
|
60
|
+
# Produces the same bytes as meta_get(key: key, skip_flags: skip_flags)
|
|
61
|
+
# without the keyword-argument handling and incremental string building.
|
|
62
|
+
def plain_meta_get(key, skip_flags)
|
|
63
|
+
skip_flags ? "mg #{encoded_key(key)} v#{TERMINATOR}" : "mg #{encoded_key(key)} v f#{TERMINATOR}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Pass terminate: false to leave off the trailing noop, for callers
|
|
67
|
+
# (the pipelined get) that send it separately.
|
|
68
|
+
def multi_meta_get(keys, skip_flags: false, return_cas: false, terminate: true, p_token: nil, l_token: nil)
|
|
59
69
|
# In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
|
|
60
70
|
# Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
|
|
61
71
|
# With return_cas a "c" flag follows, which shifts those indexes --
|
|
@@ -74,7 +84,7 @@ module Dalli
|
|
|
74
84
|
keys.each do |key|
|
|
75
85
|
buffer << 'mg ' << encoded_key(key) << post_get
|
|
76
86
|
end
|
|
77
|
-
buffer << 'mn' << TERMINATOR
|
|
87
|
+
terminate ? buffer << 'mn' << TERMINATOR : buffer
|
|
78
88
|
end
|
|
79
89
|
|
|
80
90
|
def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false,
|
|
@@ -88,7 +98,7 @@ module Dalli
|
|
|
88
98
|
cmd << ' b' if base64
|
|
89
99
|
cmd << " F#{bitflags}" if bitflags
|
|
90
100
|
cmd << cas_string(cas)
|
|
91
|
-
cmd << " T#{ttl}" if ttl
|
|
101
|
+
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
92
102
|
cmd << " M#{mode_to_token(mode)}"
|
|
93
103
|
cmd << ' q' if quiet
|
|
94
104
|
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
@@ -99,6 +109,7 @@ module Dalli
|
|
|
99
109
|
# Routing tokens apply to every entry in the batch, so the suffix is
|
|
100
110
|
# built once rather than per entry.
|
|
101
111
|
token_suffix = routing_tokens(p_token: p_token, l_token: l_token)
|
|
112
|
+
ttl = integer_flag(:ttl, ttl) if ttl
|
|
102
113
|
|
|
103
114
|
buffer = ''.b
|
|
104
115
|
entries.each do |key, pair|
|
|
@@ -140,7 +151,7 @@ module Dalli
|
|
|
140
151
|
cmd = "md #{encoded_key(key)}"
|
|
141
152
|
cmd << cas_string(cas)
|
|
142
153
|
cmd << ' I' if stale # Mark stale instead of deleting
|
|
143
|
-
cmd << " T#{
|
|
154
|
+
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
144
155
|
cmd << ' x' if drop_value # Drop the value but keep the item
|
|
145
156
|
cmd << ' q' if quiet
|
|
146
157
|
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
@@ -154,7 +165,7 @@ module Dalli
|
|
|
154
165
|
|
|
155
166
|
suffix = +''
|
|
156
167
|
suffix << ' I' if stale
|
|
157
|
-
suffix << " T#{
|
|
168
|
+
suffix << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
158
169
|
suffix << ' x' if drop_value
|
|
159
170
|
suffix << ' q'
|
|
160
171
|
suffix << routing_tokens(p_token: p_token, l_token: l_token)
|
|
@@ -170,10 +181,10 @@ module Dalli
|
|
|
170
181
|
def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false,
|
|
171
182
|
p_token: nil, l_token: nil)
|
|
172
183
|
cmd = "ma #{encoded_key(key)} v"
|
|
173
|
-
cmd << " D#{delta}" if delta
|
|
174
|
-
cmd << " J#{initial}" if initial
|
|
184
|
+
cmd << " D#{integer_flag(:delta, delta)}" if delta
|
|
185
|
+
cmd << " J#{integer_flag(:initial, initial)}" if initial
|
|
175
186
|
# Always set a TTL if an initial value is specified
|
|
176
|
-
cmd << " N#{ttl
|
|
187
|
+
cmd << " N#{ttl ? integer_flag(:ttl, ttl) : 0}" if ttl || initial
|
|
177
188
|
cmd << cas_string(cas)
|
|
178
189
|
cmd << ' q' if quiet
|
|
179
190
|
cmd << " M#{incr ? 'I' : 'D'}"
|
|
@@ -261,6 +272,17 @@ module Dalli
|
|
|
261
272
|
raise ArgumentError, "#{name} must not contain CRLF or null bytes" if value.match?(ROUTING_TOKEN_FORBIDDEN)
|
|
262
273
|
end
|
|
263
274
|
|
|
275
|
+
# Numeric flag values are written straight into the command line, so a
|
|
276
|
+
# value that isn't an integer -- e.g. a String carrying CRLF -- would let
|
|
277
|
+
# the caller inject further memcached commands. Converting to Integer
|
|
278
|
+
# means only digits ever reach the wire. Strings are parsed as base 10 so
|
|
279
|
+
# "010" means 10, as memcached would read it, rather than octal 8.
|
|
280
|
+
def integer_flag(name, value)
|
|
281
|
+
value.is_a?(String) ? Integer(value, 10) : Integer(value)
|
|
282
|
+
rescue ArgumentError, TypeError, FloatDomainError
|
|
283
|
+
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
284
|
+
end
|
|
285
|
+
|
|
264
286
|
def mode_to_token(mode)
|
|
265
287
|
case mode
|
|
266
288
|
when :add
|
|
@@ -23,6 +23,14 @@ module Dalli
|
|
|
23
23
|
VERSION = 'VERSION'
|
|
24
24
|
SERVER_ERROR = 'SERVER_ERROR'
|
|
25
25
|
|
|
26
|
+
VA_PREFIX = 'VA '
|
|
27
|
+
FLAGS_TOKEN_PREFIX = ' f'
|
|
28
|
+
BYTE_B = 'b'.ord
|
|
29
|
+
BYTE_C = 'c'.ord
|
|
30
|
+
BYTE_F = 'f'.ord
|
|
31
|
+
BYTE_K = 'k'.ord
|
|
32
|
+
BYTE_S = 's'.ord
|
|
33
|
+
|
|
26
34
|
T_OK = [OK].freeze
|
|
27
35
|
T_RESET = [RESET].freeze
|
|
28
36
|
T_EN_HD = [EN, HD].freeze
|
|
@@ -39,11 +47,24 @@ module Dalli
|
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def meta_get_with_value(cache_nils: false)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
line = read_line
|
|
51
|
+
# A hit ("VA <size> f<flags>") is parsed in place rather than split
|
|
52
|
+
# into tokens, which saves several allocations on the hottest path.
|
|
53
|
+
if line&.start_with?(VA_PREFIX)
|
|
54
|
+
return @value_marshaller.retrieve(read_data(size_from_va_line(line)), bitflags_from_va_line(line))
|
|
55
|
+
end
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
tokens = line&.split || []
|
|
58
|
+
case tokens.first
|
|
59
|
+
when EN
|
|
60
|
+
cache_nils ? ::Dalli::NOT_FOUND : nil
|
|
61
|
+
when VA # only reached for an unusually formatted hit line
|
|
62
|
+
@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
|
|
63
|
+
when HD
|
|
64
|
+
true
|
|
65
|
+
else
|
|
66
|
+
raise_unexpected!(tokens)
|
|
67
|
+
end
|
|
47
68
|
end
|
|
48
69
|
|
|
49
70
|
def meta_get_with_value_and_cas
|
|
@@ -181,11 +202,6 @@ module Dalli
|
|
|
181
202
|
non_deletions
|
|
182
203
|
end
|
|
183
204
|
|
|
184
|
-
def full_response_from_buffer(tokens, body, resp_size)
|
|
185
|
-
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
186
|
-
[tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value, resp_size]
|
|
187
|
-
end
|
|
188
|
-
|
|
189
205
|
##
|
|
190
206
|
# This method returns an array of values used in a pipelined
|
|
191
207
|
# getk process. The first value is the number of bytes by
|
|
@@ -205,8 +221,20 @@ module Dalli
|
|
|
205
221
|
tokens = header.split
|
|
206
222
|
header_len = header.bytesize + TERMINATOR.length
|
|
207
223
|
|
|
208
|
-
#
|
|
209
|
-
|
|
224
|
+
# Read the s, f, c and k flags and the b marker in one pass. As with
|
|
225
|
+
# value_from_tokens, the first token for each flag wins.
|
|
226
|
+
size = bitflags = cas = key = nil
|
|
227
|
+
base64 = false
|
|
228
|
+
tokens.each do |token|
|
|
229
|
+
case token.getbyte(0)
|
|
230
|
+
when BYTE_S then size ||= token
|
|
231
|
+
when BYTE_F then bitflags ||= token
|
|
232
|
+
when BYTE_C then cas ||= token
|
|
233
|
+
when BYTE_K then key ||= token
|
|
234
|
+
when BYTE_B then base64 ||= token == 'b'
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
body_len = flag_int(size, 's')
|
|
210
238
|
|
|
211
239
|
# We have a complete response that has no body.
|
|
212
240
|
# This is either the response to the terminating
|
|
@@ -222,7 +250,16 @@ module Dalli
|
|
|
222
250
|
# The full response is in our buffer, so parse it and return
|
|
223
251
|
# the values
|
|
224
252
|
body = buf.byteslice(offset + header_len, body_len)
|
|
225
|
-
|
|
253
|
+
value = @value_marshaller.retrieve(body, flag_int(bitflags, 'f'))
|
|
254
|
+
key = key ? key.delete_prefix!('k') : 0
|
|
255
|
+
key = KeyRegularizer.decode(key) if base64
|
|
256
|
+
[tokens.first == VA, flag_int(cas, 'c'), key, value, resp_size]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Integer value of a flag token such as "f123", or 0 when absent.
|
|
260
|
+
# Strips the prefix in place, like value_from_tokens.
|
|
261
|
+
def flag_int(token, flag)
|
|
262
|
+
token ? token.delete_prefix!(flag).to_i : 0
|
|
226
263
|
end
|
|
227
264
|
|
|
228
265
|
def error_on_unexpected!(expected_codes)
|
|
@@ -230,6 +267,10 @@ module Dalli
|
|
|
230
267
|
|
|
231
268
|
return tokens if expected_codes.include?(tokens.first)
|
|
232
269
|
|
|
270
|
+
raise_unexpected!(tokens)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def raise_unexpected!(tokens)
|
|
233
274
|
raise Dalli::ServerError, tokens.join(' ').to_s if tokens.first == SERVER_ERROR
|
|
234
275
|
|
|
235
276
|
raise Dalli::DalliError, "Response error: #{tokens.first}"
|
|
@@ -239,6 +280,17 @@ module Dalli
|
|
|
239
280
|
value_from_tokens(tokens, 'f').to_i
|
|
240
281
|
end
|
|
241
282
|
|
|
283
|
+
# String#to_i stops at the first non-digit, so these read one token's
|
|
284
|
+
# integer straight out of the header line without splitting it.
|
|
285
|
+
def size_from_va_line(line)
|
|
286
|
+
line.byteslice(VA_PREFIX.bytesize, line.bytesize).to_i
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def bitflags_from_va_line(line)
|
|
290
|
+
idx = line.index(FLAGS_TOKEN_PREFIX, VA_PREFIX.bytesize)
|
|
291
|
+
idx ? line.byteslice(idx + FLAGS_TOKEN_PREFIX.bytesize, line.bytesize).to_i : 0
|
|
292
|
+
end
|
|
293
|
+
|
|
242
294
|
def cas_from_tokens(tokens)
|
|
243
295
|
value_from_tokens(tokens, 'c').to_i
|
|
244
296
|
end
|
|
@@ -281,10 +333,6 @@ module Dalli
|
|
|
281
333
|
value_from_tokens(tokens, 't').to_i
|
|
282
334
|
end
|
|
283
335
|
|
|
284
|
-
def body_len_from_tokens(tokens)
|
|
285
|
-
value_from_tokens(tokens, 's').to_i
|
|
286
|
-
end
|
|
287
|
-
|
|
288
336
|
def value_from_tokens(tokens, flag)
|
|
289
337
|
# NB: as an optimization, we're mutating the matching token in place
|
|
290
338
|
# so there is a baked assumption that we're only accessing each token at most once.
|
data/lib/dalli/ring.rb
CHANGED
|
@@ -21,41 +21,46 @@ module Dalli
|
|
|
21
21
|
# in an equally weighted scenario.
|
|
22
22
|
POINTS_PER_SERVER = 160 # this is the default in libmemcached
|
|
23
23
|
|
|
24
|
-
attr_accessor :servers
|
|
24
|
+
attr_accessor :servers
|
|
25
|
+
attr_reader :continuum
|
|
25
26
|
|
|
26
27
|
def initialize(servers_arg, options)
|
|
27
28
|
@servers = servers_arg.map do |s|
|
|
28
29
|
Dalli::Protocol::Meta.new(s, options)
|
|
29
30
|
end
|
|
30
|
-
|
|
31
|
-
@continuum = build_continuum(servers) if servers.size > 1
|
|
31
|
+
self.continuum = (build_continuum(servers) if servers.size > 1)
|
|
32
32
|
|
|
33
33
|
threadsafe! unless options[:threadsafe] == false
|
|
34
34
|
@failover = options[:failover] != false
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def
|
|
37
|
+
def continuum=(entries)
|
|
38
|
+
@continuum = entries
|
|
39
|
+
# Plain integers, so the binary search in server_for_hash_key needn't
|
|
40
|
+
# call Entry#value at each step
|
|
41
|
+
@continuum_values = entries&.map(&:value)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# alive_cache (optional) remembers each server's alive? result, so a
|
|
45
|
+
# caller routing many keys checks each server once rather than per key.
|
|
46
|
+
def server_for_key(key, alive_cache = nil)
|
|
47
|
+
# server_from_continuum only returns a server that is alive
|
|
38
48
|
server = if @continuum
|
|
39
|
-
server_from_continuum(key)
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
server_from_continuum(key, alive_cache)
|
|
50
|
+
elsif (first = @servers.first) && server_alive?(first, alive_cache)
|
|
51
|
+
first
|
|
42
52
|
end
|
|
43
|
-
|
|
44
|
-
# Note that the call to alive? has the side effect of initializing
|
|
45
|
-
# the socket
|
|
46
|
-
return server if server&.alive?
|
|
53
|
+
return server if server
|
|
47
54
|
|
|
48
55
|
raise Dalli::RingError, 'No server available'
|
|
49
56
|
end
|
|
50
57
|
|
|
51
|
-
def server_from_continuum(key)
|
|
58
|
+
def server_from_continuum(key, alive_cache = nil)
|
|
52
59
|
hkey = hash_for(key)
|
|
53
60
|
20.times do |try|
|
|
54
61
|
server = server_for_hash_key(hkey)
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
# the socket
|
|
58
|
-
return server if server.alive?
|
|
63
|
+
return server if server_alive?(server, alive_cache)
|
|
59
64
|
break unless @failover
|
|
60
65
|
|
|
61
66
|
hkey = hash_for("#{try}#{key}")
|
|
@@ -64,8 +69,9 @@ module Dalli
|
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
def keys_grouped_by_server(key_arr)
|
|
72
|
+
alive_cache = {}.compare_by_identity
|
|
67
73
|
key_arr.group_by do |key|
|
|
68
|
-
server_for_key(key)
|
|
74
|
+
server_for_key(key, alive_cache)
|
|
69
75
|
rescue Dalli::RingError
|
|
70
76
|
Dalli.logger.debug { "unable to get key #{key}" }
|
|
71
77
|
nil
|
|
@@ -110,13 +116,21 @@ module Dalli
|
|
|
110
116
|
Zlib.crc32(key)
|
|
111
117
|
end
|
|
112
118
|
|
|
119
|
+
# Note that the call to alive? has the side effect of initializing
|
|
120
|
+
# the socket
|
|
121
|
+
def server_alive?(server, alive_cache)
|
|
122
|
+
return server.alive? unless alive_cache
|
|
123
|
+
|
|
124
|
+
alive_cache.fetch(server) { alive_cache[server] = server.alive? }
|
|
125
|
+
end
|
|
126
|
+
|
|
113
127
|
def entry_count_for(server, total_servers, total_weight)
|
|
114
128
|
((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
|
|
115
129
|
end
|
|
116
130
|
|
|
117
131
|
def server_for_hash_key(hash_key)
|
|
118
132
|
# Find the closest index in the Ring with value <= the given value
|
|
119
|
-
entryidx = @
|
|
133
|
+
entryidx = @continuum_values.bsearch_index { |value| value > hash_key }
|
|
120
134
|
if entryidx.nil?
|
|
121
135
|
entryidx = @continuum.size - 1
|
|
122
136
|
else
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -124,7 +124,7 @@ module Dalli
|
|
|
124
124
|
# Returns true for an unmodified TCPSocket on Ruby 3.0+, or for resolv-replace >= 0.2.0
|
|
125
125
|
# which forwards keyword arguments through its patch.
|
|
126
126
|
# Returns false when monkey-patched by gems like socksify or resolv-replace < 0.2.0.
|
|
127
|
-
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
127
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
128
128
|
def self.supports_connect_timeout?
|
|
129
129
|
return @supports_connect_timeout if defined?(@supports_connect_timeout)
|
|
130
130
|
|
|
@@ -135,7 +135,6 @@ module Dalli
|
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
|
-
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
139
138
|
|
|
140
139
|
def self.create_socket_with_timeout(host, port, options)
|
|
141
140
|
if supports_connect_timeout?
|
|
@@ -192,14 +191,13 @@ module Dalli
|
|
|
192
191
|
|
|
193
192
|
# Detect and cache the correct pack format for struct timeval on this platform.
|
|
194
193
|
# Different architectures have different sizes for time_t and suseconds_t.
|
|
195
|
-
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
194
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
196
195
|
def self.timeval_pack_format(sock)
|
|
197
196
|
@timeval_pack_format ||= begin
|
|
198
197
|
expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
|
|
199
198
|
TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
|
|
200
199
|
end
|
|
201
200
|
end
|
|
202
|
-
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
203
201
|
|
|
204
202
|
def self.pack_timeval(sock, seconds, microseconds)
|
|
205
203
|
[seconds, microseconds].pack(timeval_pack_format(sock))
|
data/lib/dalli/version.rb
CHANGED
data/lib/rack/session/dalli.rb
CHANGED
|
@@ -16,11 +16,10 @@ module Rack
|
|
|
16
16
|
attr_reader :data
|
|
17
17
|
|
|
18
18
|
# Don't freeze this until we fix the specs/implementation
|
|
19
|
-
# rubocop:disable Style/MutableConstant
|
|
19
|
+
# rubocop:disable-next Style/MutableConstant
|
|
20
20
|
DEFAULT_DALLI_OPTIONS = {
|
|
21
21
|
namespace: 'rack:session'
|
|
22
22
|
}
|
|
23
|
-
# rubocop:enable Style/MutableConstant
|
|
24
23
|
|
|
25
24
|
# Brings in a new Rack::Session::Dalli middleware with the given
|
|
26
25
|
# `:memcache_server`. The server is either a hostname, or a
|
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.1.
|
|
4
|
+
version: 5.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
88
|
version: '0'
|
|
89
89
|
requirements: []
|
|
90
|
-
rubygems_version: 4.0.
|
|
90
|
+
rubygems_version: 4.0.21
|
|
91
91
|
specification_version: 4
|
|
92
92
|
summary: High performance memcached client for Ruby
|
|
93
93
|
test_files: []
|