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/protocol/base.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'forwardable'
|
|
4
|
-
require 'set'
|
|
5
4
|
require 'socket'
|
|
6
5
|
require 'timeout'
|
|
7
6
|
|
|
@@ -23,9 +22,9 @@ module Dalli
|
|
|
23
22
|
|
|
24
23
|
def initialize(attribs, client_options = {})
|
|
25
24
|
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
|
25
|
+
warn_uri_credentials(user_creds)
|
|
26
26
|
@options = client_options.merge(user_creds)
|
|
27
27
|
@raw_mode = client_options[:raw]
|
|
28
|
-
@verify_reply_keys = client_options[:verify_reply_keys] ? true : false
|
|
29
28
|
@value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
30
29
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
31
30
|
end
|
|
@@ -36,36 +35,20 @@ module Dalli
|
|
|
36
35
|
@raw_mode
|
|
37
36
|
end
|
|
38
37
|
|
|
39
|
-
# True when values should come back as stored: the client is in raw mode,
|
|
40
|
-
# or this request passed raw: true. A stored value is then never
|
|
41
|
-
# deserialized or decompressed for a caller who asked for raw bytes.
|
|
42
|
-
def raw_request?(options)
|
|
43
|
-
raw_mode? || (options.is_a?(Hash) && options[:raw]) ? true : false
|
|
44
|
-
end
|
|
45
|
-
|
|
46
38
|
# Chokepoint method for error handling and ensuring liveness
|
|
47
39
|
def request(opkey, *args)
|
|
48
40
|
verify_state(opkey)
|
|
49
41
|
|
|
50
42
|
begin
|
|
51
43
|
@connection_manager.start_request!
|
|
52
|
-
drain_pending_quiet_responses(opkey)
|
|
53
44
|
response = send(opkey, *args)
|
|
54
|
-
# Quiet requests can leave replies on the socket; remember that so a
|
|
55
|
-
# later non-quiet request knows to read them first
|
|
56
|
-
@connection_manager.mark_quiet_responses_pending! if quiet? && opkey != :noop
|
|
57
45
|
|
|
58
46
|
# pipelined_get/pipelined_get_interleaved emit query but don't read the response(s)
|
|
59
47
|
@connection_manager.finish_request! unless %i[pipelined_get pipelined_get_interleaved].include?(opkey)
|
|
60
48
|
|
|
61
49
|
response
|
|
62
|
-
rescue Dalli::MarshalError
|
|
63
|
-
log_marshal_err(args.first, e)
|
|
64
|
-
# Raised only while the connection is clean: a value is marshalled
|
|
65
|
-
# before any of its request is written. Leaving the request in
|
|
66
|
-
# progress would close the connection on the next request, which
|
|
67
|
-
# discards earlier quiet writes still waiting in the write buffer.
|
|
68
|
-
@connection_manager.abort_request!
|
|
50
|
+
rescue Dalli::MarshalError => e
|
|
51
|
+
log_marshal_err(args.first, e)
|
|
69
52
|
raise
|
|
70
53
|
rescue Dalli::DalliError
|
|
71
54
|
raise
|
|
@@ -100,7 +83,7 @@ module Dalli
|
|
|
100
83
|
verify_pipelined_state(:getkq)
|
|
101
84
|
write_noop
|
|
102
85
|
# Use ensure_ready instead of reset to preserve any data already buffered
|
|
103
|
-
#
|
|
86
|
+
# during interleaved pipelined get draining
|
|
104
87
|
response_buffer.ensure_ready
|
|
105
88
|
end
|
|
106
89
|
|
|
@@ -109,25 +92,12 @@ module Dalli
|
|
|
109
92
|
# repeatedly whenever this server's socket is readable until
|
|
110
93
|
# #pipeline_complete?.
|
|
111
94
|
#
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
# Without a block, returns a Hash of { key => [value, cas] }.
|
|
115
|
-
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
116
|
-
def pipeline_next_responses(&block)
|
|
117
|
-
return abandon_pipeline if @pipeline_out_of_step
|
|
118
|
-
|
|
95
|
+
# Returns a Hash of kv pairs received.
|
|
96
|
+
def pipeline_next_responses
|
|
119
97
|
reconnect_on_pipeline_complete!
|
|
120
|
-
values =
|
|
98
|
+
values = {}
|
|
121
99
|
|
|
122
|
-
|
|
123
|
-
# this rescue, so an Errno or Timeout::Error it raises reaches the
|
|
124
|
-
# caller instead of being mistaken for a network failure (which closed
|
|
125
|
-
# the connection and retried the whole get_multi, yielding keys twice).
|
|
126
|
-
begin
|
|
127
|
-
response_buffer.read
|
|
128
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
129
|
-
@connection_manager.error_on_request!(e)
|
|
130
|
-
end
|
|
100
|
+
response_buffer.read
|
|
131
101
|
|
|
132
102
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
133
103
|
# status is not nil only if we have a full response to parse
|
|
@@ -137,55 +107,33 @@ module Dalli
|
|
|
137
107
|
# to the noop at the end of the pipeline
|
|
138
108
|
finish_pipeline && break if status && key.nil?
|
|
139
109
|
|
|
140
|
-
# A key that wasn't requested means the replies are out of step
|
|
141
|
-
# (see expect_reply_keys): keep what was read so far, drop the rest
|
|
142
|
-
if unexpected_reply_key?(key)
|
|
143
|
-
abandon_pipeline
|
|
144
|
-
break
|
|
145
|
-
end
|
|
146
|
-
|
|
147
110
|
# If the status is ok and the key is not nil, then this is a
|
|
148
111
|
# getkq response with a value that we want to set in the response hash
|
|
149
|
-
unless key.nil?
|
|
150
|
-
if block
|
|
151
|
-
yield key, value, cas
|
|
152
|
-
else
|
|
153
|
-
values ||= {}
|
|
154
|
-
values[key] = [value, cas]
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
# A request to this server from inside the block closes the
|
|
159
|
-
# connection, and with it the rest of this get_multi's replies
|
|
160
|
-
raise_pipeline_closed unless response_buffer.in_progress?
|
|
112
|
+
values[key] = [value, cas] unless key.nil?
|
|
161
113
|
|
|
162
114
|
# Get the next response from the buffer
|
|
163
115
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
164
116
|
end
|
|
165
117
|
|
|
166
|
-
values
|
|
118
|
+
values
|
|
119
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
120
|
+
@connection_manager.error_on_request!(e)
|
|
167
121
|
end
|
|
168
122
|
|
|
169
123
|
# Abort current pipelined get. Generally used to signal an external
|
|
170
|
-
# timeout during pipelined get
|
|
171
|
-
# reading all its replies (timed_out: false). The underlying socket is
|
|
124
|
+
# timeout during pipelined get. The underlying socket is
|
|
172
125
|
# disconnected, and the exception is swallowed.
|
|
173
126
|
#
|
|
174
127
|
# Returns nothing.
|
|
175
|
-
def pipeline_abort
|
|
128
|
+
def pipeline_abort
|
|
176
129
|
response_buffer.clear
|
|
177
|
-
@pipeline_out_of_step = false
|
|
178
|
-
@expected_reply_keys = nil
|
|
179
130
|
@connection_manager.abort_request!
|
|
180
131
|
return true unless connected?
|
|
181
132
|
|
|
182
|
-
# Closes the connection, which ensures that our connection
|
|
183
|
-
# clean state for future requests
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
# row would otherwise mark a healthy server down.
|
|
187
|
-
log_pipeline_abort(timed_out)
|
|
188
|
-
@connection_manager.close
|
|
133
|
+
# Closes the connection, which ensures that our connection
|
|
134
|
+
# is in a clean state for future requests
|
|
135
|
+
@connection_manager.error_on_request!('External timeout')
|
|
136
|
+
rescue NetworkError
|
|
189
137
|
true
|
|
190
138
|
end
|
|
191
139
|
|
|
@@ -194,18 +142,6 @@ module Dalli
|
|
|
194
142
|
!response_buffer.in_progress?
|
|
195
143
|
end
|
|
196
144
|
|
|
197
|
-
def username
|
|
198
|
-
@options[:username] || ENV.fetch('MEMCACHE_USERNAME', nil)
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def password
|
|
202
|
-
@options[:password] || ENV.fetch('MEMCACHE_PASSWORD', nil)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def require_auth?
|
|
206
|
-
!username.nil?
|
|
207
|
-
end
|
|
208
|
-
|
|
209
145
|
def quiet?
|
|
210
146
|
Thread.current[::Dalli::QUIET]
|
|
211
147
|
end
|
|
@@ -215,21 +151,19 @@ module Dalli
|
|
|
215
151
|
|
|
216
152
|
private
|
|
217
153
|
|
|
218
|
-
|
|
219
|
-
|
|
154
|
+
URI_CREDENTIAL_WARNING = 'Dalli 5.0 removed SASL authentication. ' \
|
|
155
|
+
'Credentials in memcached:// URIs are ignored.'
|
|
156
|
+
private_constant :URI_CREDENTIAL_WARNING
|
|
220
157
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
# them at its end, but the connection is shared: another thread or fiber
|
|
224
|
-
# (quiet? is fiber-local) can make a request while the block is still
|
|
225
|
-
# open, and would otherwise read the block's replies as its own. The
|
|
226
|
-
# noop also sends any quiet requests still buffered.
|
|
227
|
-
def drain_pending_quiet_responses(opkey)
|
|
228
|
-
return unless @connection_manager.quiet_responses_pending? && opkey != :noop && !quiet?
|
|
158
|
+
def warn_uri_credentials(user_creds)
|
|
159
|
+
return if user_creds[:username].nil? && user_creds[:password].nil?
|
|
229
160
|
|
|
230
|
-
|
|
161
|
+
Dalli.logger.warn(URI_CREDENTIAL_WARNING)
|
|
231
162
|
end
|
|
232
163
|
|
|
164
|
+
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
165
|
+
private_constant :ALLOWED_QUIET_OPS
|
|
166
|
+
|
|
233
167
|
def verify_allowed_quiet!(opkey)
|
|
234
168
|
return if ALLOWED_QUIET_OPS.include?(opkey)
|
|
235
169
|
|
|
@@ -281,8 +215,7 @@ module Dalli
|
|
|
281
215
|
|
|
282
216
|
def connect
|
|
283
217
|
@connection_manager.establish_connection
|
|
284
|
-
|
|
285
|
-
@version = version # Connect socket if not authed
|
|
218
|
+
@version = version
|
|
286
219
|
up!
|
|
287
220
|
end
|
|
288
221
|
|
|
@@ -291,7 +224,6 @@ module Dalli
|
|
|
291
224
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
292
225
|
# the expected state before pipeline_response_setup is called.
|
|
293
226
|
response_buffer.clear
|
|
294
|
-
expect_reply_keys(keys)
|
|
295
227
|
|
|
296
228
|
req = +''
|
|
297
229
|
keys.each do |key|
|
|
@@ -305,10 +237,8 @@ module Dalli
|
|
|
305
237
|
# This prevents socket buffer deadlock when sending many keys.
|
|
306
238
|
# Populates the provided results hash with any responses drained during send.
|
|
307
239
|
def pipelined_get_interleaved(keys, chunk_size, results)
|
|
308
|
-
#
|
|
309
|
-
|
|
310
|
-
response_buffer.reset
|
|
311
|
-
expect_reply_keys(keys)
|
|
240
|
+
# Initialize the response buffer for draining during send phase
|
|
241
|
+
response_buffer.ensure_ready
|
|
312
242
|
|
|
313
243
|
keys.each_slice(chunk_size) do |chunk|
|
|
314
244
|
# Build and write this chunk of requests
|
|
@@ -341,12 +271,6 @@ module Dalli
|
|
|
341
271
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
342
272
|
break if status.nil? # No complete response available
|
|
343
273
|
|
|
344
|
-
# Handled by the next pipeline_next_responses, once sending is done
|
|
345
|
-
if unexpected_reply_key?(key)
|
|
346
|
-
@pipeline_out_of_step = true
|
|
347
|
-
break
|
|
348
|
-
end
|
|
349
|
-
|
|
350
274
|
results[key] = [value, cas] unless key.nil?
|
|
351
275
|
end
|
|
352
276
|
rescue SystemCallError, Dalli::NetworkError
|
|
@@ -354,48 +278,8 @@ module Dalli
|
|
|
354
278
|
nil
|
|
355
279
|
end
|
|
356
280
|
|
|
357
|
-
# Registered with the connection manager, which clears it whenever the
|
|
358
|
-
# connection closes, so a new connection never parses an old one's bytes
|
|
359
281
|
def response_buffer
|
|
360
|
-
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
361
|
-
@connection_manager.response_buffer = buffer
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
# A multi-get only accepts replies for the keys it asked for. memcached
|
|
366
|
-
# names an item created by auto-create (ma or mg with N) for a
|
|
367
|
-
# base64-encoded key by its raw bytes, without the b flag; such a key
|
|
368
|
-
# can contain spaces or CR/LF, which split the reply header and make the
|
|
369
|
-
# replies after it parse as different ones. A key that wasn't requested
|
|
370
|
-
# is how that shows up.
|
|
371
|
-
# Only with the verify_reply_keys option, since building the set costs a
|
|
372
|
-
# multi-get about 15%. Dalli itself no longer creates such items (see
|
|
373
|
-
# Meta#decr_incr); the check guards against ones that older versions or
|
|
374
|
-
# other clients created, on memcached versions with the behavior.
|
|
375
|
-
def expect_reply_keys(keys)
|
|
376
|
-
@pipeline_out_of_step = false
|
|
377
|
-
@expected_reply_keys = (keys.to_set { |k| reply_key_form(k) } if @verify_reply_keys)
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
def unexpected_reply_key?(key)
|
|
381
|
-
!key.nil? && @expected_reply_keys && !@expected_reply_keys.include?(reply_key_form(key))
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
# Keys compared as bytes; ASCII keys compare equal in any encoding as is
|
|
385
|
-
def reply_key_form(key)
|
|
386
|
-
key.ascii_only? ? key : key.b
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
# Keeps what was read, drops the rest of this server's replies, and
|
|
390
|
-
# closes the connection so nothing unread is left on it. Not a socket
|
|
391
|
-
# failure, so it doesn't count toward socket_max_failures; the keys not
|
|
392
|
-
# read are simply missing from the result.
|
|
393
|
-
def abandon_pipeline
|
|
394
|
-
Dalli.logger.warn { "#{name} replied to a get_multi with a key that wasn't requested; closing the connection" }
|
|
395
|
-
@pipeline_out_of_step = false
|
|
396
|
-
response_buffer.clear
|
|
397
|
-
@connection_manager.close
|
|
398
|
-
{}
|
|
282
|
+
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
|
399
283
|
end
|
|
400
284
|
|
|
401
285
|
# Called after the noop response is received at the end of a set
|
|
@@ -411,19 +295,6 @@ module Dalli
|
|
|
411
295
|
@connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
|
|
412
296
|
end
|
|
413
297
|
|
|
414
|
-
def raise_pipeline_closed
|
|
415
|
-
raise Dalli::DalliError, "The connection to #{name} closed while a get_multi was reading its replies " \
|
|
416
|
-
'(a request made from inside the get_multi block?)'
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
def log_pipeline_abort(timed_out)
|
|
420
|
-
if timed_out
|
|
421
|
-
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
422
|
-
else
|
|
423
|
-
Dalli.logger.debug { "#{name} get_multi stopped before reading all replies; closing the connection" }
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
|
|
427
298
|
def log_marshal_err(key, err)
|
|
428
299
|
Dalli.logger.error "Marshalling error for key '#{key}': #{err.message}"
|
|
429
300
|
Dalli.logger.error 'You are trying to cache a Ruby object which cannot be serialized to memcached.'
|
|
@@ -28,9 +28,6 @@ module Dalli
|
|
|
28
28
|
|
|
29
29
|
attr_accessor :hostname, :port, :socket_type, :options
|
|
30
30
|
attr_reader :sock
|
|
31
|
-
# The buffer replies are read into (see Base#response_buffer). It's
|
|
32
|
-
# cleared with the write buffer whenever the connection closes.
|
|
33
|
-
attr_writer :response_buffer
|
|
34
31
|
|
|
35
32
|
def initialize(hostname, port, socket_type, client_options)
|
|
36
33
|
@hostname = hostname
|
|
@@ -38,14 +35,9 @@ module Dalli
|
|
|
38
35
|
@socket_type = socket_type
|
|
39
36
|
@options = DEFAULTS.merge(client_options)
|
|
40
37
|
@request_in_progress = false
|
|
41
|
-
@quiet_responses_pending = false
|
|
42
38
|
@sock = nil
|
|
43
39
|
@pid = nil
|
|
44
|
-
@write_buffer = []
|
|
45
|
-
@write_buffer_bytes = 0
|
|
46
|
-
@response_buffer = nil
|
|
47
40
|
|
|
48
|
-
@fail_count = 0
|
|
49
41
|
reset_down_info
|
|
50
42
|
end
|
|
51
43
|
|
|
@@ -61,13 +53,11 @@ module Dalli
|
|
|
61
53
|
Dalli.logger.debug { "Dalli::Server#connect #{name}" }
|
|
62
54
|
|
|
63
55
|
@sock = memcached_socket
|
|
64
|
-
#
|
|
65
|
-
@
|
|
66
|
-
@pid = Process.pid
|
|
56
|
+
@sock.sync = false # Enable buffered I/O for better performance
|
|
57
|
+
@pid = PIDCache.pid
|
|
67
58
|
@request_in_progress = false
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# SocketError = DNS resolution failure; SSL_ERRORS = failed TLS handshake
|
|
59
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
|
|
60
|
+
# SocketError = DNS resolution failure
|
|
71
61
|
error_on_request!(e)
|
|
72
62
|
end
|
|
73
63
|
|
|
@@ -95,8 +85,6 @@ module Dalli
|
|
|
95
85
|
def down!
|
|
96
86
|
close
|
|
97
87
|
log_down_detected
|
|
98
|
-
# Once down_retry_delay passes, the server gets a full set of attempts
|
|
99
|
-
@fail_count = 0
|
|
100
88
|
|
|
101
89
|
@error = $ERROR_INFO&.class&.name
|
|
102
90
|
@msg ||= $ERROR_INFO&.message
|
|
@@ -126,44 +114,19 @@ module Dalli
|
|
|
126
114
|
return unless @sock
|
|
127
115
|
|
|
128
116
|
begin
|
|
129
|
-
|
|
117
|
+
@sock.close
|
|
130
118
|
rescue StandardError
|
|
131
119
|
nil
|
|
132
120
|
end
|
|
133
121
|
@sock = nil
|
|
134
122
|
@pid = nil
|
|
135
|
-
@quiet_responses_pending = false
|
|
136
|
-
discard_write_buffer
|
|
137
|
-
@response_buffer&.clear
|
|
138
123
|
abort_request!
|
|
139
124
|
end
|
|
140
125
|
|
|
141
|
-
# A forked child shares the parent's connection, so it closes only its
|
|
142
|
-
# own file descriptor. Closing the TLS socket itself would send
|
|
143
|
-
# close_notify and end the parent's TLS session.
|
|
144
|
-
def close_socket
|
|
145
|
-
fork_detected? ? @sock.to_io.close : @sock.close
|
|
146
|
-
end
|
|
147
|
-
|
|
148
126
|
def connected?
|
|
149
127
|
!@sock.nil?
|
|
150
128
|
end
|
|
151
129
|
|
|
152
|
-
# True when a quiet request was written on this connection and its
|
|
153
|
-
# replies (memcached only suppresses some of them) haven't been
|
|
154
|
-
# consumed by a noop yet.
|
|
155
|
-
def quiet_responses_pending?
|
|
156
|
-
@quiet_responses_pending
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def mark_quiet_responses_pending!
|
|
160
|
-
@quiet_responses_pending = true
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
def clear_quiet_responses_pending!
|
|
164
|
-
@quiet_responses_pending = false
|
|
165
|
-
end
|
|
166
|
-
|
|
167
130
|
def request_in_progress?
|
|
168
131
|
@request_in_progress
|
|
169
132
|
end
|
|
@@ -178,88 +141,47 @@ module Dalli
|
|
|
178
141
|
raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless @request_in_progress
|
|
179
142
|
|
|
180
143
|
@request_in_progress = false
|
|
181
|
-
# A completed request is what proves the server healthy again, so the
|
|
182
|
-
# failure count resets here rather than on reconnect: a server that
|
|
183
|
-
# accepts connections but never answers would otherwise reset it on
|
|
184
|
-
# every retry and never reach socket_max_failures.
|
|
185
|
-
@fail_count = 0
|
|
186
144
|
end
|
|
187
145
|
|
|
188
146
|
def abort_request!
|
|
189
147
|
@request_in_progress = false
|
|
190
148
|
end
|
|
191
149
|
|
|
192
|
-
# The longest reply line Dalli reads, in bytes, CRLF included. A meta
|
|
193
|
-
# reply header is at most about 500 bytes: "VA <size>" plus its flags,
|
|
194
|
-
# the largest being a 250-byte key base64-encoded to 336 bytes, a
|
|
195
|
-
# 20-digit CAS, a 32-byte opaque and a few numeric fields. The longest
|
|
196
|
-
# stats line (stats settings) carries a file path such as ext_path, up
|
|
197
|
-
# to PATH_MAX (4096). 8 KiB leaves room for both, while a server that
|
|
198
|
-
# never sends CRLF can't grow the read buffer without bound.
|
|
199
|
-
MAX_LINE_BYTES = 8 * 1024
|
|
200
|
-
|
|
201
150
|
def read_line
|
|
202
|
-
|
|
203
|
-
data = @sock.gets("\r\n", MAX_LINE_BYTES)
|
|
151
|
+
data = @sock.gets("\r\n")
|
|
204
152
|
error_on_request!('EOF in read_line') if data.nil?
|
|
205
|
-
# Without its CRLF the line either ran past MAX_LINE_BYTES or was cut
|
|
206
|
-
# short by EOF; either way the connection is out of step
|
|
207
|
-
error_on_request!("Reply line without CRLF in the first #{MAX_LINE_BYTES} bytes") unless data.end_with?("\r\n")
|
|
208
153
|
data
|
|
209
154
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
210
155
|
error_on_request!(e)
|
|
211
156
|
end
|
|
212
157
|
|
|
213
|
-
# memcached can't store an item larger than 1 GiB (its -I maximum), so a
|
|
214
|
-
# reply claiming more (or a negative size) is malformed or hostile.
|
|
215
|
-
# Reading allocates the full count up front, so check before reading.
|
|
216
|
-
MAX_READ_BYTES = (1024 * 1024 * 1024) + 2 # plus the value's trailing "\r\n"
|
|
217
|
-
|
|
218
|
-
def check_read_size!(count)
|
|
219
|
-
return if count.between?(0, MAX_READ_BYTES)
|
|
220
|
-
|
|
221
|
-
raise Dalli::DalliError, "Reply size #{count} from #{name} is out of range"
|
|
222
|
-
end
|
|
223
|
-
|
|
224
158
|
def read(count)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
159
|
+
# JRuby doesn't support IO#timeout=, so use custom readfull implementation
|
|
160
|
+
# CRuby 3.3+ has IO#timeout= which makes IO#read work with timeouts
|
|
161
|
+
if RUBY_ENGINE == 'jruby'
|
|
162
|
+
@sock.readfull(count)
|
|
163
|
+
else
|
|
164
|
+
@sock.read(count)
|
|
165
|
+
end
|
|
228
166
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
229
167
|
error_on_request!(e)
|
|
230
168
|
end
|
|
231
169
|
|
|
232
|
-
# Requests are buffered here rather than in the socket's own IO buffer
|
|
233
|
-
# (the socket is sync). Ruby flushes an IO's write buffer when any process
|
|
234
|
-
# holding it closes or finalizes the IO, so after a fork a child could
|
|
235
|
-
# send bytes the parent had buffered but not sent yet: a quiet write would
|
|
236
|
-
# run twice, or the parent's replies would shift onto the wrong requests.
|
|
237
|
-
# A forked child discards this buffer instead. Like IO's own buffer, it
|
|
238
|
-
# is sent once it grows past this size, or when a reply is read.
|
|
239
|
-
WRITE_BUFFER_FLUSH_BYTES = 64 * 1024
|
|
240
|
-
|
|
241
170
|
def write(bytes)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
# IO buffer copied on write
|
|
245
|
-
@write_buffer << (bytes.frozen? ? bytes : bytes.dup)
|
|
246
|
-
@write_buffer_bytes += bytes.bytesize
|
|
247
|
-
flush_write_buffer if @write_buffer_bytes >= WRITE_BUFFER_FLUSH_BYTES
|
|
248
|
-
bytes.bytesize
|
|
249
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
|
|
171
|
+
@sock.write(bytes)
|
|
172
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
|
|
250
173
|
error_on_request!(e)
|
|
251
174
|
end
|
|
252
175
|
|
|
253
176
|
def flush
|
|
254
|
-
|
|
255
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS
|
|
177
|
+
@sock.flush
|
|
178
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
|
|
256
179
|
error_on_request!(e)
|
|
257
180
|
end
|
|
258
181
|
|
|
259
182
|
# Non-blocking read. Here to support the operation
|
|
260
183
|
# of the get_multi operation
|
|
261
184
|
def read_nonblock
|
|
262
|
-
flush_write_buffer
|
|
263
185
|
@sock.read_available
|
|
264
186
|
end
|
|
265
187
|
|
|
@@ -286,9 +208,8 @@ module Dalli
|
|
|
286
208
|
raise Dalli::RetryableNetworkError, message
|
|
287
209
|
end
|
|
288
210
|
|
|
289
|
-
# Called on connect. Deliberately leaves @fail_count alone; see
|
|
290
|
-
# finish_request!.
|
|
291
211
|
def reset_down_info
|
|
212
|
+
@fail_count = 0
|
|
292
213
|
@down_at = nil
|
|
293
214
|
@last_down_at = nil
|
|
294
215
|
@msg = nil
|
|
@@ -313,18 +234,13 @@ module Dalli
|
|
|
313
234
|
def reconnect_on_fork
|
|
314
235
|
message = 'Fork detected, re-connecting child process...'
|
|
315
236
|
Dalli.logger.info { message }
|
|
316
|
-
#
|
|
317
|
-
# without touching the parent's connection, and reconnects immediately
|
|
237
|
+
# Close socket on a fork and reconnect immediately
|
|
318
238
|
close
|
|
319
239
|
establish_connection
|
|
320
240
|
end
|
|
321
241
|
|
|
322
242
|
def fork_detected?
|
|
323
|
-
|
|
324
|
-
# Process._fork hook, and another library's fork hook (such as
|
|
325
|
-
# connection_pool closing its connections) can run in the child before
|
|
326
|
-
# it, which would close the parent's TLS session as if it were ours.
|
|
327
|
-
@pid && @pid != Process.pid
|
|
243
|
+
@pid && @pid != PIDCache.pid
|
|
328
244
|
end
|
|
329
245
|
|
|
330
246
|
def log_down_detected
|
|
@@ -345,39 +261,6 @@ module Dalli
|
|
|
345
261
|
time = Time.now - @down_at
|
|
346
262
|
Dalli.logger.warn { format('%<name>s is back (downtime was %<time>.3f seconds)', name: name, time: time) }
|
|
347
263
|
end
|
|
348
|
-
|
|
349
|
-
private
|
|
350
|
-
|
|
351
|
-
# One write per flush. A TLS socket sends each argument of
|
|
352
|
-
# write(*parts) as its own record and system call, so the parts are
|
|
353
|
-
# joined first.
|
|
354
|
-
def flush_write_buffer
|
|
355
|
-
return if @write_buffer.empty?
|
|
356
|
-
|
|
357
|
-
@sock.write(@write_buffer.size == 1 ? @write_buffer.first : joined_write_buffer)
|
|
358
|
-
discard_write_buffer
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
# Joined as bytes: requests can mix UTF-8 and binary strings, which
|
|
362
|
-
# String#<< refuses to combine when both hold non-ASCII bytes
|
|
363
|
-
if String.method_defined?(:append_as_bytes)
|
|
364
|
-
def joined_write_buffer
|
|
365
|
-
out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
|
|
366
|
-
@write_buffer.each { |part| out.append_as_bytes(part) }
|
|
367
|
-
out
|
|
368
|
-
end
|
|
369
|
-
else
|
|
370
|
-
def joined_write_buffer
|
|
371
|
-
out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
|
|
372
|
-
@write_buffer.each { |part| out << part.b }
|
|
373
|
-
out
|
|
374
|
-
end
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
def discard_write_buffer
|
|
378
|
-
@write_buffer.clear
|
|
379
|
-
@write_buffer_bytes = 0
|
|
380
|
-
end
|
|
381
264
|
end
|
|
382
265
|
end
|
|
383
266
|
end
|