dalli 5.1.1 → 5.2.2

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,7 +24,12 @@ 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
29
+ HD_PREFIX = 'HD '
27
30
  FLAGS_TOKEN_PREFIX = ' f'
31
+ CAS_TOKEN_PREFIX = ' c'
32
+ KEY_TOKEN_PREFIX = ' k'
28
33
  BYTE_B = 'b'.ord
29
34
  BYTE_C = 'c'.ord
30
35
  BYTE_F = 'f'.ord
@@ -46,12 +51,15 @@ module Dalli
46
51
  @value_marshaller = value_marshaller
47
52
  end
48
53
 
49
- def meta_get_with_value(cache_nils: false)
54
+ # raw: true (a request made with raw: true) returns the value as stored,
55
+ # ignoring any flags in the reply. The request didn't ask for flags, so
56
+ # a reply carrying them anyway must not get the value deserialized.
57
+ def meta_get_with_value(cache_nils: false, raw: false)
50
58
  line = read_line
51
59
  # A hit ("VA <size> f<flags>") is parsed in place rather than split
52
60
  # into tokens, which saves several allocations on the hottest path.
53
61
  if line&.start_with?(VA_PREFIX)
54
- return @value_marshaller.retrieve(read_data(size_from_va_line(line)), bitflags_from_va_line(line))
62
+ return retrieve(read_data(size_from_va_line(line)), raw ? 0 : bitflags_from_va_line(line), raw)
55
63
  end
56
64
 
57
65
  tokens = line&.split || []
@@ -59,7 +67,7 @@ module Dalli
59
67
  when EN
60
68
  cache_nils ? ::Dalli::NOT_FOUND : nil
61
69
  when VA # only reached for an unusually formatted hit line
62
- @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
70
+ retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
63
71
  when HD
64
72
  true
65
73
  else
@@ -67,14 +75,14 @@ module Dalli
67
75
  end
68
76
  end
69
77
 
70
- def meta_get_with_value_and_cas
78
+ def meta_get_with_value_and_cas(raw: false)
71
79
  tokens = error_on_unexpected!(T_VA_EN_HD)
72
80
  return [nil, 0] if tokens.first == EN
73
81
 
74
82
  cas = cas_from_tokens(tokens)
75
83
  return [nil, cas] unless tokens.first == VA
76
84
 
77
- [@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
85
+ [retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw), cas]
78
86
  end
79
87
 
80
88
  def meta_get_without_value
@@ -94,13 +102,13 @@ module Dalli
94
102
  # Used by meta_get for comprehensive metadata retrieval.
95
103
  # Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
96
104
  def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
97
- return_ttl_remaining: false)
105
+ return_ttl_remaining: false, raw: false)
98
106
  tokens = error_on_unexpected!(T_VA_EN_HD)
99
107
  result = build_metadata_result(tokens)
100
108
  result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
101
109
  result[:last_access] = last_access_from_tokens(tokens) if return_last_access
102
110
  result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
103
- result[:value] = parse_value_from_tokens(tokens, cache_nils)
111
+ result[:value] = parse_value_from_tokens(tokens, cache_nils, raw)
104
112
  result
105
113
  end
106
114
 
@@ -117,15 +125,21 @@ module Dalli
117
125
  }
118
126
  end
119
127
 
120
- def parse_value_from_tokens(tokens, cache_nils)
128
+ def parse_value_from_tokens(tokens, cache_nils, raw = false) # rubocop:disable Style/OptionalBooleanParameter
121
129
  return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
122
130
  return unless tokens.first == VA
123
131
 
124
- @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
132
+ retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
125
133
  end
126
134
 
127
135
  def meta_set_with_cas
128
- tokens = error_on_unexpected!(T_HD_NS_NF_EX)
136
+ line = read_line
137
+ # A stored item ("HD c<cas>") is parsed in place rather than split into tokens
138
+ # Search from just after "HD": the " c" token starts at the space that follows it
139
+ return flag_from_line(line, CAS_TOKEN_PREFIX, HD.bytesize) if line&.start_with?(HD_PREFIX)
140
+
141
+ tokens = line&.split || []
142
+ raise_unexpected!(tokens) unless T_HD_NS_NF_EX.include?(tokens.first)
129
143
  return false unless tokens.first == HD
