dalli 5.1.4 → 5.2.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.
@@ -24,9 +24,10 @@ module Dalli
24
24
  SERVER_ERROR = 'SERVER_ERROR'
25
25
 
26
26
  VA_PREFIX = 'VA '
27
- # memcached can't store an item larger than 1 GiB (its -I maximum)
28
- MAX_VALUE_BYTES = 1024 * 1024 * 1024
27
+ HD_PREFIX = 'HD '
29
28
  FLAGS_TOKEN_PREFIX = ' f'
29
+ CAS_TOKEN_PREFIX = ' c'
30
+ KEY_TOKEN_PREFIX = ' k'
30
31
  BYTE_B = 'b'.ord
31
32
  BYTE_C = 'c'.ord
32
33
  BYTE_F = 'f'.ord
@@ -48,15 +49,12 @@ module Dalli
48
49
  @value_marshaller = value_marshaller
49
50
  end
50
51
 
51
- # raw: true (a request made with raw: true) returns the value as stored,
52
- # ignoring any flags in the reply. The request didn't ask for flags, so
53
- # a reply carrying them anyway must not get the value deserialized.
54
- def meta_get_with_value(cache_nils: false, raw: false)
52
+ def meta_get_with_value(cache_nils: false)
55
53
  line = read_line
56
54
  # A hit ("VA <size> f<flags>") is parsed in place rather than split
57
55
  # into tokens, which saves several allocations on the hottest path.
58
56
  if line&.start_with?(VA_PREFIX)
59
- return retrieve(read_data(size_from_va_line(line)), raw ? 0 : bitflags_from_va_line(line), raw)
57
+ return @value_marshaller.retrieve(read_data(size_from_va_line(line)), bitflags_from_va_line(line))
60
58
  end
61
59
 
62
60
  tokens = line&.split || []
@@ -64,7 +62,7 @@ module Dalli
64
62
  when EN
65
63
  cache_nils ? ::Dalli::NOT_FOUND : nil
66
64
  when VA # only reached for an unusually formatted hit line
67
- retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
65
+ @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
68
66
  when HD
69
67
  true
70
68
  else
@@ -72,14 +70,14 @@ module Dalli
72
70
  end
73
71
  end
74
72
 
75
- def meta_get_with_value_and_cas(raw: false)
73
+ def meta_get_with_value_and_cas
76
74
  tokens = error_on_unexpected!(T_VA_EN_HD)
77
75
  return [nil, 0] if tokens.first == EN
78
76
 
79
77
  cas = cas_from_tokens(tokens)
80
78
  return [nil, cas] unless tokens.first == VA
81
79
 
82
- [retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw), cas]
80
+ [@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
83
81
  end
84
82
 
85
83
  def meta_get_without_value
@@ -99,13 +97,13 @@ module Dalli
99
97
  # Used by meta_get for comprehensive metadata retrieval.
100
98
  # Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
101
99
  def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
102
- return_ttl_remaining: false, raw: false)
100
+ return_ttl_remaining: false)
103
101
  tokens = error_on_unexpected!(T_VA_EN_HD)
104
102
  result = build_metadata_result(tokens)
105
103
  result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
106
104
  result[:last_access] = last_access_from_tokens(tokens) if return_last_access
107
105
  result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
108
- result[:value] = parse_value_from_tokens(tokens, cache_nils, raw)
106
+ result[:value] = parse_value_from_tokens(tokens, cache_nils)
109
107
  result
110
108
  end
111
109
 
@@ -122,15 +120,21 @@ module Dalli
122
120
  }
123
121
  end
124
122
 
125
- def parse_value_from_tokens(tokens, cache_nils, raw = false) # rubocop:disable Style/OptionalBooleanParameter
123
+ def parse_value_from_tokens(tokens, cache_nils)
126
124
  return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
127
125
  return unless tokens.first == VA
128
126
 
129
- retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
127
+ @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
130
128
  end
131
129
 
132
130
  def meta_set_with_cas
133
- tokens = error_on_unexpected!(T_HD_NS_NF_EX)
131
+ line = read_line
132
+ # A stored item ("HD c<cas>") is parsed in place rather than split into tokens
133
+ # Search from just after "HD": the " c" token starts at the space that follows it
134
+ return flag_from_line(line, CAS_TOKEN_PREFIX, HD.bytesize) if line&.start_with?(HD_PREFIX)
135
+
136
+ tokens = line&.split || []
137
+ raise_unexpected!(tokens) unless T_HD_NS_NF_EX.include?(tokens.first)
134
138
  return false unless tokens.first == HD
