dalli 5.0.10 → 5.1.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.
@@ -36,28 +36,40 @@ module Dalli
36
36
  # - l<N>: Seconds since last access
37
37
  def meta_get(key:, value: true, return_cas: false, ttl: nil, quiet: false,
38
38
  vivify_ttl: nil, recache_ttl: nil,
39
- return_hit_status: false, return_last_access: false, skip_lru_bump: false,
40
- skip_flags: false)
39
+ return_hit_status: false, return_last_access: false, return_ttl_remaining: false,
40
+ skip_lru_bump: false, skip_flags: false, p_token: nil, l_token: nil)
41
41
  cmd = "mg #{encoded_key(key)}"
42
42
  # In raw mode (skip_flags: true), we don't request bitflags since they're not used.
43
43
  # This saves 2 bytes per request and skips parsing on response.
44
44
  cmd << (skip_flags ? ' v' : ' v f') if value
45
45
  cmd << ' c' if return_cas
46
- cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
46
+ cmd << " T#{ttl}" if ttl
47
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
47
48
  cmd << ' k q s' if quiet # Return the key in the response if quiet
48
- cmd << " N#{integer_flag(:vivify_ttl, vivify_ttl)}" if vivify_ttl # Thundering herd: vivify on miss
49
- # Thundering herd: win recache if TTL below threshold
50
- cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
49
+ cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
50
+ cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
51
51
  cmd << ' h' if return_hit_status # Return hit status (0 or 1)
52
52
  cmd << ' l' if return_last_access # Return seconds since last access
53
+ cmd << ' t' if return_ttl_remaining # Return seconds of TTL remaining (-1 = no TTL)
53
54
  cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
54
55
  cmd << TERMINATOR
55
56
  end
56
57
 
57
- def multi_meta_get(keys, skip_flags: false)
58
+ def multi_meta_get(keys, skip_flags: false, return_cas: false, p_token: nil, l_token: nil)
58
59
  # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
59
60
  # Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
60
- post_get = skip_flags ? " v k q s\r\n" : " v f k q s\r\n"
61
+ # With return_cas a "c" flag follows, which shifts those indexes --
62
+ # callers of that variant locate tokens by flag rather than position.
63
+ # Routing tokens apply to every key in the batch, so the suffix is
64
+ # built once rather than per key.
65
+ post_get = if return_cas
66
+ skip_flags ? ' v c' : ' v f c'
67
+ else
68
+ skip_flags ? ' v' : ' v f'
69
+ end
70
+ post_get += routing_tokens(p_token: p_token, l_token: l_token)
71
+ post_get += " k q s#{TERMINATOR}"
72
+
61
73
  buffer = ''.b
62
74
  keys.each do |key|
63
75
  buffer << 'mg ' << encoded_key(key) << post_get
@@ -65,7 +77,8 @@ module Dalli
65
77
  buffer << 'mn' << TERMINATOR
66
78
  end
67
79
 
68
- def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false)
80
+ def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false,
81
+ p_token: nil, l_token: nil)
69
82
  base64 = KeyRegularizer.required?(key)
70
83
  key = KeyRegularizer.encode(key) if base64
71
84
  cmd = "ms #{key} #{value.bytesize}"
@@ -75,14 +88,18 @@ module Dalli
75
88
  cmd << ' b' if base64
76
89
  cmd << " F#{bitflags}" if bitflags
77
90
  cmd << cas_string(cas)
78
- cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
91
+ cmd << " T#{ttl}" if ttl
79
92
  cmd << " M#{mode_to_token(mode)}"
80
93
  cmd << ' q' if quiet
94
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
81
95
  cmd << TERMINATOR
82
96
  end
83
97
 
84
- def multi_meta_set(entries, ttl: nil)
85
- ttl = integer_flag(:ttl, ttl) if ttl
98
+ def multi_meta_set(entries, ttl: nil, p_token: nil, l_token: nil)
99
+ # Routing tokens apply to every entry in the batch, so the suffix is
100
+ # built once rather than per entry.
101
+ token_suffix = routing_tokens(p_token: p_token, l_token: l_token)
102
+
86
103
  buffer = ''.b
