dalli 3.2.8 → 5.0.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +235 -1
  3. data/Gemfile +16 -2
  4. data/README.md +82 -2
  5. data/lib/dalli/client.rb +239 -25
  6. data/lib/dalli/instrumentation.rb +143 -0
  7. data/lib/dalli/key_manager.rb +23 -8
  8. data/lib/dalli/options.rb +1 -1
  9. data/lib/dalli/pid_cache.rb +1 -1
  10. data/lib/dalli/pipelined_deleter.rb +82 -0
  11. data/lib/dalli/pipelined_getter.rb +44 -20
  12. data/lib/dalli/pipelined_setter.rb +87 -0
  13. data/lib/dalli/protocol/base.rb +94 -24
  14. data/lib/dalli/protocol/connection_manager.rb +23 -12
  15. data/lib/dalli/protocol/{meta/key_regularizer.rb → key_regularizer.rb} +1 -1
  16. data/lib/dalli/protocol/meta.rb +99 -12
  17. data/lib/dalli/protocol/{meta/request_formatter.rb → request_formatter.rb} +42 -10
  18. data/lib/dalli/protocol/response_buffer.rb +36 -12
  19. data/lib/dalli/protocol/{meta/response_processor.rb → response_processor.rb} +72 -26
  20. data/lib/dalli/protocol/server_config_parser.rb +2 -2
  21. data/lib/dalli/protocol/string_marshaller.rb +65 -0
  22. data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
  23. data/lib/dalli/protocol/value_compressor.rb +2 -11
  24. data/lib/dalli/protocol/value_marshaller.rb +1 -1
  25. data/lib/dalli/protocol/value_serializer.rb +59 -40
  26. data/lib/dalli/protocol.rb +10 -0
  27. data/lib/dalli/ring.rb +2 -2
  28. data/lib/dalli/servers_arg_normalizer.rb +1 -1
  29. data/lib/dalli/socket.rb +74 -14
  30. data/lib/dalli/version.rb +2 -2
  31. data/lib/dalli.rb +12 -5
  32. data/lib/rack/session/dalli.rb +43 -8
  33. metadata +26 -17
  34. data/lib/dalli/protocol/binary/request_formatter.rb +0 -117
  35. data/lib/dalli/protocol/binary/response_header.rb +0 -36
  36. data/lib/dalli/protocol/binary/response_processor.rb +0 -239
  37. data/lib/dalli/protocol/binary/sasl_authentication.rb +0 -60
  38. data/lib/dalli/protocol/binary.rb +0 -173
  39. data/lib/dalli/server.rb +0 -6
@@ -25,17 +25,8 @@ module Dalli
25
25
  FLAG_COMPRESSED = 0x2
26
26
 
27
27
  def initialize(client_options)
28
- # Support the deprecated compression option, but don't allow it to override
29
- # an explicit compress
30
- # Remove this with 4.0
31
- if client_options.key?(:compression) && !client_options.key?(:compress)
32
- Dalli.logger.warn "DEPRECATED: Dalli's :compression option is now just 'compress: true'. " \
33
- 'Please update your configuration.'
34
- client_options[:compress] = client_options.delete(:compression)
35
- end
36
-
37
28
  @compression_options =
38
- DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
29
+ DEFAULTS.merge(client_options.slice(*OPTIONS))
39
30
  end
40
31
 
41
32
  def store(value, req_options, bitflags)
@@ -47,7 +38,7 @@ module Dalli
47
38
  end
48
39
 
49
40
  def retrieve(value, bitflags)
50
- compressed = (bitflags & FLAG_COMPRESSED) != 0
41
+ compressed = bitflags.anybits?(FLAG_COMPRESSED)
51
42
  compressed ? compressor.decompress(value) : value
52
43
 
53
44
  # TODO: We likely want to move this rescue into the Dalli::Compressor / Dalli::GzipCompressor
@@ -27,7 +27,7 @@ module Dalli
27
27
  @value_compressor = ValueCompressor.new(client_options)
28
28
 
29
29
  @marshal_options =