135
139
 
136
140
  cas_from_tokens(tokens)
@@ -144,7 +148,13 @@ module Dalli
144
148
  end
145
149
 
146
150
  def meta_delete
147
- tokens = error_on_unexpected!(T_HD_NF_EX)
151
+ line = read_line
152
+ # The two common replies, compared as whole lines instead of split into tokens
153
+ return true if line == HD
154
+ return false if line == NF
155
+
156
+ tokens = line&.split || []
157
+ raise_unexpected!(tokens) unless T_HD_NF_EX.include?(tokens.first)
148
158
  tokens.first == HD
149
159
  end
150
160
 
@@ -217,13 +227,16 @@ module Dalli
217
227
  # The remaining three values in the array are the ResponseHeader,
218
228
  # key, and value.
219
229
  ##
220
- # raw: true returns values as stored, ignoring flags in the replies;
221
- # see meta_get_with_value.
222
- def getk_response_from_buffer(buf, offset = 0, raw: false)
230
+ def getk_response_from_buffer(buf, offset = 0)
223
231
  # Find the header terminator starting from offset
224
232
  term_idx = buf.byteindex(TERMINATOR, offset)
225
233
  return [0] unless term_idx
226
234
 
235
+ if buf.byteslice(offset, VA_PREFIX.bytesize) == VA_PREFIX
236
+ response = va_response_from_buffer(buf, offset, term_idx)
237
+ return response if response
238
+ end
239
+
227
240
  header = buf.byteslice(offset, term_idx - offset)
228
241
  tokens = header.split
229
242
  header_len = header.bytesize + TERMINATOR.length
@@ -241,20 +254,16 @@ module Dalli
241
254
  when BYTE_B then base64 ||= token == 'b'
242
255
  end
243
256
  end
244
- # memcached also gives the size positionally ("VA <size> ..."); use it
245
- # when there's no s flag rather than reading the value as replies
246
- body_len = size ? flag_int(size, 's') : positional_size(tokens)
247
-
248
- # A complete response with no body. Only the terminating noop's MN
249
- # ends the pipeline. Any other bodyless reply (CLIENT_ERROR,
250
- # SERVER_ERROR, ...) answers one key's request: report it with a
251
- # false status so the caller skips it and keeps reading. Treating it
252
- # as the end would leave the remaining replies on the connection for
253
- # later commands to read as their own. A hit on an empty value (VA 0)
254
- # still has a body -- just its terminator -- so it's parsed below.
255
- return [tokens.first == MN, header_len] if body_len.zero? && tokens.first != VA
256
-
257
- check_value_size!(body_len)
257
+ body_len = flag_int(size, 's')
258
+
259
+ # We have a complete response that has no body.
260
+ # This is either the response to the terminating
261
+ # noop or, if the status is not MN, an intermediate
262
+ # error response that needs to be discarded. A hit
263
+ # on an empty value (VA 0) still has a body -- just
264
+ # its terminator -- so it's parsed below as a value.
265
+ return [true, header_len] if body_len.zero? && tokens.first != VA
266
+
258
267
  resp_size = header_len + body_len + TERMINATOR.length
259
268
  # The header is in the buffer, but the body is not. As we don't have
260
269
  # a complete response, don't advance the buffer
@@ -263,19 +272,47 @@ module Dalli
263
272
  # The full response is in our buffer, so parse it and return
264
273
  # the values
265
274
  body = buf.byteslice(offset + header_len, body_len)
266
- value = retrieve(body, raw ? 0 : flag_int(bitflags, 'f'), raw)
267
- # A hit with no key can't be matched to a request: skip it (false
268
- # status) rather than yield it, or take it for the end of the pipeline
269
- return [false, resp_size] unless key
270
-
271
- key = key.delete_prefix!('k')
275
+ value = @value_marshaller.retrieve(body, flag_int(bitflags, 'f'))
276
+ key = key ? key.delete_prefix!('k') : 0
272
277
  key = KeyRegularizer.decode(key) if base64
273
278
  [tokens.first == VA, flag_int(cas, 'c'), key, value, resp_size]
274
279
  end
275
280
 