87
104
  entries.each do |key, pair|
88
105
  value, bitflags = pair
@@ -90,12 +107,12 @@ module Dalli
90
107
  base64 = KeyRegularizer.required?(key)
91
108
  key = KeyRegularizer.encode(key) if base64
92
109
 
93
- # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
110
+ # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q [P/L]\r\n"
94
111
  buffer << "ms #{key} #{value.bytesize} c"
95
112
  buffer << ' b' if base64
96
113
  buffer << " F#{bitflags}" if bitflags
97
114
  buffer << " T#{ttl}" if ttl
98
- buffer << ' MS q' << TERMINATOR << value << TERMINATOR
115
+ buffer << ' MS q' << token_suffix << TERMINATOR << value << TERMINATOR
99
116
  end
100
117
  buffer << META_NOOP
101
118
  end
@@ -103,34 +120,96 @@ module Dalli
103
120
  # Thundering herd protection flag:
104
121
  # - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
105
122
  # using N/R flags will see the X flag and know the item is being regenerated.
106
- def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false)
123
+ # Tombstone flags:
124
+ # - stale (I flag): mark the item stale instead of removing it. Readers
125
+ # using N/R flags, or get_with_metadata, see the X flag and know the
126
+ # item is being regenerated.
127
+ # - ttl (T flag): how long the stale marker lives. memcached only honors
128
+ # T on a delete when it is paired with I, so this raises rather than
129
+ # emitting a request the server would apply differently than intended.
130
+ # - drop_value (x flag): remove the item's value but leave the item, so a
131
+ # tombstone can be left without retaining the old payload.
132
+ def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false, drop_value: false,
133
+ p_token: nil, l_token: nil)
134
+ # Message uses this method's own parameter names (ttl/stale), not the
135
+ # client-facing tombstone_ttl/invalidate names Dalli::Client validates
136
+ # against -- this guard is also reachable by internal callers (tests,
137
+ # direct RequestFormatter use) that never go through the client.
138
+ raise ArgumentError, 'ttl requires stale: true' if ttl && !stale
139
+
107
140
  cmd = "md #{encoded_key(key)}"
108
141
  cmd << cas_string(cas)
109
- cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
110
142
  cmd << ' I' if stale # Mark stale instead of deleting
143
+ cmd << " T#{Integer(ttl)}" if ttl
144
+ cmd << ' x' if drop_value # Drop the value but keep the item
111
145
  cmd << ' q' if quiet
146
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
112
147
  cmd << TERMINATOR
113
148
  end
114
149
 
115
- def multi_meta_delete(keys)
150
+ # Tombstone and routing-token flags apply to every key in the batch;
151
+ # see meta_delete.
152
+ def multi_meta_delete(keys, stale: false, ttl: nil, drop_value: false, p_token: nil, l_token: nil)
153
+ raise ArgumentError, 'tombstone_ttl requires invalidate: true' if ttl && !stale
154
+
155
+ suffix = +''
156
+ suffix << ' I' if stale
157
+ suffix << " T#{Integer(ttl)}" if ttl
158
+ suffix << ' x' if drop_value
159
+ suffix << ' q'
160
+ suffix << routing_tokens(p_token: p_token, l_token: l_token)
161
+ suffix << TERMINATOR
162
+
116
163
  buffer = ''.b
117
164
  keys.each do |key|
118
- buffer << 'md ' << encoded_key(key) << ' q' << TERMINATOR
165
+ buffer << 'md ' << encoded_key(key) << suffix
119
166
  end
120
167
  buffer << META_NOOP
121
168
  end
122
169
 
123
- def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false)
170
+ def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false,
171
+ p_token: nil, l_token: nil)
124
172
  cmd = "ma #{encoded_key(key)} v"
