dalli 2.7.11 → 3.0.6

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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ ##
5
+ # This module contains methods for validating and normalizing the servers
6
+ # argument passed to the client. This argument can be nil, a string, or
7
+ # an array of strings. Each string value in the argument can represent
8
+ # a single server or a comma separated list of servers.
9
+ #
10
+ # If nil, it falls back to the values of ENV['MEMCACHE_SERVERS'] if the latter is
11
+ # defined. If that environment value is not defined, a default of '127.0.0.1:11211'
12
+ # is used.
13
+ #
14
+ # A server config string can take one of three forms:
15
+ # * A colon separated string of (host, port, weight) where both port and
16
+ # weight are optional (e.g. 'localhost', 'abc.com:12345', 'example.org:22222:3')
17
+ # * A colon separated string of (UNIX socket, weight) where the weight is optional
18
+ # (e.g. '/var/run/memcached/socket', '/tmp/xyz:3') (not supported on Windows)
19
+ # * A URI with a 'memcached' protocol, which will typically include a username/password
20
+ #
21
+ # The methods in this module do not validate the format of individual server strings, but
22
+ # rather normalize the argument into a compact array, wherein each array entry corresponds
23
+ # to a single server config string. If that normalization is not possible, then an
24
+ # ArgumentError is thrown.
25
+ ##
26
+ module ServersArgNormalizer
27
+ ENV_VAR_NAME = 'MEMCACHE_SERVERS'
28
+ DEFAULT_SERVERS = ['127.0.0.1:11211'].freeze
29
+
30
+ ##
31
+ # Normalizes the argument into an array of servers.
32
+ # If the argument is a string, or an array containing strings, it's expected that the URIs are comma separated e.g.
33
+ # "memcache1.example.com:11211,memcache2.example.com:11211,memcache3.example.com:11211"
34
+ def self.normalize_servers(arg)
35
+ arg = apply_defaults(arg)
36
+ validate_type(arg)
37
+ Array(arg).flat_map { |s| s.split(',') }.reject(&:empty?)
38
+ end
39
+
40
+ def self.apply_defaults(arg)
41
+ return arg unless arg.nil?
42
+
43
+ ENV[ENV_VAR_NAME] || DEFAULT_SERVERS
44
+ end
45
+
46
+ def self.validate_type(arg)
47
+ return if arg.is_a?(String)
48
+ return if arg.is_a?(Array) && arg.all?(String)
49
+
50
+ raise ArgumentError,
51
+ 'An explicit servers argument must be a comma separated string or an array containing strings.'
52
+ end
53
+ end
54
+ end
data/lib/dalli/socket.rb CHANGED
@@ -1,170 +1,148 @@
1
1
  # frozen_string_literal: true
2
- require 'rbconfig'
3
2
 
4
- module Dalli::Server::TCPSocketOptions
5
- def setsockopts(sock, options)
6
- sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
7
- sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) if options[:keepalive]
8
- sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
9
- sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
10
- end
11
- end
3
+ require 'openssl'
4
+ require 'rbconfig'
12
5
 
13
- begin
14
- require 'kgio'
15
- puts "Using kgio socket IO" if defined?($TESTING) && $TESTING
6
+ module Dalli
7
+ ##
8
+ # Various socket implementations used by Dalli.
9
+ ##
10
+ module Socket
11
+ ##
12
+ # Common methods for all socket implementations.
13
+ ##
14
+ module InstanceMethods
15
+ def readfull(count)
16
+ value = +''
17
+ loop do
18
+ result = read_nonblock(count - value.bytesize, exception: false)
19
+ value << result if append_to_buffer?(result)
20
+ break if value.bytesize == count
21
+ end
22
+ value
23
+ end
16
24
 
17
- class Dalli::Server::KSocket < Kgio::Socket
18
- attr_accessor :options, :server
25
+ def read_available
26
+ value = +''
27
+ loop do
28
+ result = read_nonblock(8196, exception: false)
29
+ break if WAIT_RCS.include?(result)
30
+ raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}" unless result
19
31
 
20
- def kgio_wait_readable
21
- IO.select([self], nil, nil, options[:socket_timeout]) || raise(Timeout::Error, "IO timeout")
22
- end
32
+ value << result
33
+ end
34
+ value
35
+ end
23
36
 
24
- def kgio_wait_writable
25
- IO.select(nil, [self], nil, options[:socket_timeout]) || raise(Timeout::Error, "IO timeout")
26
- end
37
+ WAIT_RCS = %i[wait_writable wait_readable].freeze
27
38
 