276
- # Values from a raw request are returned as stored
277
- def retrieve(value, bitflags, raw)
278
- raw ? value : @value_marshaller.retrieve(value, bitflags)
281
+ # getk_response_from_buffer for a "VA ..." header, read in place
282
+ # without allocating the header, its tokens or the flag strings.
283
+ # Gives the same result as the token path. Returns nil to fall back to
284
+ # it when the header has no s flag or a zero size.
285
+ def va_response_from_buffer(buf, offset, term_idx)
286
+ size = bitflags = cas = key = nil
287
+ base64 = false
288
+ pos = buf.byteindex(' ', offset + VA_PREFIX.bytesize)
289
+ while pos && pos < term_idx
290
+ start = pos + 1
291
+ pos = buf.byteindex(' ', start)
292
+ stop = pos && pos < term_idx ? pos : term_idx
293
+ case buf.getbyte(start)
294
+ when BYTE_S then size ||= flag_int_at(buf, start, stop)
295
+ when BYTE_F then bitflags ||= flag_int_at(buf, start, stop)
296
+ when BYTE_C then cas ||= flag_int_at(buf, start, stop)
297
+ when BYTE_K then key ||= buf.byteslice(start + 1, stop - start - 1)
298
+ when BYTE_B then base64 ||= stop == start + 1
299
+ end
300
+ end
301
+ return nil if size.nil? || size.zero?
302
+
303
+ header_len = term_idx - offset + TERMINATOR.length
304
+ resp_size = header_len + size + TERMINATOR.length
305
+ return [0] unless buf.bytesize >= offset + resp_size
306
+
307
+ value = @value_marshaller.retrieve(buf.byteslice(offset + header_len, size), bitflags || 0)
308
+ key = KeyRegularizer.decode(key) if base64 && key
309
+ [true, cas || 0, key || 0, value, resp_size]
310
+ end
311
+
312
+ # Integer value of a flag's token in buf, after its one-byte prefix,
313
+ # as flag_int would read it
314
+ def flag_int_at(buf, start, stop)
315
+ buf.byteslice(start + 1, stop - start - 1).to_i
279
316
  end
280
317
 
281
318
  # Integer value of a flag token such as "f123", or 0 when absent.
@@ -309,8 +346,29 @@ module Dalli
309
346
  end
310
347
 
311
348
  def bitflags_from_va_line(line)
312
- idx = line.index(FLAGS_TOKEN_PREFIX, VA_PREFIX.bytesize)
313
- idx ? line.byteslice(idx + FLAGS_TOKEN_PREFIX.bytesize, line.bytesize).to_i : 0
349
+ flag_from_line(line, FLAGS_TOKEN_PREFIX, VA_PREFIX.bytesize)
350
+ end
351
+
352
+ # The key from a VA line's k flag, decoded when the line has the b flag,
353
+ # or nil when there is no k flag
354
+ def key_from_va_line(line)
355
+ idx = line.index(KEY_TOKEN_PREFIX, VA_PREFIX.bytesize)
356
+ return unless idx
357
+
358
+ start = idx + KEY_TOKEN_PREFIX.bytesize
359
+ stop = line.index(' ', start) || line.index(TERMINATOR, start) || line.bytesize
360
+ key = line.byteslice(start, stop - start)
361
+ base64_flag_in_line?(line) ? KeyRegularizer.decode(key) : key
362
+ end
363
+
364
+ def base64_flag_in_line?(line)
365
+ line.include?(' b ') || line.end_with?(" b#{TERMINATOR}", ' b')
366
+ end
367
+
368
+ # Integer value of the first " <flag><digits>" token at or after start, or 0
369
+ def flag_from_line(line, token_prefix, start)
370
+ idx = line.index(token_prefix, start)
371
+ idx ? line.byteslice(idx + token_prefix.bytesize, line.bytesize).to_i : 0
314
372
  end
315
373
 
316
374
  def cas_from_tokens(tokens)
@@ -366,19 +424,6 @@ module Dalli
366
424
  end
367
425
  end
368
426
 
369
- # A pipelined reply claiming an impossible size would otherwise have the
370
- # buffer wait for (and accumulate) that many bytes
371
- # The size given right after "VA", or 0 when it isn't a number
372
- def positional_size(tokens)
373
- tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
374
- end
375
-
376
- def check_value_size!(size)
377
- return if size.between?(0, MAX_VALUE_BYTES)
378
-
379
- raise Dalli::DalliError, "Reply value size #{size} is out of range"
380
- end
381
-
382
427
  def read_line
