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.

@@ -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 process_single_response(start_position = 0)
24
- advance(start_position)
25
- @response_processor.getk_response_from_buffer(@buffer)
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 buffer to an empty state,
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 completed?
46
- @buffer.nil?
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, client_options)
23
- return parse_non_uri(str, client_options) unless str.start_with?(MEMCACHED_URI_PROTOCOL)
22
+ def self.parse(str)
23
+ return parse_non_uri(str) unless str.start_with?(MEMCACHED_URI_PROTOCOL)
24
24
 
25
- parse_uri(str, client_options)
25
+ parse_uri(str)
26
26
  end
27
27
 
28
- def self.parse_uri(str, client_options)
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), DEFAULT_WEIGHT, :tcp, client_options.merge(auth_details)]
34
+ [uri.host, normalize_port(uri.port), :tcp, DEFAULT_WEIGHT, auth_details]
35
35
  end
36
36
 
37
- def self.parse_non_uri(str, client_options)
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, weight, socket_type, client_options]
48
+ [hostname, port, socket_type, weight, {}]
49
49
  end
50
50
 
51
51
  def self.deconstruct_string(str)
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '3.1.0'
4
+ VERSION = '3.1.1'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
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
- MULTI_KEY = :dalli_multi
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.0
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-03 00:00:00.000000000 Z
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.31
162
+ rubygems_version: 3.2.33
162
163
  signing_key:
163
164
  specification_version: 4
164
165
  summary: High performance memcached client for Ruby