dalli 2.7.6 → 3.2.8

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.
Files changed (40) hide show
  1. checksums.yaml +5 -5
  2. data/{History.md → CHANGELOG.md} +218 -0
  3. data/Gemfile +17 -1
  4. data/README.md +30 -210
  5. data/lib/dalli/cas/client.rb +2 -57
  6. data/lib/dalli/client.rb +228 -254
  7. data/lib/dalli/compressor.rb +13 -2
  8. data/lib/dalli/key_manager.rb +121 -0
  9. data/lib/dalli/options.rb +7 -7
  10. data/lib/dalli/pid_cache.rb +40 -0
  11. data/lib/dalli/pipelined_getter.rb +177 -0
  12. data/lib/dalli/protocol/base.rb +250 -0
  13. data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
  14. data/lib/dalli/protocol/binary/response_header.rb +36 -0
  15. data/lib/dalli/protocol/binary/response_processor.rb +239 -0
  16. data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
  17. data/lib/dalli/protocol/binary.rb +173 -0
  18. data/lib/dalli/protocol/connection_manager.rb +255 -0
  19. data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
  20. data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
  21. data/lib/dalli/protocol/meta/response_processor.rb +211 -0
  22. data/lib/dalli/protocol/meta.rb +178 -0
  23. data/lib/dalli/protocol/response_buffer.rb +54 -0
  24. data/lib/dalli/protocol/server_config_parser.rb +86 -0
  25. data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
  26. data/lib/dalli/protocol/value_compressor.rb +85 -0
  27. data/lib/dalli/protocol/value_marshaller.rb +59 -0
  28. data/lib/dalli/protocol/value_serializer.rb +91 -0
  29. data/lib/dalli/protocol.rb +19 -0
  30. data/lib/dalli/ring.rb +95 -84
  31. data/lib/dalli/server.rb +4 -743
  32. data/lib/dalli/servers_arg_normalizer.rb +54 -0
  33. data/lib/dalli/socket.rb +142 -127
  34. data/lib/dalli/version.rb +5 -1
  35. data/lib/dalli.rb +46 -15
  36. data/lib/rack/session/dalli.rb +156 -50
  37. metadata +36 -128
  38. data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -81
  39. data/lib/active_support/cache/dalli_store.rb +0 -403
  40. data/lib/dalli/railtie.rb +0 -7
@@ -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,162 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
1
4
  require 'rbconfig'
2
5
 
3
- module Dalli::Server::TCPSocketOptions
4
- def setsockopts(sock, options)
5
- sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
6
- sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) if options[:keepalive]
7
- sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
8
- sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
9
- end
10
- end
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 = String.new(capacity: count + 1)
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
11
24
 
12
- begin
13
- require 'kgio'
14
- puts "Using kgio socket IO" if defined?($TESTING) && $TESTING
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
15
31
 
16
- class Dalli::Server::KSocket < Kgio::Socket
17
- attr_accessor :options, :server
32
+ value << result
33
+ end
34
+ value
35
+ end
18
36
 
19
- def kgio_wait_readable
20
- IO.select([self], nil, nil, options[:socket_timeout]) || raise(Timeout::Error, "IO timeout")
21
- end
37
+ WAIT_RCS = %i[wait_writable wait_readable].freeze
22
38
 
23
- def kgio_wait_writable
24
- IO.select(nil, [self], nil, options[:socket_timeout]) || raise(Timeout::Error, "IO timeout")
25
- end
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
42
+
43
+ !WAIT_RCS.include?(result)
44
+ end
26
45
 
27
- alias :write :kgio_write
46
+ def nonblock_timed_out?(result)
47
+ return true if result == :wait_readable && !wait_readable(options[:socket_timeout])
28
48
 
29
- def readfull(count)
30
- value = ''
31
- while true
32
- value << kgio_read!(count - value.bytesize)
33
- break if value.bytesize == count
49
+ # TODO: Do we actually need this? Looks to be only used in read_nonblock
50
+ result == :wait_writable && !wait_writable(options[:socket_timeout])
34
51
  end
35
- value
36
- end
37
52
 
38
- def read_available
39
- value = ''
40
- while true
41
- ret = kgio_tryread(8196)
42
- case ret
43
- when nil
44
- raise EOFError, 'end of stream'
45
- when :wait_readable
46
- break
47
- else
48
- value << ret
49
- end
53
+ FILTERED_OUT_OPTIONS = %i[username password].freeze
54
+ def logged_options
55
+ options.reject { |k, _| FILTERED_OUT_OPTIONS.include? k }
50
56
  end
