dalli 2.7.4 → 3.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +5 -5
  2. data/{History.md → CHANGELOG.md} +219 -0
  3. data/Gemfile +14 -5
  4. data/LICENSE +1 -1
  5. data/README.md +33 -205
  6. data/lib/dalli/cas/client.rb +2 -57
  7. data/lib/dalli/client.rb +254 -253
  8. data/lib/dalli/compressor.rb +13 -2
  9. data/lib/dalli/key_manager.rb +121 -0
  10. data/lib/dalli/options.rb +7 -7
  11. data/lib/dalli/pid_cache.rb +40 -0
  12. data/lib/dalli/pipelined_getter.rb +177 -0
  13. data/lib/dalli/protocol/base.rb +250 -0
  14. data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
  15. data/lib/dalli/protocol/binary/response_header.rb +36 -0
  16. data/lib/dalli/protocol/binary/response_processor.rb +239 -0
  17. data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
  18. data/lib/dalli/protocol/binary.rb +173 -0
  19. data/lib/dalli/protocol/connection_manager.rb +255 -0
  20. data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
  21. data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
  22. data/lib/dalli/protocol/meta/response_processor.rb +211 -0
  23. data/lib/dalli/protocol/meta.rb +178 -0
  24. data/lib/dalli/protocol/response_buffer.rb +54 -0
  25. data/lib/dalli/protocol/server_config_parser.rb +86 -0
  26. data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
  27. data/lib/dalli/protocol/value_compressor.rb +85 -0
  28. data/lib/dalli/protocol/value_marshaller.rb +59 -0
  29. data/lib/dalli/protocol/value_serializer.rb +91 -0
  30. data/lib/dalli/protocol.rb +8 -0
  31. data/lib/dalli/ring.rb +97 -86
  32. data/lib/dalli/server.rb +4 -719
  33. data/lib/dalli/servers_arg_normalizer.rb +54 -0
  34. data/lib/dalli/socket.rb +118 -120
  35. data/lib/dalli/version.rb +5 -1
  36. data/lib/dalli.rb +45 -14
  37. data/lib/rack/session/dalli.rb +162 -42
  38. metadata +40 -98
  39. data/Performance.md +0 -42
  40. data/Rakefile +0 -43
  41. data/dalli.gemspec +0 -29
  42. data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -81
  43. data/lib/active_support/cache/dalli_store.rb +0 -372
  44. data/lib/dalli/railtie.rb +0 -7
  45. data/test/benchmark_test.rb +0 -243
  46. data/test/helper.rb +0 -56
  47. data/test/memcached_mock.rb +0 -201
  48. data/test/sasl/memcached.conf +0 -1
  49. data/test/sasl/sasldb +0 -1
  50. data/test/test_active_support.rb +0 -541
  51. data/test/test_cas_client.rb +0 -107
  52. data/test/test_compressor.rb +0 -52
  53. data/test/test_dalli.rb +0 -682
  54. data/test/test_encoding.rb +0 -32
  55. data/test/test_failover.rb +0 -137
  56. data/test/test_network.rb +0 -64
  57. data/test/test_rack_session.rb +0 -341
  58. data/test/test_ring.rb +0 -85
  59. data/test/test_sasl.rb +0 -105
  60. data/test/test_serializer.rb +0 -29
  61. 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,152 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
1
4
  require 'rbconfig'
2
5
 
3
- begin
4
- require 'kgio'
5
- 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 = 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
6
24
 
7
- class Dalli::Server::KSocket < Kgio::Socket
8
- 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
9
31
 
10
- def kgio_wait_readable
11
- IO.select([self], nil, nil, options[:socket_timeout]) || raise(Timeout::Error, "IO timeout")
12
- end
32
+ value << result
33
+ end
34
+ value
35
+ end
13
36
 
14
- def kgio_wait_writable
15
- IO.select(nil, [self], nil, options[:socket_timeout]) || raise(Timeout::Error, "IO timeout")
16
- end
37
+ WAIT_RCS = %i[wait_writable wait_readable].freeze
17
38
 
18
- 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
19
42
 
20
- def readfull(count)
21
- value = ''
22
- loop do
23
- value << kgio_read!(count - value.bytesize)
24
- break if value.bytesize == count
43
+ !WAIT_RCS.include?(result)
25
44
  end
26
- value
27
- end
28
45
 
29
- def read_available
30
- value = ''
31
- loop do
32
- ret = kgio_tryread(8196)
33
- case ret
34
- when nil
35
- raise EOFError, 'end of stream'
36
- when :wait_readable
37
- break
38
- else
39
- value << ret
40
- end
41
- end
42
- value
43
- end
44
- end
46
+ def nonblock_timed_out?(result)
47
+ return true if result == :wait_readable && !wait_readable(options[:socket_timeout])
45
48
 
46
- class Dalli::Server::KSocket::TCP < Dalli::Server::KSocket
47
- def self.open(host, port, server, options = {})
48
- addr = Socket.pack_sockaddr_in(port, host)
49
- sock = start(addr)
50
- sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
51
- sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) if options[:keepalive]
52
- sock.options = options
53
- sock.server = server
54
- sock.kgio_wait_writable
55
- sock
56
- end
57
- 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
58
52
 