28
- alias :write :kgio_write
39
+ def append_to_buffer?(result)
40
+ raise Timeout::Error, "IO timeout: #{logged_options.inspect}" if nonblock_timed_out?(result)
41
+ raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}" unless result
29
42
 
30
- def readfull(count)
31
- value = String.new('')
32
- while true
33
- value << kgio_read!(count - value.bytesize)
34
- break if value.bytesize == count
43
+ !WAIT_RCS.include?(result)
35
44
  end
36
- value
37
- end
38
45
 
39
- def read_available
40
- value = String.new('')
41
- while true
42
- ret = kgio_tryread(8196)
43
- case ret
44
- when nil
45
- raise EOFError, 'end of stream'
46
- when :wait_readable
47
- break
48
- else
49
- value << ret
50
- end
51
- end
52
- value
53
- end
54
- end
46
+ def nonblock_timed_out?(result)
47
+ return true if result == :wait_readable && !wait_readable(options[:socket_timeout])
55
48
 
56
- class Dalli::Server::KSocket::TCP < Dalli::Server::KSocket
57
- extend Dalli::Server::TCPSocketOptions
58
-
59
- def self.open(host, port, server, options = {})
60
- addr = Socket.pack_sockaddr_in(port, host)
61
- sock = start(addr)
62
- setsockopts(sock, options)
63
- sock.options = options
64
- sock.server = server
65
- sock.kgio_wait_writable
66
- sock
67
- rescue Timeout::Error
68
- sock.close if sock
69
- raise
70
- end
71
- end
49
+ # TODO: Do we actually need this? Looks to be only used in read_nonblock
50
+ result == :wait_writable && !wait_writable(options[:socket_timeout])
51
+ end
72
52
 
73
- class Dalli::Server::KSocket::UNIX < Dalli::Server::KSocket
74
- def self.open(path, server, options = {})
75
- addr = Socket.pack_sockaddr_un(path)
76
- sock = start(addr)
77
- sock.options = options
78
- sock.server = server
79
- sock.kgio_wait_writable
80
- sock
81
- rescue Timeout::Error
82
- sock.close if sock
83
- raise
53
+ FILTERED_OUT_OPTIONS = %i[username password].freeze
54
+ def logged_options
55
+ options.reject { |k, _| FILTERED_OUT_OPTIONS.include? k }
56
+ end
84
57
  end
85
- end
86
-
87
- if ::Kgio.respond_to?(:wait_readable=)
88
- ::Kgio.wait_readable = :kgio_wait_readable
89
- ::Kgio.wait_writable = :kgio_wait_writable
90
- end
91
58
 
92
- rescue LoadError
59
+ ##
60
+ # Wraps the below TCP socket class in the case where the client
61
+ # has configured a TLS/SSL connection between Dalli and the
62
+ # Memcached server.
63
+ ##
64
+ class SSLSocket < ::OpenSSL::SSL::SSLSocket
65
+ include Dalli::Socket::InstanceMethods
66
+ def options
67
+ io.options
68
+ end
93
69
 
94
- puts "Using standard socket IO (#{RUBY_DESCRIPTION})" if defined?($TESTING) && $TESTING
95
- module Dalli::Server::KSocket
96
- module InstanceMethods
97
- def readfull(count)
98
- value = String.new('')
99
- begin
100
- while true
101
- value << read_nonblock(count - value.bytesize)
102
- break if value.bytesize == count
103
- end
104
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
105
- if IO.select([self], nil, nil, options[:socket_timeout])
106
- retry
107
- else
108
- safe_options = options.reject{|k,v| [:username, :password].include? k}
109
- raise Timeout::Error, "IO timeout: #{safe_options.inspect}"
110
- end
70
+ unless method_defined?(:wait_readable)
71
+ def wait_readable(timeout = nil)
72
+ to_io.wait_readable(timeout)
111
73
  end
112
- value
113
74
  end
114
75
 
115
- def read_available
116
- value = String.new('')
117
- while true
118
- begin
119
- value << read_nonblock(8196)
120
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
121
- break
122
- end
76
+ unless method_defined?(:wait_writable)
77
+ def wait_writable(timeout = nil)
78
+ to_io.wait_writable(timeout)
123
79
  end
124
- value
125
80
  end
126
81
  end
127
82
 