125
- cmd << " D#{integer_flag(:delta, delta)}" if delta
126
- cmd << " J#{integer_flag(:initial, initial)}" if initial
173
+ cmd << " D#{delta}" if delta
174
+ cmd << " J#{initial}" if initial
127
175
  # Always set a TTL if an initial value is specified
128
- cmd << " N#{ttl ? integer_flag(:ttl, ttl) : 0}" if ttl || initial
176
+ cmd << " N#{ttl || 0}" if ttl || initial
129
177
  cmd << cas_string(cas)
130
178
  cmd << ' q' if quiet
131
179
  cmd << " M#{incr ? 'I' : 'D'}"
180
+ cmd << routing_tokens(p_token: p_token, l_token: l_token)
132
181
  cmd << TERMINATOR
133
182
  end
183
+
184
+ # Builds the wire-format suffix for opaque routing tokens (P and L).
185
+ # memcached itself ignores these; they exist as hints for a proxy or
186
+ # router sitting between the client and memcached. See protocol.txt:
187
+ # "All commands accept tokens 'P' and 'L' which are completely ignored.
188
+ # The arguments to 'P' and 'L' can be used as hints or path
189
+ # specifications to a proxy or router inbetween a client and a
190
+ # memcached daemon."
191
+ #
192
+ # Empty / nil tokens are treated as no-ops. CRLF and null bytes are
193
+ # rejected with ArgumentError to prevent the token from being used as a
194
+ # wire-protocol injection vector (e.g. "foo\r\nflush_all\r\n" would
195
+ # otherwise be parsed as a second command by memcached or any
196
+ # intermediate proxy/LB).
197
+ def routing_tokens(p_token: nil, l_token: nil)
198
+ # Only an empty *String* is a no-op. Checking respond_to?(:empty?)
199
+ # instead would also swallow p_token: [] / {} before the type check
200
+ # below ever runs, silently dropping caller mistakes that should
201
+ # raise "must be a String".
202
+ p_token = nil if p_token.is_a?(String) && p_token.empty?
203
+ l_token = nil if l_token.is_a?(String) && l_token.empty?
204
+ validate_routing_token!('p_token', p_token)
205
+ validate_routing_token!('l_token', l_token)
206
+ return '' unless p_token || l_token
207
+
208
+ s = +''
209
+ s << " P#{p_token}" if p_token
210
+ s << " L#{l_token}" if l_token
211
+ s
212
+ end
134
213
  # rubocop:enable Metrics/CyclomaticComplexity
135
214
  # rubocop:enable Metrics/ParameterLists
136
215
  # rubocop:enable Metrics/PerceivedComplexity
@@ -171,16 +250,15 @@ module Dalli
171
250
 
172
251
  private
173
252
 
174
- # Numeric flag values are written straight into the command line, so a
175
- # value that isn't an integer -- e.g. a String carrying CRLF -- would let
176
- # the caller inject further memcached commands (GHSA-6wmv-xq9m-fmp7).
177
- # Converting to Integer means only digits ever reach the wire. Strings
178
- # are parsed as base 10 so "010" means 10, as memcached would read it,
179
- # rather than octal 8.
180
- def integer_flag(name, value)
181
- value.is_a?(String) ? Integer(value, 10) : Integer(value)
182
- rescue ArgumentError, TypeError, FloatDomainError
183
- raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
253
+ # Disallowed bytes: CR, LF, NUL. Any of these embedded in a routing
254
+ # token would let the caller inject a second wire-protocol command.
255
+ ROUTING_TOKEN_FORBIDDEN = /[\r\n\0]/
256
+ private_constant :ROUTING_TOKEN_FORBIDDEN
257
+
258
+ def validate_routing_token!(name, value)
259
+ return if value.nil?
260
+ raise ArgumentError, "#{name} must be a String, got #{value.class}" unless value.is_a?(String)
261
+ raise ArgumentError, "#{name} must not contain CRLF or null bytes" if value.match?(ROUTING_TOKEN_FORBIDDEN)
184
262
  end
185
263
 
