dalli 3.2.8 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7d8b3dd9e76a224d876f901dc44c3cf8016326209df3efc7fe522053a4a3c22
4
- data.tar.gz: 0ced058a6a170cadd4b74268de5417c4a1e700baf5767fc8f260db9477e2b735
3
+ metadata.gz: 86e39b2869b7c916472e88e6cbbfb6aa1f6533aa35c9e1f1c704cffcbc680f8a
4
+ data.tar.gz: bf2e56610c6bae187d561ccf09105e6f5b4a29eed1308d089745ef22cf5b673b
5
5
  SHA512:
6
- metadata.gz: 3e316a4d60c3327cecec46a0a34c52536130199124035c375bf1933676d85fbf09e99a985ae44faf4e27b0fb1f8b8faef1656a812e6bdfc387406c5e18874461
7
- data.tar.gz: bce3e0e41c280e99889bea6835c1b2f701cbcb5e4f398203ae2b4d6ebc05cdc505ee5955365715f8e00441d69701ceac84de37a98eac8581d003d1ab411a33e9
6
+ metadata.gz: e2faab814abb4b25a53d87048f7e4e81e49c609bd64725cb18607670110da3d5b8038544598c44575831b9038500f9d8220a56ec28c16b9517a21d442212d12b
7
+ data.tar.gz: 3e5a7c326c908d72d2d8ce2211b27f67acebbbf97c279c5faf3ba2914ec7409df7f259a9082d82903cc9ae6b6716a137994aba849321546b8d3d3a35f8f7f5af
data/CHANGELOG.md CHANGED
@@ -1,8 +1,30 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
- Unreleased
5
- ==========
4
+ 4.0.0
5
+ ==========
6
+
7
+ BREAKING CHANGES:
8
+
9
+ - Require Ruby 3.1+ (dropped support for Ruby 2.6, 2.7, and 3.0)
10
+ - Removed `Dalli::Server` deprecated alias - use `Dalli::Protocol::Binary` instead
11
+ - Removed `:compression` option - use `:compress` instead
12
+ - Removed `close_on_fork` method - use `reconnect_on_fork` instead
13
+
14
+ Other changes:
15
+
16
+ - Add security warning when using default Marshal serializer (silence with `silence_marshal_warning: true`)
17
+ - Add defense-in-depth input validation for stats command arguments
18
+ - Add `string_fastpath` option to skip serialization for simple strings (byroot)
19
+ - Meta protocol set performance improvement (danmayer)
20
+ - Fix connection_pool 3.0 compatibility for Rack session store
21
+ - Fix session recovery after deletion (stengineering0)
22
+ - Fix cannot read response data included terminator `\r\n` when use meta protocol (matsubara0507)
23
+ - Support SERVER_ERROR response from Memcached as per the [memcached spec](https://github.com/memcached/memcached/blob/e43364402195c8e822bb8f88755a60ab8bbed62a/doc/protocol.txt#L172) (grcooper)
24
+ - Update Socket timeout handling to use Socket#timeout= when available (nickamorim)
25
+ - Serializer: reraise all .load errors as UnmarshalError (olleolleolle)
26
+ - Reconnect gracefully when a fork is detected instead of crashing (PatrickTulskie)
27
+ - Update CI to test against memcached 1.6.40
6
28
 
7
29
  3.2.8
8
30
  ==========
data/Gemfile CHANGED
@@ -5,7 +5,9 @@ source 'https://rubygems.org'
5
5
  gemspec
6
6
 
7
7
  group :development, :test do
8
+ gem 'cgi'
8
9
  gem 'connection_pool'
10
+ gem 'debug' unless RUBY_PLATFORM == 'java'
9
11
  gem 'minitest', '~> 5'
10
12
  gem 'rack', '~> 2.0', '>= 2.2.0'
11
13
  gem 'rake', '~> 13.0'
data/lib/dalli/client.rb CHANGED
@@ -155,8 +155,8 @@ module Dalli
155
155
  # - nil if the key did not exist.
156
156
  # - false if the value was changed by someone else.
157
157
  # - true if the value was successfully updated.
158
- def cas(key, ttl = nil, req_options = nil, &block)
159
- cas_core(key, false, ttl, req_options, &block)
158
+ def cas(key, ttl = nil, req_options = nil, &)
159
+ cas_core(key, false, ttl, req_options, &)
160
160
  end
161
161
 
162
162
  ##
@@ -166,8 +166,8 @@ module Dalli
166
166
  # Returns:
167
167
  # - false if the value was changed by someone else.
168
168
  # - true if the value was successfully updated.
169
- def cas!(key, ttl = nil, req_options = nil, &block)
170
- cas_core(key, true, ttl, req_options, &block)
169
+ def cas!(key, ttl = nil, req_options = nil, &)
170
+ cas_core(key, true, ttl, req_options, &)
171
171
  end
172
172
 
173
173
  ##
@@ -30,7 +30,7 @@ module Dalli
30
30
 
31
31
  def initialize(client_options)
32
32
  @key_options =
33
- DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
33
+ DEFAULTS.merge(client_options.slice(*OPTIONS))
34
34
  validate_digest_class_option(@key_options)
35
35
 
36
36
  @namespace = namespace_from_options
@@ -77,7 +77,7 @@ module Dalli
77
77
  def namespace_regexp
78
78
  return /\A#{Regexp.escape(evaluate_namespace)}:/ if namespace.is_a?(Proc)
79
79
 
80
- @namespace_regexp ||= /\A#{Regexp.escape(namespace)}:/.freeze unless namespace.nil?
80
+ @namespace_regexp ||= /\A#{Regexp.escape(namespace)}:/ unless namespace.nil?
81
81
  end
82
82
 
83
83
  def validate_digest_class_option(opts)
@@ -154,6 +154,8 @@ module Dalli
154
154
  private
155
155
 
156
156
  ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
157
+ private_constant :ALLOWED_QUIET_OPS
158
+
157
159
  def verify_allowed_quiet!(opkey)
158
160
  return if ALLOWED_QUIET_OPS.include?(opkey)
159
161
 
@@ -119,6 +119,7 @@ module Dalli
119
119
  # if the key doesn't already exist, rather than
120
120
  # setting the initial value
121
121
  NOT_FOUND_EXPIRY = 0xFFFFFFFF
122
+ private_constant :NOT_FOUND_EXPIRY
122
123
 
123
124
  def decr_incr(opkey, key, count, ttl, initial)
124
125
  expiry = initial ? TtlSanitizer.sanitize(ttl) : NOT_FOUND_EXPIRY
@@ -100,13 +100,13 @@ module Dalli
100
100
 
101
101
  def confirm_ready!
102
102
  close if request_in_progress?
103
- close_on_fork if fork_detected?
103
+ reconnect_on_fork if fork_detected?
104
104
  end
105
105
 
106
106
  def confirm_in_progress!
107
107
  raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless request_in_progress?
108
108
 
109
- close_on_fork if fork_detected?
109
+ reconnect_on_fork if fork_detected?
110
110
  end
111
111
 
112
112
  def close
@@ -212,20 +212,18 @@ module Dalli
212
212
  end
213
213
 
214
214
  def log_warn_message(err_or_string)
215
- detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
216
215
  Dalli.logger.warn do
217
216
  detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
218
217
  "#{name} failed (count: #{@fail_count}) #{detail}"
219
218
  end
220
219
  end
221
220
 
222
- def close_on_fork
221
+ def reconnect_on_fork
223
222
  message = 'Fork detected, re-connecting child process...'
224
223
  Dalli.logger.info { message }
225
- # Close socket on a fork, setting us up for reconnect
226
- # on next request.
224
+ # Close socket on a fork and reconnect immediately
227
225
  close
228
- raise Dalli::NetworkError, message
226
+ establish_connection
229
227
  end
230
228
 
231
229
  def fork_detected?
@@ -10,7 +10,7 @@ module Dalli
10
10
  # memcached supports the use of base64 hashes for keys containing
11
11
  # whitespace or non-ASCII characters, provided the 'b' flag is included in the request.
12
12
  class KeyRegularizer
13
- WHITESPACE = /\s/.freeze
13
+ WHITESPACE = /\s/
14
14
 
15
15
  def self.encode(key)
16
16
  return [key, false] if key.ascii_only? && !WHITESPACE.match(key)
@@ -13,7 +13,6 @@ module Dalli
13
13
  # and introducing an intermediate object seems like overkill.
14
14
  #
15
15
  # rubocop:disable Metrics/CyclomaticComplexity
16
- # rubocop:disable Metrics/MethodLength
17
16
  # rubocop:disable Metrics/ParameterLists
18
17
  # rubocop:disable Metrics/PerceivedComplexity
19
18
  def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false)
