dalli 2.7.4 → 3.2.5

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 (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