186
264
  def mode_to_token(mode)
@@ -23,9 +23,6 @@ module Dalli
23
23
  VERSION = 'VERSION'
24
24
  SERVER_ERROR = 'SERVER_ERROR'
25
25
 
26
- # memcached can't store an item larger than 1 GiB (its -I maximum)
27
- MAX_VALUE_BYTES = 1024 * 1024 * 1024
28
-
29
26
  T_OK = [OK].freeze
30
27
  T_RESET = [RESET].freeze
31
28
  T_EN_HD = [EN, HD].freeze
@@ -41,25 +38,22 @@ module Dalli
41
38
  @value_marshaller = value_marshaller
42
39
  end
43
40
 
44
- # raw: true (a request made with raw: true) returns the value as stored,
45
- # ignoring any flags in the reply. The request didn't ask for flags, so
46
- # a reply carrying them anyway must not get the value deserialized.
47
- def meta_get_with_value(cache_nils: false, raw: false)
41
+ def meta_get_with_value(cache_nils: false)
48
42
  tokens = error_on_unexpected!(T_VA_EN_HD)
49
43
  return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
50
44
  return true unless tokens.first == VA
51
45
 
52
- retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
46
+ @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
53
47
  end
54
48
 
55
- def meta_get_with_value_and_cas(raw: false)
49
+ def meta_get_with_value_and_cas
56
50
  tokens = error_on_unexpected!(T_VA_EN_HD)
57
51
  return [nil, 0] if tokens.first == EN
58
52
 
59
53
  cas = cas_from_tokens(tokens)
60
54
  return [nil, cas] unless tokens.first == VA
61
55
 
62
- [retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw), cas]
56
+ [@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
63
57
  end
64
58
 
65
59
  def meta_get_without_value
@@ -79,12 +73,13 @@ module Dalli
79
73
  # Used by meta_get for comprehensive metadata retrieval.
80
74
  # Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
81
75
  def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
82
- raw: false)
76
+ return_ttl_remaining: false)
83
77
  tokens = error_on_unexpected!(T_VA_EN_HD)
84
78
  result = build_metadata_result(tokens)
85
79
  result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
86
80
  result[:last_access] = last_access_from_tokens(tokens) if return_last_access
87
- result[:value] = parse_value_from_tokens(tokens, cache_nils, raw)
81
+ result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
82
+ result[:value] = parse_value_from_tokens(tokens, cache_nils)
88
83
  result
89
84
  end
90
85
 
@@ -92,20 +87,20 @@ module Dalli
92
87
  {
93
88
  value: nil, cas: cas_from_tokens(tokens),
94
89
  won_recache: tokens.include?('W'), stale: tokens.include?('X'),
95
- lost_recache: tokens.include?('Z')
90
+ lost_recache: tokens.include?('Z'),
91
+ # Explicit miss marker: EN means the key does not exist. A tombstoned
92
+ # item is NOT a miss -- it answers VA/HD with the X flag set -- and a
93
+ # stored nil under cache_nils is not one either, so neither can be
94
+ # inferred from value or cas alone.
95
+ miss: tokens.first == EN
96
96
  }
97
97
  end
98
98
 
99
- def parse_value_from_tokens(tokens, cache_nils, raw = false) # rubocop:disable Style/OptionalBooleanParameter
99
+ def parse_value_from_tokens(tokens, cache_nils)
100
100
  return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
101
101
  return unless tokens.first == VA
102
102
 
103
- retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
104
- end
105
-
106
- # Values from a raw request are returned as stored
107
- def retrieve(value, bitflags, raw)
108
- raw ? value : @value_marshaller.retrieve(value, bitflags)
103
+ @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
109
104
  end
110
105
 
111
106
  def meta_set_with_cas
@@ -187,10 +182,6 @@ module Dalli
187
182
  end
188
183
 
189
184
  def full_response_from_buffer(tokens, body, resp_size)
