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.
Files changed (60) hide show
  1. checksums.yaml +5 -5
  2. data/{History.md → CHANGELOG.md} +211 -0
  3. data/Gemfile +3 -6
  4. data/LICENSE +1 -1
  5. data/README.md +30 -208
  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/pipelined_getter.rb +177 -0
  12. data/lib/dalli/protocol/base.rb +241 -0
  13. data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
  14. data/lib/dalli/protocol/binary/response_header.rb +36 -0
  15. data/lib/dalli/protocol/binary/response_processor.rb +239 -0
  16. data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
  17. data/lib/dalli/protocol/binary.rb +173 -0
  18. data/lib/dalli/protocol/connection_manager.rb +252 -0
  19. data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
  20. data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
  21. data/lib/dalli/protocol/meta/response_processor.rb +211 -0
  22. data/lib/dalli/protocol/meta.rb +178 -0
  23. data/lib/dalli/protocol/response_buffer.rb +54 -0
  24. data/lib/dalli/protocol/server_config_parser.rb +86 -0
  25. data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
  26. data/lib/dalli/protocol/value_compressor.rb +85 -0
  27. data/lib/dalli/protocol/value_marshaller.rb +59 -0
  28. data/lib/dalli/protocol/value_serializer.rb +91 -0
  29. data/lib/dalli/protocol.rb +8 -0
  30. data/lib/dalli/ring.rb +97 -86
  31. data/lib/dalli/server.rb +4 -719
  32. data/lib/dalli/servers_arg_normalizer.rb +54 -0
  33. data/lib/dalli/socket.rb +123 -115
  34. data/lib/dalli/version.rb +5 -1
  35. data/lib/dalli.rb +45 -14
  36. data/lib/rack/session/dalli.rb +162 -42
  37. metadata +136 -63
  38. data/Performance.md +0 -42
  39. data/Rakefile +0 -43
  40. data/dalli.gemspec +0 -29
  41. data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -81
  42. data/lib/active_support/cache/dalli_store.rb +0 -372
  43. data/lib/dalli/railtie.rb +0 -7
  44. data/test/benchmark_test.rb +0 -243
  45. data/test/helper.rb +0 -56
  46. data/test/memcached_mock.rb +0 -201
  47. data/test/sasl/memcached.conf +0 -1
  48. data/test/sasl/sasldb +0 -1
  49. data/test/test_active_support.rb +0 -541
  50. data/test/test_cas_client.rb +0 -107
  51. data/test/test_compressor.rb +0 -52
  52. data/test/test_dalli.rb +0 -682
  53. data/test/test_encoding.rb +0 -32
  54. data/test/test_failover.rb +0 -137
  55. data/test/test_network.rb +0 -64
  56. data/test/test_rack_session.rb +0 -341
  57. data/test/test_ring.rb +0 -85
  58. data/test/test_sasl.rb +0 -105
  59. data/test/test_serializer.rb +0 -29
  60. 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
- begin
2
- require 'kgio'
3
- puts "Using kgio socket IO" if defined?($TESTING) && $TESTING
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
- class Dalli::Server::KSocket < Kgio::Socket
6
- 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
7
31
 
8
- def kgio_wait_readable
9
- IO.select([self], nil, nil, options[:socket_timeout]) || raise(Timeout::Error, "IO timeout")
10
- end
32
+ value << result
33
+ end
34
+ value
35
+ end
11
36
 
12
- def kgio_wait_writable
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
- 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
17
42
 
18
- def readfull(count)
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
- def read_available
28
- value = ''
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
- class Dalli::Server::KSocket::TCP < Dalli::Server::KSocket
45
- def self.open(host, port, server, options = {})
46
- addr = Socket.pack_sockaddr_in(port, host)
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
- class Dalli::Server::KSocket::UNIX < Dalli::Server::KSocket
58
- def self.open(path, server, options = {})
59
- addr = Socket.pack_sockaddr_un(path)
60
- sock = start(addr)
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
- 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
74
69
 
75
- puts "Using standard socket IO (#{RUBY_DESCRIPTION})" if defined?($TESTING) && $TESTING
76
- module Dalli::Server::KSocket
77
- module InstanceMethods
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
- def read_available
96
- value = ''
97
- loop do
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
- def self.included(receiver)
109
- receiver.send(:attr_accessor, :options, :server)
110
- receiver.send(:include, InstanceMethods)
111
- end
112
- 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
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
- class Dalli::Server::KSocket::TCP < TCPSocket
115
- include Dalli::Server::KSocket
116
-
117
- def self.open(host, port, server, options = {})
118
- Timeout.timeout(options[:socket_timeout]) do
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
- class Dalli::Server::KSocket::UNIX < UNIXSocket
130
- include Dalli::Server::KSocket
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
- def self.open(path, server, options = {})
133
- Timeout.timeout(options[:socket_timeout]) do
134
- sock = new(path)
135
- sock.options = {:path => path}.merge(options)
136
- sock.server = server
137
- 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
146
+ end
138
147
  end
139
148
  end
140
149
  end
141
-
142
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.3'
4
+ VERSION = '3.2.3'
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 /undefined class/.match?($ERROR_INFO.message)
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