@@ -36,8 +35,6 @@ module Dalli
36
35
  cmd << " M#{mode_to_token(mode)}"
37
36
  cmd << ' q' if quiet
38
37
  cmd << TERMINATOR
39
- cmd << value
40
- cmd + TERMINATOR
41
38
  end
42
39
 
43
40
  def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false)
@@ -62,7 +59,6 @@ module Dalli
62
59
  cmd + TERMINATOR
63
60
  end
64
61
  # rubocop:enable Metrics/CyclomaticComplexity
65
- # rubocop:enable Metrics/MethodLength
66
62
  # rubocop:enable Metrics/ParameterLists
67
63
  # rubocop:enable Metrics/PerceivedComplexity
68
64
 
@@ -81,13 +77,16 @@ module Dalli
81
77
  cmd + TERMINATOR
82
78
  end
83
79
 
80
+ ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
81
+
84
82
  def self.stats(arg = nil)
83
+ raise ArgumentError, "Invalid stats argument: #{arg.inspect}" unless ALLOWED_STATS_ARGS.include?(arg)
84
+
85
85
  cmd = +'stats'
86
- cmd << " #{arg}" if arg
86
+ cmd << " #{arg}" if arg && !arg.empty?
87
87
  cmd + TERMINATOR
88
88
  end
