dalli 5.0.6 → 5.1.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 +57 -0
- data/README.md +6 -1
- data/lib/dalli/client.rb +259 -49
- data/lib/dalli/pipelined_deleter.rb +29 -12
- data/lib/dalli/pipelined_getter.rb +57 -8
- data/lib/dalli/pipelined_setter.rb +15 -3
- data/lib/dalli/protocol/base.rb +48 -4
- data/lib/dalli/protocol/key_regularizer.rb +18 -1
- data/lib/dalli/protocol/meta.rb +83 -30
- data/lib/dalli/protocol/request_formatter.rb +105 -13
- data/lib/dalli/protocol/response_processor.rb +23 -2
- data/lib/dalli/version.rb +2 -2
- metadata +2 -2
|
@@ -36,27 +36,40 @@ 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#{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#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
|
|
49
50
|
cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
|
|
50
51
|
cmd << ' h' if return_hit_status # Return hit status (0 or 1)
|
|
51
52
|
cmd << ' l' if return_last_access # Return seconds since last access
|
|
53
|
+
cmd << ' t' if return_ttl_remaining # Return seconds of TTL remaining (-1 = no TTL)
|
|
52
54
|
cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
|
|
53
55
|
cmd << TERMINATOR
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
def multi_meta_get(keys, skip_flags: false)
|
|
58
|
+
def multi_meta_get(keys, skip_flags: false, return_cas: false, p_token: nil, l_token: nil)
|
|
57
59
|
# In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
|
|
58
60
|
# Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
|
|
59
|
-
|
|
61
|
+
# With return_cas a "c" flag follows, which shifts those indexes --
|
|
62
|
+
# callers of that variant locate tokens by flag rather than position.
|
|
63
|
+
# Routing tokens apply to every key in the batch, so the suffix is
|
|
64
|
+
# built once rather than per key.
|
|
65
|
+
post_get = if return_cas
|
|
66
|
+
skip_flags ? ' v c' : ' v f c'
|
|
67
|
+
else
|
|
68
|
+
skip_flags ? ' v' : ' v f'
|
|
69
|
+
end
|
|
70
|
+
post_get += routing_tokens(p_token: p_token, l_token: l_token)
|
|
71
|
+
post_get += " k q s#{TERMINATOR}"
|
|
72
|
+
|
|
60
73
|
buffer = ''.b
|
|
61
74
|
keys.each do |key|
|
|
62
75
|
buffer << 'mg ' << encoded_key(key) << post_get
|
|
@@ -64,7 +77,8 @@ module Dalli
|
|
|
64
77
|
buffer << 'mn' << TERMINATOR
|
|
65
78
|
end
|
|
66
79
|
|
|
67
|
-
def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false
|
|
80
|
+
def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false,
|
|
81
|
+
p_token: nil, l_token: nil)
|
|
68
82
|
base64 = KeyRegularizer.required?(key)
|
|
69
83
|
key = KeyRegularizer.encode(key) if base64
|
|
70
84
|
cmd = "ms #{key} #{value.bytesize}"
|
|
@@ -77,10 +91,15 @@ module Dalli
|
|
|
77
91
|
cmd << " T#{ttl}" if ttl
|
|
78
92
|
cmd << " M#{mode_to_token(mode)}"
|
|
79
93
|
cmd << ' q' if quiet
|
|
94
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
80
95
|
cmd << TERMINATOR
|
|
81
96
|
end
|
|
82
97
|
|
|
83
|
-
def multi_meta_set(entries, ttl: nil)
|
|
98
|
+
def multi_meta_set(entries, ttl: nil, p_token: nil, l_token: nil)
|
|
99
|
+
# Routing tokens apply to every entry in the batch, so the suffix is
|
|
100
|
+
# built once rather than per entry.
|
|
101
|
+
token_suffix = routing_tokens(p_token: p_token, l_token: l_token)
|
|
102
|
+
|
|
84
103
|
buffer = ''.b
|
|
85
104
|
entries.each do |key, pair|
|
|
86
105
|
value, bitflags = pair
|
|
@@ -88,12 +107,12 @@ module Dalli
|
|
|
88
107
|
base64 = KeyRegularizer.required?(key)
|
|
89
108
|
key = KeyRegularizer.encode(key) if base64
|
|
90
109
|
|
|
91
|
-
# Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
|
|
110
|
+
# Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q [P/L]\r\n"
|
|
92
111
|
buffer << "ms #{key} #{value.bytesize} c"
|
|
93
112
|
buffer << ' b' if base64
|
|
94
113
|
buffer << " F#{bitflags}" if bitflags
|
|
95
114
|
buffer << " T#{ttl}" if ttl
|
|
96
|
-
buffer << ' MS q' << TERMINATOR << value << TERMINATOR
|
|
115
|
+
buffer << ' MS q' << token_suffix << TERMINATOR << value << TERMINATOR
|
|
97
116
|
end
|
|
98
117
|
buffer << META_NOOP
|
|
99
118
|
end
|
|
@@ -101,24 +120,55 @@ module Dalli
|
|
|
101
120
|
# Thundering herd protection flag:
|
|
102
121
|
# - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
|
|
103
122
|
# using N/R flags will see the X flag and know the item is being regenerated.
|
|
104
|
-
|
|
123
|
+
# Tombstone flags:
|
|
124
|
+
# - stale (I flag): mark the item stale instead of removing it. Readers
|
|
125
|
+
# using N/R flags, or get_with_metadata, see the X flag and know the
|
|
126
|
+
# item is being regenerated.
|
|
127
|
+
# - ttl (T flag): how long the stale marker lives. memcached only honors
|
|
128
|
+
# T on a delete when it is paired with I, so this raises rather than
|
|
129
|
+
# emitting a request the server would apply differently than intended.
|
|
130
|
+
# - drop_value (x flag): remove the item's value but leave the item, so a
|
|
131
|
+
# tombstone can be left without retaining the old payload.
|
|
132
|
+
def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false, drop_value: false,
|
|
133
|
+
p_token: nil, l_token: nil)
|
|
134
|
+
# Message uses this method's own parameter names (ttl/stale), not the
|
|
135
|
+
# client-facing tombstone_ttl/invalidate names Dalli::Client validates
|
|
136
|
+
# against -- this guard is also reachable by internal callers (tests,
|
|
137
|
+
# direct RequestFormatter use) that never go through the client.
|
|
138
|
+
raise ArgumentError, 'ttl requires stale: true' if ttl && !stale
|
|
139
|
+
|
|
105
140
|
cmd = "md #{encoded_key(key)}"
|
|
106
141
|
cmd << cas_string(cas)
|
|
107
|
-
cmd << " T#{ttl}" if ttl
|
|
108
142
|
cmd << ' I' if stale # Mark stale instead of deleting
|
|
143
|
+
cmd << " T#{Integer(ttl)}" if ttl
|
|
144
|
+
cmd << ' x' if drop_value # Drop the value but keep the item
|
|
109
145
|
cmd << ' q' if quiet
|
|
146
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
110
147
|
cmd << TERMINATOR
|
|
111
148
|
end
|
|
112
149
|
|
|
113
|
-
|
|
150
|
+
# Tombstone and routing-token flags apply to every key in the batch;
|
|
151
|
+
# see meta_delete.
|
|
152
|
+
def multi_meta_delete(keys, stale: false, ttl: nil, drop_value: false, p_token: nil, l_token: nil)
|
|
153
|
+
raise ArgumentError, 'tombstone_ttl requires invalidate: true' if ttl && !stale
|
|
154
|
+
|
|
155
|
+
suffix = +''
|
|
156
|
+
suffix << ' I' if stale
|
|
157
|
+
suffix << " T#{Integer(ttl)}" if ttl
|
|
158
|
+
suffix << ' x' if drop_value
|
|
159
|
+
suffix << ' q'
|
|
160
|
+
suffix << routing_tokens(p_token: p_token, l_token: l_token)
|
|
161
|
+
suffix << TERMINATOR
|
|
162
|
+
|
|
114
163
|
buffer = ''.b
|
|
115
164
|
keys.each do |key|
|
|
116
|
-
buffer << 'md ' << encoded_key(key) <<
|
|
165
|
+
buffer << 'md ' << encoded_key(key) << suffix
|
|
117
166
|
end
|
|
118
167
|
buffer << META_NOOP
|
|
119
168
|
end
|
|
120
169
|
|
|
121
|
-
def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false
|
|
170
|
+
def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false,
|
|
171
|
+
p_token: nil, l_token: nil)
|
|
122
172
|
cmd = "ma #{encoded_key(key)} v"
|
|
123
173
|
cmd << " D#{delta}" if delta
|
|
124
174
|
cmd << " J#{initial}" if initial
|
|
@@ -127,8 +177,39 @@ module Dalli
|
|
|
127
177
|
cmd << cas_string(cas)
|
|
128
178
|
cmd << ' q' if quiet
|
|
129
179
|
cmd << " M#{incr ? 'I' : 'D'}"
|
|
180
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
130
181
|
cmd << TERMINATOR
|
|
131
182
|
end
|
|
183
|
+
|
|
184
|
+
# Builds the wire-format suffix for opaque routing tokens (P and L).
|
|
185
|
+
# memcached itself ignores these; they exist as hints for a proxy or
|
|
186
|
+
# router sitting between the client and memcached. See protocol.txt:
|
|
187
|
+
# "All commands accept tokens 'P' and 'L' which are completely ignored.
|
|
188
|
+
# The arguments to 'P' and 'L' can be used as hints or path
|
|
189
|
+
# specifications to a proxy or router inbetween a client and a
|
|
190
|
+
# memcached daemon."
|
|
191
|
+
#
|
|
192
|
+
# Empty / nil tokens are treated as no-ops. CRLF and null bytes are
|
|
193
|
+
# rejected with ArgumentError to prevent the token from being used as a
|
|
194
|
+
# wire-protocol injection vector (e.g. "foo\r\nflush_all\r\n" would
|
|
195
|
+
# otherwise be parsed as a second command by memcached or any
|
|
196
|
+
# intermediate proxy/LB).
|
|
197
|
+
def routing_tokens(p_token: nil, l_token: nil)
|
|
198
|
+
# Only an empty *String* is a no-op. Checking respond_to?(:empty?)
|
|
199
|
+
# instead would also swallow p_token: [] / {} before the type check
|
|
200
|
+
# below ever runs, silently dropping caller mistakes that should
|
|
201
|
+
# raise "must be a String".
|
|
202
|
+
p_token = nil if p_token.is_a?(String) && p_token.empty?
|
|
203
|
+
l_token = nil if l_token.is_a?(String) && l_token.empty?
|
|
204
|
+
validate_routing_token!('p_token', p_token)
|
|
205
|
+
validate_routing_token!('l_token', l_token)
|
|
206
|
+
return '' unless p_token || l_token
|
|
207
|
+
|
|
208
|
+
s = +''
|
|
209
|
+
s << " P#{p_token}" if p_token
|
|
210
|
+
s << " L#{l_token}" if l_token
|
|
211
|
+
s
|
|
212
|
+
end
|
|
132
213
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
133
214
|
# rubocop:enable Metrics/ParameterLists
|
|
134
215
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
@@ -169,6 +250,17 @@ module Dalli
|
|
|
169
250
|
|
|
170
251
|
private
|
|
171
252
|
|
|
253
|
+
# Disallowed bytes: CR, LF, NUL. Any of these embedded in a routing
|
|
254
|
+
# token would let the caller inject a second wire-protocol command.
|
|
255
|
+
ROUTING_TOKEN_FORBIDDEN = /[\r\n\0]/
|
|
256
|
+
private_constant :ROUTING_TOKEN_FORBIDDEN
|
|
257
|
+
|
|
258
|
+
def validate_routing_token!(name, value)
|
|
259
|
+
return if value.nil?
|
|
260
|
+
raise ArgumentError, "#{name} must be a String, got #{value.class}" unless value.is_a?(String)
|
|
261
|
+
raise ArgumentError, "#{name} must not contain CRLF or null bytes" if value.match?(ROUTING_TOKEN_FORBIDDEN)
|
|
262
|
+
end
|
|
263
|
+
|
|
172
264
|
def mode_to_token(mode)
|
|
173
265
|
case mode
|
|
174
266
|
when :add
|
|
@@ -72,11 +72,13 @@ module Dalli
|
|
|
72
72
|
#
|
|
73
73
|
# Used by meta_get for comprehensive metadata retrieval.
|
|
74
74
|
# 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
|
|
75
|
+
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
|
|
76
|
+
return_ttl_remaining: false)
|
|
76
77
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
77
78
|
result = build_metadata_result(tokens)
|
|
78
79
|
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
79
80
|
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
81
|
+
result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
|
|
80
82
|
result[:value] = parse_value_from_tokens(tokens, cache_nils)
|
|
81
83
|
result
|
|
82
84
|
end
|
|
@@ -85,7 +87,12 @@ module Dalli
|
|
|
85
87
|
{
|
|
86
88
|
value: nil, cas: cas_from_tokens(tokens),
|
|
87
89
|
won_recache: tokens.include?('W'), stale: tokens.include?('X'),
|
|
88
|
-
lost_recache: tokens.include?('Z')
|
|
90
|
+
lost_recache: tokens.include?('Z'),
|
|
91
|
+
# Explicit miss marker: EN means the key does not exist. A tombstoned
|
|
92
|
+
# item is NOT a miss -- it answers VA/HD with the X flag set -- and a
|
|
93
|
+
# stored nil under cache_nils is not one either, so neither can be
|
|
94
|
+
# inferred from value or cas alone.
|
|
95
|
+
miss: tokens.first == EN
|
|
89
96
|
}
|
|
90
97
|
end
|
|
91
98
|
|
|
@@ -236,6 +243,14 @@ module Dalli
|
|
|
236
243
|
value_from_tokens(tokens, 'c').to_i
|
|
237
244
|
end
|
|
238
245
|
|
|
246
|
+
# Detects the X presence flag, set when an item has been marked stale by a
|
|
247
|
+
# prior `md key I`. Uses strict equality (Array#any? with a String pattern
|
|
248
|
+
# compares with ==) so a future value-bearing flag beginning with X cannot
|
|
249
|
+
# be mistaken for it.
|
|
250
|
+
def stale_from_tokens(tokens)
|
|
251
|
+
tokens.any?('X')
|
|
252
|
+
end
|
|
253
|
+
|
|
239
254
|
def key_from_tokens(tokens)
|
|
240
255
|
encoded_key = value_from_tokens(tokens, 'k')
|
|
241
256
|
if tokens.delete('b')
|
|
@@ -260,6 +275,12 @@ module Dalli
|
|
|
260
275
|
value_from_tokens(tokens, 'l').to_i
|
|
261
276
|
end
|
|
262
277
|
|
|
278
|
+
# Returns seconds of TTL remaining; -1 when the item has no expiry.
|
|
279
|
+
# The t flag returns t<seconds>.
|
|
280
|
+
def ttl_remaining_from_tokens(tokens)
|
|
281
|
+
value_from_tokens(tokens, 't').to_i
|
|
282
|
+
end
|
|
283
|
+
|
|
263
284
|
def body_len_from_tokens(tokens)
|
|
264
285
|
value_from_tokens(tokens, 's').to_i
|
|
265
286
|
end
|
data/lib/dalli/version.rb
CHANGED
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.0
|
|
4
|
+
version: 5.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
87
87
|
- !ruby/object:Gem::Version
|
|
88
88
|
version: '0'
|
|
89
89
|
requirements: []
|
|
90
|
-
rubygems_version: 4.0.
|
|
90
|
+
rubygems_version: 4.0.18
|
|
91
91
|
specification_version: 4
|
|
92
92
|
summary: High performance memcached client for Ruby
|
|
93
93
|
test_files: []
|