190
- # A hit with no key can't be matched to a request: skip it (false
191
- # status) rather than yield it, or take it for the end of the pipeline
192
- return [false, resp_size] unless tokens.any? { |t| t.start_with?('k') }
193
-
194
185
  value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
195
186
  [tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value, resp_size]
196
187
  end
@@ -214,21 +205,15 @@ module Dalli
214
205
  tokens = header.split
215
206
  header_len = header.bytesize + TERMINATOR.length
216
207
 
217
- # The body len is removed from the tokens array. memcached also gives
218
- # the size positionally ("VA <size> ..."); use it when there's no s
219
- # flag rather than reading the value as replies.
220
- body_len = tokens.any? { |t| t.start_with?('s') } ? body_len_from_tokens(tokens) : positional_size(tokens)
221
-
222
- # A complete response with no body. Only the terminating noop's MN
223
- # ends the pipeline. Any other bodyless reply (CLIENT_ERROR,
224
- # SERVER_ERROR, ...) answers one key's request: report it with a
225
- # false status so the caller skips it and keeps reading. Treating it
226
- # as the end would leave the remaining replies on the connection for
227
- # later commands to read as their own. A hit on an empty value (VA 0)
228
- # still has a body -- just its terminator -- so it's parsed below.
229
- return [tokens.first == MN, header_len] if body_len.zero? && tokens.first != VA
230
-
231
- check_value_size!(body_len)
208
+ # The body len is removed from the tokens array
209
+ body_len = body_len_from_tokens(tokens)
210
+
211
+ # We have a complete response that has no body.
212
+ # This is either the response to the terminating
213
+ # noop or, if the status is not MN, an intermediate
214
+ # error response that needs to be discarded.
215
+ return [true, header_len] if body_len.zero?
216
+
232
217
  resp_size = header_len + body_len + TERMINATOR.length
233
218
  # The header is in the buffer, but the body is not. As we don't have
234
219
  # a complete response, don't advance the buffer
@@ -258,6 +243,14 @@ module Dalli
258
243
  value_from_tokens(tokens, 'c').to_i
259
244
  end
260
245
 
246
+ # Detects the X presence flag, set when an item has been marked stale by a
247
+ # prior `md key I`. Uses strict equality (Array#any? with a String pattern
248
+ # compares with ==) so a future value-bearing flag beginning with X cannot
249
+ # be mistaken for it.
250
+ def stale_from_tokens(tokens)
251
+ tokens.any?('X')
252
+ end
253
+
261
254
  def key_from_tokens(tokens)
262
255
  encoded_key = value_from_tokens(tokens, 'k')
263
256
  if tokens.delete('b')
@@ -282,6 +275,12 @@ module Dalli
282
275
  value_from_tokens(tokens, 'l').to_i
283
276
  end
284
277
 
278
+ # Returns seconds of TTL remaining; -1 when the item has no expiry.
279
+ # The t flag returns t<seconds>.
280
+ def ttl_remaining_from_tokens(tokens)
281
+ value_from_tokens(tokens, 't').to_i
282
+ end
283
+
285
284
  def body_len_from_tokens(tokens)
286
285
  value_from_tokens(tokens, 's').to_i
287
286
  end
@@ -297,19 +296,6 @@ module Dalli
297
296
  end
298
297
  end
299
298
 
300
- # A pipelined reply claiming an impossible size would otherwise have the
301
- # buffer wait for (and accumulate) that many bytes
302
- # The size given right after "VA", or 0 when it isn't a number
303
- def positional_size(tokens)
304
- tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
305
- end
306
-
307
- def check_value_size!(size)
308
- return if size.between?(0, MAX_VALUE_BYTES)
309
-
310
- raise Dalli::DalliError, "Reply value size #{size} is out of range"
311
- end
312
-
313
299
  def read_line
314
300
  @io_source.read_line&.chomp!(TERMINATOR)
315
301
  end
@@ -320,9 +306,6 @@ module Dalli
320
306
  end
321
307
 
322
308
  def read_data(data_size)