59
- class Dalli::Server::KSocket::UNIX < Dalli::Server::KSocket
60
- def self.open(path, server, options = {})
61
- addr = Socket.pack_sockaddr_un(path)
62
- sock = start(addr)
63
- sock.options = options
64
- sock.server = server
65
- sock.kgio_wait_writable
66
- sock
53
+ FILTERED_OUT_OPTIONS = %i[username password].freeze
54
+ def logged_options
55
+ options.reject { |k, _| FILTERED_OUT_OPTIONS.include? k }
56
+ end
67
57
  end
68
- end
69
-
70
- if ::Kgio.respond_to?(:wait_readable=)
71
- ::Kgio.wait_readable = :kgio_wait_readable
72
- ::Kgio.wait_writable = :kgio_wait_writable
73
- end
74
58
 
75
- 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
76
69
 
77
- puts "Using standard socket IO (#{RUBY_DESCRIPTION})" if defined?($TESTING) && $TESTING
78
- module Dalli::Server::KSocket
79
- module InstanceMethods
80
- def readfull(count)
81
- value = ''
82
- begin
83
- loop do
84
- value << read_nonblock(count - value.bytesize)
85
- break if value.bytesize == count
86
- end
87
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
88
- if IO.select([self], nil, nil, options[:socket_timeout])
89
- retry
90
- else
91
- raise Timeout::Error, "IO timeout: #{options.inspect}"
92
- end
70
+ unless method_defined?(:wait_readable)
71
+ def wait_readable(timeout = nil)
72
+ to_io.wait_readable(timeout)
93
73
  end
94
- value
95
74
  end
96
75
 
97
- def read_available
98
- value = ''
99
- loop do
100
- begin
101
- value << read_nonblock(8196)
102
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
103
- break
104
- end
76
+ unless method_defined?(:wait_writable)
77
+ def wait_writable(timeout = nil)
78
+ to_io.wait_writable(timeout)
105
79
  end
106
- value
107
80
  end
108
81
  end
109
82
 
110
- def self.included(receiver)
111
- receiver.send(:attr_accessor, :options, :server)
112
- receiver.send(:include, InstanceMethods)
113
- end
114
- 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
115
90
 
116
- class Dalli::Server::KSocket::TCP < TCPSocket
117
- include Dalli::Server::KSocket
118
-
119
- def self.open(host, port, server, options = {})
120
- Timeout.timeout(options[:socket_timeout]) do
121
- sock = new(host, port)
122
- sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
123
- sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) if options[:keepalive]
124
- sock.options = {:host => host, :port => port}.merge(options)
125
- sock.server = server
126
- 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]
127
106
  end
128
- end
129
- end
130
107
 
131
- if RbConfig::CONFIG['host_os'] =~ /mingw|mswin/
132
- class Dalli::Server::KSocket::UNIX
133
- def initialize(*args)
134
- 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
135
114
  end
136
115
  end
137
- else
138
- class Dalli::Server::KSocket::UNIX < UNIXSocket
139
- include Dalli::Server::KSocket
140
116
 
141
- def self.open(path, server, options = {})
142
- Timeout.timeout(options[:socket_timeout]) do
143
- sock = new(path)
144
- sock.options = {:path => path}.merge(options)
145
- sock.server = server
146
- 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
147
146
  end
148
147
  end
149
148
  end
150
-
151
149
  end
152
150
  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.4'
4
+ VERSION = '3.2.5'
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
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'
@@ -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
- class Dalli < Abstract::ID
7
- attr_reader :pool, :mutex
10
+ # Rack::Session::Dalli provides memcached based session management.
11
+ class Dalli < Abstract::PersistedSecure
12
+ attr_reader :data
8
13
 
9
- DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
10
- :namespace => 'rack:session',
11
- :memcache_server => 'localhost:11211'
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
- def initialize(app, options={})
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
- @mutex = Mutex.new
16
- mserv = @default_options[:memcache_server]
17
- mopts = @default_options.reject{|k,v| !DEFAULT_OPTIONS.include? k }
18
- @pool = options[:cache] || ::Dalli::Client.new(mserv, mopts)
19
- @pool.alive!
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
- def generate_sid
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 = super
25
- break sid unless @pool.get(sid)
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 get_session(env, sid)
30
- with_lock(env, [nil, {}]) do
31
- unless sid and !sid.empty? and session = @pool.get(sid)
32
- sid, session = generate_sid, {}
33
- unless @pool.add(sid, session)
34
- raise "Session collision on '#{sid.inspect}'"
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 set_session(env, session_id, new_session, options)
42
- return false unless session_id
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
- with_lock(env, false) do
47
- @pool.set session_id, new_session, expiry
48
- session_id
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 destroy_session(env, session_id, options)
53
- with_lock(env) do
54
- @pool.delete(session_id)
55
- generate_sid unless options[:drop]
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 with_lock(env, default=nil)
60
- @mutex.lock if env['rack.multithread']
61
- yield
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 $!.message =~ /undefined class/
181
+ raise if $ERROR_INFO.message.include?('undefined class')
182
+
64
183
  if $VERBOSE
65
184
  warn "#{self} is unable to find memcached server."
66
- warn $!.inspect
185
+ warn $ERROR_INFO.inspect
67
186
  end
68
- default
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