383
428
  @io_source.read_line&.chomp!(TERMINATOR)
384
429
  end
@@ -389,9 +434,6 @@ module Dalli
389
434
  end
390
435
 
391
436
  def read_data(data_size)
392
- # Checked before the terminator is added: -1 or -2 would otherwise
393
- # read 1 or 0 bytes and leave the connection out of step
394
- check_value_size!(data_size)
395
437
  @io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
396
438
  end
397
439
  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/ring.rb CHANGED
@@ -36,9 +36,10 @@ module Dalli
36
36
 
37
37
  def continuum=(entries)
38
38
  @continuum = entries
39
- # Plain integers, so the binary search in server_for_hash_key needn't
40
- # call Entry#value at each step
39
+ # Plain integers, so the lookup in server_for_hash_key needn't call
40
+ # Entry#value at each step
41
41
  @continuum_values = entries&.map(&:value)
42
+ build_buckets
42
43
  end
43
44
 
44
45
  # alive_cache (optional) remembers each server's alive? result, so a
@@ -55,15 +56,17 @@ module Dalli
55
56
  raise Dalli::RingError, 'No server available'
56
57
  end
57
58
 
59
+ # Tries the key's own server first, then (with failover) up to 19
60
+ # rehashes of "<try><key>". The first try is outside the loop because it
61
+ # is almost always the one that's used.
58
62
  def server_from_continuum(key, alive_cache = nil)
59
- hkey = hash_for(key)
60
- 20.times do |try|
61
- server = server_for_hash_key(hkey)
63
+ server = server_for_hash_key(hash_for(key))
64
+ return server if server_alive?(server, alive_cache)
65
+ return nil unless @failover
62
66
 
67
+ 19.times do |try|
68
+ server = server_for_hash_key(hash_for("#{try}#{key}"))
63
69
  return server if server_alive?(server, alive_cache)
64
- break unless @failover
65
-
66
- hkey = hash_for("#{try}#{key}")
67
70
  end
68
71
  nil
69
72
  end
@@ -87,8 +90,13 @@ module Dalli
87
90
  end
88
91
  end
89
92
 
93
+ # Drains the replies left by quiet requests. Only servers that were sent a
94
+ # quiet request need it, so the others (and servers never connected) are
95
+ # skipped rather than each costing a noop round trip.
90
96
  def pipeline_consume_and_ignore_responses
91
97
  @servers.each do |s|
98
+ next unless s.quiet_responses_pending?
99
+
92
100
  s.request(:noop)
93
101
  rescue Dalli::NetworkError
94
102
  # Ignore this error, as it indicates the socket is unavailable
@@ -121,22 +129,41 @@ module Dalli
121
129
  def server_alive?(server, alive_cache)
122
130
  return server.alive? unless alive_cache
123
131
 
124
- alive_cache.fetch(server) { alive_cache[server] = server.alive? }
132
+ alive = alive_cache[server]
133
+ alive.nil? ? (alive_cache[server] = server.alive?) : alive
125
134
  end
126
135
 
127
136
  def entry_count_for(server, total_servers, total_weight)