323
- # Checked before the terminator is added: -1 or -2 would otherwise
324
- # read 1 or 0 bytes and leave the connection out of step
325
- check_value_size!(data_size)
326
309
  @io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
327
310
  end
328
311
  end
@@ -15,17 +15,11 @@ module Dalli
15
15
  compress: true,
16
16
  compressor: ::Dalli::Compressor,
17
17
  # min byte size to attempt compression
18
- compression_min_size: 4 * 1024, # 4K
19
- # max size a stored value may decompress to (nil: no limit). Guards
20
- # against a small compressed value expanding into gigabytes on read.
21
- decompressed_max_bytes: 128 * 1024 * 1024 # 128 MiB
18
+ compression_min_size: 4 * 1024 # 4K
22
19
  }.freeze
23
20
 
24
21
  OPTIONS = DEFAULTS.keys.freeze
25
22
 
26
- KEYWORD_PARAMETER_TYPES = %i[key keyreq].freeze
27
- private_constant :KEYWORD_PARAMETER_TYPES
28
-
29
23
  def initialize(client_options)
30
24
  @compression_options =
31
25
  DEFAULTS.merge(client_options.slice(*OPTIONS))
@@ -40,16 +34,8 @@ module Dalli
40
34
  end
41
35
 
42
36
  def retrieve(value, bitflags)
43
- return value unless bitflags.anybits?(Flags::COMPRESSED)
44
-
45
- max_bytes = @compression_options[:decompressed_max_bytes]
46
- # Custom compressors that only define decompress(data) keep working,
47
- # without the limit
48
- if max_bytes && compressor_accepts_max_bytes?
49
- compressor.decompress(value, max_bytes: max_bytes)
50
- else
51
- compressor.decompress(value)
52
- end
37
+ compressed = bitflags.anybits?(Flags::COMPRESSED)
38
+ compressed ? compressor.decompress(value) : value
53
39
 
54
40
  # TODO: We likely want to move this rescue into the Dalli::Compressor / Dalli::GzipCompressor
55
41
  # itself, since not all compressors necessarily use Zlib. For now keep it here, so the behavior
@@ -58,19 +44,6 @@ module Dalli
58
44
  raise UnmarshalError, "Unable to uncompress value: #{$ERROR_INFO.message}"
59
45
  end
60
46
 
61
- def compressor_accepts_max_bytes?
62
- return @compressor_accepts_max_bytes if defined?(@compressor_accepts_max_bytes)
63
-
64
- @compressor_accepts_max_bytes =
65
- begin
66
- compressor.method(:decompress).parameters.any? do |type, name|
67
- name == :max_bytes && KEYWORD_PARAMETER_TYPES.include?(type)
68
- end
69
- rescue NameError # an object without a reflectable decompress method
70
- false
71
- end
72
- end
73
-
74
47
  def compress_by_default?
75
48
  @compression_options[:compress]
76
49
  end
@@ -100,7 +100,7 @@ module Dalli
100
100
  Dalli.logger.warn 'SECURITY WARNING: Dalli is using Marshal for serialization. ' \
101
101
  'Marshal can execute arbitrary code during deserialization. ' \
102
102
  'If your memcached server could be compromised, consider using ' \
103
- 'a safer serializer: Dalli::Client.new(servers, serializer: Dalli::JSONSerializer)'
103
+ 'a safer serializer like JSON: Dalli::Client.new(servers, serializer: JSON)'
104
104
  @@marshal_warning_logged = true # rubocop:disable Style/ClassVars
105
105
  end
106
106
  end
data/lib/dalli/socket.rb CHANGED
@@ -124,7 +124,7 @@ module Dalli
124
124
  # Returns true for an unmodified TCPSocket on Ruby 3.0+, or for resolv-replace >= 0.2.0
125
125
  # which forwards keyword arguments through its patch.
126
126
  # Returns false when monkey-patched by gems like socksify or resolv-replace < 0.2.0.
