dalli 4.3.10 → 5.0.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 +23 -161
- data/Gemfile +1 -1
- data/README.md +4 -55
- data/lib/dalli/client.rb +39 -98
- data/lib/dalli/compressor.rb +5 -53
- data/lib/dalli/instrumentation.rb +1 -1
- data/lib/dalli/key_manager.rb +2 -28
- data/lib/dalli/options.rb +2 -2
- data/lib/dalli/pipelined_getter.rb +32 -46
- data/lib/dalli/pipelined_setter.rb +8 -17
- data/lib/dalli/protocol/base.rb +31 -160
- data/lib/dalli/protocol/connection_manager.rb +20 -137
- data/lib/dalli/protocol/meta.rb +15 -54
- data/lib/dalli/protocol/{meta/request_formatter.rb → request_formatter.rb} +8 -21
- data/lib/dalli/protocol/response_buffer.rb +12 -27
- data/lib/dalli/protocol/{meta/response_processor.rb → response_processor.rb} +32 -64
- data/lib/dalli/protocol/server_config_parser.rb +1 -1
- data/lib/dalli/protocol/value_compressor.rb +7 -36
- data/lib/dalli/protocol/value_serializer.rb +1 -1
- data/lib/dalli/protocol.rb +9 -7
- data/lib/dalli/ring.rb +2 -2
- data/lib/dalli/servers_arg_normalizer.rb +1 -1
- data/lib/dalli/socket.rb +0 -29
- data/lib/dalli/version.rb +2 -2
- data/lib/dalli.rb +0 -3
- data/lib/rack/session/dalli.rb +2 -1
- metadata +7 -14
- data/lib/dalli/json_serializer.rb +0 -28
- data/lib/dalli/protocol/binary/request_formatter.rb +0 -116
- data/lib/dalli/protocol/binary/response_header.rb +0 -36
- data/lib/dalli/protocol/binary/response_processor.rb +0 -245
- data/lib/dalli/protocol/binary/sasl_authentication.rb +0 -60
- data/lib/dalli/protocol/binary.rb +0 -202
- data/lib/dalli/protocol_deprecations.rb +0 -45
- /data/lib/dalli/protocol/{meta/key_regularizer.rb → key_regularizer.rb} +0 -0
data/lib/dalli/compressor.rb
CHANGED
|
@@ -13,54 +13,8 @@ module Dalli
|
|
|
13
13
|
Zlib::Deflate.deflate(data)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# Compressed input is fed to zlib this many bytes at a time. MRI's zlib
|
|
19
|
-
# yields the output in small chunks; one that returns it instead still
|
|
20
|
-
# produces at most MAX_EXPANSION times this per step.
|
|
21
|
-
INFLATE_SLICE_BYTES = 64 * 1024
|
|
22
|
-
|
|
23
|
-
# max_bytes caps the decompressed size; inflating stops and raises as soon
|
|
24
|
-
# as the output passes it, so a small compressed value can't expand into
|
|
25
|
-
# gigabytes of memory.
|
|
26
|
-
def self.decompress(data, max_bytes: nil)
|
|
27
|
-
return Zlib::Inflate.inflate(data) unless may_exceed?(data, max_bytes)
|
|
28
|
-
|
|
29
|
-
inflate_within_limit(Zlib::Inflate.new, data, max_bytes)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# False when even the largest possible expansion stays within max_bytes,
|
|
33
|
-
# so the value can be inflated in one step
|
|
34
|
-
def self.may_exceed?(data, max_bytes)
|
|
35
|
-
max_bytes ? data.bytesize * MAX_EXPANSION > max_bytes : false
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def self.inflate_within_limit(inflater, data, max_bytes)
|
|
39
|
-
out = String.new(encoding: Encoding::BINARY)
|
|
40
|
-
0.step(data.bytesize - 1, INFLATE_SLICE_BYTES) do |pos|
|
|
41
|
-
inflate_slice(inflater, data.byteslice(pos, INFLATE_SLICE_BYTES), out, max_bytes)
|
|
42
|
-
# Anything after the end of the stream isn't part of the value, as
|
|
43
|
-
# with Zlib::Inflate.inflate; finish would return it as output
|
|
44
|
-
break if inflater.finished?
|
|
45
|
-
end
|
|
46
|
-
return out if inflater.finished?
|
|
47
|
-
|
|
48
|
-
append_within_limit(out, inflater.finish, max_bytes)
|
|
49
|
-
ensure
|
|
50
|
-
inflater.close
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Both yielded and returned output count toward the limit
|
|
54
|
-
def self.inflate_slice(inflater, slice, out, max_bytes)
|
|
55
|
-
rest = inflater.inflate(slice) { |chunk| append_within_limit(out, chunk, max_bytes) }
|
|
56
|
-
append_within_limit(out, rest, max_bytes)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def self.append_within_limit(out, chunk, max_bytes)
|
|
60
|
-
out << chunk if chunk
|
|
61
|
-
return out if out.bytesize <= max_bytes
|
|
62
|
-
|
|
63
|
-
raise Dalli::UnmarshalError, "Decompressed value exceeds #{max_bytes} bytes (decompressed_max_bytes)"
|
|
16
|
+
def self.decompress(data)
|
|
17
|
+
Zlib::Inflate.inflate(data)
|
|
64
18
|
end
|
|
65
19
|
end
|
|
66
20
|
|
|
@@ -78,11 +32,9 @@ module Dalli
|
|
|
78
32
|
io.string
|
|
79
33
|
end
|
|
80
34
|
|
|
81
|
-
def self.decompress(data
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# MAX_WBITS + 16 makes zlib read the gzip format (and check its CRC)
|
|
85
|
-
Compressor.inflate_within_limit(Zlib::Inflate.new(Zlib::MAX_WBITS + 16), data, max_bytes)
|
|
35
|
+
def self.decompress(data)
|
|
36
|
+
io = StringIO.new(data, 'rb')
|
|
37
|
+
Zlib::GzipReader.new(io).read
|
|
86
38
|
end
|
|
87
39
|
end
|
|
88
40
|
end
|
|
@@ -8,7 +8,7 @@ module Dalli
|
|
|
8
8
|
# When OpenTelemetry is loaded, Dalli automatically creates spans for cache operations.
|
|
9
9
|
# When OpenTelemetry is not available, all tracing methods are no-ops with zero overhead.
|
|
10
10
|
#
|
|
11
|
-
# Dalli
|
|
11
|
+
# Dalli 5.0 uses the stable OTel semantic conventions for database spans.
|
|
12
12
|
#
|
|
13
13
|
# == Span Attributes
|
|
14
14
|
#
|
data/lib/dalli/key_manager.rb
CHANGED
|
@@ -41,9 +41,6 @@ module Dalli
|
|
|
41
41
|
|
|
42
42
|
@namespace = namespace_from_options
|
|
43
43
|
@namespace_separator = @key_options[:namespace_separator]
|
|
44
|
-
# The meta protocol base64-encodes keys with whitespace or non-ASCII
|
|
45
|
-
# characters, so their length on the wire differs from their length here
|
|
46
|
-
@meta_protocol = client_options[:protocol]&.to_s == 'meta'
|
|
47
44
|
end
|
|
48
45
|
|
|
49
46
|
##
|
|
@@ -61,7 +58,7 @@ module Dalli
|
|
|
61
58
|
raise ArgumentError, 'key cannot be blank' unless key&.length&.positive?
|
|
62
59
|
|
|
63
60
|
key = key_with_namespace(key)
|
|
64
|
-
|
|
61
|
+
key.length > MAX_KEY_LENGTH ? truncated_key(key) : key
|
|
65
62
|
end
|
|
66
63
|
|
|
67
64
|
##
|
|
@@ -125,30 +122,7 @@ module Dalli
|
|
|
125
122
|
##
|
|
126
123
|
def truncated_key(key)
|
|
127
124
|
digest = digest_class.hexdigest(key)
|
|
128
|
-
|
|
129
|
-
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
130
|
-
# A prefix with whitespace or non-ASCII characters sends the whole key
|
|
131
|
-
# base64-encoded, which can still be too long on the wire; shorten it.
|
|
132
|
-
while wire_length(truncated) > MAX_KEY_LENGTH && !prefix.empty?
|
|
133
|
-
prefix = prefix[0...-1]
|
|
134
|
-
truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
135
|
-
end
|
|
136
|
-
return truncated if wire_length(truncated) <= MAX_KEY_LENGTH
|
|
137
|
-
|
|
138
|
-
# Only possible with a digest_class whose digests aren't short hex strings
|
|
139
|
-
raise ArgumentError, "#{digest_class} digests are too long to fit in a truncated key"
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# memcached's key limit applies to the key as sent: in bytes, and, with
|
|
143
|
-
# the meta protocol, after base64 encoding when the key needs it. A key
|
|
144
|
-
# within the limit in characters can still be too long. With the meta
|
|
145
|
-
# protocol memcached rejects it with a CLIENT_ERROR that must never reach
|
|
146
|
-
# a pipelined get; with the binary protocol it drops the connection.
|
|
147
|
-
def wire_length(key)
|
|
148
|
-
return key.bytesize unless @meta_protocol
|
|
149
|
-
|
|
150
|
-
encoded, = Dalli::Protocol::Meta::KeyRegularizer.encode(key)
|
|
151
|
-
encoded.bytesize
|
|
125
|
+
"#{key[0, prefix_length(digest)]}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
|
152
126
|
end
|
|
153
127
|
|
|
154
128
|
def prefix_length(digest)
|
data/lib/dalli/options.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Dalli
|
|
|
6
6
|
# Make Dalli threadsafe by using a lock around all
|
|
7
7
|
# public server methods.
|
|
8
8
|
#
|
|
9
|
-
# Dalli::Protocol::
|
|
9
|
+
# Dalli::Protocol::Meta.extend(Dalli::Threadsafe)
|
|
10
10
|
#
|
|
11
11
|
module Threadsafe
|
|
12
12
|
def self.extended(obj)
|
|
@@ -43,7 +43,7 @@ module Dalli
|
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def pipeline_abort
|
|
46
|
+
def pipeline_abort
|
|
47
47
|
@lock.synchronize do
|
|
48
48
|
super
|
|
49
49
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'set'
|
|
4
|
-
|
|
5
3
|
module Dalli
|
|
6
4
|
##
|
|
7
5
|
# Contains logic for the pipelined gets implemented by the client.
|
|
@@ -26,21 +24,15 @@ module Dalli
|
|
|
26
24
|
return {} if keys.empty?
|
|
27
25
|
|
|
28
26
|
@ring.lock do
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@partial_results = {}
|
|
34
|
-
setup_requests(servers, keys)
|
|
35
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
27
|
+
# Stores partial results collected during interleaved send phase
|
|
28
|
+
@partial_results = {}
|
|
29
|
+
servers = setup_requests(keys)
|
|
30
|
+
start_time = Time.now
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
# First yield any partial results collected during interleaved send
|
|
33
|
+
yield_partial_results(&block)
|
|
39
34
|
|
|
40
|
-
|
|
41
|
-
ensure
|
|
42
|
-
abort_unfinished(servers)
|
|
43
|
-
end
|
|
35
|
+
servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
|
|
44
36
|
end
|
|
45
37
|
rescue Dalli::RetryableNetworkError => e
|
|
46
38
|
Dalli.logger.debug { e.inspect }
|
|
@@ -57,15 +49,12 @@ module Dalli
|
|
|
57
49
|
@partial_results.clear
|
|
58
50
|
end
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
# their queries, so #process can abort them if it stops early.
|
|
62
|
-
def setup_requests(servers, keys)
|
|
52
|
+
def setup_requests(keys)
|
|
63
53
|
groups = groups_for_keys(keys)
|
|
64
|
-
servers.concat(groups.keys)
|
|
65
54
|
make_getkq_requests(groups)
|
|
66
55
|
|
|
67
56
|
# TODO: How does this exit on a NetworkError
|
|
68
|
-
finish_queries(
|
|
57
|
+
finish_queries(groups.keys)
|
|
69
58
|
end
|
|
70
59
|
|
|
71
60
|
##
|
|
@@ -127,39 +116,29 @@ module Dalli
|
|
|
127
116
|
raise
|
|
128
117
|
end
|
|
129
118
|
|
|
130
|
-
# Swallows Dalli::NetworkError
|
|
131
|
-
# again.
|
|
119
|
+
# Swallows Dalli::NetworkError
|
|
132
120
|
def abort_without_timeout(servers)
|
|
133
121
|
servers.each(&:pipeline_abort)
|
|
134
|
-
servers.clear
|
|
135
122
|
end
|
|
136
123
|
|
|
137
|
-
# When #process stops before reading every server's replies (the caller's
|
|
138
|
-
# block raises or breaks, a Timeout.timeout fires, a value can't be
|
|
139
|
-
# deserialized), the rest are still on the socket and some may be in the
|
|
140
|
-
# response buffer. Closing those connections keeps them from being read
|
|
141
|
-
# as a later request's replies. Aborts that #process already handled
|
|
142
|
-
# emptied servers.
|
|
143
|
-
def abort_unfinished(servers)
|
|
144
|
-
servers.each { |server| server.pipeline_abort(timed_out: false) }
|
|
145
|
-
servers.clear
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Reads the replies available, removing finished servers from servers
|
|
149
124
|
def fetch_responses(servers, start_time, timeout, &block)
|
|
150
125
|
# Remove any servers which are not connected
|
|
151
126
|
servers.select!(&:connected?)
|
|
152
|
-
return if servers.empty?
|
|
127
|
+
return [] if servers.empty?
|
|
153
128
|
|
|
154
129
|
time_left = remaining_time(start_time, timeout)
|
|
155
130
|
readable_servers = servers_with_response(servers, time_left)
|
|
156
|
-
|
|
131
|
+
if readable_servers.empty?
|
|
132
|
+
abort_with_timeout(servers)
|
|
133
|
+
return []
|
|
134
|
+
end
|
|
157
135
|
|
|
158
136
|
# Loop through the servers with responses, and
|
|
159
137
|
# delete any from our list that are finished
|
|
160
138
|
readable_servers.each do |server|
|
|
161
139
|
servers.delete(server) if process_server(server, &block)
|
|
162
140
|
end
|
|
141
|
+
servers
|
|
163
142
|
rescue NetworkError
|
|
164
143
|
# Abort and raise if we encountered a network error. This triggers
|
|
165
144
|
# a retry at the top level on RetryableNetworkError.
|
|
@@ -168,7 +147,7 @@ module Dalli
|
|
|
168
147
|
end
|
|
169
148
|
|
|
170
149
|
def remaining_time(start, timeout)
|
|
171
|
-
elapsed =
|
|
150
|
+
elapsed = Time.now - start
|
|
172
151
|
return 0 if elapsed > timeout
|
|
173
152
|
|
|
174
153
|
timeout - elapsed
|
|
@@ -176,17 +155,19 @@ module Dalli
|
|
|
176
155
|
|
|
177
156
|
# Swallows Dalli::NetworkError
|
|
178
157
|
def abort_with_timeout(servers)
|
|
158
|
+
abort_without_timeout(servers)
|
|
179
159
|
servers.each do |server|
|
|
180
160
|
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
|
181
161
|
end
|
|
182
|
-
|
|
162
|
+
|
|
163
|
+
true # Required to simplify caller
|
|
183
164
|
end
|
|
184
165
|
|
|
185
166
|
# Processes responses from a server. Returns true if there are no
|
|
186
167
|
# additional responses from this server.
|
|
187
168
|
def process_server(server)
|
|
188
|
-
server.pipeline_next_responses do |key,
|
|
189
|
-
yield @key_manager.key_without_namespace(key),
|
|
169
|
+
server.pipeline_next_responses.each_pair do |key, value_list|
|
|
170
|
+
yield @key_manager.key_without_namespace(key), value_list
|
|
190
171
|
end
|
|
191
172
|
|
|
192
173
|
server.pipeline_complete?
|
|
@@ -195,13 +176,18 @@ module Dalli
|
|
|
195
176
|
def servers_with_response(servers, timeout)
|
|
196
177
|
return [] if servers.empty?
|
|
197
178
|
|
|
198
|
-
|
|
199
|
-
|
|
179
|
+
# TODO: - This is a bit challenging. Essentially the PipelinedGetter
|
|
180
|
+
# is a reactor, but without the benefit of a Fiber or separate thread.
|
|
181
|
+
# My suspicion is that we may want to try and push this down into the
|
|
182
|
+
# individual servers, but I'm not sure. For now, we keep the
|
|
183
|
+
# mapping between the alerted object (the socket) and the
|
|
184
|
+
# corrresponding server here.
|
|
185
|
+
server_map = servers.each_with_object({}) { |s, h| h[s.sock] = s }
|
|
186
|
+
|
|
187
|
+
readable, = IO.select(server_map.keys, nil, nil, timeout)
|
|
200
188
|
return [] if readable.nil?
|
|
201
189
|
|
|
202
|
-
|
|
203
|
-
# building and looking up a hash map
|
|
204
|
-
readable.filter_map { |sock| servers.find { |s| s.sock == sock } }
|
|
190
|
+
readable.map { |sock| server_map[sock] }
|
|
205
191
|
end
|
|
206
192
|
|
|
207
193
|
def groups_for_keys(*keys)
|
|
@@ -16,9 +16,6 @@ module Dalli
|
|
|
16
16
|
# Writes multiple key-value pairs to memcached.
|
|
17
17
|
# Raises an error if any server is unavailable.
|
|
18
18
|
#
|
|
19
|
-
# A value that can't be marshalled or is over value_max_bytes is skipped;
|
|
20
|
-
# the other keys are still stored, then the first such error is raised.
|
|
21
|
-
#
|
|
22
19
|
# @param hash [Hash] key-value pairs to set
|
|
23
20
|
# @param ttl [Integer] time-to-live in seconds
|
|
24
21
|
# @param req_options [Hash] options passed to each set operation
|
|
@@ -27,12 +24,10 @@ module Dalli
|
|
|
27
24
|
def process(hash, ttl, req_options)
|
|
28
25
|
return if hash.empty?
|
|
29
26
|
|
|
30
|
-
@value_error = nil
|
|
31
27
|
@ring.lock do
|
|
32
28
|
servers = setup_requests(hash, ttl, req_options)
|
|
33
29
|
finish_requests(servers)
|
|
34
30
|
end
|
|
35
|
-
raise @value_error if @value_error
|
|
36
31
|
rescue Dalli::RetryableNetworkError => e
|
|
37
32
|
Dalli.logger.debug { e.inspect }
|
|
38
33
|
Dalli.logger.debug { 'retrying pipelined sets because of network error' }
|
|
@@ -41,12 +36,9 @@ module Dalli
|
|
|
41
36
|
|
|
42
37
|
private
|
|
43
38
|
|
|
44
|
-
# Values are looked up by each key as sent. The caller's key can't be
|
|
45
|
-
# rebuilt from it: a long key is digested, and a Symbol becomes a String.
|
|
46
39
|
def setup_requests(hash, ttl, req_options)
|
|
47
|
-
|
|
48
|
-
groups
|
|
49
|
-
make_set_requests(groups, pairs, ttl, req_options)
|
|
40
|
+
groups = groups_for_keys(hash.keys)
|
|
41
|
+
make_set_requests(groups, hash, ttl, req_options)
|
|
50
42
|
groups.keys
|
|
51
43
|
end
|
|
52
44
|
|
|
@@ -54,14 +46,12 @@ module Dalli
|
|
|
54
46
|
# Loop through the server-grouped sets of keys, writing
|
|
55
47
|
# the corresponding quiet set requests to the appropriate servers
|
|
56
48
|
##
|
|
57
|
-
def make_set_requests(groups,
|
|
49
|
+
def make_set_requests(groups, hash, ttl, req_options)
|
|
58
50
|
groups.each do |server, keys_for_server|
|
|
59
51
|
keys_for_server.each do |key|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# unaffected; raised by #process once the batch is complete
|
|
64
|
-
@value_error ||= e
|
|
52
|
+
original_key = @key_manager.key_without_namespace(key)
|
|
53
|
+
value = hash[original_key]
|
|
54
|
+
server.request(:pipelined_set, key, value, ttl, req_options)
|
|
65
55
|
rescue DalliError, NetworkError => e
|
|
66
56
|
Dalli.logger.debug { e.inspect }
|
|
67
57
|
Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
|
|
@@ -82,7 +72,8 @@ module Dalli
|
|
|
82
72
|
end
|
|
83
73
|
|
|
84
74
|
def groups_for_keys(keys)
|
|
85
|
-
|
|
75
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
76
|
+
groups = @ring.keys_grouped_by_server(validated_keys)
|
|
86
77
|
|
|
87
78
|
if (unfound_keys = groups.delete(nil))
|
|
88
79
|
Dalli.logger.debug do
|