30
- DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
30
+ DEFAULTS.merge(client_options.slice(*OPTIONS))
31
31
  end
32
32
 
33
33
  def store(key, value, options = nil)
@@ -18,57 +18,39 @@ module Dalli
18
18
  # https://www.hjp.at/zettel/m/memcached_flags.rxml
19
19
  # Looks like most clients use bit 0 to indicate native language serialization
20
20
  FLAG_SERIALIZED = 0x1
21
+ FLAG_UTF8 = 0x2
22
+
23
+ # Class variable to track whether the Marshal warning has been logged
24
+ @@marshal_warning_logged = false # rubocop:disable Style/ClassVars
21
25
 
22
26
  attr_accessor :serialization_options
23
27
 
24
28
  def initialize(protocol_options)
25
29
  @serialization_options =
26
- DEFAULTS.merge(protocol_options.select { |k, _| OPTIONS.include?(k) })
30
+ DEFAULTS.merge(protocol_options.slice(*OPTIONS))
31
+ warn_if_marshal_default(protocol_options) unless protocol_options[:silence_marshal_warning]
27
32
  end
28
33
 
29
34
  def store(value, req_options, bitflags)
30
- do_serialize = !(req_options && req_options[:raw])
31
- store_value = do_serialize ? serialize_value(value) : value.to_s
32
- bitflags |= FLAG_SERIALIZED if do_serialize
33
- [store_value, bitflags]
34
- end
35
-
36
- # TODO: Some of these error messages need to be validated. It's not obvious
37
- # that all of them are actually generated by the invoked code
38
- # in current systems
39
- # rubocop:disable Layout/LineLength
40
- TYPE_ERR_REGEXP = %r{needs to have method `_load'|exception class/object expected|instance of IO needed|incompatible marshal file format}.freeze
41
- ARGUMENT_ERR_REGEXP = /undefined class|marshal data too short/.freeze
42
- NAME_ERR_STR = 'uninitialized constant'
43
- # rubocop:enable Layout/LineLength
44
-
45
- def retrieve(value, bitflags)
46
- serialized = (bitflags & FLAG_SERIALIZED) != 0
47
- serialized ? serializer.load(value) : value
48
- rescue TypeError => e
49
- filter_type_error(e)
50
- rescue ArgumentError => e
51
- filter_argument_error(e)
52
- rescue NameError => e
53
- filter_name_error(e)
54
- end
35
+ return store_raw(value, bitflags) if req_options&.dig(:raw)
36
+ return store_string_fastpath(value, bitflags) if use_string_fastpath?(value, req_options)
55
37
 
56
- def filter_type_error(err)
57
- raise err unless TYPE_ERR_REGEXP.match?(err.message)
58
-
59
- raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
60
- end
61
-
62
- def filter_argument_error(err)
63
- raise err unless ARGUMENT_ERR_REGEXP.match?(err.message)
64
-
65
- raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
38
+ [serialize_value(value), bitflags | FLAG_SERIALIZED]
66
39
  end
67
40
 
68
- def filter_name_error(err)
69
- raise err unless err.message.include?(NAME_ERR_STR)
70
-
71
- raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
41
+ def retrieve(value, bitflags)
42
+ serialized = bitflags.anybits?(FLAG_SERIALIZED)
43
+ if serialized
44
+ begin
45
+ serializer.load(value)
46
+ rescue StandardError
47
+ raise UnmarshalError, 'Unable to unmarshal value'
48
+ end
49
+ elsif bitflags.anybits?(FLAG_UTF8)
50
+ value.force_encoding(Encoding::UTF_8)
51
+ else
52
+ value
53
+ end
72
54
  end
73
55
 
74
56
  def serializer
@@ -86,6 +68,43 @@ module Dalli
86
68
  exc.set_backtrace e.backtrace
87
69
  raise exc
88
70
  end
71
+
72
+ private
73
+
74
+ def store_raw(value, bitflags)
75
+ unless value.is_a?(String)
76
+ raise Dalli::MarshalError, "Dalli raw mode requires string values, got: #{value.class}"
77
+ end
78
+
79
+ [value, bitflags]
80
+ end
81
+
82
+ # If the value is a simple string, going through serialization is costly
83
+ # for no benefit other than preserving encoding.
84
+ # Assuming most strings are either UTF-8 or BINARY we can just store
85
+ # that information in the bitflags.
86
+ def store_string_fastpath(value, bitflags)
87
+ case value.encoding
88
+ when Encoding::BINARY then [value, bitflags]
89
+ when Encoding::UTF_8 then [value, bitflags | FLAG_UTF8]
90
+ else [serialize_value(value), bitflags | FLAG_SERIALIZED]
91
+ end
92
+ end
93
+
94
+ def use_string_fastpath?(value, req_options)
95
+ req_options&.dig(:string_fastpath) && value.instance_of?(String)
96
+ end
97
+
98
+ def warn_if_marshal_default(protocol_options)
99
+ return if protocol_options.key?(:serializer)
100
+ return if @@marshal_warning_logged
101
+
102
+ Dalli.logger.warn 'SECURITY WARNING: Dalli is using Marshal for serialization. ' \
103
+ 'Marshal can execute arbitrary code during deserialization. ' \
104
+ 'If your memcached server could be compromised, consider using ' \
105
+ 'a safer serializer like JSON: Dalli::Client.new(servers, serializer: JSON)'
106
+ @@marshal_warning_logged = true # rubocop:disable Style/ClassVars
107
+ end
89
108
  end
90
109
  end
91
110
  end
@@ -15,5 +15,15 @@ module Dalli
15
15
  else
16
16
  [Timeout::Error]
17
17
  end
18
+
19
+ # SSL errors that occur during read/write operations (not during initial
20
+ # handshake) should trigger reconnection. These indicate transient network
21
+ # issues, not configuration problems.
22
+ SSL_ERRORS =
23
+ if defined?(OpenSSL::SSL::SSLError)
24
+ [OpenSSL::SSL::SSLError]
25
+ else
26
+ []
27
+ end
18
28
  end
19
29
  end
data/lib/dalli/ring.rb CHANGED
@@ -23,9 +23,9 @@ module Dalli
23
23
 
24
24
  attr_accessor :servers, :continuum
25
25
 
26
- def initialize(servers_arg, protocol_implementation, options)
26
+ def initialize(servers_arg, options)
27
27
  @servers = servers_arg.map do |s|
28
- protocol_implementation.new(s, options)
28
+ Dalli::Protocol::Meta.new(s, options)
29
29
  end
30
30
  @continuum = nil
31
31
  @continuum = build_continuum(servers) if servers.size > 1
@@ -16,7 +16,7 @@ module Dalli
16
16
  # weight are optional (e.g. 'localhost', 'abc.com:12345', 'example.org:22222:3')
17
17
  # * A colon separated string of (UNIX socket, weight) where the weight is optional
18
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
19
+ # * A URI with a 'memcached' protocol (e.g. 'memcached://localhost:11211')
20
20
  #
21
21
  # The methods in this module do not validate the format of individual server strings, but
22
22
  # rather normalize the argument into a compact array, wherein each array entry corresponds
data/lib/dalli/socket.rb CHANGED
@@ -52,7 +52,7 @@ module Dalli
52
52
 
53
53
  FILTERED_OUT_OPTIONS = %i[username password].freeze
54
54
  def logged_options
55
- options.reject { |k, _| FILTERED_OUT_OPTIONS.include? k }
55
+ options.except(*FILTERED_OUT_OPTIONS)
56
56
  end
57
57
  end
58
58
 
@@ -63,6 +63,7 @@ module Dalli
63
63
  ##
64
64
  class SSLSocket < ::OpenSSL::SSL::SSLSocket
65
65
  include Dalli::Socket::InstanceMethods
66
+
66
67
  def options
67
68
  io.options
68
69
  end
@@ -85,9 +86,16 @@ module Dalli
85
86
  ##
86
87
  class TCP < TCPSocket
87
88
  include Dalli::Socket::InstanceMethods
89
+
88
90
  # options - supports enhanced logging in the case of a timeout
89
91
  attr_accessor :options
90
92
 
93
+ # Expected parameter signature for unmodified TCPSocket#initialize.
94
+ # Used to detect when gems like socksify or resolv-replace have monkey-patched
95
+ # TCPSocket, which breaks the connect_timeout: keyword argument.
96
+ TCPSOCKET_NATIVE_PARAMETERS = [[:rest]].freeze
97
+ private_constant :TCPSOCKET_NATIVE_PARAMETERS
98
+
91
99
  def self.open(host, port, options = {})
92
100
  create_socket_with_timeout(host, port, options) do |sock|
93
101
  sock.options = { host: host, port: port }.merge(options)
@@ -97,15 +105,20 @@ module Dalli
97
105
  end
98
106
  end
99
107
 
108
+ # Detect and cache whether TCPSocket supports the connect_timeout: keyword argument.
109
+ # Returns false if TCPSocket#initialize has been monkey-patched by gems like
110
+ # socksify or resolv-replace, which don't support keyword arguments.
111
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
112
+ def self.supports_connect_timeout?
113
+ return @supports_connect_timeout if defined?(@supports_connect_timeout)
114
+
115
+ @supports_connect_timeout = RUBY_VERSION >= '3.0' &&
116
+ ::TCPSocket.instance_method(:initialize).parameters == TCPSOCKET_NATIVE_PARAMETERS
117
+ end
118
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
119
+
100
120
  def self.create_socket_with_timeout(host, port, options)
101
- # Check that TCPSocket#initialize was not overwritten by resolv-replace gem
102
- # (part of ruby standard library since 3.0.0, should be removed in 3.4.0),
103
- # as it does not handle keyword arguments correctly.
104
- # To check this we are using the fact that resolv-replace
105
- # aliases TCPSocket#initialize method to #original_resolv_initialize.
106
- # https://github.com/ruby/resolv-replace/blob/v0.1.1/lib/resolv-replace.rb#L21
107
- if RUBY_VERSION >= '3.0' &&
108
- !::TCPSocket.private_instance_methods.include?(:original_resolv_initialize)
121
+ if supports_connect_timeout?
109
122
  sock = new(host, port, connect_timeout: options[:socket_timeout])
110
123
  yield(sock)
111
124
  else
@@ -117,19 +130,59 @@ module Dalli
117
130
  end
118
131
 
119
132
  def self.init_socket_options(sock, options)
133
+ configure_tcp_options(sock, options)
134
+ configure_socket_buffers(sock, options)
135
+ configure_timeout(sock, options)
136
+ end
137
+
138
+ def self.configure_tcp_options(sock, options)
120
139
  sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
121
140
  sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options[:keepalive]
141
+ end
142
+
143
+ def self.configure_socket_buffers(sock, options)
122
144
  sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
123
145
  sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
146
+ end
124
147
 
148
+ def self.configure_timeout(sock, options)
125
149
  return unless options[:socket_timeout]
126
150
 
127
- seconds, fractional = options[:socket_timeout].divmod(1)
128
- microseconds = fractional * 1_000_000
129
- timeval = [seconds, microseconds].pack('l_2')
151
+ if sock.respond_to?(:timeout=)
152
+ # Ruby 3.2+ has IO#timeout for reliable cross-platform timeout handling
153
+ sock.timeout = options[:socket_timeout]
154
+ else
155
+ # Ruby 3.1 fallback using socket options
156
+ # struct timeval has architecture-dependent sizes (time_t, suseconds_t)
157
+ seconds, fractional = options[:socket_timeout].divmod(1)
158
+ microseconds = (fractional * 1_000_000).to_i
159
+ timeval = pack_timeval(sock, seconds, microseconds)
160
+
161
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
162
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
163
+ end
164
+ end
130
165
 
131
- sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
132
- sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
166
+ # Pack formats for struct timeval across architectures.
167
+ # Uses fixed-size formats for JRuby compatibility (JRuby doesn't support _ modifier on q).
168
+ # - ll: 8 bytes (32-bit time_t, 32-bit suseconds_t)
169
+ # - qq: 16 bytes (64-bit time_t, 64-bit suseconds_t or padded 32-bit)
170
+ TIMEVAL_PACK_FORMATS = %w[ll qq].freeze
171
+ TIMEVAL_TEST_VALUES = [0, 0].freeze
172
+
173
+ # Detect and cache the correct pack format for struct timeval on this platform.
174
+ # Different architectures have different sizes for time_t and suseconds_t.
175
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
176
+ def self.timeval_pack_format(sock)
177
+ @timeval_pack_format ||= begin
178
+ expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
179
+ TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
180
+ end
181
+ end
182
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
183
+
184
+ def self.pack_timeval(sock, seconds, microseconds)
185
+ [seconds, microseconds].pack(timeval_pack_format(sock))
133
186
  end
134
187
 
135
188
  def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
@@ -168,9 +221,16 @@ module Dalli
168
221
  Timeout.timeout(options[:socket_timeout]) do
169
222
  sock = new(path)
170
223
  sock.options = { path: path }.merge(options)
224
+ init_socket_options(sock, options)
171
225
  sock
172
226
  end
173
227
  end
228
+
229
+ def self.init_socket_options(sock, options)
230
+ # https://man7.org/linux/man-pages/man7/unix.7.html
231
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
232
+ sock.timeout = options[:socket_timeout] if options[:socket_timeout] && sock.respond_to?(:timeout=)
233
+ end
174
234
  end
175
235
  end
176
236
  end
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '3.2.8'
4
+ VERSION = '5.0.1'
5
5
 
6
- MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
6
+ MIN_SUPPORTED_MEMCACHED_VERSION = '1.6'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -4,8 +4,6 @@
4
4
  # Namespace for all Dalli code.
5
5
  ##
6
6
  module Dalli
7
- autoload :Server, 'dalli/server'
8
-
9
7
  # generic error
10
8
  class DalliError < RuntimeError; end
11
9
 
@@ -27,6 +25,12 @@ module Dalli
27
25
  # operation is not permitted in a multi block
28
26
  class NotPermittedMultiOpError < DalliError; end
29
27
 
28
+ # raised when Memcached response with a SERVER_ERROR
29
+ class ServerError < DalliError; end
30
+
31
+ # socket/server communication error that can be retried
32
+ class RetryableNetworkError < NetworkError; end
33
+
30
34
  # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
31
35
  class NilObject; end # rubocop:disable Lint/EmptyClass
32
36
  NOT_FOUND = NilObject.new
@@ -34,7 +38,7 @@ module Dalli
34
38
  QUIET = :dalli_multi
35
39
 
36
40
  def self.logger
37
- @logger ||= rails_logger || default_logger
41
+ @logger ||= rails_logger || default_logger # rubocop:disable ThreadSafety/ClassInstanceVariable
38
42
  end
39
43
 
40
44
  def self.rails_logger
@@ -50,20 +54,22 @@ module Dalli
50
54
  end
51
55
 
52
56
  def self.logger=(logger)
53
- @logger = logger
57
+ @logger = logger # rubocop:disable ThreadSafety/ClassInstanceVariable
54
58
  end
55
59
  end
56
60
 
57
61
  require_relative 'dalli/version'
62
+ require_relative 'dalli/instrumentation'
58
63
 
59
64
  require_relative 'dalli/compressor'
60
65
  require_relative 'dalli/client'
61
66
  require_relative 'dalli/key_manager'
62
67
  require_relative 'dalli/pipelined_getter'
68
+ require_relative 'dalli/pipelined_setter'
69
+ require_relative 'dalli/pipelined_deleter'
63
70
  require_relative 'dalli/ring'
64
71
  require_relative 'dalli/protocol'
65
72
  require_relative 'dalli/protocol/base'
66
- require_relative 'dalli/protocol/binary'
67
73
  require_relative 'dalli/protocol/connection_manager'
68
74
  require_relative 'dalli/protocol/meta'
69
75
  require_relative 'dalli/protocol/response_buffer'
@@ -71,6 +77,7 @@ require_relative 'dalli/protocol/server_config_parser'
71
77
  require_relative 'dalli/protocol/ttl_sanitizer'
72
78
  require_relative 'dalli/protocol/value_compressor'
73
79
  require_relative 'dalli/protocol/value_marshaller'
80
+ require_relative 'dalli/protocol/string_marshaller'
74
81
  require_relative 'dalli/protocol/value_serializer'
75
82
  require_relative 'dalli/servers_arg_normalizer'
76
83
  require_relative 'dalli/socket'
@@ -9,6 +9,10 @@ module Rack
9
9
  module Session
10
10
  # Rack::Session::Dalli provides memcached based session management.
11
11
  class Dalli < Abstract::PersistedSecure
12
+ class MissingSessionError < StandardError; end
13
+
14
+ RACK_SESSION_PERSISTED = 'rack.session.persisted'
15
+
12
16
  attr_reader :data
13
17
 
14
18
  # Don't freeze this until we fix the specs/implementation
@@ -70,23 +74,37 @@ module Rack
70
74
  @data = build_data_source(options)
71
75
  end
72
76
 
73
- def find_session(_req, sid)
77
+ def call(*_args)
78
+ super
79
+ rescue MissingSessionError
80
+ [401, {}, ['Wrong session ID']]
81
+ end
82
+
83
+ def find_session(req, sid)
74
84
  with_dalli_client([nil, {}]) do |dc|
75
85
  existing_session = existing_session_for_sid(dc, sid)
76
- return [sid, existing_session] unless existing_session.nil?
86
+ if existing_session.nil?
87
+ sid = create_sid_with_empty_session(dc)
88
+ existing_session = {}
89
+ end
77
90
 
78
- [create_sid_with_empty_session(dc), {}]
91
+ update_session_persisted_data(req, { id: sid })
92
+ return [sid, existing_session]
79
93
  end
80
94
  end
81
95
 
82
- def write_session(_req, sid, session, options)
96
+ def write_session(req, sid, session, options)
83
97
  return false unless sid
84
98
 
85
99
  key = memcached_key_from_sid(sid)
86
100
  return false unless key
87
101
 
88
102
  with_dalli_client(false) do |dc|
89
- dc.set(memcached_key_from_sid(sid), session, ttl(options[:expire_after]))
103
+ write_session_safely!(
104
+ dc, sid, session_persisted_data(req),
105
+ write_args: [memcached_key_from_sid(sid), session, ttl(options[:expire_after])]
106
+ )
107
+
90
108
  sid
91
109
  end
92
110
  end
@@ -139,12 +157,21 @@ module Rack
139
157
  ::Dalli::Client.new(server_configurations, client_options)
140
158
  else
141
159
  ensure_connection_pool_added!
142
- ConnectionPool.new(pool_options) do
160
+ ConnectionPool.new(**pool_options) do
143
161
  ::Dalli::Client.new(server_configurations, client_options.merge(threadsafe: false))
144
162
  end
145
163
  end
146
164
  end
147
165
 
166
+ def write_session_safely!(dalli_client, sid, persisted_data, write_args:)
167
+ if persisted_data && persisted_data[:id] == sid # That means that we update the existing session
168
+ # Override the session only if it still exists in the store!
169
+ raise MissingSessionError unless dalli_client.replace(*write_args)
170
+ else
171
+ dalli_client.set(*write_args)
172
+ end
173
+ end
174
+
148
175
  def extract_dalli_options(options)
149
176
  raise 'Rack::Session::Dalli no longer supports the :cache option.' if options[:cache]
150
177
 
@@ -175,8 +202,8 @@ module Rack
175
202
  raise e
176
203
  end
177
204
 
178
- def with_dalli_client(result_on_error = nil, &block)
179
- @data.with(&block)
205
+ def with_dalli_client(result_on_error = nil, &)
206
+ @data.with(&)
180
207
  rescue ::Dalli::DalliError, Errno::ECONNREFUSED
181
208
  raise if $ERROR_INFO.message.include?('undefined class')
182
209
 
@@ -190,6 +217,14 @@ module Rack
190
217
  def ttl(expire_after)
191
218
  expire_after.nil? ? 0 : expire_after + 1
192
219
  end
220
+
221
+ def session_persisted_data(req)
222
+ req.get_header RACK_SESSION_PERSISTED
223
+ end
224
+
225
+ def update_session_persisted_data(req, data)
226
+ req.set_header RACK_SESSION_PERSISTED, data
227
+ end
193
228
  end
194
229
  end
195
230
  end
metadata CHANGED
@@ -1,16 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dalli
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.8
4
+ version: 5.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
8
8
  - Mike Perham
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2024-02-12 00:00:00.000000000 Z
13
- dependencies: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logger
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
14
27
  description: High performance memcached client for Ruby
15
28
  email:
16
29
  - peter.m.goldstein@gmail.com
@@ -27,30 +40,28 @@ files:
27
40
  - lib/dalli/cas/client.rb
28
41
  - lib/dalli/client.rb
29
42
  - lib/dalli/compressor.rb
43
+ - lib/dalli/instrumentation.rb
30
44
  - lib/dalli/key_manager.rb
31
45
  - lib/dalli/options.rb
32
46
  - lib/dalli/pid_cache.rb
47
+ - lib/dalli/pipelined_deleter.rb
33
48
  - lib/dalli/pipelined_getter.rb
49
+ - lib/dalli/pipelined_setter.rb
34
50
  - lib/dalli/protocol.rb
35
51
  - lib/dalli/protocol/base.rb
36
- - lib/dalli/protocol/binary.rb
37
- - lib/dalli/protocol/binary/request_formatter.rb
38
- - lib/dalli/protocol/binary/response_header.rb
39
- - lib/dalli/protocol/binary/response_processor.rb
40
- - lib/dalli/protocol/binary/sasl_authentication.rb
41
52
  - lib/dalli/protocol/connection_manager.rb
53
+ - lib/dalli/protocol/key_regularizer.rb
42
54
  - lib/dalli/protocol/meta.rb
43
- - lib/dalli/protocol/meta/key_regularizer.rb
44
- - lib/dalli/protocol/meta/request_formatter.rb
45
- - lib/dalli/protocol/meta/response_processor.rb
55
+ - lib/dalli/protocol/request_formatter.rb
46
56
  - lib/dalli/protocol/response_buffer.rb
57
+ - lib/dalli/protocol/response_processor.rb
47
58
  - lib/dalli/protocol/server_config_parser.rb
59
+ - lib/dalli/protocol/string_marshaller.rb
48
60
  - lib/dalli/protocol/ttl_sanitizer.rb
49
61
  - lib/dalli/protocol/value_compressor.rb
50
62
  - lib/dalli/protocol/value_marshaller.rb
51
63
  - lib/dalli/protocol/value_serializer.rb
52
64
  - lib/dalli/ring.rb
53
- - lib/dalli/server.rb
54
65
  - lib/dalli/servers_arg_normalizer.rb
55
66
  - lib/dalli/socket.rb
56
67
  - lib/dalli/version.rb
@@ -62,7 +73,6 @@ metadata:
62
73
  bug_tracker_uri: https://github.com/petergoldstein/dalli/issues
63
74
  changelog_uri: https://github.com/petergoldstein/dalli/blob/main/CHANGELOG.md
64
75
  rubygems_mfa_required: 'true'
65
- post_install_message:
66
76
  rdoc_options: []
67
77
  require_paths:
68
78
  - lib
@@ -70,15 +80,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
70
80
  requirements:
71
81
  - - ">="
72
82
  - !ruby/object:Gem::Version
73
- version: '2.6'
83
+ version: '3.3'
74
84
  required_rubygems_version: !ruby/object:Gem::Requirement
75
85
  requirements:
76
86
  - - ">="
77
87
  - !ruby/object:Gem::Version
78
88
  version: '0'
79
89
  requirements: []
80
- rubygems_version: 3.5.6
81
- signing_key:
90
+ rubygems_version: 4.0.6
82
91
  specification_version: 4
83
92
  summary: High performance memcached client for Ruby
84
93
  test_files: []