dalli 3.2.15 → 4.0.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 +24 -102
- data/Gemfile +2 -0
- data/README.md +0 -35
- data/lib/dalli/client.rb +5 -30
- data/lib/dalli/compressor.rb +5 -53
- data/lib/dalli/key_manager.rb +4 -30
- data/lib/dalli/protocol/base.rb +9 -79
- data/lib/dalli/protocol/binary/response_processor.rb +4 -20
- data/lib/dalli/protocol/binary.rb +6 -8
- data/lib/dalli/protocol/connection_manager.rb +12 -69
- data/lib/dalli/protocol/meta/key_regularizer.rb +1 -1
- data/lib/dalli/protocol/meta/request_formatter.rb +11 -25
- data/lib/dalli/protocol/meta/response_processor.rb +16 -54
- data/lib/dalli/protocol/meta.rb +9 -40
- data/lib/dalli/protocol/server_config_parser.rb +1 -1
- data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
- data/lib/dalli/protocol/value_compressor.rb +8 -46
- data/lib/dalli/protocol/value_marshaller.rb +1 -1
- data/lib/dalli/protocol/value_serializer.rb +49 -40
- data/lib/dalli/protocol.rb +0 -7
- data/lib/dalli/socket.rb +29 -36
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +3 -3
- data/lib/rack/session/dalli.rb +42 -7
- metadata +3 -5
- data/lib/dalli/json_serializer.rb +0 -28
- data/lib/dalli/server.rb +0 -6
|
@@ -22,7 +22,7 @@ module Dalli
|
|
|
22
22
|
def get(key, options = nil)
|
|
23
23
|
req = RequestFormatter.standard_request(opkey: :get, key: key)
|
|
24
24
|
write(req)
|
|
25
|
-
response_processor.get(cache_nils: cache_nils?(options)
|
|
25
|
+
response_processor.get(cache_nils: cache_nils?(options))
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def quiet_get_request(key)
|
|
@@ -33,7 +33,7 @@ module Dalli
|
|
|
33
33
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
34
34
|
req = RequestFormatter.standard_request(opkey: :gat, key: key, ttl: ttl)
|
|
35
35
|
write(req)
|
|
36
|
-
response_processor.get(cache_nils: cache_nils?(options)
|
|
36
|
+
response_processor.get(cache_nils: cache_nils?(options))
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def touch(key, ttl)
|
|
@@ -44,10 +44,10 @@ module Dalli
|
|
|
44
44
|
|
|
45
45
|
# TODO: This is confusing, as there's a cas command in memcached
|
|
46
46
|
# and this isn't it. Maybe rename? Maybe eliminate?
|
|
47
|
-
def cas(key
|
|
47
|
+
def cas(key)
|
|
48
48
|
req = RequestFormatter.standard_request(opkey: :get, key: key)
|
|
49
49
|
write(req)
|
|
50
|
-
response_processor.data_cas_response
|
|
50
|
+
response_processor.data_cas_response
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Storage Commands
|
|
@@ -119,6 +119,7 @@ module Dalli
|
|
|
119
119
|
# if the key doesn't already exist, rather than
|
|
120
120
|
# setting the initial value
|
|
121
121
|
NOT_FOUND_EXPIRY = 0xFFFFFFFF
|
|
122
|
+
private_constant :NOT_FOUND_EXPIRY
|
|
122
123
|
|
|
123
124
|
def decr_incr(opkey, key, count, ttl, initial)
|
|
124
125
|
expiry = initial ? TtlSanitizer.sanitize(ttl) : NOT_FOUND_EXPIRY
|
|
@@ -139,10 +140,7 @@ module Dalli
|
|
|
139
140
|
# We need to read all the responses at once.
|
|
140
141
|
def noop
|
|
141
142
|
write_noop
|
|
142
|
-
|
|
143
|
-
# Everything up to and including the noop reply has been read
|
|
144
|
-
@connection_manager.quiet_responses_pending = false
|
|
145
|
-
result
|
|
143
|
+
response_processor.consume_all_responses_until_noop
|
|
146
144
|
end
|
|
147
145
|
|
|
148
146
|
def stats(info = '')
|
|
@@ -28,7 +28,6 @@ module Dalli
|
|
|
28
28
|
|
|
29
29
|
attr_accessor :hostname, :port, :socket_type, :options
|
|
30
30
|
attr_reader :sock
|
|
31
|
-
attr_writer :quiet_responses_pending
|
|
32
31
|
|
|
33
32
|
def initialize(hostname, port, socket_type, client_options)
|
|
34
33
|
@hostname = hostname
|
|
@@ -36,11 +35,9 @@ module Dalli
|
|
|
36
35
|
@socket_type = socket_type
|
|
37
36
|
@options = DEFAULTS.merge(client_options)
|
|
38
37
|
@request_in_progress = false
|
|
39
|
-
@quiet_responses_pending = false
|
|
40
38
|
@sock = nil
|
|
41
39
|
@pid = nil
|
|
42
40
|
|
|
43
|
-
@fail_count = 0
|
|
44
41
|
reset_down_info
|
|
45
42
|
end
|
|
46
43
|
|
|
@@ -56,11 +53,10 @@ module Dalli
|
|
|
56
53
|
Dalli.logger.debug { "Dalli::Server#connect #{name}" }
|
|
57
54
|
|
|
58
55
|
@sock = memcached_socket
|
|
59
|
-
@pid =
|
|
56
|
+
@pid = PIDCache.pid
|
|
60
57
|
@request_in_progress = false
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# SocketError = DNS resolution failure; SSL_ERRORS = failed TLS handshake
|
|
58
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
|
|
59
|
+
# SocketError = DNS resolution failure
|
|
64
60
|
error_on_request!(e)
|
|
65
61
|
end
|
|
66
62
|
|
|
@@ -88,8 +84,6 @@ module Dalli
|
|
|
88
84
|
def down!
|
|
89
85
|
close
|
|
90
86
|
log_down_detected
|
|
91
|
-
# Once down_retry_delay passes, the server gets a full set of attempts
|
|
92
|
-
@fail_count = 0
|
|
93
87
|
|
|
94
88
|
@error = $ERROR_INFO&.class&.name
|
|
95
89
|
@msg ||= $ERROR_INFO&.message
|
|
@@ -106,47 +100,32 @@ module Dalli
|
|
|
106
100
|
|
|
107
101
|
def confirm_ready!
|
|
108
102
|
close if request_in_progress?
|
|
109
|
-
|
|
103
|
+
reconnect_on_fork if fork_detected?
|
|
110
104
|
end
|
|
111
105
|
|
|
112
106
|
def confirm_in_progress!
|
|
113
107
|
raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless request_in_progress?
|
|
114
108
|
|
|
115
|
-
|
|
109
|
+
reconnect_on_fork if fork_detected?
|
|
116
110
|
end
|
|
117
111
|
|
|
118
112
|
def close
|
|
119
113
|
return unless @sock
|
|
120
114
|
|
|
121
115
|
begin
|
|
122
|
-
|
|
116
|
+
@sock.close
|
|
123
117
|
rescue StandardError
|
|
124
118
|
nil
|
|
125
119
|
end
|
|
126
120
|
@sock = nil
|
|
127
121
|
@pid = nil
|
|
128
|
-
@quiet_responses_pending = false
|
|
129
122
|
abort_request!
|
|
130
123
|
end
|
|
131
124
|
|
|
132
|
-
# A forked child shares the parent's connection, so it closes only its
|
|
133
|
-
# own file descriptor. Closing the TLS socket itself would send
|
|
134
|
-
# close_notify and end the parent's TLS session.
|
|
135
|
-
def close_socket
|
|
136
|
-
fork_detected? ? @sock.to_io.close : @sock.close
|
|
137
|
-
end
|
|
138
|
-
|
|
139
125
|
def connected?
|
|
140
126
|
!@sock.nil?
|
|
141
127
|
end
|
|
142
128
|
|
|
143
|
-
# True when a quiet request was written on this connection and its
|
|
144
|
-
# replies (memcached only suppresses some of them) haven't been
|
|
145
|
-
# consumed by a noop yet.
|
|
146
|
-
def quiet_responses_pending?
|
|
147
|
-
@quiet_responses_pending
|
|
148
|
-
end
|
|
149
|
-
|
|
150
129
|
def request_in_progress?
|
|
151
130
|
@request_in_progress
|
|
152
131
|
end
|
|
@@ -161,50 +140,21 @@ module Dalli
|
|
|
161
140
|
raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless @request_in_progress
|
|
162
141
|
|
|
163
142
|
@request_in_progress = false
|
|
164
|
-
# A completed request is what proves the server healthy again, so the
|
|
165
|
-
# failure count resets here rather than on reconnect: a server that
|
|
166
|
-
# accepts connections but never answers would otherwise reset it on
|
|
167
|
-
# every retry and never reach socket_max_failures.
|
|
168
|
-
@fail_count = 0
|
|
169
143
|
end
|
|
170
144
|
|
|
171
145
|
def abort_request!
|
|
172
146
|
@request_in_progress = false
|
|
173
147
|
end
|
|
174
148
|
|
|
175
|
-
# The longest reply line Dalli reads, in bytes, CRLF included. A meta
|
|
176
|
-
# reply header is at most about 500 bytes: "VA <size>" plus its flags,
|
|
177
|
-
# the largest being a 250-byte key base64-encoded to 336 bytes, a
|
|
178
|
-
# 20-digit CAS, a 32-byte opaque and a few numeric fields. The longest
|
|
179
|
-
# stats line (stats settings) carries a file path such as ext_path, up
|
|
180
|
-
# to PATH_MAX (4096). 8 KiB leaves room for both, while a server that
|
|
181
|
-
# never sends CRLF can't grow the read buffer without bound.
|
|
182
|
-
MAX_LINE_BYTES = 8 * 1024
|
|
183
|
-
|
|
184
149
|
def read_line
|
|
185
|
-
data = @sock.gets("\r\n"
|
|
150
|
+
data = @sock.gets("\r\n")
|
|
186
151
|
error_on_request!('EOF in read_line') if data.nil?
|
|
187
|
-
# Without its CRLF the line either ran past MAX_LINE_BYTES or was cut
|
|
188
|
-
# short by EOF; either way the connection is out of step
|
|
189
|
-
error_on_request!("Reply line without CRLF in the first #{MAX_LINE_BYTES} bytes") unless data.end_with?("\r\n")
|
|
190
152
|
data
|
|
191
153
|
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
192
154
|
error_on_request!(e)
|
|
193
155
|
end
|
|
194
156
|
|
|
195
|
-
# memcached can't store an item larger than 1 GiB (its -I maximum), so a
|
|
196
|
-
# reply claiming more (or a negative size) is malformed or hostile.
|
|
197
|
-
# Reading allocates the full count up front, so check before reading.
|
|
198
|
-
MAX_READ_BYTES = (1024 * 1024 * 1024) + 2 # plus the value's trailing "\r\n"
|
|
199
|
-
|
|
200
|
-
def check_read_size!(count)
|
|
201
|
-
return if count.between?(0, MAX_READ_BYTES)
|
|
202
|
-
|
|
203
|
-
raise Dalli::DalliError, "Reply size #{count} from #{name} is out of range"
|
|
204
|
-
end
|
|
205
|
-
|
|
206
157
|
def read(count)
|
|
207
|
-
check_read_size!(count)
|
|
208
158
|
@sock.readfull(count)
|
|
209
159
|
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
|
|
210
160
|
error_on_request!(e)
|
|
@@ -245,9 +195,8 @@ module Dalli
|
|
|
245
195
|
raise Dalli::NetworkError, message
|
|
246
196
|
end
|
|
247
197
|
|
|
248
|
-
# Called on connect. Deliberately leaves @fail_count alone; see
|
|
249
|
-
# finish_request!.
|
|
250
198
|
def reset_down_info
|
|
199
|
+
@fail_count = 0
|
|
251
200
|
@down_at = nil
|
|
252
201
|
@last_down_at = nil
|
|
253
202
|
@msg = nil
|
|
@@ -263,28 +212,22 @@ module Dalli
|
|
|
263
212
|
end
|
|
264
213
|
|
|
265
214
|
def log_warn_message(err_or_string)
|
|
266
|
-
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
|
267
215
|
Dalli.logger.warn do
|
|
268
216
|
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
|
269
217
|
"#{name} failed (count: #{@fail_count}) #{detail}"
|
|
270
218
|
end
|
|
271
219
|
end
|
|
272
220
|
|
|
273
|
-
def
|
|
221
|
+
def reconnect_on_fork
|
|
274
222
|
message = 'Fork detected, re-connecting child process...'
|
|
275
223
|
Dalli.logger.info { message }
|
|
276
|
-
#
|
|
277
|
-
# connection, setting us up for reconnect on next request.
|
|
224
|
+
# Close socket on a fork and reconnect immediately
|
|
278
225
|
close
|
|
279
|
-
|
|
226
|
+
establish_connection
|
|
280
227
|
end
|
|
281
228
|
|
|
282
229
|
def fork_detected?
|
|
283
|
-
|
|
284
|
-
# Process._fork hook, and another library's fork hook (such as
|
|
285
|
-
# connection_pool closing its connections) can run in the child before
|
|
286
|
-
# it, which would close the parent's TLS session as if it were ours.
|
|
287
|
-
@pid && @pid != Process.pid
|
|
230
|
+
@pid && @pid != PIDCache.pid
|
|
288
231
|
end
|
|
289
232
|
|
|
290
233
|
def log_down_detected
|
|
@@ -10,7 +10,7 @@ module Dalli
|
|
|
10
10
|
# memcached supports the use of base64 hashes for keys containing
|
|
11
11
|
# whitespace or non-ASCII characters, provided the 'b' flag is included in the request.
|
|
12
12
|
class KeyRegularizer
|
|
13
|
-
WHITESPACE = /\s
|
|
13
|
+
WHITESPACE = /\s/
|
|
14
14
|
|
|
15
15
|
def self.encode(key)
|
|
16
16
|
return [key, false] if key.ascii_only? && !WHITESPACE.match(key)
|
|
@@ -13,7 +13,6 @@ module Dalli
|
|
|
13
13
|
# and introducing an intermediate object seems like overkill.
|
|
14
14
|
#
|
|
15
15
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
16
|
-
# rubocop:disable Metrics/MethodLength
|
|
17
16
|
# rubocop:disable Metrics/ParameterLists
|
|
18
17
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
19
18
|
def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false)
|
|
@@ -21,7 +20,7 @@ module Dalli
|
|
|
21
20
|
cmd << ' v f' if value
|
|
22
21
|
cmd << ' c' if return_cas
|
|
23
22
|
cmd << ' b' if base64
|
|
24
|
-
cmd << " T#{
|
|
23
|
+
cmd << " T#{ttl}" if ttl
|
|
25
24
|
cmd << ' k q s' if quiet # Return the key in the response if quiet
|
|
26
25
|
cmd + TERMINATOR
|
|
27
26
|
end
|
|
@@ -32,19 +31,17 @@ module Dalli
|
|
|
32
31
|
cmd << ' b' if base64
|
|
33
32
|
cmd << " F#{bitflags}" if bitflags
|
|
34
33
|
cmd << cas_string(cas)
|
|
35
|
-
cmd << " T#{
|
|
34
|
+
cmd << " T#{ttl}" if ttl
|
|
36
35
|
cmd << " M#{mode_to_token(mode)}"
|
|
37
36
|
cmd << ' q' if quiet
|
|
38
37
|
cmd << TERMINATOR
|
|
39
|
-
cmd << value
|
|
40
|
-
cmd + TERMINATOR
|
|
41
38
|
end
|
|
42
39
|
|
|
43
40
|
def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false)
|
|
44
41
|
cmd = "md #{key}"
|
|
45
42
|
cmd << ' b' if base64
|
|
46
43
|
cmd << cas_string(cas)
|
|
47
|
-
cmd << " T#{
|
|
44
|
+
cmd << " T#{ttl}" if ttl
|
|
48
45
|
cmd << ' q' if quiet
|
|
49
46
|
cmd + TERMINATOR
|
|
50
47
|
end
|
|
@@ -52,17 +49,16 @@ module Dalli
|
|
|
52
49
|
def self.meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, base64: false, quiet: false)
|
|
53
50
|
cmd = "ma #{key} v"
|
|
54
51
|
cmd << ' b' if base64
|
|
55
|
-
cmd << " D#{
|
|
56
|
-
cmd << " J#{
|
|
52
|
+
cmd << " D#{delta}" if delta
|
|
53
|
+
cmd << " J#{initial}" if initial
|
|
57
54
|
# Always set a TTL if an initial value is specified
|
|
58
|
-
cmd << " N#{ttl
|
|
55
|
+
cmd << " N#{ttl || 0}" if ttl || initial
|
|
59
56
|
cmd << cas_string(cas)
|
|
60
57
|
cmd << ' q' if quiet
|
|
61
58
|
cmd << " M#{incr ? 'I' : 'D'}"
|
|
62
59
|
cmd + TERMINATOR
|
|
63
60
|
end
|
|
64
61
|
# rubocop:enable Metrics/CyclomaticComplexity
|
|
65
|
-
# rubocop:enable Metrics/MethodLength
|
|
66
62
|
# rubocop:enable Metrics/ParameterLists
|
|
67
63
|
# rubocop:enable Metrics/PerceivedComplexity
|
|
68
64
|
|
|
@@ -81,13 +77,16 @@ module Dalli
|
|
|
81
77
|
cmd + TERMINATOR
|
|
82
78
|
end
|
|
83
79
|
|
|
80
|
+
ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
|
|
81
|
+
|
|
84
82
|
def self.stats(arg = nil)
|
|
83
|
+
raise ArgumentError, "Invalid stats argument: #{arg.inspect}" unless ALLOWED_STATS_ARGS.include?(arg)
|
|
84
|
+
|
|
85
85
|
cmd = +'stats'
|
|
86
|
-
cmd << " #{arg}" if arg
|
|
86
|
+
cmd << " #{arg}" if arg && !arg.empty?
|
|
87
87
|
cmd + TERMINATOR
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
# rubocop:disable Metrics/MethodLength
|
|
91
90
|
def self.mode_to_token(mode)
|
|
92
91
|
case mode
|
|
93
92
|
when :add
|
|
@@ -102,19 +101,6 @@ module Dalli
|
|
|
102
101
|
'S'
|
|
103
102
|
end
|
|
104
103
|
end
|
|
105
|
-
# rubocop:enable Metrics/MethodLength
|
|
106
|
-
|
|
107
|
-
# Numeric flag values are written straight into the command line, so a
|
|
108
|
-
# value that isn't an integer -- e.g. a String carrying CRLF -- would let
|
|
109
|
-
# the caller inject further memcached commands (GHSA-6wmv-xq9m-fmp7).
|
|
110
|
-
# Converting to Integer means only digits ever reach the wire. Strings
|
|
111
|
-
# are parsed as base 10 so "010" means 10, as memcached would read it,
|
|
112
|
-
# rather than octal 8.
|
|
113
|
-
def self.integer_flag(name, value)
|
|
114
|
-
value.is_a?(String) ? Integer(value, 10) : Integer(value)
|
|
115
|
-
rescue ArgumentError, TypeError, FloatDomainError
|
|
116
|
-
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
117
|
-
end
|
|
118
104
|
|
|
119
105
|
def self.cas_string(cas)
|
|
120
106
|
cas = parse_to_64_bit_int(cas, nil)
|
|
@@ -21,33 +21,29 @@ module Dalli
|
|
|
21
21
|
STAT = 'STAT'
|
|
22
22
|
VA = 'VA'
|
|
23
23
|
VERSION = 'VERSION'
|
|
24
|
+
SERVER_ERROR = 'SERVER_ERROR'
|
|
24
25
|
|
|
25
26
|
def initialize(io_source, value_marshaller)
|
|
26
27
|
@io_source = io_source
|
|
27
28
|
@value_marshaller = value_marshaller
|
|
28
29
|
end
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
# decompressing it, whatever flags it was stored with
|
|
32
|
-
def meta_get_with_value(cache_nils: false, raw: false)
|
|
31
|
+
def meta_get_with_value(cache_nils: false)
|
|
33
32
|
tokens = error_on_unexpected!([VA, EN, HD])
|
|
34
33
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
35
34
|
return true unless tokens.first == VA
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
raw ? value : @value_marshaller.retrieve(value, bitflags_from_tokens(tokens))
|
|
36
|
+
@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
|
|
39
37
|
end
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
def meta_get_with_value_and_cas(raw: false)
|
|
39
|
+
def meta_get_with_value_and_cas
|
|
43
40
|
tokens = error_on_unexpected!([VA, EN, HD])
|
|
44
41
|
return [nil, 0] if tokens.first == EN
|
|
45
42
|
|
|
46
43
|
cas = cas_from_tokens(tokens)
|
|
47
44
|
return [nil, cas] unless tokens.first == VA
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
[raw ? value : @value_marshaller.retrieve(value, bitflags_from_tokens(tokens)), cas]
|
|
46
|
+
[@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
|
|
51
47
|
end
|
|
52
48
|
|
|
53
49
|
def meta_get_without_value
|
|
@@ -125,10 +121,6 @@ module Dalli
|
|
|
125
121
|
end
|
|
126
122
|
|
|
127
123
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
128
|
-
# A hit with no key can't be matched to a request: skip it (false
|
|
129
|
-
# status) rather than yield it, or take it for the end of the pipeline
|
|
130
|
-
return [resp_size, false, nil, nil, nil] unless tokens.any? { |t| t.start_with?('k') }
|
|
131
|
-
|
|
132
124
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
133
125
|
[resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
|
|
134
126
|
end
|
|
@@ -149,14 +141,11 @@ module Dalli
|
|
|
149
141
|
|
|
150
142
|
tokens, header_len, body_len = tokens_from_header_buffer(buf)
|
|
151
143
|
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
# later commands to read as their own. A hit on an empty value (VA 0)
|
|
158
|
-
# still has a body -- just its terminator -- so it's parsed below.
|
|
159
|
-
return [header_len, tokens.first == MN, nil, nil, nil] if no_body?(tokens, body_len)
|
|
144
|
+
# We have a complete response that has no body.
|
|
145
|
+
# This is either the response to the terminating
|
|
146
|
+
# noop or, if the status is not MN, an intermediate
|
|
147
|
+
# error response that needs to be discarded.
|
|
148
|
+
return [header_len, true, nil, nil, nil] if body_len.zero?
|
|
160
149
|
|
|
161
150
|
resp_size = header_len + body_len + TERMINATOR.length
|
|
162
151
|
# The header is in the buffer, but the body is not. As we don't have
|
|
@@ -169,27 +158,6 @@ module Dalli
|
|
|
169
158
|
full_response_from_buffer(tokens, body, resp_size)
|
|
170
159
|
end
|
|
171
160
|
|
|
172
|
-
# memcached can't store an item larger than 1 GiB (its -I maximum). A
|
|
173
|
-
# pipelined reply claiming an impossible size would otherwise have the
|
|
174
|
-
# buffer wait for (and accumulate) that many bytes.
|
|
175
|
-
MAX_VALUE_BYTES = 1024 * 1024 * 1024
|
|
176
|
-
|
|
177
|
-
# The size given right after "VA", or 0 when it isn't a number
|
|
178
|
-
def positional_size(tokens)
|
|
179
|
-
tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def check_value_size!(size)
|
|
183
|
-
return if size.between?(0, MAX_VALUE_BYTES)
|
|
184
|
-
|
|
185
|
-
raise Dalli::DalliError, "Reply value size #{size} is out of range"
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# A zero-size reply has no body unless it's a VA (a hit on an empty value)
|
|
189
|
-
def no_body?(tokens, body_len)
|
|
190
|
-
body_len.zero? && tokens.first != VA
|
|
191
|
-
end
|
|
192
|
-
|
|
193
161
|
def contains_header?(buf)
|
|
194
162
|
buf.include?(TERMINATOR)
|
|
195
163
|
end
|
|
@@ -200,9 +168,12 @@ module Dalli
|
|
|
200
168
|
|
|
201
169
|
def error_on_unexpected!(expected_codes)
|
|
202
170
|
tokens = next_line_to_tokens
|
|
203
|
-
raise Dalli::DalliError, "Response error: #{tokens.first}" unless expected_codes.include?(tokens.first)
|
|
204
171
|
|
|
205
|
-
tokens
|
|
172
|
+
return tokens if expected_codes.include?(tokens.first)
|
|
173
|
+
|
|
174
|
+
raise Dalli::ServerError, tokens.join(' ').to_s if tokens.first == SERVER_ERROR
|
|
175
|
+
|
|
176
|
+
raise Dalli::DalliError, "Response error: #{tokens.first}"
|
|
206
177
|
end
|
|
207
178
|
|
|
208
179
|
def bitflags_from_tokens(tokens)
|
|
@@ -220,11 +191,7 @@ module Dalli
|
|
|
220
191
|
end
|
|
221
192
|
|
|
222
193
|
def body_len_from_tokens(tokens)
|
|
223
|
-
|
|
224
|
-
# when there's no s flag rather than reading the value as replies
|
|
225
|
-
size = tokens.any? { |t| t.start_with?('s') } ? value_from_tokens(tokens, 's')&.to_i : positional_size(tokens)
|
|
226
|
-
check_value_size!(size) if size
|
|
227
|
-
size
|
|
194
|
+
value_from_tokens(tokens, 's')&.to_i
|
|
228
195
|
end
|
|
229
196
|
|
|
230
197
|
def value_from_tokens(tokens, flag)
|
|
@@ -243,12 +210,7 @@ module Dalli
|
|
|
243
210
|
line&.split || []
|
|
244
211
|
end
|
|
245
212
|
|
|
246
|
-
# Values are read by the size in the VA line, not up to the next CRLF:
|
|
247
|
-
# a value can contain CRLF and can be longer than MAX_LINE_BYTES
|
|
248
213
|
def read_data(data_size)
|
|
249
|
-
# Checked before the terminator is added: -1 or -2 would otherwise
|
|
250
|
-
# read 1 or 0 bytes and leave the connection out of step
|
|
251
|
-
check_value_size!(data_size)
|
|
252
214
|
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
253
215
|
end
|
|
254
216
|
end
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -13,7 +13,6 @@ module Dalli
|
|
|
13
13
|
##
|
|
14
14
|
class Meta < Base
|
|
15
15
|
TERMINATOR = "\r\n"
|
|
16
|
-
COUNTER_ADD_OPTIONS = { raw: true }.freeze # counters are stored raw
|
|
17
16
|
|
|
18
17
|
def response_processor
|
|
19
18
|
@response_processor ||= ResponseProcessor.new(@connection_manager, @value_marshaller)
|
|
@@ -28,7 +27,7 @@ module Dalli
|
|
|
28
27
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
29
28
|
req = RequestFormatter.meta_get(key: encoded_key, base64: base64)
|
|
30
29
|
write(req)
|
|
31
|
-
response_processor.meta_get_with_value(cache_nils: cache_nils?(options)
|
|
30
|
+
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
def quiet_get_request(key)
|
|
@@ -41,7 +40,7 @@ module Dalli
|
|
|
41
40
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
42
41
|
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64)
|
|
43
42
|
write(req)
|
|
44
|
-
response_processor.meta_get_with_value(cache_nils: cache_nils?(options)
|
|
43
|
+
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
def touch(key, ttl)
|
|
@@ -54,11 +53,11 @@ module Dalli
|
|
|
54
53
|
|
|
55
54
|
# TODO: This is confusing, as there's a cas command in memcached
|
|
56
55
|
# and this isn't it. Maybe rename? Maybe eliminate?
|
|
57
|
-
def cas(key
|
|
56
|
+
def cas(key)
|
|
58
57
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
59
58
|
req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
|
|
60
59
|
write(req)
|
|
61
|
-
response_processor.meta_get_with_value_and_cas
|
|
60
|
+
response_processor.meta_get_with_value_and_cas
|
|
62
61
|
end
|
|
63
62
|
|
|
64
63
|
# Storage Commands
|
|
@@ -86,6 +85,8 @@ module Dalli
|
|
|
86
85
|
bitflags: bitflags, cas: cas,
|
|
87
86
|
ttl: ttl, mode: mode, quiet: quiet?, base64: base64)
|
|
88
87
|
write(req)
|
|
88
|
+
write(value)
|
|
89
|
+
write(TERMINATOR)
|
|
89
90
|
end
|
|
90
91
|
# rubocop:enable Metrics/ParameterLists
|
|
91
92
|
|
|
@@ -106,6 +107,8 @@ module Dalli
|
|
|
106
107
|
req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
|
|
107
108
|
cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
|
|
108
109
|
write(req)
|
|
110
|
+
write(value)
|
|
111
|
+
write(TERMINATOR)
|
|
109
112
|
end
|
|
110
113
|
# rubocop:enable Metrics/ParameterLists
|
|
111
114
|
|
|
@@ -130,42 +133,11 @@ module Dalli
|
|
|
130
133
|
def decr_incr(incr, key, delta, ttl, initial)
|
|
131
134
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
132
135
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
133
|
-
# memcached stores an item created by ma's auto-create (N/J) for a
|
|
134
|
-
# base64-encoded key without its binary-key flag, and later replies
|
|
135
|
-
# name it with the raw key bytes. Create such counters with an add.
|
|
136
|
-
return decr_incr_with_add(incr, key, delta, ttl, initial) if initial && base64
|
|
137
|
-
|
|
138
136
|
write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
139
137
|
quiet: quiet?, base64: base64))
|
|
140
138
|
response_processor.decr_incr unless quiet?
|
|
141
139
|
end
|
|
142
140
|
|
|
143
|
-
# Like decr_incr with an initial value, without memcached's auto-create:
|
|
144
|
-
# on a miss the counter is added with the initial value (which is
|
|
145
|
-
# returned, as auto-create would), and the add losing a race to another
|
|
146
|
-
# client means the counter now exists, so the incr is tried again. In a
|
|
147
|
-
# quiet block both requests are sent and whichever applies takes effect.
|
|
148
|
-
def decr_incr_with_add(incr, key, delta, ttl, initial)
|
|
149
|
-
return quiet_decr_incr_with_add(incr, key, delta, ttl, initial) if quiet?
|
|
150
|
-
|
|
151
|
-
2.times do
|
|
152
|
-
value = decr_incr(incr, key, delta, nil, nil)
|
|
153
|
-
return value unless value.nil?
|
|
154
|
-
|
|
155
|
-
write_storage_req(:add, key, initial.to_s, ttl, nil, COUNTER_ADD_OPTIONS)
|
|
156
|
-
return initial if response_processor.meta_set_with_cas
|
|
157
|
-
end
|
|
158
|
-
decr_incr(incr, key, delta, nil, nil)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def quiet_decr_incr_with_add(incr, key, delta, ttl, initial)
|
|
162
|
-
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
163
|
-
write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: nil, incr: incr, ttl: nil,
|
|
164
|
-
quiet: true, base64: base64))
|
|
165
|
-
# Sent with the rest of the quiet block, by its closing noop
|
|
166
|
-
write_storage_req(:add, key, initial.to_s, ttl, nil, COUNTER_ADD_OPTIONS)
|
|
167
|
-
end
|
|
168
|
-
|
|
169
141
|
# Other Commands
|
|
170
142
|
def flush(delay = 0)
|
|
171
143
|
write(RequestFormatter.flush(delay: delay))
|
|
@@ -176,10 +148,7 @@ module Dalli
|
|
|
176
148
|
# We need to read all the responses at once.
|
|
177
149
|
def noop
|
|
178
150
|
write_noop
|
|
179
|
-
|
|
180
|
-
# Everything up to and including the MN has been read
|
|
181
|
-
@connection_manager.quiet_responses_pending = false
|
|
182
|
-
result
|
|
151
|
+
response_processor.consume_all_responses_until_mn
|
|
183
152
|
end
|
|
184
153
|
|
|
185
154
|
def stats(info = nil)
|
|
@@ -16,7 +16,7 @@ module Dalli
|
|
|
16
16
|
# can limit character set to LDH + '.'. Hex digit section
|
|
17
17
|
# is there to support IPv6 addresses, which need to be specified with
|
|
18
18
|
# a bounding []
|
|
19
|
-
SERVER_CONFIG_REGEXP = /\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z
|
|
19
|
+
SERVER_CONFIG_REGEXP = /\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z/
|
|
20
20
|
|
|
21
21
|
DEFAULT_PORT = 11_211
|
|
22
22
|
DEFAULT_WEIGHT = 1
|
|
@@ -31,7 +31,7 @@ module Dalli
|
|
|
31
31
|
return ttl_as_i if ttl_as_i > now # Already a timestamp
|
|
32
32
|
|
|
33
33
|
Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached " \
|
|
34
|
-
'and too short to be a future timestamp,' \
|
|
34
|
+
'and too short to be a future timestamp, ' \
|
|
35
35
|
'converting to an expiration timestamp'
|
|
36
36
|
now + ttl_as_i
|
|
37
37
|
end
|