128
- def self.included(receiver)
129
- receiver.send(:attr_accessor, :options, :server)
130
- receiver.send(:include, InstanceMethods)
131
- end
132
- end
83
+ ##
84
+ # A standard TCP socket between the Dalli client and the Memcached server.
85
+ ##
86
+ class TCP < TCPSocket
87
+ include Dalli::Socket::InstanceMethods
88
+ attr_accessor :options, :server
89
+
90
+ def self.open(host, port, server, options = {})
91
+ Timeout.timeout(options[:socket_timeout]) do
92
+ sock = new(host, port)
93
+ sock.options = { host: host, port: port }.merge(options)
94
+ sock.server = server
95
+ init_socket_options(sock, options)
133
96
 
134
- class Dalli::Server::KSocket::TCP < TCPSocket
135
- extend Dalli::Server::TCPSocketOptions
136
- include Dalli::Server::KSocket
137
-
138
- def self.open(host, port, server, options = {})
139
- Timeout.timeout(options[:socket_timeout]) do
140
- sock = new(host, port)
141
- setsockopts(sock, options)
142
- sock.options = {:host => host, :port => port}.merge(options)
143
- sock.server = server
144
- sock
97
+ options[:ssl_context] ? wrapping_ssl_socket(sock, host, options[:ssl_context]) : sock
98
+ end
99
+ end
100
+
101
+ def self.init_socket_options(sock, options)
102
+ sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
103
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options[:keepalive]
104
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
105
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
145
106
  end
146
- end
147
- end
148
107
 
149
- if RbConfig::CONFIG['host_os'] =~ /mingw|mswin/
150
- class Dalli::Server::KSocket::UNIX
151
- def initialize(*args)
152
- raise Dalli::DalliError, "Unix sockets are not supported on Windows platform."
108
+ def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
109
+ ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
110
+ ssl_socket.hostname = host
111
+ ssl_socket.sync_close = true
112
+ ssl_socket.connect
113
+ ssl_socket
153
114
  end
154
115
  end
155
- else
156
- class Dalli::Server::KSocket::UNIX < UNIXSocket
157
- include Dalli::Server::KSocket
158
116
 
159
- def self.open(path, server, options = {})
160
- Timeout.timeout(options[:socket_timeout]) do
161
- sock = new(path)
162
- sock.options = {:path => path}.merge(options)
163
- sock.server = server
164
- sock
117
+ if /mingw|mswin/.match?(RbConfig::CONFIG['host_os'])
118
+ ##
119
+ # UNIX domain sockets are not supported on Windows platforms.
120
+ ##
121
+ class UNIX
122
+ def initialize(*_args)
123
+ raise Dalli::DalliError, 'Unix sockets are not supported on Windows platform.'
124
+ end
125
+ end
126
+ else
127
+
128
+ ##
129
+ # UNIX represents a UNIX domain socket, which is an interprocess communication
130
+ # mechanism between processes on the same host. Used when the Memcached server
131
+ # is running on the same machine as the Dalli client.
132
+ ##
133
+ class UNIX < UNIXSocket
134
+ include Dalli::Socket::InstanceMethods
135
+ attr_accessor :options, :server
136
+
137
+ def self.open(path, server, options = {})
138
+ Timeout.timeout(options[:socket_timeout]) do
139
+ sock = new(path)
140
+ sock.options = { path: path }.merge(options)
141
+ sock.server = server
142
+ sock
143
+ end
165
144
  end
166
145
  end
167
146
  end
168
-
169
147
  end
170
148
  end
data/lib/dalli/version.rb CHANGED
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Dalli
3
- VERSION = '2.7.11'
4
+ VERSION = '3.0.6'
5
+
6
+ MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
4
7
  end
data/lib/dalli.rb CHANGED
@@ -1,39 +1,45 @@
1
1
  # frozen_string_literal: true
2
- require 'dalli/compressor'
3
- require 'dalli/client'
4
- require 'dalli/ring'
5
- require 'dalli/server'
6
- require 'dalli/socket'
7
- require 'dalli/version'
8
- require 'dalli/options'
9
- require 'dalli/railtie' if defined?(::Rails::Railtie)
10
2
 
3
+ ##
4
+ # Namespace for all Dalli code.
5
+ ##
11
6
  module Dalli
7
+ autoload :Server, 'dalli/server'
8
+
12
9
  # generic error
13
10
  class DalliError < RuntimeError; end
11
+
14
12
  # socket/server communication error
15
13
  class NetworkError < DalliError; end
14
+
16
15
  # no server available/alive error
17
16
  class RingError < DalliError; end
17
+
18
18
  # application error in marshalling serialization
19
19
  class MarshalError < DalliError; end
20
+
20
21
  # application error in marshalling deserialization or decompression
21
22
  class UnmarshalError < DalliError; end
23
+
22
24
  # payload too big for memcached
23
25
  class ValueOverMaxSize < DalliError; end
24
26
 
