dalli 2.7.11 → 3.2.0
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/Gemfile +7 -6
- data/History.md +124 -0
- data/README.md +26 -200
- data/lib/dalli/cas/client.rb +1 -57
- data/lib/dalli/client.rb +230 -263
- data/lib/dalli/compressor.rb +12 -2
- data/lib/dalli/key_manager.rb +113 -0
- data/lib/dalli/options.rb +6 -7
- data/lib/dalli/pipelined_getter.rb +177 -0
- data/lib/dalli/protocol/base.rb +241 -0
- data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
- data/lib/dalli/protocol/binary/response_header.rb +36 -0
- data/lib/dalli/protocol/binary/response_processor.rb +239 -0
- data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
- data/lib/dalli/protocol/binary.rb +173 -0
- data/lib/dalli/protocol/connection_manager.rb +252 -0
- data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
- data/lib/dalli/protocol/meta/request_formatter.rb +108 -0
- data/lib/dalli/protocol/meta/response_processor.rb +211 -0
- data/lib/dalli/protocol/meta.rb +177 -0
- data/lib/dalli/protocol/response_buffer.rb +54 -0
- data/lib/dalli/protocol/server_config_parser.rb +84 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
- data/lib/dalli/protocol/value_compressor.rb +85 -0
- data/lib/dalli/protocol/value_marshaller.rb +59 -0
- data/lib/dalli/protocol/value_serializer.rb +91 -0
- data/lib/dalli/protocol.rb +8 -0
- data/lib/dalli/ring.rb +90 -81
- data/lib/dalli/server.rb +3 -749
- data/lib/dalli/servers_arg_normalizer.rb +54 -0
- data/lib/dalli/socket.rb +117 -137
- data/lib/dalli/version.rb +4 -1
- data/lib/dalli.rb +42 -14
- data/lib/rack/session/dalli.rb +95 -95
- metadata +118 -10
- data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -82
- data/lib/active_support/cache/dalli_store.rb +0 -441
- 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[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,150 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require 'rbconfig'
|
3
2
|
|
4
|
-
|
5
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
32
|
+
value << result
|
33
|
+
end
|
34
|
+
value
|
35
|
+
end
|
23
36
|
|
24
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
57
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
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
data/lib/dalli.rb
CHANGED
@@ -1,39 +1,50 @@
|
|
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
|
+
# 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
|
+
|
25
36
|
def self.logger
|
26
37
|
@logger ||= (rails_logger || default_logger)
|
27
38
|
end
|
28
39
|
|
29
40
|
def self.rails_logger
|
30
41
|
(defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
|
31
|
-
|
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(
|
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
|
-
|
48
|
-
|
49
|
-
|
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'
|