dalli 2.7.3 → 3.2.3
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} +211 -0
- data/Gemfile +3 -6
- data/LICENSE +1 -1
- data/README.md +30 -208
- data/lib/dalli/cas/client.rb +2 -57
- data/lib/dalli/client.rb +254 -253
- 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/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 +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 +8 -0
- data/lib/dalli/ring.rb +97 -86
- data/lib/dalli/server.rb +4 -719
- data/lib/dalli/servers_arg_normalizer.rb +54 -0
- data/lib/dalli/socket.rb +123 -115
- data/lib/dalli/version.rb +5 -1
- data/lib/dalli.rb +45 -14
- data/lib/rack/session/dalli.rb +162 -42
- metadata +136 -63
- data/Performance.md +0 -42
- data/Rakefile +0 -43
- data/dalli.gemspec +0 -29
- data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -81
- data/lib/active_support/cache/dalli_store.rb +0 -372
- data/lib/dalli/railtie.rb +0 -7
- data/test/benchmark_test.rb +0 -243
- data/test/helper.rb +0 -56
- data/test/memcached_mock.rb +0 -201
- data/test/sasl/memcached.conf +0 -1
- data/test/sasl/sasldb +0 -1
- data/test/test_active_support.rb +0 -541
- data/test/test_cas_client.rb +0 -107
- data/test/test_compressor.rb +0 -52
- data/test/test_dalli.rb +0 -682
- data/test/test_encoding.rb +0 -32
- data/test/test_failover.rb +0 -137
- data/test/test_network.rb +0 -64
- data/test/test_rack_session.rb +0 -341
- data/test/test_ring.rb +0 -85
- data/test/test_sasl.rb +0 -105
- data/test/test_serializer.rb +0 -29
- data/test/test_server.rb +0 -110
@@ -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,142 +1,150 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'rbconfig'
|
5
|
+
|
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
|
4
24
|
|
5
|
-
|
6
|
-
|
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
|
7
31
|
|
8
|
-
|
9
|
-
|
10
|
-
|
32
|
+
value << result
|
33
|
+
end
|
34
|
+
value
|
35
|
+
end
|
11
36
|
|
12
|
-
|
13
|
-
IO.select(nil, [self], nil, options[:socket_timeout]) || raise(Timeout::Error, "IO timeout")
|
14
|
-
end
|
37
|
+
WAIT_RCS = %i[wait_writable wait_readable].freeze
|
15
38
|
|
16
|
-
|
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
|
17
42
|
|
18
|
-
|
19
|
-
value = ''
|
20
|
-
loop do
|
21
|
-
value << kgio_read!(count - value.bytesize)
|
22
|
-
break if value.bytesize == count
|
43
|
+
!WAIT_RCS.include?(result)
|
23
44
|
end
|
24
|
-
value
|
25
|
-
end
|
26
45
|
|
27
|
-
|
28
|
-
|
29
|
-
loop do
|
30
|
-
ret = kgio_tryread(8196)
|
31
|
-
case ret
|
32
|
-
when nil
|
33
|
-
raise EOFError, 'end of stream'
|
34
|
-
when :wait_readable
|
35
|
-
break
|
36
|
-
else
|
37
|
-
value << ret
|
38
|
-
end
|
39
|
-
end
|
40
|
-
value
|
41
|
-
end
|
42
|
-
end
|
46
|
+
def nonblock_timed_out?(result)
|
47
|
+
return true if result == :wait_readable && !wait_readable(options[:socket_timeout])
|
43
48
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
sock = start(addr)
|
48
|
-
sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
|
49
|
-
sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) if options[:keepalive]
|
50
|
-
sock.options = options
|
51
|
-
sock.server = server
|
52
|
-
sock.kgio_wait_writable
|
53
|
-
sock
|
54
|
-
end
|
55
|
-
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
|
56
52
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
sock.options = options
|
62
|
-
sock.server = server
|
63
|
-
sock.kgio_wait_writable
|
64
|
-
sock
|
53
|
+
FILTERED_OUT_OPTIONS = %i[username password].freeze
|
54
|
+
def logged_options
|
55
|
+
options.reject { |k, _| FILTERED_OUT_OPTIONS.include? k }
|
56
|
+
end
|
65
57
|
end
|
66
|
-
end
|
67
|
-
|
68
|
-
if ::Kgio.respond_to?(:wait_readable=)
|
69
|
-
::Kgio.wait_readable = :kgio_wait_readable
|
70
|
-
::Kgio.wait_writable = :kgio_wait_writable
|
71
|
-
end
|
72
58
|
|
73
|
-
|
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
|
74
69
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
def readfull(count)
|
79
|
-
value = ''
|
80
|
-
begin
|
81
|
-
loop do
|
82
|
-
value << read_nonblock(count - value.bytesize)
|
83
|
-
break if value.bytesize == count
|
84
|
-
end
|
85
|
-
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
86
|
-
if IO.select([self], nil, nil, options[:socket_timeout])
|
87
|
-
retry
|
88
|
-
else
|
89
|
-
raise Timeout::Error, "IO timeout: #{options.inspect}"
|
90
|
-
end
|
70
|
+
unless method_defined?(:wait_readable)
|
71
|
+
def wait_readable(timeout = nil)
|
72
|
+
to_io.wait_readable(timeout)
|
91
73
|
end
|
92
|
-
value
|
93
74
|
end
|
94
75
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
begin
|
99
|
-
value << read_nonblock(8196)
|
100
|
-
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
|
101
|
-
break
|
102
|
-
end
|
76
|
+
unless method_defined?(:wait_writable)
|
77
|
+
def wait_writable(timeout = nil)
|
78
|
+
to_io.wait_writable(timeout)
|
103
79
|
end
|
104
|
-
value
|
105
80
|
end
|
106
81
|
end
|
107
82
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
90
|
+
|
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
|
113
100
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
sock = new(host, port)
|
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.options = {:host => host, :port => port}.merge(options)
|
123
|
-
sock.server = server
|
124
|
-
sock
|
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]
|
125
106
|
end
|
126
|
-
end
|
127
|
-
end
|
128
107
|
|
129
|
-
|
130
|
-
|
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
|
114
|
+
end
|
115
|
+
end
|
131
116
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
146
|
+
end
|
138
147
|
end
|
139
148
|
end
|
140
149
|
end
|
141
|
-
|
142
150
|
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
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'
|
data/lib/rack/session/dalli.rb
CHANGED
@@ -1,75 +1,195 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rack/session/abstract/id'
|
2
4
|
require 'dalli'
|
5
|
+
require 'connection_pool'
|
6
|
+
require 'English'
|
3
7
|
|
4
8
|
module Rack
|
5
9
|
module Session
|
6
|
-
|
7
|
-
|
10
|
+
# Rack::Session::Dalli provides memcached based session management.
|
11
|
+
class Dalli < Abstract::PersistedSecure
|
12
|
+
attr_reader :data
|
8
13
|
|
9
|
-
|
10
|
-
|
11
|
-
|
14
|
+
# Don't freeze this until we fix the specs/implementation
|
15
|
+
# rubocop:disable Style/MutableConstant
|
16
|
+
DEFAULT_DALLI_OPTIONS = {
|
17
|
+
namespace: 'rack:session'
|
18
|
+
}
|
19
|
+
# rubocop:enable Style/MutableConstant
|
12
20
|
|
13
|
-
|
21
|
+
# Brings in a new Rack::Session::Dalli middleware with the given
|
22
|
+
# `:memcache_server`. The server is either a hostname, or a
|
23
|
+
# host-with-port string in the form of "host_name:port", or an array of
|
24
|
+
# such strings. For example:
|
25
|
+
#
|
26
|
+
# use Rack::Session::Dalli,
|
27
|
+
# :memcache_server => "mc.example.com:1234"
|
28
|
+
#
|
29
|
+
# If no `:memcache_server` option is specified, Rack::Session::Dalli will
|
30
|
+
# connect to localhost, port 11211 (the default memcached port). If
|
31
|
+
# `:memcache_server` is set to nil, Dalli::Client will look for
|
32
|
+
# ENV['MEMCACHE_SERVERS'] and use that value if it is available, or fall
|
33
|
+
# back to the same default behavior described above.
|
34
|
+
#
|
35
|
+
# Rack::Session::Dalli accepts the same options as Dalli::Client, so
|
36
|
+
# it's worth reviewing its documentation. Perhaps most importantly,
|
37
|
+
# if you don't specify a `:namespace` option, Rack::Session::Dalli
|
38
|
+
# will default to using 'rack:session'.
|
39
|
+
#
|
40
|
+
# It is not recommended to set `:expires_in`. Instead, use `:expire_after`,
|
41
|
+
# which will control both the expiration of the client cookie as well
|
42
|
+
# as the expiration of the corresponding entry in memcached.
|
43
|
+
#
|
44
|
+
# Rack::Session::Dalli also accepts a host of options that control how
|
45
|
+
# the sessions and session cookies are managed, including the
|
46
|
+
# aforementioned `:expire_after` option. Please see the documentation for
|
47
|
+
# Rack::Session::Abstract::Persisted for a detailed explanation of these
|
48
|
+
# options and their default values.
|
49
|
+
#
|
50
|
+
# Finally, if your web application is multithreaded, the
|
51
|
+
# Rack::Session::Dalli middleware can become a source of contention. You
|
52
|
+
# can use a connection pool of Dalli clients by passing in the
|
53
|
+
# `:pool_size` and/or `:pool_timeout` options. For example:
|
54
|
+
#
|
55
|
+
# use Rack::Session::Dalli,
|
56
|
+
# :memcache_server => "mc.example.com:1234",
|
57
|
+
# :pool_size => 10
|
58
|
+
#
|
59
|
+
# You must include the `connection_pool` gem in your project if you wish
|
60
|
+
# to use pool support. Please see the documentation for ConnectionPool
|
61
|
+
# for more information about it and its default options (which would only
|
62
|
+
# be applicable if you supplied one of the two options, but not both).
|
63
|
+
#
|
64
|
+
def initialize(app, options = {})
|
65
|
+
# Parent uses DEFAULT_OPTIONS to build @default_options for Rack::Session
|
14
66
|
super
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@
|
19
|
-
|
67
|
+
|
68
|
+
# Determine the default TTL for newly-created sessions
|
69
|
+
@default_ttl = ttl(@default_options[:expire_after])
|
70
|
+
@data = build_data_source(options)
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_session(_req, sid)
|
74
|
+
with_dalli_client([nil, {}]) do |dc|
|
75
|
+
existing_session = existing_session_for_sid(dc, sid)
|
76
|
+
return [sid, existing_session] unless existing_session.nil?
|
77
|
+
|
78
|
+
[create_sid_with_empty_session(dc), {}]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def write_session(_req, sid, session, options)
|
83
|
+
return false unless sid
|
84
|
+
|
85
|
+
key = memcached_key_from_sid(sid)
|
86
|
+
return false unless key
|
87
|
+
|
88
|
+
with_dalli_client(false) do |dc|
|
89
|
+
dc.set(memcached_key_from_sid(sid), session, ttl(options[:expire_after]))
|
90
|
+
sid
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def delete_session(_req, sid, options)
|
95
|
+
with_dalli_client do |dc|
|
96
|
+
key = memcached_key_from_sid(sid)
|
97
|
+
dc.delete(key) if key
|
98
|
+
generate_sid_with(dc) unless options[:drop]
|
99
|
+
end
|
20
100
|
end
|
21
101
|
|
22
|
-
|
102
|
+
private
|
103
|
+
|
104
|
+
def memcached_key_from_sid(sid)
|
105
|
+
sid.private_id if sid.respond_to?(:private_id)
|
106
|
+
end
|
107
|
+
|
108
|
+
def existing_session_for_sid(client, sid)
|
109
|
+
return nil unless sid && !sid.empty?
|
110
|
+
|
111
|
+
key = memcached_key_from_sid(sid)
|
112
|
+
return nil if key.nil?
|
113
|
+
|
114
|
+
client.get(key)
|
115
|
+
end
|
116
|
+
|
117
|
+
def create_sid_with_empty_session(client)
|
23
118
|
loop do
|
24
|
-
sid =
|
25
|
-
|
119
|
+
sid = generate_sid_with(client)
|
120
|
+
key = memcached_key_from_sid(sid)
|
121
|
+
|
122
|
+
break sid if key && client.add(key, {}, @default_ttl)
|
26
123
|
end
|
27
124
|
end
|
28
125
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
36
|
-
end
|
37
|
-
[sid, session]
|
126
|
+
def generate_sid_with(client)
|
127
|
+
loop do
|
128
|
+
raw_sid = generate_sid
|
129
|
+
sid = raw_sid.is_a?(String) ? Rack::Session::SessionId.new(raw_sid) : raw_sid
|
130
|
+
key = memcached_key_from_sid(sid)
|
131
|
+
break sid unless key && client.get(key)
|
38
132
|
end
|
39
133
|
end
|
40
134
|
|
41
|
-
def
|
42
|
-
|
43
|
-
expiry = options[:expire_after]
|
44
|
-
expiry = expiry.nil? ? 0 : expiry + 1
|
135
|
+
def build_data_source(options)
|
136
|
+
server_configurations, client_options, pool_options = extract_dalli_options(options)
|
45
137
|
|
46
|
-
|
47
|
-
|
48
|
-
|
138
|
+
if pool_options.empty?
|
139
|
+
::Dalli::Client.new(server_configurations, client_options)
|
140
|
+
else
|
141
|
+
ensure_connection_pool_added!
|
142
|
+
ConnectionPool.new(pool_options) do
|
143
|
+
::Dalli::Client.new(server_configurations, client_options.merge(threadsafe: false))
|
144
|
+
end
|
49
145
|
end
|
50
146
|
end
|
51
147
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
|
148
|
+
def extract_dalli_options(options)
|
149
|
+
raise 'Rack::Session::Dalli no longer supports the :cache option.' if options[:cache]
|
150
|
+
|
151
|
+
client_options = retrieve_client_options(options)
|
152
|
+
server_configurations = client_options.delete(:memcache_server)
|
153
|
+
|
154
|
+
[server_configurations, client_options, retrieve_pool_options(options)]
|
155
|
+
end
|
156
|
+
|
157
|
+
def retrieve_client_options(options)
|
158
|
+
# Filter out Rack::Session-specific options and apply our defaults
|
159
|
+
filtered_opts = options.reject { |k, _| DEFAULT_OPTIONS.key? k }
|
160
|
+
DEFAULT_DALLI_OPTIONS.merge(filtered_opts)
|
161
|
+
end
|
162
|
+
|
163
|
+
def retrieve_pool_options(options)
|
164
|
+
{}.tap do |pool_options|
|
165
|
+
pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
|
166
|
+
pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
|
56
167
|
end
|
57
168
|
end
|
58
169
|
|
59
|
-
def
|
60
|
-
|
61
|
-
|
170
|
+
def ensure_connection_pool_added!
|
171
|
+
require 'connection_pool'
|
172
|
+
rescue LoadError => e
|
173
|
+
warn "You don't have connection_pool installed in your application. " \
|
174
|
+
'Please add it to your Gemfile and run bundle install'
|
175
|
+
raise e
|
176
|
+
end
|
177
|
+
|
178
|
+
def with_dalli_client(result_on_error = nil, &block)
|
179
|
+
@data.with(&block)
|
62
180
|
rescue ::Dalli::DalliError, Errno::ECONNREFUSED
|
63
|
-
raise if
|
181
|
+
raise if /undefined class/.match?($ERROR_INFO.message)
|
182
|
+
|
64
183
|
if $VERBOSE
|
65
184
|
warn "#{self} is unable to find memcached server."
|
66
|
-
warn
|
185
|
+
warn $ERROR_INFO.inspect
|
67
186
|
end
|
68
|
-
|
69
|
-
ensure
|
70
|
-
@mutex.unlock if @mutex.locked?
|
187
|
+
result_on_error
|
71
188
|
end
|
72
189
|
|
190
|
+
def ttl(expire_after)
|
191
|
+
expire_after.nil? ? 0 : expire_after + 1
|
192
|
+
end
|
73
193
|
end
|
74
194
|
end
|
75
195
|
end
|