89
89
 
90
- # rubocop:disable Metrics/MethodLength
91
90
  def self.mode_to_token(mode)
92
91
  case mode
93
92
  when :add
@@ -102,7 +101,6 @@ module Dalli
102
101
  'S'
103
102
  end
104
103
  end
105
- # rubocop:enable Metrics/MethodLength
106
104
 
107
105
  def self.cas_string(cas)
108
106
  cas = parse_to_64_bit_int(cas, nil)
@@ -21,6 +21,7 @@ module Dalli
21
21
  STAT = 'STAT'
22
22
  VA = 'VA'
23
23
  VERSION = 'VERSION'
24
+ SERVER_ERROR = 'SERVER_ERROR'
24
25
 
25
26
  def initialize(io_source, value_marshaller)
26
27
  @io_source = io_source
@@ -32,7 +33,7 @@ module Dalli
32
33
  return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
33
34
  return true unless tokens.first == VA
34
35
 
35
- @value_marshaller.retrieve(read_line, bitflags_from_tokens(tokens))
36
+ @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
36
37
  end
37
38
 
38
39
  def meta_get_with_value_and_cas
@@ -42,7 +43,7 @@ module Dalli
42
43
  cas = cas_from_tokens(tokens)
43
44
  return [nil, cas] unless tokens.first == VA
44
45
 
45
- [@value_marshaller.retrieve(read_line, bitflags_from_tokens(tokens)), cas]
46
+ [@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
46
47
  end
47
48
 
48
49
  def meta_get_without_value
@@ -167,9 +168,12 @@ module Dalli
167
168
 
168
169
  def error_on_unexpected!(expected_codes)
169
170
  tokens = next_line_to_tokens
170
- raise Dalli::DalliError, "Response error: #{tokens.first}" unless expected_codes.include?(tokens.first)
171
171
 
172
- tokens
172
+ return tokens if expected_codes.include?(tokens.first)
173
+
174
+ raise Dalli::ServerError, tokens.join(' ').to_s if tokens.first == SERVER_ERROR
175
+
176
+ raise Dalli::DalliError, "Response error: #{tokens.first}"
173
177
  end
174
178
 
175
179
  def bitflags_from_tokens(tokens)
@@ -205,6 +209,10 @@ module Dalli
205
209
  line = read_line
206
210
  line&.split || []
207
211
  end
212
+
213
+ def read_data(data_size)
214
+ @io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
215
+ end
208
216
  end
209
217
  end
210
218
  end
@@ -85,6 +85,8 @@ module Dalli
85
85
  bitflags: bitflags, cas: cas,
86
86
  ttl: ttl, mode: mode, quiet: quiet?, base64: base64)