27
+ # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
28
+ class NilObject; end # rubocop:disable Lint/EmptyClass
29
+ NOT_FOUND = NilObject.new
30
+
25
31
  def self.logger
26
32
  @logger ||= (rails_logger || default_logger)
27
33
  end
28
34
 
29
35
  def self.rails_logger
30
36
  (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
31
- (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:debug) && RAILS_DEFAULT_LOGGER)
37
+ (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:debug) && RAILS_DEFAULT_LOGGER)
32
38
  end
33
39
 
34
40
  def self.default_logger
35
41
  require 'logger'
36
- l = Logger.new(STDOUT)
42
+ l = Logger.new($stdout)
37
43
  l.level = Logger::INFO
38
44
  l
39
45
  end
@@ -41,9 +47,21 @@ module Dalli
41
47
  def self.logger=(logger)
42
48
  @logger = logger
43
49
  end
44
-
45
50
  end
46
51
 
47
- if defined?(RAILS_VERSION) && RAILS_VERSION < '3'
48
- raise Dalli::DalliError, "Dalli #{Dalli::VERSION} does not support Rails version < 3.0"
49
- end
52
+ require_relative 'dalli/version'
53
+
54
+ require_relative 'dalli/compressor'
55
+ require_relative 'dalli/client'
56
+ require_relative 'dalli/key_manager'
57
+ require_relative 'dalli/ring'
58
+ require_relative 'dalli/protocol'
59
+ require_relative 'dalli/protocol/binary'
60
+ require_relative 'dalli/protocol/server_config_parser'
61
+ require_relative 'dalli/protocol/ttl_sanitizer'
62
+ require_relative 'dalli/protocol/value_compressor'
63
+ require_relative 'dalli/protocol/value_marshaller'
64
+ require_relative 'dalli/protocol/value_serializer'
65
+ require_relative 'dalli/servers_arg_normalizer'
66
+ require_relative 'dalli/socket'
67
+ require_relative 'dalli/options'
@@ -1,16 +1,23 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'rack/session/abstract/id'
3
4
  require 'dalli'
5
+ require 'connection_pool'
6
+ require 'English'
4
7
 
5
8
  module Rack
6
9
  module Session
7
- class Dalli < defined?(Abstract::Persisted) ? Abstract::Persisted : Abstract::ID
8
- attr_reader :pool, :mutex
10
+ # Rack::Session::Dalli provides memcached based session management.
11
+ class Dalli < Abstract::Persisted
12
+ attr_reader :pool
9
13
 
14
+ # Don't freeze this until we fix the specs/implementation
15
+ # rubocop:disable Style/MutableConstant
10
16
  DEFAULT_DALLI_OPTIONS = {
11
- :namespace => 'rack:session',
12
- :memcache_server => 'localhost:11211'
17
+ namespace: 'rack:session',
18
+ memcache_server: 'localhost:11211'
13
19
  }
20
+ # rubocop:enable Style/MutableConstant
14
21
 
15
22
  # Brings in a new Rack::Session::Dalli middleware with the given
16
23
  # `:memcache_server`. The server is either a hostname, or a
@@ -66,7 +73,7 @@ module Rack
66
73
  # for more information about it and its default options (which would only
67
74
  # be applicable if you supplied one of the two options, but not both).
68
75
  #
69
- def initialize(app, options={})
76
+ def initialize(app, options = {})
70
77
  # Parent uses DEFAULT_OPTIONS to build @default_options for Rack::Session
71
78
  super
72
79
 
@@ -74,28 +81,17 @@ module Rack
74
81
  @default_ttl = ttl @default_options[:expire_after]
75
82
 
76
83
  # Normalize and validate passed options
77
- cache, mserv, mopts, popts = extract_dalli_options options
78
-
79
- @pool =
80
- if cache # caller passed a Dalli::Client or ConnectionPool instance
81
- cache
82
- elsif popts # caller passed ConnectionPool options
83
- ConnectionPool.new(popts) { ::Dalli::Client.new(mserv, mopts) }
84
- else
85
- ::Dalli::Client.new(mserv, mopts)
86
- end
87
-
88
- if @pool.respond_to?(:alive!) # is a Dalli::Client
89
- @mutex = Mutex.new
84
+ mserv, mopts, popts = extract_dalli_options(options)
90
85
 
91
- @pool.alive!
92
- end
86
+ @pool = ConnectionPool.new(popts || {}) { ::Dalli::Client.new(mserv, mopts) }
93
87
  end
94
88
 
