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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -60
- data/README.md +27 -5
- data/lib/dalli/cache_result.rb +65 -0
- data/lib/dalli/client.rb +84 -36
- data/lib/dalli/compressor.rb +5 -53
- data/lib/dalli/key_manager.rb +2 -23
- data/lib/dalli/protocol/base.rb +22 -27
- data/lib/dalli/protocol/connection_manager.rb +27 -90
- data/lib/dalli/protocol/meta.rb +55 -39
- data/lib/dalli/protocol/request_formatter.rb +27 -12
- data/lib/dalli/protocol/response_buffer.rb +1 -4
- data/lib/dalli/protocol/response_processor.rb +102 -60
- data/lib/dalli/protocol/value_compressor.rb +3 -30
- 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 +1 -1
- metadata +2 -2
- data/lib/dalli/json_serializer.rb +0 -28
|
@@ -24,9 +24,10 @@ module Dalli
|
|
|
24
24
|
SERVER_ERROR = 'SERVER_ERROR'
|
|
25
25
|
|
|
26
26
|
VA_PREFIX = 'VA '
|
|
27
|
-
|
|
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
|
-
|
|
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)),
|
|
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),
|
|
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
|
|
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),
|
|
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
|
|
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
|
|
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
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
#
|
|
249
|
-
#
|
|
250
|
-
#
|
|
251
|
-
#
|
|
252
|
-
|
|
253
|
-
|
|
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,
|
|
267
|
-
|
|
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
|
-
#
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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,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.
|
|
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
|