87
87
  write(req)
88
+ write(value)
89
+ write(TERMINATOR)
88
90
  end
89
91
  # rubocop:enable Metrics/ParameterLists
90
92
 
@@ -105,6 +107,8 @@ module Dalli
105
107
  req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
106
108
  cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
107
109
  write(req)
110
+ write(value)
111
+ write(TERMINATOR)
108
112
  end
109
113
  # rubocop:enable Metrics/ParameterLists
110
114
 
@@ -16,7 +16,7 @@ module Dalli
16
16
  # can limit character set to LDH + '.'. Hex digit section
17
17
  # is there to support IPv6 addresses, which need to be specified with
18
18
  # a bounding []
19
- SERVER_CONFIG_REGEXP = /\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z/.freeze
19
+ SERVER_CONFIG_REGEXP = /\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z/
20
20
 
21
21
  DEFAULT_PORT = 11_211
22
22
  DEFAULT_WEIGHT = 1
@@ -31,7 +31,7 @@ module Dalli
31
31
  return ttl_as_i if ttl_as_i > now # Already a timestamp
32
32
 
33
33
  Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached " \
34
- 'and too short to be a future timestamp,' \
34
+ 'and too short to be a future timestamp, ' \
35
35
  'converting to an expiration timestamp'
36
36
  now + ttl_as_i
37
37
  end
@@ -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,53 @@ 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]
35
+ if req_options
36
+ return [value.to_s, bitflags] if req_options[:raw]
37
+
38
+ # If the value is a simple string, going through serialization is costly
39
+ # for no benefit other than preserving encoding.
40
+ # Assuming most strings are either UTF-8 or BINARY we can just store
41
+ # that information in the bitflags.
42
+ if req_options[:string_fastpath] && value.instance_of?(String)
43
+ case value.encoding
44
+ when Encoding::BINARY
45
+ return [value, bitflags]
46
+ when Encoding::UTF_8
47
+ return [value, bitflags | FLAG_UTF8]
48
+ end
49
+ end
50
+ end
51
+
52
+ [serialize_value(value), bitflags | FLAG_SERIALIZED]
34
53
  end
35
54
 
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
55
  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
55
-
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}"
66
- end
67
-
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}"
56
+ serialized = bitflags.anybits?(FLAG_SERIALIZED)
57
+ if serialized
58
+ begin
59
+ serializer.load(value)
60
+ rescue StandardError
61
+ raise UnmarshalError, 'Unable to unmarshal value'
62
+ end
63
+ elsif bitflags.anybits?(FLAG_UTF8)
64
+ value.force_encoding(Encoding::UTF_8)
65
+ else
66
+ value
67
+ end
72
68
  end
73
69
 
74
70
  def serializer
@@ -86,6 +82,19 @@ module Dalli
86
82
  exc.set_backtrace e.backtrace
87
83
  raise exc
88
84
  end
85
+
86
+ private
87
+
88
+ def warn_if_marshal_default(protocol_options)
89
+ return if protocol_options.key?(:serializer)
90
+ return if @@marshal_warning_logged
91
+
92
+ Dalli.logger.warn 'SECURITY WARNING: Dalli is using Marshal for serialization. ' \
93
+ 'Marshal can execute arbitrary code during deserialization. ' \
94
+ 'If your memcached server could be compromised, consider using ' \
95
+ 'a safer serializer like JSON: Dalli::Client.new(servers, serializer: JSON)'
96
+ @@marshal_warning_logged = true # rubocop:disable Style/ClassVars
97
+ end
89
98
  end