95
- def get_session(env, sid)
96
- with_block(env, [nil, {}]) do |dc|
97
- unless sid and !sid.empty? and session = dc.get(sid)
98
- old_sid, sid, session = sid, generate_sid_with(dc), {}
89
+ def get_session(_env, sid)
90
+ with_block([nil, {}]) do |dc|
91
+ unless sid && !sid.empty? && (session = dc.get(sid))
92
+ old_sid = sid
93
+ sid = generate_sid_with(dc)
94
+ session = {}
99
95
  unless dc.add(sid, session, @default_ttl)
100
96
  sid = old_sid
101
97
  redo # generate a new sid and try again
@@ -105,77 +101,72 @@ module Rack
105
101
  end
106
102
  end
107
103
 
108
- def set_session(env, session_id, new_session, options)
104
+ def set_session(_env, session_id, new_session, options)
109
105
  return false unless session_id
110
106
 
111
- with_block(env, false) do |dc|
107
+ with_block(false) do |dc|
112
108
  dc.set(session_id, new_session, ttl(options[:expire_after]))
113
109
  session_id
114
110
  end
115
111
  end
116
112
 
117
- def destroy_session(env, session_id, options)
118
- with_block(env) do |dc|
113
+ def destroy_session(_env, session_id, options)
114
+ with_block do |dc|
119
115
  dc.delete(session_id)
120
116
  generate_sid_with(dc) unless options[:drop]
121
117
  end
122
118
  end
123
119
 
124
- if defined?(Abstract::Persisted)
125
- def find_session(req, sid)
126
- get_session req.env, sid
127
- end
120
+ def find_session(req, sid)
121
+ get_session req.env, sid
122
+ end
128
123
 
129
- def write_session(req, sid, session, options)
130
- set_session req.env, sid, session, options
131
- end
124
+ def write_session(req, sid, session, options)
125
+ set_session req.env, sid, session, options
126
+ end
132
127
 
133
- def delete_session(req, sid, options)
134
- destroy_session req.env, sid, options
135
- end
128
+ def delete_session(req, sid, options)
129
+ destroy_session req.env, sid, options
136
130
  end
137
131
 
138
132
  private
139
133
 
140
134
  def extract_dalli_options(options)
141
- return [options[:cache]] if options[:cache]
135
+ raise 'Rack::Session::Dalli no longer supports the :cache option.' if options[:cache]
142
136
 
143
137
  # Filter out Rack::Session-specific options and apply our defaults
144
- mopts = DEFAULT_DALLI_OPTIONS.merge \
145
- options.reject {|k, _| DEFAULT_OPTIONS.key? k }
138
+ # Filter out Rack::Session-specific options and apply our defaults
139
+ filtered_opts = options.reject { |k, _| DEFAULT_OPTIONS.key? k }
140
+ mopts = DEFAULT_DALLI_OPTIONS.merge(filtered_opts)
146
141
  mserv = mopts.delete :memcache_server
147
142
 
143
+ popts = {}
148
144
  if mopts[:pool_size] || mopts[:pool_timeout]
149
- popts = {}
150
145
  popts[:size] = mopts.delete :pool_size if mopts[:pool_size]
151
146
  popts[:timeout] = mopts.delete :pool_timeout if mopts[:pool_timeout]
152
-
153
- # For a connection pool, locking is handled at the pool level
154
- mopts[:threadsafe] = false unless mopts.key? :threadsafe
147
+ mopts[:threadsafe] = true
155
148
  end
156
149
 
157
- [nil, mserv, mopts, popts]
150
+ [mserv, mopts, popts]
158
151
  end
159
152
 
160
- def generate_sid_with(dc)
161
- while true
153
+ def generate_sid_with(client)
154
+ loop do
162
155
  sid = generate_sid
163
- break sid unless dc.get(sid)
156
+ break sid unless client.get(sid)
164
157
  end
165
158
  end
166
159
 
167
- def with_block(env, default=nil, &block)
168
- @mutex.lock if @mutex and env['rack.multithread']
160
+ def with_block(default = nil, &block)
169
161
  @pool.with(&block)
170
162
  rescue ::Dalli::DalliError, Errno::ECONNREFUSED
171
- raise if $!.message =~ /undefined class/
163
+ raise if /undefined class/.match?($ERROR_INFO.message)
164
+
172
165
  if $VERBOSE
173
166
  warn "#{self} is unable to find memcached server."
174
- warn $!.inspect
167
+ warn $ERROR_INFO.inspect
175
168
  end
176
169
  default
177
- ensure
178
- @mutex.unlock if @mutex and @mutex.locked?
179
170
  end
180
171
 
181
172
  def ttl(expire_after)