130
144
 
131
145
  cas_from_tokens(tokens)
@@ -139,7 +153,13 @@ module Dalli
139
153
  end
140
154
 
141
155
  def meta_delete
142
- tokens = error_on_unexpected!(T_HD_NF_EX)
156
+ line = read_line
157
+ # The two common replies, compared as whole lines instead of split into tokens
158
+ return true if line == HD
159
+ return false if line == NF
160
+
161
+ tokens = line&.split || []
162
+ raise_unexpected!(tokens) unless T_HD_NF_EX.include?(tokens.first)
143
163
  tokens.first == HD
144
164
  end
145
165
 
@@ -212,11 +232,18 @@ module Dalli
212
232
  # The remaining three values in the array are the ResponseHeader,
213
233
  # key, and value.
214
234
  ##
215
- def getk_response_from_buffer(buf, offset = 0)
235
+ # raw: true returns values as stored, ignoring flags in the replies;
236
+ # see meta_get_with_value.
237
+ def getk_response_from_buffer(buf, offset = 0, raw: false)
216
238
  # Find the header terminator starting from offset
217
239
  term_idx = buf.byteindex(TERMINATOR, offset)
218
240
  return [0] unless term_idx
219
241
 
242
+ if buf.byteslice(offset, VA_PREFIX.bytesize) == VA_PREFIX
243
+ response = va_response_from_buffer(buf, offset, term_idx, raw)
244
+ return response if response
245
+ end
246
+
220
247
  header = buf.byteslice(offset, term_idx - offset)
221
248
  tokens = header.split
222
249
  header_len = header.bytesize + TERMINATOR.length
@@ -234,14 +261,20 @@ module Dalli
234
261
  when BYTE_B then base64 ||= token == 'b'
235
262
  end
236
263
  end
237
- body_len = flag_int(size, 's')
238
-
239
- # We have a complete response that has no body.
240
- # This is either the response to the terminating
241
- # noop or, if the status is not MN, an intermediate
242
- # error response that needs to be discarded.
243
- return [true, header_len] if body_len.zero?
244
-
264
+ # memcached also gives the size positionally ("VA <size> ..."); use it
265
+ # when there's no s flag rather than reading the value as replies
266
+ body_len = size ? flag_int(size, 's') : positional_size(tokens)
267
+
268
+ # A complete response with no body. Only the terminating noop's MN
269
+ # ends the pipeline. Any other bodyless reply (CLIENT_ERROR,
270
+ # SERVER_ERROR, ...) answers one key's request: report it with a
271
+ # false status so the caller skips it and keeps reading. Treating it
272
+ # as the end would leave the remaining replies on the connection for
273
+ # later commands to read as their own. A hit on an empty value (VA 0)
274
+ # still has a body -- just its terminator -- so it's parsed below.
275
+ return [tokens.first == MN, header_len] if body_len.zero? && tokens.first != VA
276
+
277
+ check_value_size!(body_len)
245
278
  resp_size = header_len + body_len + TERMINATOR.length
246
279
  # The header is in the buffer, but the body is not. As we don't have
247
280
  # a complete response, don't advance the buffer
@@ -250,12 +283,62 @@ module Dalli
250
283
  # The full response is in our buffer, so parse it and return
251
284
  # the values
252
285
  body = buf.byteslice(offset + header_len, body_len)
253
- value = @value_marshaller.retrieve(body, flag_int(bitflags, 'f'))
254
- key = key ? key.delete_prefix!('k') : 0
286
+ value = retrieve(body, raw ? 0 : flag_int(bitflags, 'f'), raw)
287
+ # A hit with no key can't be matched to a request: skip it (false
288
+ # status) rather than yield it, or take it for the end of the pipeline
289
+ return [false, resp_size] unless key
290
+
291
+ key = key.delete_prefix!('k')
255
292
  key = KeyRegularizer.decode(key) if base64