127
- # rubocop:disable-next ThreadSafety/ClassInstanceVariable
127
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
128
128
  def self.supports_connect_timeout?
129
129
  return @supports_connect_timeout if defined?(@supports_connect_timeout)
130
130
 
@@ -135,6 +135,7 @@ module Dalli
135
135
  end
136
136
  end
137
137
  end
138
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
138
139
 
139
140
  def self.create_socket_with_timeout(host, port, options)
140
141
  if supports_connect_timeout?
@@ -191,13 +192,14 @@ module Dalli
191
192
 
192
193
  # Detect and cache the correct pack format for struct timeval on this platform.
193
194
  # Different architectures have different sizes for time_t and suseconds_t.
194
- # rubocop:disable-next ThreadSafety/ClassInstanceVariable
195
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
195
196
  def self.timeval_pack_format(sock)
196
197
  @timeval_pack_format ||= begin
197
198
  expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
198
199
  TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
199
200
  end
200
201
  end
202
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
201
203
 
202
204
  def self.pack_timeval(sock, seconds, microseconds)
203
205
  [seconds, microseconds].pack(timeval_pack_format(sock))
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '5.0.10'
4
+ VERSION = '5.1.0'
5
5
 
6
- MIN_SUPPORTED_MEMCACHED_VERSION = '1.6'
6
+ MIN_SUPPORTED_MEMCACHED_VERSION = '1.6.27'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -62,7 +62,6 @@ require_relative 'dalli/version'
62
62
  require_relative 'dalli/instrumentation'
63
63
 
64
64
  require_relative 'dalli/flags'
65
- require_relative 'dalli/json_serializer'
66
65
  require_relative 'dalli/compressor'
67
66
  require_relative 'dalli/client'
68
67
  require_relative 'dalli/key_manager'
@@ -16,10 +16,11 @@ module Rack
16
16
  attr_reader :data
17
17
 
18
18
  # Don't freeze this until we fix the specs/implementation
19
- # rubocop:disable-next Style/MutableConstant
19
+ # rubocop:disable Style/MutableConstant
20
20
  DEFAULT_DALLI_OPTIONS = {
21
21
  namespace: 'rack:session'
22
22
  }
23
+ # rubocop:enable Style/MutableConstant
23
24
 
24
25
  # Brings in a new Rack::Session::Dalli middleware with the given
25
26
  # `:memcache_server`. The server is either a hostname, or a
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dalli
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.10
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -42,7 +42,6 @@ files:
42
42
  - lib/dalli/compressor.rb
43
43
  - lib/dalli/flags.rb
44
44
  - lib/dalli/instrumentation.rb
45
- - lib/dalli/json_serializer.rb
46
45
  - lib/dalli/key_manager.rb
47
46
  - lib/dalli/options.rb
48
47
  - lib/dalli/pipelined_deleter.rb
@@ -88,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
87
  - !ruby/object:Gem::Version
89
88
  version: '0'
90
89
  requirements: []
91
- rubygems_version: 4.0.21
90
+ rubygems_version: 4.0.18
92
91
  specification_version: 4
93
92
  summary: High performance memcached client for Ruby
94
93
  test_files: []
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- module Dalli
6
- ##
7
- # A serializer that stores values as JSON and never creates Ruby objects
8
- # from what it reads.
9
- #
10
- # Passing the JSON module itself as the serializer (serializer: JSON) reads
11
- # values back with JSON.load, which on json gem versions before 3.0 honors
12
- # "json_class" and instantiates other classes when the json gem's additions
13
- # are loaded. This serializer reads with JSON.parse, so a value written to
14
- # memcached by someone else can only come back as plain hashes, arrays,
15
- # strings, numbers, booleans and nil.
16
- #
17
- # Dalli::Client.new(servers, serializer: Dalli::JSONSerializer)
18
- ##
19
- module JSONSerializer
20
- def self.dump(value)
21
- JSON.generate(value)
22
- end
23
-
24
- def self.load(data)
25
- JSON.parse(data)
26
- end
27
- end
28
- end