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.
- checksums.yaml +5 -5
- data/{History.md → CHANGELOG.md} +218 -0
- data/Gemfile +17 -1
- data/README.md +30 -210
- data/lib/dalli/cas/client.rb +2 -57
- data/lib/dalli/client.rb +228 -254
- data/lib/dalli/compressor.rb +13 -2
- data/lib/dalli/key_manager.rb +121 -0
- data/lib/dalli/options.rb +7 -7
- data/lib/dalli/pid_cache.rb +40 -0
- data/lib/dalli/pipelined_getter.rb +177 -0
- data/lib/dalli/protocol/base.rb +250 -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 +255 -0
- data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
- data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
- data/lib/dalli/protocol/meta/response_processor.rb +211 -0
- data/lib/dalli/protocol/meta.rb +178 -0
- data/lib/dalli/protocol/response_buffer.rb +54 -0
- data/lib/dalli/protocol/server_config_parser.rb +86 -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 +19 -0
- data/lib/dalli/ring.rb +95 -84
- data/lib/dalli/server.rb +4 -743
- data/lib/dalli/servers_arg_normalizer.rb +54 -0
- data/lib/dalli/socket.rb +142 -127
- data/lib/dalli/version.rb +5 -1
- data/lib/dalli.rb +46 -15
- data/lib/rack/session/dalli.rb +156 -50
- metadata +36 -128
- data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -81
- data/lib/active_support/cache/dalli_store.rb +0 -403
- 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
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
32
|
+
value << result
|
33
|
+
end
|
34
|
+
value
|
35
|
+
end
|
18
36
|
|
19
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
46
|
+
def nonblock_timed_out?(result)
|
47
|
+
return true if result == :wait_readable && !wait_readable(options[:socket_timeout])
|
28
48
|
|
29
|
-
|
30
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
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
|
-
|
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
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
sock
|
133
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
data/lib/dalli.rb
CHANGED
@@ -1,36 +1,50 @@
|
|
1
|
-
|
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 ||=
|
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
|
-
|
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(
|
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
|
-
|
45
|
-
|
46
|
-
|
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'
|