dalli 2.7.8 → 3.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/{History.md → CHANGELOG.md} +168 -0
  3. data/Gemfile +5 -1
  4. data/README.md +27 -223
  5. data/lib/dalli/cas/client.rb +1 -57
  6. data/lib/dalli/client.rb +227 -254
  7. data/lib/dalli/compressor.rb +12 -2
  8. data/lib/dalli/key_manager.rb +121 -0
  9. data/lib/dalli/options.rb +6 -7
  10. data/lib/dalli/pipelined_getter.rb +177 -0
  11. data/lib/dalli/protocol/base.rb +241 -0
  12. data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
  13. data/lib/dalli/protocol/binary/response_header.rb +36 -0
  14. data/lib/dalli/protocol/binary/response_processor.rb +239 -0
  15. data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
  16. data/lib/dalli/protocol/binary.rb +173 -0
  17. data/lib/dalli/protocol/connection_manager.rb +252 -0
  18. data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
  19. data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
  20. data/lib/dalli/protocol/meta/response_processor.rb +211 -0
  21. data/lib/dalli/protocol/meta.rb +178 -0
  22. data/lib/dalli/protocol/response_buffer.rb +54 -0
  23. data/lib/dalli/protocol/server_config_parser.rb +86 -0
  24. data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
  25. data/lib/dalli/protocol/value_compressor.rb +85 -0
  26. data/lib/dalli/protocol/value_marshaller.rb +59 -0
  27. data/lib/dalli/protocol/value_serializer.rb +91 -0
  28. data/lib/dalli/protocol.rb +8 -0
  29. data/lib/dalli/ring.rb +94 -83
  30. data/lib/dalli/server.rb +3 -746
  31. data/lib/dalli/servers_arg_normalizer.rb +54 -0
  32. data/lib/dalli/socket.rb +117 -137
  33. data/lib/dalli/version.rb +4 -1
  34. data/lib/dalli.rb +43 -15
  35. data/lib/rack/session/dalli.rb +103 -94
  36. metadata +65 -28
  37. data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -82
  38. data/lib/active_support/cache/dalli_store.rb +0 -429
  39. data/lib/dalli/railtie.rb +0 -8
@@ -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.fetch(ENV_VAR_NAME, nil) || 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,150 @@
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
58
 
87
- if ::Kgio.respond_to?(:wait_readable=)
88
- ::Kgio.wait_readable = :kgio_wait_readable
89
- ::Kgio.wait_writable = :kgio_wait_writable
90
- end
91
-
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
+ # options - supports enhanced logging in the case of a timeout
89
+ attr_accessor :options
133
90
 
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
91
+ def self.open(host, port, options = {})
92
+ Timeout.timeout(options[:socket_timeout]) do
93
+ sock = new(host, port)
94
+ sock.options = { host: host, port: port }.merge(options)
95
+ init_socket_options(sock, options)
96
+
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
+
136
+ # options - supports enhanced logging in the case of a timeout
137
+ # server - used to support IO.select in the pipelined getter
138
+ attr_accessor :options
139
+
140
+ def self.open(path, options = {})
141
+ Timeout.timeout(options[:socket_timeout]) do
142
+ sock = new(path)
143
+ sock.options = { path: path }.merge(options)
144
+ sock
145
+ end
165
146
  end
166
147
  end
167
148
  end
168
-
169
149
  end
170
150
  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.8'
4
+ VERSION = '3.2.3'
5
+
6
+ MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
4
7
  end
data/lib/dalli.rb CHANGED
@@ -1,26 +1,37 @@
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
- class ValueOverMaxSize < RuntimeError; end
25
+ class ValueOverMaxSize < DalliError; end
26
+
27
+ # operation is not permitted in a multi block
28
+ class NotPermittedMultiOpError < DalliError; end
29
+
30
+ # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
31
+ class NilObject; end # rubocop:disable Lint/EmptyClass
32
+ NOT_FOUND = NilObject.new
33
+
34
+ QUIET = :dalli_multi
24
35
 
25
36
  def self.logger
26
37
  @logger ||= (rails_logger || default_logger)
@@ -28,12 +39,12 @@ module Dalli
28
39
 
29
40
  def self.rails_logger
30
41
  (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
31
- (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:debug) && RAILS_DEFAULT_LOGGER)
42
+ (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER.respond_to?(:debug) && RAILS_DEFAULT_LOGGER)
32
43
  end
33
44
 
34
45
  def self.default_logger
35
46
  require 'logger'
36
- l = Logger.new(STDOUT)
47
+ l = Logger.new($stdout)
37
48
  l.level = Logger::INFO
38
49
  l
39
50
  end
@@ -41,9 +52,26 @@ module Dalli
41
52
  def self.logger=(logger)
42
53
  @logger = logger
43
54
  end
44
-
45
55
  end
46
56
 
47
- if defined?(RAILS_VERSION) && RAILS_VERSION < '3'
48
- raise Dalli::DalliError, "Dalli #{Dalli::VERSION} does not support Rails version < 3.0"
49
- end
57
+ require_relative 'dalli/version'
58
+
59
+ require_relative 'dalli/compressor'
60
+ require_relative 'dalli/client'
61
+ require_relative 'dalli/key_manager'
62
+ require_relative 'dalli/pipelined_getter'
63
+ require_relative 'dalli/ring'
64
+ require_relative 'dalli/protocol'
65
+ require_relative 'dalli/protocol/base'
66
+ require_relative 'dalli/protocol/binary'
67
+ require_relative 'dalli/protocol/connection_manager'
68
+ require_relative 'dalli/protocol/meta'
69
+ require_relative 'dalli/protocol/response_buffer'
70
+ require_relative 'dalli/protocol/server_config_parser'
71
+ require_relative 'dalli/protocol/ttl_sanitizer'
72
+ require_relative 'dalli/protocol/value_compressor'
73
+ require_relative 'dalli/protocol/value_marshaller'
74
+ require_relative 'dalli/protocol/value_serializer'
75
+ require_relative 'dalli/servers_arg_normalizer'
76
+ require_relative 'dalli/socket'
77
+ require_relative 'dalli/options'