dalli 5.0.10 → 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 +47 -80
- data/Gemfile +1 -1
- data/README.md +7 -6
- data/lib/dalli/client.rb +272 -96
- data/lib/dalli/compressor.rb +5 -53
- data/lib/dalli/key_manager.rb +2 -23
- 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 +55 -28
- data/lib/dalli/protocol/connection_manager.rb +9 -90
- data/lib/dalli/protocol/key_regularizer.rb +18 -1
- data/lib/dalli/protocol/meta.rb +89 -37
- data/lib/dalli/protocol/request_formatter.rb +110 -32
- data/lib/dalli/protocol/response_processor.rb +38 -55
- data/lib/dalli/protocol/value_compressor.rb +3 -30
- data/lib/dalli/protocol/value_serializer.rb +1 -1
- data/lib/dalli/socket.rb +4 -2
- data/lib/dalli/version.rb +2 -2
- data/lib/dalli.rb +0 -1
- data/lib/rack/session/dalli.rb +2 -1
- metadata +2 -3
- data/lib/dalli/json_serializer.rb +0 -28
|
@@ -36,28 +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
|
-
cmd << " T#{
|
|
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
|
-
cmd << " N#{
|
|
49
|
-
# Thundering herd: win recache if TTL below threshold
|
|
50
|
-
cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
|
|
49
|
+
cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
|
|
50
|
+
cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
|
|
51
51
|
cmd << ' h' if return_hit_status # Return hit status (0 or 1)
|
|
52
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)
|
|
53
54
|
cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
|
|
54
55
|
cmd << TERMINATOR
|
|
55
56
|
end
|
|
56
57
|
|
|
57
|
-
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)
|
|
58
59
|
# In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
|
|
59
60
|
# Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
|
|
60
|
-
|
|
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
|
+
|
|
61
73
|
buffer = ''.b
|
|
62
74
|
keys.each do |key|
|
|
63
75
|
buffer << 'mg ' << encoded_key(key) << post_get
|
|
@@ -65,7 +77,8 @@ module Dalli
|
|
|
65
77
|
buffer << 'mn' << TERMINATOR
|
|
66
78
|
end
|
|
67
79
|
|
|
68
|
-
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)
|
|
69
82
|
base64 = KeyRegularizer.required?(key)
|
|
70
83
|
key = KeyRegularizer.encode(key) if base64
|
|
71
84
|
cmd = "ms #{key} #{value.bytesize}"
|
|
@@ -75,14 +88,18 @@ module Dalli
|
|
|
75
88
|
cmd << ' b' if base64
|
|
76
89
|
cmd << " F#{bitflags}" if bitflags
|
|
77
90
|
cmd << cas_string(cas)
|
|
78
|
-
cmd << " T#{
|
|
91
|
+
cmd << " T#{ttl}" if ttl
|
|
79
92
|
cmd << " M#{mode_to_token(mode)}"
|
|
80
93
|
cmd << ' q' if quiet
|
|
94
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
81
95
|
cmd << TERMINATOR
|
|
82
96
|
end
|
|
83
97
|
|
|
84
|
-
def multi_meta_set(entries, ttl: nil)
|
|
85
|
-
|
|
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
|
+
|
|
86
103
|
buffer = ''.b
|
|
87
104
|
entries.each do |key, pair|
|
|
88
105
|
value, bitflags = pair
|
|
@@ -90,12 +107,12 @@ module Dalli
|
|
|
90
107
|
base64 = KeyRegularizer.required?(key)
|
|
91
108
|
key = KeyRegularizer.encode(key) if base64
|
|
92
109
|
|
|
93
|
-
# 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"
|
|
94
111
|
buffer << "ms #{key} #{value.bytesize} c"
|
|
95
112
|
buffer << ' b' if base64
|
|
96
113
|
buffer << " F#{bitflags}" if bitflags
|
|
97
114
|
buffer << " T#{ttl}" if ttl
|
|
98
|
-
buffer << ' MS q' << TERMINATOR << value << TERMINATOR
|
|
115
|
+
buffer << ' MS q' << token_suffix << TERMINATOR << value << TERMINATOR
|
|
99
116
|
end
|
|
100
117
|
buffer << META_NOOP
|
|
101
118
|
end
|
|
@@ -103,34 +120,96 @@ module Dalli
|
|
|
103
120
|
# Thundering herd protection flag:
|
|
104
121
|
# - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
|
|
105
122
|
# using N/R flags will see the X flag and know the item is being regenerated.
|
|
106
|
-
|
|
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
|
+
|
|
107
140
|
cmd = "md #{encoded_key(key)}"
|
|
108
141
|
cmd << cas_string(cas)
|
|
109
|
-
cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
110
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
|
|
111
145
|
cmd << ' q' if quiet
|
|
146
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
112
147
|
cmd << TERMINATOR
|
|
113
148
|
end
|
|
114
149
|
|
|
115
|
-
|
|
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
|
+
|
|
116
163
|
buffer = ''.b
|
|
117
164
|
keys.each do |key|
|
|
118
|
-
buffer << 'md ' << encoded_key(key) <<
|
|
165
|
+
buffer << 'md ' << encoded_key(key) << suffix
|
|
119
166
|
end
|
|
120
167
|
buffer << META_NOOP
|
|
121
168
|
end
|
|
122
169
|
|
|
123
|
-
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)
|
|
124
172
|
cmd = "ma #{encoded_key(key)} v"
|
|
125
|
-
cmd << " D#{
|
|
126
|
-
cmd << " J#{
|
|
173
|
+
cmd << " D#{delta}" if delta
|
|
174
|
+
cmd << " J#{initial}" if initial
|
|
127
175
|
# Always set a TTL if an initial value is specified
|
|
128
|
-
cmd << " N#{ttl
|
|
176
|
+
cmd << " N#{ttl || 0}" if ttl || initial
|
|
129
177
|
cmd << cas_string(cas)
|
|
130
178
|
cmd << ' q' if quiet
|
|
131
179
|
cmd << " M#{incr ? 'I' : 'D'}"
|
|
180
|
+
cmd << routing_tokens(p_token: p_token, l_token: l_token)
|
|
132
181
|
cmd << TERMINATOR
|
|
133
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
|
|
134
213
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
135
214
|
# rubocop:enable Metrics/ParameterLists
|
|
136
215
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
@@ -171,16 +250,15 @@ module Dalli
|
|
|
171
250
|
|
|
172
251
|
private
|
|
173
252
|
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
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)
|
|
184
262
|
end
|
|
185
263
|
|
|
186
264
|
def mode_to_token(mode)
|
|
@@ -23,9 +23,6 @@ module Dalli
|
|
|
23
23
|
VERSION = 'VERSION'
|
|
24
24
|
SERVER_ERROR = 'SERVER_ERROR'
|
|
25
25
|
|
|
26
|
-
# memcached can't store an item larger than 1 GiB (its -I maximum)
|
|
27
|
-
MAX_VALUE_BYTES = 1024 * 1024 * 1024
|
|
28
|
-
|
|
29
26
|
T_OK = [OK].freeze
|
|
30
27
|
T_RESET = [RESET].freeze
|
|
31
28
|
T_EN_HD = [EN, HD].freeze
|
|
@@ -41,25 +38,22 @@ module Dalli
|
|
|
41
38
|
@value_marshaller = value_marshaller
|
|
42
39
|
end
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
# ignoring any flags in the reply. The request didn't ask for flags, so
|
|
46
|
-
# a reply carrying them anyway must not get the value deserialized.
|
|
47
|
-
def meta_get_with_value(cache_nils: false, raw: false)
|
|
41
|
+
def meta_get_with_value(cache_nils: false)
|
|
48
42
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
49
43
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
50
44
|
return true unless tokens.first == VA
|
|
51
45
|
|
|
52
|
-
retrieve(read_data(tokens[1].to_i),
|
|
46
|
+
@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
|
|
53
47
|
end
|
|
54
48
|
|
|
55
|
-
def meta_get_with_value_and_cas
|
|
49
|
+
def meta_get_with_value_and_cas
|
|
56
50
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
57
51
|
return [nil, 0] if tokens.first == EN
|
|
58
52
|
|
|
59
53
|
cas = cas_from_tokens(tokens)
|
|
60
54
|
return [nil, cas] unless tokens.first == VA
|
|
61
55
|
|
|
62
|
-
[retrieve(read_data(tokens[1].to_i),
|
|
56
|
+
[@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
|
|
63
57
|
end
|
|
64
58
|
|
|
65
59
|
def meta_get_without_value
|
|
@@ -79,12 +73,13 @@ module Dalli
|
|
|
79
73
|
# Used by meta_get for comprehensive metadata retrieval.
|
|
80
74
|
# Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
|
|
81
75
|
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
|
|
82
|
-
|
|
76
|
+
return_ttl_remaining: false)
|
|
83
77
|
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
84
78
|
result = build_metadata_result(tokens)
|
|
85
79
|
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
86
80
|
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
87
|
-
result[:
|
|
81
|
+
result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
|
|
82
|
+
result[:value] = parse_value_from_tokens(tokens, cache_nils)
|
|
88
83
|
result
|
|
89
84
|
end
|
|
90
85
|
|
|
@@ -92,20 +87,20 @@ module Dalli
|
|
|
92
87
|
{
|
|
93
88
|
value: nil, cas: cas_from_tokens(tokens),
|
|
94
89
|
won_recache: tokens.include?('W'), stale: tokens.include?('X'),
|
|
95
|
-
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
|
|
96
96
|
}
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
-
def parse_value_from_tokens(tokens, cache_nils
|
|
99
|
+
def parse_value_from_tokens(tokens, cache_nils)
|
|
100
100
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
101
101
|
return unless tokens.first == VA
|
|
102
102
|
|
|
103
|
-
retrieve(read_data(tokens[1].to_i),
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Values from a raw request are returned as stored
|
|
107
|
-
def retrieve(value, bitflags, raw)
|
|
108
|
-
raw ? value : @value_marshaller.retrieve(value, bitflags)
|
|
103
|
+
@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
|
|
109
104
|
end
|
|
110
105
|
|
|
111
106
|
def meta_set_with_cas
|
|
@@ -187,10 +182,6 @@ module Dalli
|
|
|
187
182
|
end
|
|
188
183
|
|
|
189
184
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
190
|
-
# A hit with no key can't be matched to a request: skip it (false
|
|
191
|
-
# status) rather than yield it, or take it for the end of the pipeline
|
|
192
|
-
return [false, resp_size] unless tokens.any? { |t| t.start_with?('k') }
|
|
193
|
-
|
|
194
185
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
195
186
|
[tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value, resp_size]
|
|
196
187
|
end
|
|
@@ -214,21 +205,15 @@ module Dalli
|
|
|
214
205
|
tokens = header.split
|
|
215
206
|
header_len = header.bytesize + TERMINATOR.length
|
|
216
207
|
|
|
217
|
-
# The body len is removed from the tokens array
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
#
|
|
223
|
-
#
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
# as the end would leave the remaining replies on the connection for
|
|
227
|
-
# later commands to read as their own. A hit on an empty value (VA 0)
|
|
228
|
-
# still has a body -- just its terminator -- so it's parsed below.
|
|
229
|
-
return [tokens.first == MN, header_len] if body_len.zero? && tokens.first != VA
|
|
230
|
-
|
|
231
|
-
check_value_size!(body_len)
|
|
208
|
+
# The body len is removed from the tokens array
|
|
209
|
+
body_len = body_len_from_tokens(tokens)
|
|
210
|
+
|
|
211
|
+
# We have a complete response that has no body.
|
|
212
|
+
# This is either the response to the terminating
|
|
213
|
+
# noop or, if the status is not MN, an intermediate
|
|
214
|
+
# error response that needs to be discarded.
|
|
215
|
+
return [true, header_len] if body_len.zero?
|
|
216
|
+
|
|
232
217
|
resp_size = header_len + body_len + TERMINATOR.length
|
|
233
218
|
# The header is in the buffer, but the body is not. As we don't have
|
|
234
219
|
# a complete response, don't advance the buffer
|
|
@@ -258,6 +243,14 @@ module Dalli
|
|
|
258
243
|
value_from_tokens(tokens, 'c').to_i
|
|
259
244
|
end
|
|
260
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
|
+
|
|
261
254
|
def key_from_tokens(tokens)
|
|
262
255
|
encoded_key = value_from_tokens(tokens, 'k')
|
|
263
256
|
if tokens.delete('b')
|
|
@@ -282,6 +275,12 @@ module Dalli
|
|
|
282
275
|
value_from_tokens(tokens, 'l').to_i
|
|
283
276
|
end
|
|
284
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
|
+
|
|
285
284
|
def body_len_from_tokens(tokens)
|
|
286
285
|
value_from_tokens(tokens, 's').to_i
|
|
287
286
|
end
|
|
@@ -297,19 +296,6 @@ module Dalli
|
|
|
297
296
|
end
|
|
298
297
|
end
|
|
299
298
|
|
|
300
|
-
# A pipelined reply claiming an impossible size would otherwise have the
|
|
301
|
-
# buffer wait for (and accumulate) that many bytes
|
|
302
|
-
# The size given right after "VA", or 0 when it isn't a number
|
|
303
|
-
def positional_size(tokens)
|
|
304
|
-
tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def check_value_size!(size)
|
|
308
|
-
return if size.between?(0, MAX_VALUE_BYTES)
|
|
309
|
-
|
|
310
|
-
raise Dalli::DalliError, "Reply value size #{size} is out of range"
|
|
311
|
-
end
|
|
312
|
-
|
|
313
299
|
def read_line
|
|
314
300
|
@io_source.read_line&.chomp!(TERMINATOR)
|
|
315
301
|
end
|
|
@@ -320,9 +306,6 @@ module Dalli
|
|
|
320
306
|
end
|
|
321
307
|
|
|
322
308
|
def read_data(data_size)
|
|
323
|
-
# Checked before the terminator is added: -1 or -2 would otherwise
|
|
324
|
-
# read 1 or 0 bytes and leave the connection out of step
|
|
325
|
-
check_value_size!(data_size)
|
|
326
309
|
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
327
310
|
end
|
|
328
311
|
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/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
|
|
127
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
128
128
|
def self.supports_connect_timeout?
|
|
129
129
|
return @supports_connect_timeout if defined?(@supports_connect_timeout)
|
|
130
130
|
|
|
@@ -135,6 +135,7 @@ module Dalli
|
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
138
139
|
|
|
139
140
|
def self.create_socket_with_timeout(host, port, options)
|
|
140
141
|
if supports_connect_timeout?
|
|
@@ -191,13 +192,14 @@ module Dalli
|
|
|
191
192
|
|
|
192
193
|
# Detect and cache the correct pack format for struct timeval on this platform.
|
|
193
194
|
# Different architectures have different sizes for time_t and suseconds_t.
|
|
194
|
-
# rubocop:disable
|
|
195
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
195
196
|
def self.timeval_pack_format(sock)
|
|
196
197
|
@timeval_pack_format ||= begin
|
|
197
198
|
expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
|
|
198
199
|
TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
|
|
199
200
|
end
|
|
200
201
|
end
|
|
202
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
201
203
|
|
|
202
204
|
def self.pack_timeval(sock, seconds, microseconds)
|
|
203
205
|
[seconds, microseconds].pack(timeval_pack_format(sock))
|
data/lib/dalli/version.rb
CHANGED
data/lib/dalli.rb
CHANGED
|
@@ -62,7 +62,6 @@ 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'
|
|
67
66
|
require_relative 'dalli/client'
|
|
68
67
|
require_relative 'dalli/key_manager'
|
data/lib/rack/session/dalli.rb
CHANGED
|
@@ -16,10 +16,11 @@ module Rack
|
|
|
16
16
|
attr_reader :data
|
|
17
17
|
|
|
18
18
|
# Don't freeze this until we fix the specs/implementation
|
|
19
|
-
# rubocop:disable
|
|
19
|
+
# rubocop:disable Style/MutableConstant
|
|
20
20
|
DEFAULT_DALLI_OPTIONS = {
|
|
21
21
|
namespace: 'rack:session'
|
|
22
22
|
}
|
|
23
|
+
# rubocop:enable Style/MutableConstant
|
|
23
24
|
|
|
24
25
|
# Brings in a new Rack::Session::Dalli middleware with the given
|
|
25
26
|
# `:memcache_server`. The server is either a hostname, or a
|
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
|
|
@@ -42,7 +42,6 @@ files:
|
|
|
42
42
|
- lib/dalli/compressor.rb
|
|
43
43
|
- lib/dalli/flags.rb
|
|
44
44
|
- lib/dalli/instrumentation.rb
|
|
45
|
-
- lib/dalli/json_serializer.rb
|
|
46
45
|
- lib/dalli/key_manager.rb
|
|
47
46
|
- lib/dalli/options.rb
|
|
48
47
|
- lib/dalli/pipelined_deleter.rb
|
|
@@ -88,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
88
87
|
- !ruby/object:Gem::Version
|
|
89
88
|
version: '0'
|
|
90
89
|
requirements: []
|
|
91
|
-
rubygems_version: 4.0.
|
|
90
|
+
rubygems_version: 4.0.18
|
|
92
91
|
specification_version: 4
|
|
93
92
|
summary: High performance memcached client for Ruby
|
|
94
93
|
test_files: []
|
|
@@ -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
|