256
293
  [tokens.first == VA, flag_int(cas, 'c'), key, value, resp_size]
257
294
  end
258
295
 
296
+ # getk_response_from_buffer for a "VA ..." header, read in place
297
+ # without allocating the header, its tokens or the flag strings.
298
+ # Gives the same result as the token path. Returns nil to fall back to
299
+ # it when the header has no s flag or a zero size.
300
+ def va_response_from_buffer(buf, offset, term_idx, raw = false) # rubocop:disable Style/OptionalBooleanParameter
301
+ size = bitflags = cas = key = nil
302
+ base64 = false
303
+ pos = buf.byteindex(' ', offset + VA_PREFIX.bytesize)
304
+ while pos && pos < term_idx
305
+ start = pos + 1
306
+ pos = buf.byteindex(' ', start)
307
+ stop = pos && pos < term_idx ? pos : term_idx
308
+ case buf.getbyte(start)
309
+ when BYTE_S then size ||= flag_int_at(buf, start, stop)
310
+ when BYTE_F then bitflags ||= flag_int_at(buf, start, stop)
311
+ when BYTE_C then cas ||= flag_int_at(buf, start, stop)
312
+ when BYTE_K then key ||= buf.byteslice(start + 1, stop - start - 1)
313
+ when BYTE_B then base64 ||= stop == start + 1
314
+ end
315
+ end
316
+ return nil if size.nil? || size.zero?
317
+
318
+ check_value_size!(size)
319
+ header_len = term_idx - offset + TERMINATOR.length
320
+ resp_size = header_len + size + TERMINATOR.length
321
+ return [0] unless buf.bytesize >= offset + resp_size
322
+
323
+ value = retrieve(buf.byteslice(offset + header_len, size), raw ? 0 : bitflags || 0, raw)
324
+ # Skipped like the token path does; see getk_response_from_buffer
325
+ return [false, resp_size] unless key
326
+
327
+ key = KeyRegularizer.decode(key) if base64
328
+ [true, cas || 0, key, value, resp_size]
329
+ end
330
+
331
+ # Values from a raw request are returned as stored
332
+ def retrieve(value, bitflags, raw)
333
+ raw ? value : @value_marshaller.retrieve(value, bitflags)
334
+ end
335
+
336
+ # Integer value of a flag's token in buf, after its one-byte prefix,
337
+ # as flag_int would read it
338
+ def flag_int_at(buf, start, stop)
339
+ buf.byteslice(start + 1, stop - start - 1).to_i
340
+ end
341
+
259
342
  # Integer value of a flag token such as "f123", or 0 when absent.
260
343
  # Strips the prefix in place, like value_from_tokens.
261
344
  def flag_int(token, flag)
@@ -287,8 +370,29 @@ module Dalli
287
370
  end
288
371
 
289
372
  def bitflags_from_va_line(line)
290
- idx = line.index(FLAGS_TOKEN_PREFIX, VA_PREFIX.bytesize)
291
- idx ? line.byteslice(idx + FLAGS_TOKEN_PREFIX.bytesize, line.bytesize).to_i : 0
373
+ flag_from_line(line, FLAGS_TOKEN_PREFIX, VA_PREFIX.bytesize)
374
+ end
375
+
376
+ # The key from a VA line's k flag, decoded when the line has the b flag,
377
+ # or nil when there is no k flag
378
+ def key_from_va_line(line)
379
+ idx = line.index(KEY_TOKEN_PREFIX, VA_PREFIX.bytesize)
380
+ return unless idx
381
+
382
+ start = idx + KEY_TOKEN_PREFIX.bytesize
383
+ stop = line.index(' ', start) || line.index(TERMINATOR, start) || line.bytesize
384
+ key = line.byteslice(start, stop - start)
385
+ base64_flag_in_line?(line) ? KeyRegularizer.decode(key) : key
386
+ end
387
+
388
+ def base64_flag_in_line?(line)
389
+ line.include?(' b ') || line.end_with?(" b#{TERMINATOR}", ' b')
390
+ end
391
+
392
+ # Integer value of the first " <flag><digits>" token at or after start, or 0
393
+ def flag_from_line(line, token_prefix, start)
394
+ idx = line.index(token_prefix, start)
395
+ idx ? line.byteslice(idx + token_prefix.bytesize, line.bytesize).to_i : 0
292
396
  end