90
99
  end
91
100
  end
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,6 +86,7 @@ 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
 
@@ -117,19 +119,33 @@ module Dalli
117
119
  end
118
120
 
119
121
  def self.init_socket_options(sock, options)
122
+ configure_tcp_options(sock, options)
123
+ configure_socket_buffers(sock, options)
124
+ configure_timeout(sock, options)
125
+ end
126
+
127
+ def self.configure_tcp_options(sock, options)
120
128
  sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
121
129
  sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options[:keepalive]
130
+ end
131
+
132
+ def self.configure_socket_buffers(sock, options)
122
133
  sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
123
134
  sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
135
+ end
124
136
 
137
+ def self.configure_timeout(sock, options)
125
138
  return unless options[:socket_timeout]
126
139
 
127
- seconds, fractional = options[:socket_timeout].divmod(1)
128
- microseconds = fractional * 1_000_000
129
- timeval = [seconds, microseconds].pack('l_2')
140
+ if sock.respond_to?(:timeout=)
141
+ sock.timeout = options[:socket_timeout]
142
+ else
143
+ seconds, fractional = options[:socket_timeout].divmod(1)
144
+ timeval = [seconds, fractional * 1_000_000].pack('l_2')
130
145
 
131
- sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
132
- sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
146
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
147
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
148
+ end
133
149
  end
134
150
 
135
151
  def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
@@ -168,9 +184,16 @@ module Dalli
168
184
  Timeout.timeout(options[:socket_timeout]) do
169
185
  sock = new(path)
170
186
  sock.options = { path: path }.merge(options)
187
+ init_socket_options(sock, options)
171
188
  sock
172
189
  end
173
190
  end
191
+
192
+ def self.init_socket_options(sock, options)
193
+ # https://man7.org/linux/man-pages/man7/unix.7.html
194
+ sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
195
+ sock.timeout = options[:socket_timeout] if options[:socket_timeout] && sock.respond_to?(:timeout=)
196
+ end
174
197
  end
175
198
  end
176
199
  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 = '4.0.0'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
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,9 @@ 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
+
30
31
  # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
31
32
  class NilObject; end # rubocop:disable Lint/EmptyClass
32
33
  NOT_FOUND = NilObject.new
@@ -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: 4.0.0
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
@@ -50,7 +63,6 @@ files:
50
63
  - lib/dalli/protocol/value_marshaller.rb
51
64
  - lib/dalli/protocol/value_serializer.rb
52
65
  - lib/dalli/ring.rb
53
- - lib/dalli/server.rb
54
66
  - lib/dalli/servers_arg_normalizer.rb
55
67
  - lib/dalli/socket.rb
56
68
  - lib/dalli/version.rb
@@ -62,7 +74,6 @@ metadata:
62
74
  bug_tracker_uri: https://github.com/petergoldstein/dalli/issues
63
75
  changelog_uri: https://github.com/petergoldstein/dalli/blob/main/CHANGELOG.md
64
76
  rubygems_mfa_required: 'true'
65
- post_install_message:
66
77
  rdoc_options: []
67
78
  require_paths:
68
79
  - lib
@@ -70,15 +81,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
70
81
  requirements:
71
82
  - - ">="
72
83
  - !ruby/object:Gem::Version
73
- version: '2.6'
84
+ version: '3.1'
74
85
  required_rubygems_version: !ruby/object:Gem::Requirement
75
86
  requirements:
76
87
  - - ">="
77
88
  - !ruby/object:Gem::Version
78
89
  version: '0'
79
90
  requirements: []
80
- rubygems_version: 3.5.6
81
- signing_key:
91
+ rubygems_version: 4.0.3
82
92
  specification_version: 4
83
93
  summary: High performance memcached client for Ruby
84
94
  test_files: []
data/lib/dalli/server.rb DELETED
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Dalli # rubocop:disable Style/Documentation
4
- warn 'Dalli::Server is deprecated, use Dalli::Protocol::Binary instead'
5
- Server = Protocol::Binary
6
- end