51
- value
52
57
  end
53
- end
54
58
 
55
- class Dalli::Server::KSocket::TCP < Dalli::Server::KSocket
56
- extend Dalli::Server::TCPSocketOptions
57
-
58
- def self.open(host, port, server, options = {})
59
- addr = Socket.pack_sockaddr_in(port, host)
60
- sock = start(addr)
61
- setsockopts(sock, options)
62
- sock.options = options
63
- sock.server = server
64
- sock.kgio_wait_writable
65
- sock
66
- end
67
- end
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
69
+
70
+ unless method_defined?(:wait_readable)
71
+ def wait_readable(timeout = nil)
72
+ to_io.wait_readable(timeout)
73
+ end
74
+ end
68
75
 
69
- class Dalli::Server::KSocket::UNIX < Dalli::Server::KSocket
70
- def self.open(path, server, options = {})
71
- addr = Socket.pack_sockaddr_un(path)
72
- sock = start(addr)
73
- sock.options = options
74
- sock.server = server
75
- sock.kgio_wait_writable
76
- sock
76
+ unless method_defined?(:wait_writable)
77
+ def wait_writable(timeout = nil)
78
+ to_io.wait_writable(timeout)
79
+ end
80
+ end
77
81
  end
78
- end
79
82
 
80
- if ::Kgio.respond_to?(:wait_readable=)
81
- ::Kgio.wait_readable = :kgio_wait_readable
82
- ::Kgio.wait_writable = :kgio_wait_writable
83
- 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
84
90
 
85
- rescue LoadError
91
+ def self.open(host, port, options = {})
92
+ create_socket_with_timeout(host, port, options) do |sock|
93
+ sock.options = { host: host, port: port }.merge(options)
94
+ init_socket_options(sock, options)
86
95
 
87
- puts "Using standard socket IO (#{RUBY_DESCRIPTION})" if defined?($TESTING) && $TESTING
88
- module Dalli::Server::KSocket
89
- module InstanceMethods
90
- def readfull(count)
91
- value = ''
92
- begin
93
- while true
94
- value << read_nonblock(count - value.bytesize)
95
- break if value.bytesize == count
96
- end
97
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
98
- if IO.select([self], nil, nil, options[:socket_timeout])
99
- retry
100
- else
101
- raise Timeout::Error, "IO timeout: #{options.inspect}"
102
- end
96
+ options[:ssl_context] ? wrapping_ssl_socket(sock, host, options[:ssl_context]) : sock
103
97
  end
104
- value
105
98
  end
106
99
 
107
- def read_available
108
- value = ''
109
- while true
110
- begin
111
- value << read_nonblock(8196)
112
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
113
- break
100
+ def self.create_socket_with_timeout(host, port, options)
101
+ # Check that TCPSocket#initialize was not overwritten by resolv-replace gem
102
+ # (part of ruby standard library since 3.0.0, should be removed in 3.4.0),
103
+ # as it does not handle keyword arguments correctly.
104
+ # To check this we are using the fact that resolv-replace
105
+ # aliases TCPSocket#initialize method to #original_resolv_initialize.
106
+ # https://github.com/ruby/resolv-replace/blob/v0.1.1/lib/resolv-replace.rb#L21
107
+ if RUBY_VERSION >= '3.0' &&
108
+ !::TCPSocket.private_instance_methods.include?(:original_resolv_initialize)
109
+ sock = new(host, port, connect_timeout: options[:socket_timeout])
110
+ yield(sock)
111
+ else
112
+ Timeout.timeout(options[:socket_timeout]) do
113
+ sock = new(host, port)
114
+ yield(sock)
114
115
  end
115
116
  end
116
- value
117
117
  end
118
- end
119
118
 
120
- def self.included(receiver)
121
- receiver.send(:attr_accessor, :options, :server)
122
- receiver.send(:include, InstanceMethods)
123
- end
124
- end
119
+ def self.init_socket_options(sock, options)
120
+ sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
121
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options[:keepalive]
122
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
123
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
125
124
 
126
- class Dalli::Server::KSocket::TCP < TCPSocket
127
- extend Dalli::Server::TCPSocketOptions
128
- include Dalli::Server::KSocket
129
-
130
- def self.open(host, port, server, options = {})
131
- Timeout.timeout(options[:socket_timeout]) do
132
- sock = new(host, port)
133
- setsockopts(sock, options)
134
- sock.options = {:host => host, :port => port}.merge(options)
135
- sock.server = server
136
- sock
125
+ return unless options[:socket_timeout]
126
+
127
+ seconds, fractional = options[:socket_timeout].divmod(1)
128
+ microseconds = fractional * 1_000_000
129
+ timeval = [seconds, microseconds].pack('l_2')
130
+
131
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
132
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
137
133
  end
