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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +126 -0
- data/README.md +54 -1
- data/lib/dalli/cache_result.rb +65 -0
- data/lib/dalli/client.rb +96 -24
- data/lib/dalli/compressor.rb +53 -5
- data/lib/dalli/json_serializer.rb +28 -0
- data/lib/dalli/key_manager.rb +23 -2
- data/lib/dalli/protocol/base.rb +41 -8
- data/lib/dalli/protocol/connection_manager.rb +108 -9
- data/lib/dalli/protocol/meta.rb +60 -34
- data/lib/dalli/protocol/request_formatter.rb +30 -9
- data/lib/dalli/protocol/response_buffer.rb +4 -1
- data/lib/dalli/protocol/response_processor.rb +144 -24
- data/lib/dalli/protocol/value_compressor.rb +30 -3
- data/lib/dalli/protocol/value_serializer.rb +1 -1
- data/lib/dalli/ring.rb +44 -17
- data/lib/dalli/socket.rb +27 -2
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +2 -0
- metadata +3 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
|
|
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 =
|
|
254
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
|
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
|
|
40
|
-
#
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
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.
|
|
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
|