dalli 5.0.7 → 5.1.1
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 +80 -1
- data/Gemfile +1 -1
- data/README.md +6 -1
- data/lib/dalli/client.rb +265 -56
- data/lib/dalli/pipelined_deleter.rb +29 -12
- data/lib/dalli/pipelined_getter.rb +84 -47
- data/lib/dalli/pipelined_setter.rb +15 -3
- data/lib/dalli/protocol/base.rb +73 -11
- data/lib/dalli/protocol/key_regularizer.rb +25 -1
- data/lib/dalli/protocol/meta.rb +101 -33
- data/lib/dalli/protocol/request_formatter.rb +118 -18
- data/lib/dalli/protocol/response_processor.rb +85 -16
- data/lib/dalli/ring.rb +31 -17
- data/lib/dalli/socket.rb +2 -4
- data/lib/dalli/version.rb +2 -2
- data/lib/rack/session/dalli.rb +1 -2
- metadata +1 -1
|
@@ -36,36 +36,59 @@ module Dalli
|
|
|
36
36
|
# - l<N>: Seconds since last access
|
|
37
37
|
def meta_get(key:, value: true, return_cas: false, ttl: nil, quiet: false,
|
|
38
38
|
vivify_ttl: nil, recache_ttl: nil,
|
|
39
|
-
return_hit_status: false, return_last_access: false,
|
|
40
|
-
skip_flags: false)
|
|
39
|
+
return_hit_status: false, return_last_access: false, return_ttl_remaining: false,
|
|
40
|
+
skip_lru_bump: false, skip_flags: false, p_token: nil, l_token: nil)
|
|
41
41
|
cmd = "mg #{encoded_key(key)}"
|
|
42
42
|
# In raw mode (skip_flags: true), we don't request bitflags since they're not used.
|
|
43
43
|
# This saves 2 bytes per request and skips parsing on response.
|
|
44
44
|
cmd << (skip_flags ? ' v' : ' v f') if value
|
|
45
45
|
cmd << ' c' if return_cas
|
|
46
46
|
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
47
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
47
48
|
cmd << ' k q s' if quiet # Return the key in the response if quiet
|
|
48
49
|
cmd << " N#{integer_flag(:vivify_ttl, vivify_ttl)}" if vivify_ttl # Thundering herd: vivify on miss
|
|
49
50
|
# Thundering herd: win recache if TTL below threshold
|
|
50
51
|
cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
|
|
51
52
|
cmd << ' h' if return_hit_status # Return hit status (0 or 1)
|
|
52
53
|
cmd << ' l' if return_last_access # Return seconds since last access
|
|
54
|
+
cmd << ' t' if return_ttl_remaining # Return seconds of TTL remaining (-1 = no TTL)
|
|
53
55
|
cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
|
|
54
56
|
cmd << TERMINATOR
|
|
55
57
|
end
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
# Fast path for the common single-key get with no optional flags.
|
|
60
|
+
# Produces the same bytes as meta_get(key: key, skip_flags: skip_flags)
|
|
61
|
+
# without the keyword-argument handling and incremental string building.
|
|
62
|
+
def plain_meta_get(key, skip_flags)
|
|
63
|
+
skip_flags ? "mg #{encoded_key(key)} v#{TERMINATOR}" : "mg #{encoded_key(key)} v f#{TERMINATOR}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Pass terminate: false to leave off the trailing noop, for callers
|
|
67
|
+
# (the pipelined get) that send it separately.
|
|
68
|
+
def multi_meta_get(keys, skip_flags: false, return_cas: false, terminate: true, p_token: nil, l_token: nil)
|
|
58
69
|
# In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
|
|
59
70
|
# Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
|
|
60
|
-
|
|
71
|
+
# With return_cas a "c" flag follows, which shifts those indexes --
|
|
72
|
+
# callers of that variant locate tokens by flag rather than position.
|
|
73
|
+
# Routing tokens apply to every key in the batch, so the suffix is
|
|
74
|
+
# built once rather than per key.
|
|
75
|
+
post_get = if return_cas
|
|
76
|
+
skip_flags ? ' v c' : ' v f c'
|
|
77
|
+
else
|
|
78
|
+
skip_flags ? ' v' : ' v f'
|
|
79
|
+
end
|
|
80
|
+
post_get += routing_tokens(p_token: p_token, l_token: l_token)
|
|
81
|
+
post_get += " k q s#{TERMINATOR}"
|
|
82
|
+
|
|
61
83
|
buffer = ''.b
|
|
62
84
|
keys.each do |key|
|
|
63
85
|
buffer << 'mg ' << encoded_key(key) << post_get
|
|
64
86
|
end
|
|
65
|
-
buffer << 'mn' << TERMINATOR
|
|
87
|
+
terminate ? buffer << 'mn' << TERMINATOR : buffer
|
|
66
88
|
end
|
|
67
89
|
|
|
68
|
-
def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false
|
|
90
|
+
def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false,
|
|
91
|
+
p_token: nil, l_token: nil)
|
|
69
92
|
base64 = KeyRegularizer.required?(key)
|
|
70
93
|
key = KeyRegularizer.encode(key) if base64
|
|
71
94
|
cmd = "ms #{key} #{value.bytesize}"
|
|
@@ -78,11 +101,16 @@ module Dalli
|
|
|
78
101
|
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
79
102
|
cmd << " M#{mode_to_token(mode)}"
|
|
80
103
|
cmd << ' q' if quiet
|
|
104
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
81
105
|
cmd << TERMINATOR
|
|
82
106
|
end
|
|
83
107
|
|
|
84
|
-
def multi_meta_set(entries, ttl: nil)
|
|
108
|
+
def multi_meta_set(entries, ttl: nil, p_token: nil, l_token: nil)
|
|
109
|
+
# Routing tokens apply to every entry in the batch, so the suffix is
|
|
110
|
+
# built once rather than per entry.
|
|
111
|
+
token_suffix = routing_tokens(p_token: p_token, l_token: l_token)
|
|
85
112
|
ttl = integer_flag(:ttl, ttl) if ttl
|
|
113
|
+
|
|
86
114
|
buffer = ''.b
|
|
87
115
|
entries.each do |key, pair|
|
|
88
116
|
value, bitflags = pair
|
|
@@ -90,12 +118,12 @@ module Dalli
|
|
|
90
118
|
base64 = KeyRegularizer.required?(key)
|
|
91
119
|
key = KeyRegularizer.encode(key) if base64
|
|
92
120
|
|
|
93
|
-
# Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
|
|
121
|
+
# Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q [P/L]\r\n"
|
|
94
122
|
buffer << "ms #{key} #{value.bytesize} c"
|
|
95
123
|
buffer << ' b' if base64
|
|
96
124
|
buffer << " F#{bitflags}" if bitflags
|
|
97
125
|
buffer << " T#{ttl}" if ttl
|
|
98
|
-
buffer << ' MS q' << TERMINATOR << value << TERMINATOR
|
|
126
|
+
buffer << ' MS q' << token_suffix << TERMINATOR << value << TERMINATOR
|
|
99
127
|
end
|
|
100
128
|
buffer << META_NOOP
|
|
101
129
|
end
|
|
@@ -103,24 +131,55 @@ module Dalli
|
|
|
103
131
|
# Thundering herd protection flag:
|
|
104
132
|
# - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
|
|
105
133
|
# using N/R flags will see the X flag and know the item is being regenerated.
|
|
106
|
-
|
|
134
|
+
# Tombstone flags:
|
|
135
|
+
# - stale (I flag): mark the item stale instead of removing it. Readers
|
|
136
|
+
# using N/R flags, or get_with_metadata, see the X flag and know the
|
|
137
|
+
# item is being regenerated.
|
|
138
|
+
# - ttl (T flag): how long the stale marker lives. memcached only honors
|
|
139
|
+
# T on a delete when it is paired with I, so this raises rather than
|
|
140
|
+
# emitting a request the server would apply differently than intended.
|
|
141
|
+
# - drop_value (x flag): remove the item's value but leave the item, so a
|
|
142
|
+
# tombstone can be left without retaining the old payload.
|
|
143
|
+
def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false, drop_value: false,
|
|
144
|
+
p_token: nil, l_token: nil)
|
|
145
|
+
# Message uses this method's own parameter names (ttl/stale), not the
|
|
146
|
+
# client-facing tombstone_ttl/invalidate names Dalli::Client validates
|
|
147
|
+
# against -- this guard is also reachable by internal callers (tests,
|
|
148
|
+
# direct RequestFormatter use) that never go through the client.
|
|
149
|
+
raise ArgumentError, 'ttl requires stale: true' if ttl && !stale
|
|
150
|
+
|
|
107
151
|
cmd = "md #{encoded_key(key)}"
|
|
108
152
|
cmd << cas_string(cas)
|
|
109
|
-
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
110
153
|
cmd << ' I' if stale # Mark stale instead of deleting
|
|
154
|
+
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
155
|
+
cmd << ' x' if drop_value # Drop the value but keep the item
|
|
111
156
|
cmd << ' q' if quiet
|
|
157
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
112
158
|
cmd << TERMINATOR
|
|
113
159
|
end
|
|
114
160
|
|
|
115
|
-
|
|
161
|
+
# Tombstone and routing-token flags apply to every key in the batch;
|
|
162
|
+
# see meta_delete.
|
|
163
|
+
def multi_meta_delete(keys, stale: false, ttl: nil, drop_value: false, p_token: nil, l_token: nil)
|
|
164
|
+
raise ArgumentError, 'tombstone_ttl requires invalidate: true' if ttl && !stale
|
|
165
|
+
|
|
166
|
+
suffix = +''
|
|
167
|
+
suffix << ' I' if stale
|
|
168
|
+
suffix << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
169
|
+
suffix << ' x' if drop_value
|
|
170
|
+
suffix << ' q'
|
|
171
|
+
suffix << routing_tokens(p_token: p_token, l_token: l_token)
|
|
172
|
+
suffix << TERMINATOR
|
|
173
|
+
|
|
116
174
|
buffer = ''.b
|
|
117
175
|
keys.each do |key|
|
|
118
|
-
buffer << 'md ' << encoded_key(key) <<
|
|
176
|
+
buffer << 'md ' << encoded_key(key) << suffix
|
|
119
177
|
end
|
|
120
178
|
buffer << META_NOOP
|
|
121
179
|
end
|
|
122
180
|
|
|
123
|
-
def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false
|
|
181
|
+
def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false,
|
|
182
|
+
p_token: nil, l_token: nil)
|
|
124
183
|
cmd = "ma #{encoded_key(key)} v"
|
|
125
184
|
cmd << " D#{integer_flag(:delta, delta)}" if delta
|
|
126
185
|
cmd << " J#{integer_flag(:initial, initial)}" if initial
|
|
@@ -129,8 +188,39 @@ module Dalli
|
|
|
129
188
|
cmd << cas_string(cas)
|
|
130
189
|
cmd << ' q' if quiet
|
|
131
190
|
cmd << " M#{incr ? 'I' : 'D'}"
|
|
191
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
132
192
|
cmd << TERMINATOR
|
|
133
193
|
end
|
|
194
|
+
|
|
195
|
+
# Builds the wire-format suffix for opaque routing tokens (P and L).
|
|
196
|
+
# memcached itself ignores these; they exist as hints for a proxy or
|
|
197
|
+
# router sitting between the client and memcached. See protocol.txt:
|
|
198
|
+
# "All commands accept tokens 'P' and 'L' which are completely ignored.
|
|
199
|
+
# The arguments to 'P' and 'L' can be used as hints or path
|
|
200
|
+
# specifications to a proxy or router inbetween a client and a
|
|
201
|
+
# memcached daemon."
|
|
202
|
+
#
|
|
203
|
+
# Empty / nil tokens are treated as no-ops. CRLF and null bytes are
|
|
204
|
+
# rejected with ArgumentError to prevent the token from being used as a
|
|
205
|
+
# wire-protocol injection vector (e.g. "foo\r\nflush_all\r\n" would
|
|
206
|
+
# otherwise be parsed as a second command by memcached or any
|
|
207
|
+
# intermediate proxy/LB).
|
|
208
|
+
def routing_tokens(p_token: nil, l_token: nil)
|
|
209
|
+
# Only an empty *String* is a no-op. Checking respond_to?(:empty?)
|
|
210
|
+
# instead would also swallow p_token: [] / {} before the type check
|
|
211
|
+
# below ever runs, silently dropping caller mistakes that should
|
|
212
|
+
# raise "must be a String".
|
|
213
|
+
p_token = nil if p_token.is_a?(String) && p_token.empty?
|
|
214
|
+
l_token = nil if l_token.is_a?(String) && l_token.empty?
|
|
215
|
+
validate_routing_token!('p_token', p_token)
|
|
216
|
+
validate_routing_token!('l_token', l_token)
|
|
217
|
+
return '' unless p_token || l_token
|
|
218
|
+
|
|
219
|
+
s = +''
|
|
220
|
+
s << " P#{p_token}" if p_token
|
|
221
|
+
s << " L#{l_token}" if l_token
|
|
222
|
+
s
|
|
223
|
+
end
|
|
134
224
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
135
225
|
# rubocop:enable Metrics/ParameterLists
|
|
136
226
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
@@ -171,12 +261,22 @@ module Dalli
|
|
|
171
261
|
|
|
172
262
|
private
|
|
173
263
|
|
|
264
|
+
# Disallowed bytes: CR, LF, NUL. Any of these embedded in a routing
|
|
265
|
+
# token would let the caller inject a second wire-protocol command.
|
|
266
|
+
ROUTING_TOKEN_FORBIDDEN = /[\r\n\0]/
|
|
267
|
+
private_constant :ROUTING_TOKEN_FORBIDDEN
|
|
268
|
+
|
|
269
|
+
def validate_routing_token!(name, value)
|
|
270
|
+
return if value.nil?
|
|
271
|
+
raise ArgumentError, "#{name} must be a String, got #{value.class}" unless value.is_a?(String)
|
|
272
|
+
raise ArgumentError, "#{name} must not contain CRLF or null bytes" if value.match?(ROUTING_TOKEN_FORBIDDEN)
|
|
273
|
+
end
|
|
274
|
+
|
|
174
275
|
# Numeric flag values are written straight into the command line, so a
|
|
175
276
|
# value that isn't an integer -- e.g. a String carrying CRLF -- would let
|
|
176
|
-
# the caller inject further memcached commands
|
|
177
|
-
#
|
|
178
|
-
#
|
|
179
|
-
# rather than octal 8.
|
|
277
|
+
# the caller inject further memcached commands. Converting to Integer
|
|
278
|
+
# means only digits ever reach the wire. Strings are parsed as base 10 so
|
|
279
|
+
# "010" means 10, as memcached would read it, rather than octal 8.
|
|
180
280
|
def integer_flag(name, value)
|
|
181
281
|
value.is_a?(String) ? Integer(value, 10) : Integer(value)
|
|
182
282
|
rescue ArgumentError, TypeError, FloatDomainError
|
|
@@ -23,6 +23,14 @@ module Dalli
|
|
|
23
23
|
VERSION = 'VERSION'
|
|
24
24
|
SERVER_ERROR = 'SERVER_ERROR'
|
|
25
25
|
|
|
26
|
+
VA_PREFIX = 'VA '
|
|
27
|
+
FLAGS_TOKEN_PREFIX = ' f'
|
|
28
|
+
BYTE_B = 'b'.ord
|
|
29
|
+
BYTE_C = 'c'.ord
|
|
30
|
+
BYTE_F = 'f'.ord
|
|
31
|
+
BYTE_K = 'k'.ord
|
|
32
|
+
BYTE_S = 's'.ord
|
|
33
|
+
|
|
26
34
|
T_OK = [OK].freeze
|
|
27
35
|
T_RESET = [RESET].freeze
|
|
28
36
|
T_EN_HD = [EN, HD].freeze
|
|
@@ -39,11 +47,24 @@ module Dalli
|
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def meta_get_with_value(cache_nils: false)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
line = read_line
|
|
51
|
+
# A hit ("VA <size> f<flags>") is parsed in place rather than split
|
|
52
|
+
# into tokens, which saves several allocations on the hottest path.
|
|
53
|
+
if line&.start_with?(VA_PREFIX)
|
|
54
|
+
return @value_marshaller.retrieve(read_data(size_from_va_line(line)), bitflags_from_va_line(line))
|
|
55
|
+
end
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
tokens = line&.split || []
|
|
58
|
+
case tokens.first
|
|
59
|
+
when EN
|
|
60
|
+
cache_nils ? ::Dalli::NOT_FOUND : nil
|
|
61
|
+
when VA # only reached for an unusually formatted hit line
|
|
62
|
+
@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
|
|
63
|
+
when HD
|
|
64
|
+
true
|
|
65
|
+
else
|
|
66
|
+
raise_unexpected!(tokens)
|
|
67
|
+
end
|
|
47
68
|
end
|
|
48
69
|
|
|
49
70
|
def meta_get_with_value_and_cas
|
|
@@ -72,11 +93,13 @@ module Dalli
|
|
|
72
93
|
#
|
|
73
94
|
# Used by meta_get for comprehensive metadata retrieval.
|
|
74
95
|
# Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
|
|
75
|
-
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false
|
|
96
|
+
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
|
|
97
|
+
return_ttl_remaining: false)
|
|
76
98
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
77
99
|
result = build_metadata_result(tokens)
|
|
78
100
|
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
79
101
|
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
102
|
+
result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
|
|
80
103
|
result[:value] = parse_value_from_tokens(tokens, cache_nils)
|
|
81
104
|
result
|
|
82
105
|
end
|
|
@@ -85,7 +108,12 @@ module Dalli
|
|
|
85
108
|
{
|
|
86
109
|
value: nil, cas: cas_from_tokens(tokens),
|
|
87
110
|
won_recache: tokens.include?('W'), stale: tokens.include?('X'),
|
|
88
|
-
lost_recache: tokens.include?('Z')
|
|
111
|
+
lost_recache: tokens.include?('Z'),
|
|
112
|
+
# Explicit miss marker: EN means the key does not exist. A tombstoned
|
|
113
|
+
# item is NOT a miss -- it answers VA/HD with the X flag set -- and a
|
|
114
|
+
# stored nil under cache_nils is not one either, so neither can be
|
|
115
|
+
# inferred from value or cas alone.
|
|
116
|
+
miss: tokens.first == EN
|
|
89
117
|
}
|
|
90
118
|
end
|
|
91
119
|
|
|
@@ -174,11 +202,6 @@ module Dalli
|
|
|
174
202
|
non_deletions
|
|
175
203
|
end
|
|
176
204
|
|
|
177
|
-
def full_response_from_buffer(tokens, body, resp_size)
|
|
178
|
-
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
179
|
-
[tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value, resp_size]
|
|
180
|
-
end
|
|
181
|
-
|
|
182
205
|
##
|
|
183
206
|
# This method returns an array of values used in a pipelined
|
|
184
207
|
# getk process. The first value is the number of bytes by
|
|
@@ -198,8 +221,20 @@ module Dalli
|
|
|
198
221
|
tokens = header.split
|
|
199
222
|
header_len = header.bytesize + TERMINATOR.length
|
|
200
223
|
|
|
201
|
-
#
|
|
202
|
-
|
|
224
|
+
# Read the s, f, c and k flags and the b marker in one pass. As with
|
|
225
|
+
# value_from_tokens, the first token for each flag wins.
|
|
226
|
+
size = bitflags = cas = key = nil
|
|
227
|
+
base64 = false
|
|
228
|
+
tokens.each do |token|
|
|
229
|
+
case token.getbyte(0)
|
|
230
|
+
when BYTE_S then size ||= token
|
|
231
|
+
when BYTE_F then bitflags ||= token
|
|
232
|
+
when BYTE_C then cas ||= token
|
|
233
|
+
when BYTE_K then key ||= token
|
|
234
|
+
when BYTE_B then base64 ||= token == 'b'
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
body_len = flag_int(size, 's')
|
|
203
238
|
|
|
204
239
|
# We have a complete response that has no body.
|
|
205
240
|
# This is either the response to the terminating
|
|
@@ -215,7 +250,16 @@ module Dalli
|
|
|
215
250
|
# The full response is in our buffer, so parse it and return
|
|
216
251
|
# the values
|
|
217
252
|
body = buf.byteslice(offset + header_len, body_len)
|
|
218
|
-
|
|
253
|
+
value = @value_marshaller.retrieve(body, flag_int(bitflags, 'f'))
|
|
254
|
+
key = key ? key.delete_prefix!('k') : 0
|
|
255
|
+
key = KeyRegularizer.decode(key) if base64
|
|
256
|
+
[tokens.first == VA, flag_int(cas, 'c'), key, value, resp_size]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Integer value of a flag token such as "f123", or 0 when absent.
|
|
260
|
+
# Strips the prefix in place, like value_from_tokens.
|
|
261
|
+
def flag_int(token, flag)
|
|
262
|
+
token ? token.delete_prefix!(flag).to_i : 0
|
|
219
263
|
end
|
|
220
264
|
|
|
221
265
|
def error_on_unexpected!(expected_codes)
|
|
@@ -223,6 +267,10 @@ module Dalli
|
|
|
223
267
|
|
|
224
268
|
return tokens if expected_codes.include?(tokens.first)
|
|
225
269
|
|
|
270
|
+
raise_unexpected!(tokens)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def raise_unexpected!(tokens)
|
|
226
274
|
raise Dalli::ServerError, tokens.join(' ').to_s if tokens.first == SERVER_ERROR
|
|
227
275
|
|
|
228
276
|
raise Dalli::DalliError, "Response error: #{tokens.first}"
|
|
@@ -232,10 +280,29 @@ module Dalli
|
|
|
232
280
|
value_from_tokens(tokens, 'f').to_i
|
|
233
281
|
end
|
|
234
282
|
|
|
283
|
+
# String#to_i stops at the first non-digit, so these read one token's
|
|
284
|
+
# integer straight out of the header line without splitting it.
|
|
285
|
+
def size_from_va_line(line)
|
|
286
|
+
line.byteslice(VA_PREFIX.bytesize, line.bytesize).to_i
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
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
|
|
292
|
+
end
|
|
293
|
+
|
|
235
294
|
def cas_from_tokens(tokens)
|
|
236
295
|
value_from_tokens(tokens, 'c').to_i
|
|
237
296
|
end
|
|
238
297
|
|
|
298
|
+
# Detects the X presence flag, set when an item has been marked stale by a
|
|
299
|
+
# prior `md key I`. Uses strict equality (Array#any? with a String pattern
|
|
300
|
+
# compares with ==) so a future value-bearing flag beginning with X cannot
|
|
301
|
+
# be mistaken for it.
|
|
302
|
+
def stale_from_tokens(tokens)
|
|
303
|
+
tokens.any?('X')
|
|
304
|
+
end
|
|
305
|
+
|
|
239
306
|
def key_from_tokens(tokens)
|
|
240
307
|
encoded_key = value_from_tokens(tokens, 'k')
|
|
241
308
|
if tokens.delete('b')
|
|
@@ -260,8 +327,10 @@ module Dalli
|
|
|
260
327
|
value_from_tokens(tokens, 'l').to_i
|
|
261
328
|
end
|
|
262
329
|
|
|
263
|
-
|
|
264
|
-
|
|
330
|
+
# Returns seconds of TTL remaining; -1 when the item has no expiry.
|
|
331
|
+
# The t flag returns t<seconds>.
|
|
332
|
+
def ttl_remaining_from_tokens(tokens)
|
|
333
|
+
value_from_tokens(tokens, 't').to_i
|
|
265
334
|
end
|
|
266
335
|
|
|
267
336
|
def value_from_tokens(tokens, flag)
|
data/lib/dalli/ring.rb
CHANGED
|
@@ -21,41 +21,46 @@ module Dalli
|
|
|
21
21
|
# in an equally weighted scenario.
|
|
22
22
|
POINTS_PER_SERVER = 160 # this is the default in libmemcached
|
|
23
23
|
|
|
24
|
-
attr_accessor :servers
|
|
24
|
+
attr_accessor :servers
|
|
25
|
+
attr_reader :continuum
|
|
25
26
|
|
|
26
27
|
def initialize(servers_arg, options)
|
|
27
28
|
@servers = servers_arg.map do |s|
|
|
28
29
|
Dalli::Protocol::Meta.new(s, options)
|
|
29
30
|
end
|
|
30
|
-
|
|
31
|
-
@continuum = build_continuum(servers) if servers.size > 1
|
|
31
|
+
self.continuum = (build_continuum(servers) if servers.size > 1)
|
|
32
32
|
|
|
33
33
|
threadsafe! unless options[:threadsafe] == false
|
|
34
34
|
@failover = options[:failover] != false
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def
|
|
37
|
+
def continuum=(entries)
|
|
38
|
+
@continuum = entries
|
|
39
|
+
# Plain integers, so the binary search in server_for_hash_key needn't
|
|
40
|
+
# call Entry#value at each step
|
|
41
|
+
@continuum_values = entries&.map(&:value)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# alive_cache (optional) remembers each server's alive? result, so a
|
|
45
|
+
# caller routing many keys checks each server once rather than per key.
|
|
46
|
+
def server_for_key(key, alive_cache = nil)
|
|
47
|
+
# server_from_continuum only returns a server that is alive
|
|
38
48
|
server = if @continuum
|
|
39
|
-
server_from_continuum(key)
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
server_from_continuum(key, alive_cache)
|
|
50
|
+
elsif (first = @servers.first) && server_alive?(first, alive_cache)
|
|
51
|
+
first
|
|
42
52
|
end
|
|
43
|
-
|
|
44
|
-
# Note that the call to alive? has the side effect of initializing
|
|
45
|
-
# the socket
|
|
46
|
-
return server if server&.alive?
|
|
53
|
+
return server if server
|
|
47
54
|
|
|
48
55
|
raise Dalli::RingError, 'No server available'
|
|
49
56
|
end
|
|
50
57
|
|
|
51
|
-
def server_from_continuum(key)
|
|
58
|
+
def server_from_continuum(key, alive_cache = nil)
|
|
52
59
|
hkey = hash_for(key)
|
|
53
60
|
20.times do |try|
|
|
54
61
|
server = server_for_hash_key(hkey)
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
# the socket
|
|
58
|
-
return server if server.alive?
|
|
63
|
+
return server if server_alive?(server, alive_cache)
|
|
59
64
|
break unless @failover
|
|
60
65
|
|
|
61
66
|
hkey = hash_for("#{try}#{key}")
|
|
@@ -64,8 +69,9 @@ module Dalli
|
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
def keys_grouped_by_server(key_arr)
|
|
72
|
+
alive_cache = {}.compare_by_identity
|
|
67
73
|
key_arr.group_by do |key|
|
|
68
|
-
server_for_key(key)
|
|
74
|
+
server_for_key(key, alive_cache)
|
|
69
75
|
rescue Dalli::RingError
|
|
70
76
|
Dalli.logger.debug { "unable to get key #{key}" }
|
|
71
77
|
nil
|
|
@@ -110,13 +116,21 @@ module Dalli
|
|
|
110
116
|
Zlib.crc32(key)
|
|
111
117
|
end
|
|
112
118
|
|
|
119
|
+
# Note that the call to alive? has the side effect of initializing
|
|
120
|
+
# the socket
|
|
121
|
+
def server_alive?(server, alive_cache)
|
|
122
|
+
return server.alive? unless alive_cache
|
|
123
|
+
|
|
124
|
+
alive_cache.fetch(server) { alive_cache[server] = server.alive? }
|
|
125
|
+
end
|
|
126
|
+
|
|
113
127
|
def entry_count_for(server, total_servers, total_weight)
|
|
114
128
|
((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
|
|
115
129
|
end
|
|
116
130
|
|
|
117
131
|
def server_for_hash_key(hash_key)
|
|
118
132
|
# Find the closest index in the Ring with value <= the given value
|
|
119
|
-
entryidx = @
|
|
133
|
+
entryidx = @continuum_values.bsearch_index { |value| value > hash_key }
|
|
120
134
|
if entryidx.nil?
|
|
121
135
|
entryidx = @continuum.size - 1
|
|
122
136
|
else
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -124,7 +124,7 @@ module Dalli
|
|
|
124
124
|
# Returns true for an unmodified TCPSocket on Ruby 3.0+, or for resolv-replace >= 0.2.0
|
|
125
125
|
# which forwards keyword arguments through its patch.
|
|
126
126
|
# Returns false when monkey-patched by gems like socksify or resolv-replace < 0.2.0.
|
|
127
|
-
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
127
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
128
128
|
def self.supports_connect_timeout?
|
|
129
129
|
return @supports_connect_timeout if defined?(@supports_connect_timeout)
|
|
130
130
|
|
|
@@ -135,7 +135,6 @@ module Dalli
|
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
|
-
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
139
138
|
|
|
140
139
|
def self.create_socket_with_timeout(host, port, options)
|
|
141
140
|
if supports_connect_timeout?
|
|
@@ -192,14 +191,13 @@ module Dalli
|
|
|
192
191
|
|
|
193
192
|
# Detect and cache the correct pack format for struct timeval on this platform.
|
|
194
193
|
# Different architectures have different sizes for time_t and suseconds_t.
|
|
195
|
-
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
194
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
196
195
|
def self.timeval_pack_format(sock)
|
|
197
196
|
@timeval_pack_format ||= begin
|
|
198
197
|
expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
|
|
199
198
|
TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
|
|
200
199
|
end
|
|
201
200
|
end
|
|
202
|
-
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
203
201
|
|
|
204
202
|
def self.pack_timeval(sock, seconds, microseconds)
|
|
205
203
|
[seconds, microseconds].pack(timeval_pack_format(sock))
|
data/lib/dalli/version.rb
CHANGED
data/lib/rack/session/dalli.rb
CHANGED
|
@@ -16,11 +16,10 @@ module Rack
|
|
|
16
16
|
attr_reader :data
|
|
17
17
|
|
|
18
18
|
# Don't freeze this until we fix the specs/implementation
|
|
19
|
-
# rubocop:disable Style/MutableConstant
|
|
19
|
+
# rubocop:disable-next Style/MutableConstant
|
|
20
20
|
DEFAULT_DALLI_OPTIONS = {
|
|
21
21
|
namespace: 'rack:session'
|
|
22
22
|
}
|
|
23
|
-
# rubocop:enable Style/MutableConstant
|
|
24
23
|
|
|
25
24
|
# Brings in a new Rack::Session::Dalli middleware with the given
|
|
26
25
|
# `:memcache_server`. The server is either a hostname, or a
|