138
- end
139
- end
140
134
 
141
- if RbConfig::CONFIG['host_os'] =~ /mingw|mswin/
142
- class Dalli::Server::KSocket::UNIX
143
- def initialize(*args)
144
- raise Dalli::DalliError, "Unix sockets are not supported on Windows platform."
135
+ def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
136
+ ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
137
+ ssl_socket.hostname = host
138
+ ssl_socket.sync_close = true
139
+ ssl_socket.connect
140
+ ssl_socket
145
141
  end
146
142
  end
147
- else
148
- class Dalli::Server::KSocket::UNIX < UNIXSocket
149
- include Dalli::Server::KSocket
150
-
151
- def self.open(path, server, options = {})
152
- Timeout.timeout(options[:socket_timeout]) do
153
- sock = new(path)
154
- sock.options = {:path => path}.merge(options)
155
- sock.server = server
156
- sock
143
+
144
+ if /mingw|mswin/.match?(RbConfig::CONFIG['host_os'])
145
+ ##
146
+ # UNIX domain sockets are not supported on Windows platforms.
147
+ ##
148
+ class UNIX
149
+ def initialize(*_args)
150
+ raise Dalli::DalliError, 'Unix sockets are not supported on Windows platform.'
151
+ end
152
+ end
153
+ else
154
+
155
+ ##
156
+ # UNIX represents a UNIX domain socket, which is an interprocess communication
157
+ # mechanism between processes on the same host. Used when the Memcached server
158
+ # is running on the same machine as the Dalli client.
159
+ ##
160
+ class UNIX < UNIXSocket
161
+ include Dalli::Socket::InstanceMethods
162
+
163
+ # options - supports enhanced logging in the case of a timeout
164
+ # server - used to support IO.select in the pipelined getter
165
+ attr_accessor :options
166
+
167
+ def self.open(path, options = {})
168
+ Timeout.timeout(options[:socket_timeout]) do
169
+ sock = new(path)
170
+ sock.options = { path: path }.merge(options)
171
+ sock
172
+ end
157
173
  end
158
174
  end
159
175
  end
160
-
161
176
  end
162
177
  end
data/lib/dalli/version.rb CHANGED
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dalli
2
- VERSION = '2.7.6'
4
+ VERSION = '3.2.8'
5
+
6
+ MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
3
7
  end
data/lib/dalli.rb CHANGED
@@ -1,36 +1,50 @@
1
- require 'dalli/compressor'
2
- require 'dalli/client'
3
- require 'dalli/ring'
4
- require 'dalli/server'
5
- require 'dalli/socket'
6
- require 'dalli/version'
7
- require 'dalli/options'
8
- require 'dalli/railtie' if defined?(::Rails::Railtie)
1
+ # frozen_string_literal: true
9
2
 
3
+ ##
4
+ # Namespace for all Dalli code.
5
+ ##
10
6
  module Dalli
7
+ autoload :Server, 'dalli/server'
8
+
11
9
  # generic error
12
10
  class DalliError < RuntimeError; end
11
+
13
12
  # socket/server communication error
14
13
  class NetworkError < DalliError; end
14
+
15
15
  # no server available/alive error
16
16
  class RingError < DalliError; end
17
+
17
18
  # application error in marshalling serialization
18
19
  class MarshalError < DalliError; end
20
+
19
21
  # application error in marshalling deserialization or decompression
20
22
  class UnmarshalError < DalliError; end
21
23
 
24
+ # payload too big for memcached
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
35
+
22
36
  def self.logger
23
- @logger ||= (rails_logger || default_logger)
37
+ @logger ||= rails_logger || default_logger
24
38
  end
25
39
 
26
40
  def self.rails_logger
27
41
  (defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
28
- (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)
29
43
  end
30
44
 
31
45
  def self.default_logger
32
46
  require 'logger'
33
- l = Logger.new(STDOUT)
47
+ l = Logger.new($stdout)
34
48
  l.level = Logger::INFO
35
49
  l
36
50
  end
@@ -38,9 +52,26 @@ module Dalli
38
52
  def self.logger=(logger)
39
53
  @logger = logger
40
54
  end
41
-
42
55
  end
43
56
 
44
- if defined?(RAILS_VERSION) && RAILS_VERSION < '3'
45
- raise Dalli::DalliError, "Dalli #{Dalli::VERSION} does not support Rails version < 3.0"
46
- 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'