dalli 3.1.0 → 3.1.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of dalli might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +10 -0
- data/lib/dalli/client.rb +155 -120
- data/lib/dalli/options.rb +3 -3
- data/lib/dalli/pipelined_getter.rb +13 -11
- data/lib/dalli/protocol/binary/request_formatter.rb +11 -3
- data/lib/dalli/protocol/binary/response_processor.rb +37 -15
- data/lib/dalli/protocol/binary.rb +183 -349
- data/lib/dalli/protocol/connection_manager.rb +242 -0
- data/lib/dalli/protocol/response_buffer.rb +9 -6
- data/lib/dalli/protocol/server_config_parser.rb +7 -7
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +2 -1
- metadata +4 -3
@@ -0,0 +1,242 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
require 'socket'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
module Dalli
|
8
|
+
module Protocol
|
9
|
+
##
|
10
|
+
# Manages the socket connection to the server, including ensuring liveness
|
11
|
+
# and retries.
|
12
|
+
##
|
13
|
+
class ConnectionManager
|
14
|
+
DEFAULTS = {
|
15
|
+
# seconds between trying to contact a remote server
|
16
|
+
down_retry_delay: 30,
|
17
|
+
# connect/read/write timeout for socket operations
|
18
|
+
socket_timeout: 1,
|
19
|
+
# times a socket operation may fail before considering the server dead
|
20
|
+
socket_max_failures: 2,
|
21
|
+
# amount of time to sleep between retries when a failure occurs
|
22
|
+
socket_failure_delay: 0.1,
|
23
|
+
# Set keepalive
|
24
|
+
keepalive: true
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
attr_accessor :hostname, :port, :socket_type, :options
|
28
|
+
attr_reader :sock
|
29
|
+
|
30
|
+
def initialize(hostname, port, socket_type, client_options)
|
31
|
+
@hostname = hostname
|
32
|
+
@port = port
|
33
|
+
@socket_type = socket_type
|
34
|
+
@options = DEFAULTS.merge(client_options)
|
35
|
+
@request_in_progress = false
|
36
|
+
@sock = nil
|
37
|
+
@pid = nil
|
38
|
+
|
39
|
+
reset_down_info
|
40
|
+
end
|
41
|
+
|
42
|
+
def name
|
43
|
+
if socket_type == :unix
|
44
|
+
hostname
|
45
|
+
else
|
46
|
+
"#{hostname}:#{port}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def establish_connection
|
51
|
+
Dalli.logger.debug { "Dalli::Server#connect #{name}" }
|
52
|
+
|
53
|
+
@sock = memcached_socket
|
54
|
+
@pid = Process.pid
|
55
|
+
rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
|
56
|
+
# SocketError = DNS resolution failure
|
57
|
+
error_on_request!(e)
|
58
|
+
end
|
59
|
+
|
60
|
+
def reconnect_down_server?
|
61
|
+
return true unless @last_down_at
|
62
|
+
|
63
|
+
time_to_next_reconnect = @last_down_at + options[:down_retry_delay] - Time.now
|
64
|
+
return true unless time_to_next_reconnect.positive?
|
65
|
+
|
66
|
+
Dalli.logger.debug do
|
67
|
+
format('down_retry_delay not reached for %<name>s (%<time>.3f seconds left)', name: name,
|
68
|
+
time: time_to_next_reconnect)
|
69
|
+
end
|
70
|
+
false
|
71
|
+
end
|
72
|
+
|
73
|
+
def up!
|
74
|
+
log_up_detected
|
75
|
+
reset_down_info
|
76
|
+
end
|
77
|
+
|
78
|
+
# Marks the server instance as down. Updates the down_at state
|
79
|
+
# and raises an Dalli::NetworkError that includes the underlying
|
80
|
+
# error in the message. Calls close to clean up socket state
|
81
|
+
def down!
|
82
|
+
close
|
83
|
+
log_down_detected
|
84
|
+
|
85
|
+
@error = $ERROR_INFO&.class&.name
|
86
|
+
@msg ||= $ERROR_INFO&.message
|
87
|
+
raise_down_error
|
88
|
+
end
|
89
|
+
|
90
|
+
def raise_down_error
|
91
|
+
raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def socket_timeout
|
95
|
+
@socket_timeout ||= @options[:socket_timeout]
|
96
|
+
end
|
97
|
+
|
98
|
+
def confirm_ready!
|
99
|
+
error_on_request!(RuntimeError.new('Already writing to socket')) if request_in_progress?
|
100
|
+
close_on_fork if fork_detected?
|
101
|
+
end
|
102
|
+
|
103
|
+
def close
|
104
|
+
return unless @sock
|
105
|
+
|
106
|
+
begin
|
107
|
+
@sock.close
|
108
|
+
rescue StandardError
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
@sock = nil
|
112
|
+
@pid = nil
|
113
|
+
abort_request!
|
114
|
+
end
|
115
|
+
|
116
|
+
def connected?
|
117
|
+
!@sock.nil?
|
118
|
+
end
|
119
|
+
|
120
|
+
def request_in_progress?
|
121
|
+
@request_in_progress
|
122
|
+
end
|
123
|
+
|
124
|
+
def start_request!
|
125
|
+
@request_in_progress = true
|
126
|
+
end
|
127
|
+
|
128
|
+
def finish_request!
|
129
|
+
@request_in_progress = false
|
130
|
+
end
|
131
|
+
|
132
|
+
def abort_request!
|
133
|
+
@request_in_progress = false
|
134
|
+
end
|
135
|
+
|
136
|
+
def read(count)
|
137
|
+
start_request!
|
138
|
+
data = @sock.readfull(count)
|
139
|
+
finish_request!
|
140
|
+
data
|
141
|
+
rescue SystemCallError, Timeout::Error, EOFError => e
|
142
|
+
error_on_request!(e)
|
143
|
+
end
|
144
|
+
|
145
|
+
def write(bytes)
|
146
|
+
start_request!
|
147
|
+
result = @sock.write(bytes)
|
148
|
+
finish_request!
|
149
|
+
result
|
150
|
+
rescue SystemCallError, Timeout::Error => e
|
151
|
+
error_on_request!(e)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Non-blocking read. Should only be used in the context
|
155
|
+
# of a caller who has called start_request!, but not yet
|
156
|
+
# called finish_request!. Here to support the operation
|
157
|
+
# of the get_multi operation
|
158
|
+
def read_nonblock
|
159
|
+
@sock.read_available
|
160
|
+
end
|
161
|
+
|
162
|
+
def max_allowed_failures
|
163
|
+
@max_allowed_failures ||= @options[:socket_max_failures] || 2
|
164
|
+
end
|
165
|
+
|
166
|
+
def error_on_request!(err_or_string)
|
167
|
+
log_warn_message(err_or_string)
|
168
|
+
|
169
|
+
@fail_count += 1
|
170
|
+
if @fail_count >= max_allowed_failures
|
171
|
+
down!
|
172
|
+
else
|
173
|
+
# Closes the existing socket, setting up for a reconnect
|
174
|
+
# on next request
|
175
|
+
reconnect!('Socket operation failed, retrying...')
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def reconnect!(message)
|
180
|
+
close
|
181
|
+
sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
|
182
|
+
raise Dalli::NetworkError, message
|
183
|
+
end
|
184
|
+
|
185
|
+
def reset_down_info
|
186
|
+
@fail_count = 0
|
187
|
+
@down_at = nil
|
188
|
+
@last_down_at = nil
|
189
|
+
@msg = nil
|
190
|
+
@error = nil
|
191
|
+
end
|
192
|
+
|
193
|
+
def memcached_socket
|
194
|
+
if socket_type == :unix
|
195
|
+
Dalli::Socket::UNIX.open(hostname, options)
|
196
|
+
else
|
197
|
+
Dalli::Socket::TCP.open(hostname, port, options)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def log_warn_message(err_or_string)
|
202
|
+
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
203
|
+
Dalli.logger.warn do
|
204
|
+
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
205
|
+
"#{name} failed (count: #{@fail_count}) #{detail}"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def close_on_fork
|
210
|
+
message = 'Fork detected, re-connecting child process...'
|
211
|
+
Dalli.logger.info { message }
|
212
|
+
# Close socket on a fork, setting us up for reconnect
|
213
|
+
# on next request.
|
214
|
+
close
|
215
|
+
raise Dalli::NetworkError, message
|
216
|
+
end
|
217
|
+
|
218
|
+
def fork_detected?
|
219
|
+
@pid && @pid != Process.pid
|
220
|
+
end
|
221
|
+
|
222
|
+
def log_down_detected
|
223
|
+
@last_down_at = Time.now
|
224
|
+
|
225
|
+
if @down_at
|
226
|
+
time = Time.now - @down_at
|
227
|
+
Dalli.logger.debug { format('%<name>s is still down (for %<time>.3f seconds now)', name: name, time: time) }
|
228
|
+
else
|
229
|
+
@down_at = @last_down_at
|
230
|
+
Dalli.logger.warn("#{name} is down")
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def log_up_detected
|
235
|
+
return unless @down_at
|
236
|
+
|
237
|
+
time = Time.now - @down_at
|
238
|
+
Dalli.logger.warn { format('%<name>s is back (downtime was %<time>.3f seconds)', name: name, time: time) }
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -20,18 +20,21 @@ module Dalli
|
|
20
20
|
|
21
21
|
# Attempts to process a single response from the buffer. Starts
|
22
22
|
# by advancing the buffer to the specified start position
|
23
|
-
def
|
24
|
-
|
25
|
-
|
23
|
+
def process_single_getk_response
|
24
|
+
bytes, resp_header, key, value = @response_processor.getk_response_from_buffer(@buffer)
|
25
|
+
advance(bytes)
|
26
|
+
[resp_header, key, value]
|
26
27
|
end
|
27
28
|
|
28
29
|
# Advances the internal response buffer by bytes_to_advance
|
29
30
|
# bytes. The
|
30
31
|
def advance(bytes_to_advance)
|
32
|
+
return unless bytes_to_advance.positive?
|
33
|
+
|
31
34
|
@buffer = @buffer[bytes_to_advance..-1]
|
32
35
|
end
|
33
36
|
|
34
|
-
# Resets the internal
|
37
|
+
# Resets the internal buffer to an empty state,
|
35
38
|
# so that we're ready to read pipelined responses
|
36
39
|
def reset
|
37
40
|
@buffer = +''
|
@@ -42,8 +45,8 @@ module Dalli
|
|
42
45
|
@buffer = nil
|
43
46
|
end
|
44
47
|
|
45
|
-
def
|
46
|
-
|
48
|
+
def in_progress?
|
49
|
+
!@buffer.nil?
|
47
50
|
end
|
48
51
|
end
|
49
52
|
end
|
@@ -19,22 +19,22 @@ module Dalli
|
|
19
19
|
DEFAULT_PORT = 11_211
|
20
20
|
DEFAULT_WEIGHT = 1
|
21
21
|
|
22
|
-
def self.parse(str
|
23
|
-
return parse_non_uri(str
|
22
|
+
def self.parse(str)
|
23
|
+
return parse_non_uri(str) unless str.start_with?(MEMCACHED_URI_PROTOCOL)
|
24
24
|
|
25
|
-
parse_uri(str
|
25
|
+
parse_uri(str)
|
26
26
|
end
|
27
27
|
|
28
|
-
def self.parse_uri(str
|
28
|
+
def self.parse_uri(str)
|
29
29
|
uri = URI.parse(str)
|
30
30
|
auth_details = {
|
31
31
|
username: uri.user,
|
32
32
|
password: uri.password
|
33
33
|
}
|
34
|
-
[uri.host, normalize_port(uri.port),
|
34
|
+
[uri.host, normalize_port(uri.port), :tcp, DEFAULT_WEIGHT, auth_details]
|
35
35
|
end
|
36
36
|
|
37
|
-
def self.parse_non_uri(str
|
37
|
+
def self.parse_non_uri(str)
|
38
38
|
res = deconstruct_string(str)
|
39
39
|
|
40
40
|
hostname = normalize_host_from_match(str, res)
|
@@ -45,7 +45,7 @@ module Dalli
|
|
45
45
|
socket_type = :tcp
|
46
46
|
port, weight = attributes_for_tcp_socket(res)
|
47
47
|
end
|
48
|
-
[hostname, port,
|
48
|
+
[hostname, port, socket_type, weight, {}]
|
49
49
|
end
|
50
50
|
|
51
51
|
def self.deconstruct_string(str)
|
data/lib/dalli/version.rb
CHANGED
data/lib/dalli.rb
CHANGED
@@ -31,7 +31,7 @@ module Dalli
|
|
31
31
|
class NilObject; end # rubocop:disable Lint/EmptyClass
|
32
32
|
NOT_FOUND = NilObject.new
|
33
33
|
|
34
|
-
|
34
|
+
QUIET = :dalli_multi
|
35
35
|
|
36
36
|
def self.logger
|
37
37
|
@logger ||= (rails_logger || default_logger)
|
@@ -63,6 +63,7 @@ require_relative 'dalli/pipelined_getter'
|
|
63
63
|
require_relative 'dalli/ring'
|
64
64
|
require_relative 'dalli/protocol'
|
65
65
|
require_relative 'dalli/protocol/binary'
|
66
|
+
require_relative 'dalli/protocol/connection_manager'
|
66
67
|
require_relative 'dalli/protocol/response_buffer'
|
67
68
|
require_relative 'dalli/protocol/server_config_parser'
|
68
69
|
require_relative 'dalli/protocol/ttl_sanitizer'
|
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: 3.1.
|
4
|
+
version: 3.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter M. Goldstein
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-12-
|
12
|
+
date: 2021-12-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: connection_pool
|
@@ -126,6 +126,7 @@ files:
|
|
126
126
|
- lib/dalli/protocol/binary/response_header.rb
|
127
127
|
- lib/dalli/protocol/binary/response_processor.rb
|
128
128
|
- lib/dalli/protocol/binary/sasl_authentication.rb
|
129
|
+
- lib/dalli/protocol/connection_manager.rb
|
129
130
|
- lib/dalli/protocol/response_buffer.rb
|
130
131
|
- lib/dalli/protocol/server_config_parser.rb
|
131
132
|
- lib/dalli/protocol/ttl_sanitizer.rb
|
@@ -158,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
159
|
- !ruby/object:Gem::Version
|
159
160
|
version: '0'
|
160
161
|
requirements: []
|
161
|
-
rubygems_version: 3.2.
|
162
|
+
rubygems_version: 3.2.33
|
162
163
|
signing_key:
|
163
164
|
specification_version: 4
|
164
165
|
summary: High performance memcached client for Ruby
|