293
397
 
294
398
  def cas_from_tokens(tokens)
@@ -344,6 +448,19 @@ module Dalli
344
448
  end
345
449
  end
346
450
 
451
+ # A pipelined reply claiming an impossible size would otherwise have the
452
+ # buffer wait for (and accumulate) that many bytes
453
+ # The size given right after "VA", or 0 when it isn't a number
454
+ def positional_size(tokens)
455
+ tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
456
+ end
457
+
458
+ def check_value_size!(size)
459
+ return if size.between?(0, MAX_VALUE_BYTES)
460
+
461
+ raise Dalli::DalliError, "Reply value size #{size} is out of range"
462
+ end
463
+
347
464
  def read_line
348
465
  @io_source.read_line&.chomp!(TERMINATOR)
349
466
  end
@@ -354,6 +471,9 @@ module Dalli
354
471
  end
355
472
 
356
473
  def read_data(data_size)
474
+ # Checked before the terminator is added: -1 or -2 would otherwise
475
+ # read 1 or 0 bytes and leave the connection out of step
476
+ check_value_size!(data_size)
357
477
  @io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
358
478
  end
359
479
  end
@@ -15,11 +15,17 @@ 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
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
19
22
  }.freeze
20
23
 
21
24
  OPTIONS = DEFAULTS.keys.freeze
22
25
 
26
+ KEYWORD_PARAMETER_TYPES = %i[key keyreq].freeze
27
+ private_constant :KEYWORD_PARAMETER_TYPES
28
+
23
29
  def initialize(client_options)
24
30
  @compression_options =
25
31
  DEFAULTS.merge(client_options.slice(*OPTIONS))
@@ -34,8 +40,16 @@ module Dalli
34
40
  end
35
41
 
36
42
  def retrieve(value, bitflags)
37
- compressed = bitflags.anybits?(Flags::COMPRESSED)
38
- compressed ? compressor.decompress(value) : value
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
39
53
 
40
54
  # TODO: We likely want to move this rescue into the Dalli::Compressor / Dalli::GzipCompressor
41
55
  # itself, since not all compressors necessarily use Zlib. For now keep it here, so the behavior
@@ -44,6 +58,19 @@ module Dalli
44
58
  raise UnmarshalError, "Unable to uncompress value: #{$ERROR_INFO.message}"
45
59
  end
46
60
 
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
+
47
74
  def compress_by_default?
48
75
  @compression_options[:compress]
49
76
  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 like JSON: Dalli::Client.new(servers, serializer: JSON)'
103
+ 'a safer serializer: Dalli::Client.new(servers, serializer: Dalli::JSONSerializer)'
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.1'
4
+ VERSION = '5.2.2'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.6.27'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -62,7 +62,9 @@ require_relative 'dalli/version'
62
62
  require_relative 'dalli/instrumentation'
63
63
 
64
64
  require_relative 'dalli/flags'
65
+ require_relative 'dalli/json_serializer'
65
66
  require_relative 'dalli/compressor'
67
+ require_relative 'dalli/cache_result'
66
68
  require_relative 'dalli/client'
67
69
  require_relative 'dalli/key_manager'
68
70
  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.1
4
+ version: 5.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -37,11 +37,13 @@ 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
46
+ - lib/dalli/json_serializer.rb
45
47
  - lib/dalli/key_manager.rb
46
48
  - lib/dalli/options.rb
47
49
  - lib/dalli/pipelined_deleter.rb