128
137
  ((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
129
138
  end
130
139
 
131
- def server_for_hash_key(hash_key)
132
- # Find the closest index in the Ring with value <= the given value
133
- entryidx = @continuum_values.bsearch_index { |value| value > hash_key }
134
- if entryidx.nil?
135
- entryidx = @continuum.size - 1
136
- else
137
- entryidx -= 1
140
+ # Hash values are 32 bits. Splitting that range into about one bucket per
141
+ # continuum entry, and noting the first entry at or after each bucket's
142
+ # start, lets server_for_hash_key skip the binary search: it starts at the
143
+ # key's bucket and steps over the few entries before the key's hash.
144
+ def build_buckets
145
+ return unless @continuum_values
146
+
147
+ bits = [@continuum_values.size.bit_length, 1].max
148
+ @bucket_shift = 32 - bits
149
+ @bucket_first = Array.new(1 << bits)
150
+ idx = 0
151
+ size = @continuum_values.size
152
+ @bucket_first.each_index do |bucket|
153
+ floor = bucket << @bucket_shift
154
+ idx += 1 while idx < size && @continuum_values[idx] < floor
155
+ @bucket_first[bucket] = idx
138
156
  end
139
- @continuum[entryidx].server
157
+ end
158
+
159
+ def server_for_hash_key(hash_key)
160
+ # Find the last entry with value <= hash_key, wrapping around to the
161
+ # last entry when every value is greater (idx - 1 is then -1)
162
+ idx = @bucket_first[hash_key >> @bucket_shift]
163
+ values = @continuum_values
164
+ size = values.size
165
+ idx += 1 while idx < size && values[idx] <= hash_key
166
+ @continuum[idx - 1].server
140
167
  end
141
168
 
142
169
  def build_continuum(servers)
data/lib/dalli/socket.rb CHANGED
@@ -12,22 +12,29 @@ module Dalli
12
12
  # Common methods for all socket implementations.
13
13
  ##
14
14
  module InstanceMethods
15
+ READ_CHUNK_SIZE = 8196
16
+
17
+ # Reads whatever is available without blocking. A read that returns less
18
+ # than a full chunk means the socket was drained, so we stop there rather
19
+ # than spend another read_nonblock call just to get :wait_readable back.
20
+ # Callers keep waiting on IO.select for anything that arrives later.
15
21
  def read_available(reusable_buffer = nil)
16
22
  if reusable_buffer
17
- value = read_nonblock(8196, reusable_buffer, exception: false)
23
+ value = read_nonblock(READ_CHUNK_SIZE, reusable_buffer, exception: false)
18
24
  case value
19
25
  when :wait_writable, :wait_readable
20
26
  return reusable_buffer.clear
21
27
  when nil
22
28
  raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}"
23
29
  end
30
+ return value if drained_after?(value)
24
31
  else
25
32
  value = ''.b
26
33
  end
27
34
 
28
35
  buffer = ''.b
29
36
  loop do
30
- result = read_nonblock(8196, buffer, exception: false)
37
+ result = read_nonblock(READ_CHUNK_SIZE, buffer, exception: false)
31
38
  case result
32
39
  when :wait_writable, :wait_readable
33
40
  buffer.clear
@@ -36,10 +43,21 @@ module Dalli
36
43
  raise Errno::ECONNRESET, "Connection reset: #{logged_options.inspect}"
37
44
  else
38
45
  value << result
46
+ return value if drained_after?(result)
39
47
  end
40
48
  end
41
49
  end
42
50
 
51
+ # True when a read came back short and nothing is left buffered in-process.
52
+ def drained_after?(chunk)
53
+ chunk.bytesize < READ_CHUNK_SIZE && !buffered_data?
54
+ end
55
+
56
+ # Plain sockets keep nothing buffered in-process. See SSLSocket.
57
+ def buffered_data?
58
+ false
59
+ end
60
+
43
61
  FILTERED_OUT_OPTIONS = %i[username password].freeze
44
62
  def logged_options
45
63
  options.except(*FILTERED_OUT_OPTIONS)
@@ -83,6 +101,13 @@ module Dalli
83
101
  io.options
84
102
  end
85
103
 
104
+ # OpenSSL can hold decrypted bytes that IO.select won't report as readable.
105
+ # With OpenSSL's default (no read-ahead) a short read has already used up
106
+ # the current record, so this is normally 0; it guards read-ahead setups.
107
+ def buffered_data?
108
+ pending.positive?
109
+ end
110
+
86
111
  unless method_defined?(:wait_readable)
87
112
  def wait_readable(timeout = nil)
88
113
  to_io.wait_readable(timeout)
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.1.4'
4
+ VERSION = '5.2.0'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.6.27'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -62,8 +62,8 @@ 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'
66
+ require_relative 'dalli/cache_result'
67
67
  require_relative 'dalli/client'
68
68
  require_relative 'dalli/key_manager'
69
69
  require_relative 'dalli/pipelined_getter'
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.1.4
4
+ version: 5.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -37,12 +37,12 @@ files:
37
37
  - LICENSE
38
38
  - README.md
39
39
  - lib/dalli.rb
40
+ - lib/dalli/cache_result.rb
40
41
  - lib/dalli/cas/client.rb
41
42
  - lib/dalli/client.rb
42
43
  - lib/dalli/compressor.rb
43
44
  - lib/dalli/flags.rb
44
45
  - lib/dalli/instrumentation.rb
45
- - lib/dalli/json_serializer.rb
46
46
  - lib/dalli/key_manager.rb
47
47
  - lib/dalli/options.rb
48
48
  - lib/dalli/pipelined_deleter.rb
@@ -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