dalli 3.2.8 → 5.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +305 -0
- data/Gemfile +16 -2
- data/README.md +96 -2
- data/lib/dalli/client.rb +289 -28
- data/lib/dalli/flags.rb +15 -0
- data/lib/dalli/instrumentation.rb +153 -0
- data/lib/dalli/key_manager.rb +23 -8
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pid_cache.rb +1 -1
- data/lib/dalli/pipelined_deleter.rb +82 -0
- data/lib/dalli/pipelined_getter.rb +44 -20
- data/lib/dalli/pipelined_setter.rb +87 -0
- data/lib/dalli/protocol/base.rb +95 -25
- data/lib/dalli/protocol/connection_manager.rb +37 -14
- data/lib/dalli/protocol/{meta/key_regularizer.rb → key_regularizer.rb} +1 -1
- data/lib/dalli/protocol/meta.rb +185 -20
- data/lib/dalli/protocol/{meta/request_formatter.rb → request_formatter.rb} +49 -16
- data/lib/dalli/protocol/response_buffer.rb +36 -12
- data/lib/dalli/protocol/{meta/response_processor.rb → response_processor.rb} +93 -37
- data/lib/dalli/protocol/server_config_parser.rb +2 -2
- data/lib/dalli/protocol/string_marshaller.rb +65 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
- data/lib/dalli/protocol/value_compressor.rb +3 -16
- data/lib/dalli/protocol/value_marshaller.rb +1 -1
- data/lib/dalli/protocol/value_serializer.rb +61 -44
- data/lib/dalli/protocol.rb +10 -0
- data/lib/dalli/ring.rb +2 -2
- data/lib/dalli/servers_arg_normalizer.rb +1 -1
- data/lib/dalli/socket.rb +79 -14
- data/lib/dalli/version.rb +2 -2
- data/lib/dalli.rb +13 -5
- data/lib/rack/session/dalli.rb +43 -8
- metadata +27 -17
- data/lib/dalli/protocol/binary/request_formatter.rb +0 -117
- data/lib/dalli/protocol/binary/response_header.rb +0 -36
- data/lib/dalli/protocol/binary/response_processor.rb +0 -239
- data/lib/dalli/protocol/binary/sasl_authentication.rb +0 -60
- data/lib/dalli/protocol/binary.rb +0 -173
- data/lib/dalli/server.rb +0 -6
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# Contains logic for the pipelined delete operations implemented by the client.
|
|
6
|
+
# Efficiently deletes multiple keys by grouping requests by server
|
|
7
|
+
# and using quiet mode to minimize round trips.
|
|
8
|
+
##
|
|
9
|
+
class PipelinedDeleter
|
|
10
|
+
def initialize(ring, key_manager)
|
|
11
|
+
@ring = ring
|
|
12
|
+
@key_manager = key_manager
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# Deletes multiple keys from memcached.
|
|
17
|
+
#
|
|
18
|
+
# @param keys [Array<String>] keys to delete
|
|
19
|
+
# @return [void]
|
|
20
|
+
##
|
|
21
|
+
def process(keys)
|
|
22
|
+
return if keys.empty?
|
|
23
|
+
|
|
24
|
+
@ring.lock do
|
|
25
|
+
servers = setup_requests(keys)
|
|
26
|
+
finish_requests(servers)
|
|
27
|
+
end
|
|
28
|
+
rescue NetworkError => e
|
|
29
|
+
Dalli.logger.debug { e.inspect }
|
|
30
|
+
Dalli.logger.debug { 'retrying pipelined deletes because of network error' }
|
|
31
|
+
retry
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def setup_requests(keys)
|
|
37
|
+
groups = groups_for_keys(keys)
|
|
38
|
+
make_delete_requests(groups)
|
|
39
|
+
groups.keys
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# Loop through the server-grouped sets of keys, writing
|
|
44
|
+
# the corresponding quiet delete requests to the appropriate servers
|
|
45
|
+
##
|
|
46
|
+
def make_delete_requests(groups)
|
|
47
|
+
groups.each do |server, keys_for_server|
|
|
48
|
+
keys_for_server.each do |key|
|
|
49
|
+
server.request(:pipelined_delete, key)
|
|
50
|
+
rescue DalliError, NetworkError => e
|
|
51
|
+
Dalli.logger.debug { e.inspect }
|
|
52
|
+
Dalli.logger.debug { "unable to delete key #{key} for server #{server.name}" }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Sends noop to each server to flush responses and ensure all deletes complete.
|
|
59
|
+
##
|
|
60
|
+
def finish_requests(servers)
|
|
61
|
+
servers.each do |server|
|
|
62
|
+
server.request(:noop)
|
|
63
|
+
rescue DalliError, NetworkError => e
|
|
64
|
+
Dalli.logger.debug { e.inspect }
|
|
65
|
+
Dalli.logger.debug { "unable to complete pipelined delete on server #{server.name}" }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def groups_for_keys(keys)
|
|
70
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
71
|
+
groups = @ring.keys_grouped_by_server(validated_keys)
|
|
72
|
+
|
|
73
|
+
if (unfound_keys = groups.delete(nil))
|
|
74
|
+
Dalli.logger.debug do
|
|
75
|
+
"unable to delete #{unfound_keys.length} keys because no matching server was found"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
groups
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -5,6 +5,13 @@ module Dalli
|
|
|
5
5
|
# Contains logic for the pipelined gets implemented by the client.
|
|
6
6
|
##
|
|
7
7
|
class PipelinedGetter
|
|
8
|
+
# For large batches, interleave sends with response draining to prevent
|
|
9
|
+
# socket buffer deadlock. Only kicks in above this threshold.
|
|
10
|
+
INTERLEAVE_THRESHOLD = 10_000
|
|
11
|
+
|
|
12
|
+
# Number of keys to send before draining responses during interleaved mode
|
|
13
|
+
CHUNK_SIZE = 10_000
|
|
14
|
+
|
|
8
15
|
def initialize(ring, key_manager)
|
|
9
16
|
@ring = ring
|
|
10
17
|
@key_manager = key_manager
|
|
@@ -17,16 +24,31 @@ module Dalli
|
|
|
17
24
|
return {} if keys.empty?
|
|
18
25
|
|
|
19
26
|
@ring.lock do
|
|
27
|
+
# Stores partial results collected during interleaved send phase
|
|
28
|
+
@partial_results = {}
|
|
20
29
|
servers = setup_requests(keys)
|
|
21
|
-
start_time =
|
|
30
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
|
+
|
|
32
|
+
# First yield any partial results collected during interleaved send
|
|
33
|
+
yield_partial_results(&block)
|
|
34
|
+
|
|
22
35
|
servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
|
|
23
36
|
end
|
|
24
|
-
rescue
|
|
37
|
+
rescue Dalli::RetryableNetworkError => e
|
|
25
38
|
Dalli.logger.debug { e.inspect }
|
|
26
39
|
Dalli.logger.debug { 'retrying pipelined gets because of timeout' }
|
|
27
40
|
retry
|
|
28
41
|
end
|
|
29
42
|
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def yield_partial_results
|
|
46
|
+
@partial_results.each_pair do |key, value_list|
|
|
47
|
+
yield @key_manager.key_without_namespace(key), value_list
|
|
48
|
+
end
|
|
49
|
+
@partial_results.clear
|
|
50
|
+
end
|
|
51
|
+
|
|
30
52
|
def setup_requests(keys)
|
|
31
53
|
groups = groups_for_keys(keys)
|
|
32
54
|
make_getkq_requests(groups)
|
|
@@ -45,7 +67,14 @@ module Dalli
|
|
|
45
67
|
##
|
|
46
68
|
def make_getkq_requests(groups)
|
|
47
69
|
groups.each do |server, keys_for_server|
|
|
48
|
-
|
|
70
|
+
if keys_for_server.size <= INTERLEAVE_THRESHOLD
|
|
71
|
+
# Small batch - send all at once (existing behavior)
|
|
72
|
+
server.request(:pipelined_get, keys_for_server)
|
|
73
|
+
else
|
|
74
|
+
# Large batch - interleave sends with response draining
|
|
75
|
+
# Pass @partial_results directly to avoid hash allocation/merge overhead
|
|
76
|
+
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results)
|
|
77
|
+
end
|
|
49
78
|
rescue DalliError, NetworkError => e
|
|
50
79
|
Dalli.logger.debug { e.inspect }
|
|
51
80
|
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
@@ -57,7 +86,7 @@ module Dalli
|
|
|
57
86
|
# our set, sending the noop to terminate the set of queries.
|
|
58
87
|
##
|
|
59
88
|
def finish_queries(servers)
|
|
60
|
-
deleted =
|
|
89
|
+
deleted = Set.new
|
|
61
90
|
|
|
62
91
|
servers.each do |server|
|
|
63
92
|
next unless server.connected?
|
|
@@ -67,7 +96,7 @@ module Dalli
|
|
|
67
96
|
rescue Dalli::NetworkError
|
|
68
97
|
raise
|
|
69
98
|
rescue Dalli::DalliError
|
|
70
|
-
deleted
|
|
99
|
+
deleted << server
|
|
71
100
|
end
|
|
72
101
|
end
|
|
73
102
|
|
|
@@ -94,7 +123,7 @@ module Dalli
|
|
|
94
123
|
|
|
95
124
|
def fetch_responses(servers, start_time, timeout, &block)
|
|
96
125
|
# Remove any servers which are not connected
|
|
97
|
-
servers.
|
|
126
|
+
servers.select!(&:connected?)
|
|
98
127
|
return [] if servers.empty?
|
|
99
128
|
|
|
100
129
|
time_left = remaining_time(start_time, timeout)
|
|
@@ -112,13 +141,13 @@ module Dalli
|
|
|
112
141
|
servers
|
|
113
142
|
rescue NetworkError
|
|
114
143
|
# Abort and raise if we encountered a network error. This triggers
|
|
115
|
-
# a retry at the top level.
|
|
144
|
+
# a retry at the top level on RetryableNetworkError.
|
|
116
145
|
abort_without_timeout(servers)
|
|
117
146
|
raise
|
|
118
147
|
end
|
|
119
148
|
|
|
120
149
|
def remaining_time(start, timeout)
|
|
121
|
-
elapsed =
|
|
150
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
122
151
|
return 0 if elapsed > timeout
|
|
123
152
|
|
|
124
153
|
timeout - elapsed
|
|
@@ -137,8 +166,8 @@ module Dalli
|
|
|
137
166
|
# Processes responses from a server. Returns true if there are no
|
|
138
167
|
# additional responses from this server.
|
|
139
168
|
def process_server(server)
|
|
140
|
-
server.pipeline_next_responses
|
|
141
|
-
yield @key_manager.key_without_namespace(key),
|
|
169
|
+
server.pipeline_next_responses do |key, value, cas|
|
|
170
|
+
yield @key_manager.key_without_namespace(key), [value, cas]
|
|
142
171
|
end
|
|
143
172
|
|
|
144
173
|
server.pipeline_complete?
|
|
@@ -147,18 +176,13 @@ module Dalli
|
|
|
147
176
|
def servers_with_response(servers, timeout)
|
|
148
177
|
return [] if servers.empty?
|
|
149
178
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# My suspicion is that we may want to try and push this down into the
|
|
153
|
-
# individual servers, but I'm not sure. For now, we keep the
|
|
154
|
-
# mapping between the alerted object (the socket) and the
|
|
155
|
-
# corrresponding server here.
|
|
156
|
-
server_map = servers.each_with_object({}) { |s, h| h[s.sock] = s }
|
|
157
|
-
|
|
158
|
-
readable, = IO.select(server_map.keys, nil, nil, timeout)
|
|
179
|
+
sockets = servers.map(&:sock)
|
|
180
|
+
readable, = IO.select(sockets, nil, nil, timeout)
|
|
159
181
|
return [] if readable.nil?
|
|
160
182
|
|
|
161
|
-
|
|
183
|
+
# For typical server counts (1-5), linear scan is faster than
|
|
184
|
+
# building and looking up a hash map
|
|
185
|
+
readable.filter_map { |sock| servers.find { |s| s.sock == sock } }
|
|
162
186
|
end
|
|
163
187
|
|
|
164
188
|
def groups_for_keys(*keys)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# Contains logic for the pipelined set operations implemented by the client.
|
|
6
|
+
# Efficiently writes multiple key-value pairs by grouping requests by server
|
|
7
|
+
# and using quiet mode to minimize round trips.
|
|
8
|
+
##
|
|
9
|
+
class PipelinedSetter
|
|
10
|
+
def initialize(ring, key_manager)
|
|
11
|
+
@ring = ring
|
|
12
|
+
@key_manager = key_manager
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
##
|
|
16
|
+
# Writes multiple key-value pairs to memcached.
|
|
17
|
+
# Raises an error if any server is unavailable.
|
|
18
|
+
#
|
|
19
|
+
# @param hash [Hash] key-value pairs to set
|
|
20
|
+
# @param ttl [Integer] time-to-live in seconds
|
|
21
|
+
# @param req_options [Hash] options passed to each set operation
|
|
22
|
+
# @return [void]
|
|
23
|
+
##
|
|
24
|
+
def process(hash, ttl, req_options)
|
|
25
|
+
return if hash.empty?
|
|
26
|
+
|
|
27
|
+
@ring.lock do
|
|
28
|
+
servers = setup_requests(hash, ttl, req_options)
|
|
29
|
+
finish_requests(servers)
|
|
30
|
+
end
|
|
31
|
+
rescue Dalli::RetryableNetworkError => e
|
|
32
|
+
Dalli.logger.debug { e.inspect }
|
|
33
|
+
Dalli.logger.debug { 'retrying pipelined sets because of network error' }
|
|
34
|
+
retry
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def setup_requests(hash, ttl, req_options)
|
|
40
|
+
groups = groups_for_keys(hash.keys)
|
|
41
|
+
make_set_requests(groups, hash, ttl, req_options)
|
|
42
|
+
groups.keys
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# Loop through the server-grouped sets of keys, writing
|
|
47
|
+
# the corresponding quiet set requests to the appropriate servers
|
|
48
|
+
##
|
|
49
|
+
def make_set_requests(groups, hash, ttl, req_options)
|
|
50
|
+
groups.each do |server, keys_for_server|
|
|
51
|
+
keys_for_server.each do |key|
|
|
52
|
+
original_key = @key_manager.key_without_namespace(key)
|
|
53
|
+
value = hash[original_key]
|
|
54
|
+
server.request(:pipelined_set, key, value, ttl, req_options)
|
|
55
|
+
rescue DalliError, NetworkError => e
|
|
56
|
+
Dalli.logger.debug { e.inspect }
|
|
57
|
+
Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Sends noop to each server to flush responses and ensure all writes complete.
|
|
64
|
+
##
|
|
65
|
+
def finish_requests(servers)
|
|
66
|
+
servers.each do |server|
|
|
67
|
+
server.request(:noop)
|
|
68
|
+
rescue DalliError, NetworkError => e
|
|
69
|
+
Dalli.logger.debug { e.inspect }
|
|
70
|
+
Dalli.logger.debug { "unable to complete pipelined set on server #{server.name}" }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def groups_for_keys(keys)
|
|
75
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
76
|
+
groups = @ring.keys_grouped_by_server(validated_keys)
|
|
77
|
+
|
|
78
|
+
if (unfound_keys = groups.delete(nil))
|
|
79
|
+
Dalli.logger.debug do
|
|
80
|
+
"unable to set #{unfound_keys.length} keys because no matching server was found"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
groups
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -18,15 +18,23 @@ 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, :sock, :hostname, :port, :close, :connected?, :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, :flushed_write
|
|
22
22
|
|
|
23
23
|
def initialize(attribs, client_options = {})
|
|
24
24
|
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
|
25
|
+
warn_uri_credentials(user_creds)
|
|
25
26
|
@options = client_options.merge(user_creds)
|
|
26
|
-
@
|
|
27
|
+
@raw_mode = client_options[:raw]
|
|
28
|
+
@value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
27
29
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
28
30
|
end
|
|
29
31
|
|
|
32
|
+
# Returns true if client is in raw mode (no serialization/compression).
|
|
33
|
+
# In raw mode, we can skip requesting bitflags from the server.
|
|
34
|
+
def raw_mode?
|
|
35
|
+
@raw_mode
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
# Chokepoint method for error handling and ensuring liveness
|
|
31
39
|
def request(opkey, *args)
|
|
32
40
|
verify_state(opkey)
|
|
@@ -35,8 +43,8 @@ module Dalli
|
|
|
35
43
|
@connection_manager.start_request!
|
|
36
44
|
response = send(opkey, *args)
|
|
37
45
|
|
|
38
|
-
# pipelined_get emit query but
|
|
39
|
-
@connection_manager.finish_request! unless opkey
|
|
46
|
+
# pipelined_get/pipelined_get_interleaved emit query but don't read the response(s)
|
|
47
|
+
@connection_manager.finish_request! unless %i[pipelined_get pipelined_get_interleaved].include?(opkey)
|
|
40
48
|
|
|
41
49
|
response
|
|
42
50
|
rescue Dalli::MarshalError => e
|
|
@@ -74,7 +82,9 @@ module Dalli
|
|
|
74
82
|
def pipeline_response_setup
|
|
75
83
|
verify_pipelined_state(:getkq)
|
|
76
84
|
write_noop
|
|
77
|
-
|
|
85
|
+
# Use ensure_ready instead of reset to preserve any data already buffered
|
|
86
|
+
# during interleaved pipelined get draining
|
|
87
|
+
response_buffer.ensure_ready
|
|
78
88
|
end
|
|
79
89
|
|
|
80
90
|
# Attempt to receive and parse as many key/value pairs as possible
|
|
@@ -82,10 +92,13 @@ module Dalli
|
|
|
82
92
|
# repeatedly whenever this server's socket is readable until
|
|
83
93
|
# #pipeline_complete?.
|
|
84
94
|
#
|
|
85
|
-
#
|
|
86
|
-
|
|
95
|
+
# When a block is given, yields (key, value, cas) for each response,
|
|
96
|
+
# avoiding intermediate Hash allocation. Returns nil.
|
|
97
|
+
# Without a block, returns a Hash of { key => [value, cas] }.
|
|
98
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
99
|
+
def pipeline_next_responses(&block)
|
|
87
100
|
reconnect_on_pipeline_complete!
|
|
88
|
-
values =
|
|
101
|
+
values = nil
|
|
89
102
|
|
|
90
103
|
response_buffer.read
|
|
91
104
|
|
|
@@ -99,16 +112,24 @@ module Dalli
|
|
|
99
112
|
|
|
100
113
|
# If the status is ok and the key is not nil, then this is a
|
|
101
114
|
# getkq response with a value that we want to set in the response hash
|
|
102
|
-
|
|
115
|
+
unless key.nil?
|
|
116
|
+
if block
|
|
117
|
+
yield key, value, cas
|
|
118
|
+
else
|
|
119
|
+
values ||= {}
|
|
120
|
+
values[key] = [value, cas]
|
|
121
|
+
end
|
|
122
|
+
end
|
|
103
123
|
|
|
104
124
|
# Get the next response from the buffer
|
|
105
125
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
106
126
|
end
|
|
107
127
|
|
|
108
|
-
values
|
|
109
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
128
|
+
values || {}
|
|
129
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
110
130
|
@connection_manager.error_on_request!(e)
|
|
111
131
|
end
|
|
132
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
112
133
|
|
|
113
134
|
# Abort current pipelined get. Generally used to signal an external
|
|
114
135
|
# timeout during pipelined get. The underlying socket is
|
|
@@ -132,18 +153,6 @@ module Dalli
|
|
|
132
153
|
!response_buffer.in_progress?
|
|
133
154
|
end
|
|
134
155
|
|
|
135
|
-
def username
|
|
136
|
-
@options[:username] || ENV.fetch('MEMCACHE_USERNAME', nil)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def password
|
|
140
|
-
@options[:password] || ENV.fetch('MEMCACHE_PASSWORD', nil)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def require_auth?
|
|
144
|
-
!username.nil?
|
|
145
|
-
end
|
|
146
|
-
|
|
147
156
|
def quiet?
|
|
148
157
|
Thread.current[::Dalli::QUIET]
|
|
149
158
|
end
|
|
@@ -153,7 +162,19 @@ module Dalli
|
|
|
153
162
|
|
|
154
163
|
private
|
|
155
164
|
|
|
165
|
+
URI_CREDENTIAL_WARNING = 'Dalli 5.0 removed SASL authentication. ' \
|
|
166
|
+
'Credentials in memcached:// URIs are ignored.'
|
|
167
|
+
private_constant :URI_CREDENTIAL_WARNING
|
|
168
|
+
|
|
169
|
+
def warn_uri_credentials(user_creds)
|
|
170
|
+
return if user_creds[:username].nil? && user_creds[:password].nil?
|
|
171
|
+
|
|
172
|
+
Dalli.logger.warn(URI_CREDENTIAL_WARNING)
|
|
173
|
+
end
|
|
174
|
+
|
|
156
175
|
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
176
|
+
private_constant :ALLOWED_QUIET_OPS
|
|
177
|
+
|
|
157
178
|
def verify_allowed_quiet!(opkey)
|
|
158
179
|
return if ALLOWED_QUIET_OPS.include?(opkey)
|
|
159
180
|
|
|
@@ -205,12 +226,16 @@ module Dalli
|
|
|
205
226
|
|
|
206
227
|
def connect
|
|
207
228
|
@connection_manager.establish_connection
|
|
208
|
-
|
|
209
|
-
@version = version # Connect socket if not authed
|
|
229
|
+
@version = version
|
|
210
230
|
up!
|
|
211
231
|
end
|
|
212
232
|
|
|
213
233
|
def pipelined_get(keys)
|
|
234
|
+
# Clear buffer to remove any stale data from interrupted operations.
|
|
235
|
+
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
236
|
+
# the expected state before pipeline_response_setup is called.
|
|
237
|
+
response_buffer.clear
|
|
238
|
+
|
|
214
239
|
req = +''
|
|
215
240
|
keys.each do |key|
|
|
216
241
|
req << quiet_get_request(key)
|
|
@@ -219,6 +244,51 @@ module Dalli
|
|
|
219
244
|
write(req)
|
|
220
245
|
end
|
|
221
246
|
|
|
247
|
+
# For large batches, interleave writing requests with draining responses.
|
|
248
|
+
# This prevents socket buffer deadlock when sending many keys.
|
|
249
|
+
# Populates the provided results hash with any responses drained during send.
|
|
250
|
+
def pipelined_get_interleaved(keys, chunk_size, results)
|
|
251
|
+
# Initialize the response buffer for draining during send phase
|
|
252
|
+
response_buffer.ensure_ready
|
|
253
|
+
|
|
254
|
+
keys.each_slice(chunk_size) do |chunk|
|
|
255
|
+
# Build and write this chunk of requests
|
|
256
|
+
req = +''
|
|
257
|
+
chunk.each do |key|
|
|
258
|
+
req << quiet_get_request(key)
|
|
259
|
+
end
|
|
260
|
+
write(req)
|
|
261
|
+
@connection_manager.flush
|
|
262
|
+
|
|
263
|
+
# Drain any available responses directly into results hash
|
|
264
|
+
drain_pipeline_responses(results)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Non-blocking read and processing of any available pipeline responses.
|
|
269
|
+
# Used during interleaved pipelined gets to prevent buffer deadlock.
|
|
270
|
+
# Populates the provided results hash directly to avoid allocation overhead.
|
|
271
|
+
def drain_pipeline_responses(results)
|
|
272
|
+
return unless connected?
|
|
273
|
+
|
|
274
|
+
# Non-blocking check if socket has data available
|
|
275
|
+
return unless sock.wait_readable(0)
|
|
276
|
+
|
|
277
|
+
# Read available data without blocking
|
|
278
|
+
response_buffer.read
|
|
279
|
+
|
|
280
|
+
# Process any complete responses in the buffer
|
|
281
|
+
loop do
|
|
282
|
+
status, cas, key, value = response_buffer.process_single_getk_response
|
|
283
|
+
break if status.nil? # No complete response available
|
|
284
|
+
|
|
285
|
+
results[key] = [value, cas] unless key.nil?
|
|
286
|
+
end
|
|
287
|
+
rescue SystemCallError, Dalli::NetworkError
|
|
288
|
+
# Ignore errors during drain - they'll be handled in fetch_responses
|
|
289
|
+
nil
|
|
290
|
+
end
|
|
291
|
+
|
|
222
292
|
def response_buffer
|
|
223
293
|
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
224
294
|
end
|
|
@@ -53,6 +53,7 @@ module Dalli
|
|
|
53
53
|
Dalli.logger.debug { "Dalli::Server#connect #{name}" }
|
|
54
54
|
|
|
55
55
|
@sock = memcached_socket
|
|
56
|
+
@sock.sync = false # Enable buffered I/O for better performance
|
|
56
57
|
@pid = PIDCache.pid
|
|
57
58
|
@request_in_progress = false
|
|
58
59
|
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
|
|
@@ -100,13 +101,13 @@ module Dalli
|
|
|
100
101
|
|
|
101
102
|
def confirm_ready!
|
|
102
103
|
close if request_in_progress?
|
|
103
|
-
|
|
104
|
+
reconnect_on_fork if fork_detected?
|
|
104
105
|
end
|
|
105
106
|
|
|
106
107
|
def confirm_in_progress!
|
|
107
108
|
raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless request_in_progress?
|
|
108
109
|
|
|
109
|
-
|
|
110
|
+
reconnect_on_fork if fork_detected?
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def close
|
|
@@ -150,19 +151,43 @@ module Dalli
|
|
|
150
151
|
data = @sock.gets("\r\n")
|
|
151
152
|
error_on_request!('EOF in read_line') if data.nil?
|
|
152
153
|
data
|
|
153
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
154
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
154
155
|
error_on_request!(e)
|
|
155
156
|
end
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
# JRuby doesn't support IO#timeout=, so use custom readfull implementation
|
|
159
|
+
# CRuby 3.3+ has IO#timeout= which makes IO#read work with timeouts
|
|
160
|
+
if RUBY_ENGINE == 'jruby'
|
|
161
|
+
def read(count)
|
|
162
|
+
@sock.readfull(count)
|
|
163
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
164
|
+
error_on_request!(e)
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
def read(count)
|
|
168
|
+
@sock.read(count)
|
|
169
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
170
|
+
error_on_request!(e)
|
|
171
|
+
end
|
|
161
172
|
end
|
|
162
173
|
|
|
163
174
|
def write(bytes)
|
|
164
175
|
@sock.write(bytes)
|
|
165
|
-
rescue SystemCallError, *TIMEOUT_ERRORS => e
|
|
176
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
|
|
177
|
+
error_on_request!(e)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def flush
|
|
181
|
+
@sock.flush
|
|
182
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
|
|
183
|
+
error_on_request!(e)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def flushed_write(bytes)
|
|
187
|
+
written = @sock.write(bytes)
|
|
188
|
+
@sock.flush
|
|
189
|
+
written
|
|
190
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
|
|
166
191
|
error_on_request!(e)
|
|
167
192
|
end
|
|
168
193
|
|
|
@@ -192,7 +217,7 @@ module Dalli
|
|
|
192
217
|
def reconnect!(message)
|
|
193
218
|
close
|
|
194
219
|
sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
|
|
195
|
-
raise Dalli::
|
|
220
|
+
raise Dalli::RetryableNetworkError, message
|
|
196
221
|
end
|
|
197
222
|
|
|
198
223
|
def reset_down_info
|
|
@@ -212,20 +237,18 @@ module Dalli
|
|
|
212
237
|
end
|
|
213
238
|
|
|
214
239
|
def log_warn_message(err_or_string)
|
|
215
|
-
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
|
216
240
|
Dalli.logger.warn do
|
|
217
241
|
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
|
218
242
|
"#{name} failed (count: #{@fail_count}) #{detail}"
|
|
219
243
|
end
|
|
220
244
|
end
|
|
221
245
|
|
|
222
|
-
def
|
|
246
|
+
def reconnect_on_fork
|
|
223
247
|
message = 'Fork detected, re-connecting child process...'
|
|
224
248
|
Dalli.logger.info { message }
|
|
225
|
-
# Close socket on a fork
|
|
226
|
-
# on next request.
|
|
249
|
+
# Close socket on a fork and reconnect immediately
|
|
227
250
|
close
|
|
228
|
-
|
|
251
|
+
establish_connection
|
|
229
252
|
end
|
|
230
253
|
|
|
231
254
|
def fork_detected?
|
|
@@ -10,7 +10,7 @@ module Dalli
|
|
|
10
10
|
# memcached supports the use of base64 hashes for keys containing
|
|
11
11
|
# whitespace or non-ASCII characters, provided the 'b' flag is included in the request.
|
|
12
12
|
class KeyRegularizer
|
|
13
|
-
WHITESPACE = /\s
|
|
13
|
+
WHITESPACE = /\s/
|
|
14
14
|
|
|
15
15
|
def self.encode(key)
|
|
16
16
|
return [key, false] if key.ascii_only? && !WHITESPACE.match(key)
|