dalli 5.1.1 → 5.2.2
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 +126 -0
- data/README.md +54 -1
- data/lib/dalli/cache_result.rb +65 -0
- data/lib/dalli/client.rb +96 -24
- data/lib/dalli/compressor.rb +53 -5
- data/lib/dalli/json_serializer.rb +28 -0
- data/lib/dalli/key_manager.rb +23 -2
- data/lib/dalli/protocol/base.rb +41 -8
- data/lib/dalli/protocol/connection_manager.rb +108 -9
- data/lib/dalli/protocol/meta.rb +60 -34
- data/lib/dalli/protocol/request_formatter.rb +30 -9
- data/lib/dalli/protocol/response_buffer.rb +4 -1
- data/lib/dalli/protocol/response_processor.rb +144 -24
- data/lib/dalli/protocol/value_compressor.rb +30 -3
- data/lib/dalli/protocol/value_serializer.rb +1 -1
- data/lib/dalli/ring.rb +44 -17
- data/lib/dalli/socket.rb +27 -2
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +2 -0
- metadata +3 -1
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -18,7 +18,8 @@ module Dalli
|
|
|
18
18
|
|
|
19
19
|
def_delegators :@value_marshaller, :serializer, :compressor, :compression_min_size, :compress_by_default?
|
|
20
20
|
def_delegators :@connection_manager, :name, :hostname, :port, :close, :socket_timeout,
|
|
21
|
-
:socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
|
|
21
|
+
:socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error,
|
|
22
|
+
:quiet_responses_pending?
|
|
22
23
|
|
|
23
24
|
# Delegated by hand rather than with def_delegators because they run on
|
|
24
25
|
# every request, and a plain method call is cheaper than a Forwardable one.
|
|
@@ -41,6 +42,7 @@ module Dalli
|
|
|
41
42
|
warn_uri_credentials(user_creds)
|
|
42
43
|
@options = client_options.merge(user_creds)
|
|
43
44
|
@raw_mode = client_options[:raw]
|
|
45
|
+
@defer_drain = client_options[:defer_drain] ? true : false
|
|
44
46
|
@value_marshaller = @raw_mode ? StringMarshaller.new(@options) : ValueMarshaller.new(@options)
|
|
45
47
|
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
|
46
48
|
end
|
|
@@ -51,6 +53,14 @@ module Dalli
|
|
|
51
53
|
@raw_mode
|
|
52
54
|
end
|
|
53
55
|
|
|
56
|
+
# True when values should come back as stored: the client is in raw mode,
|
|
57
|
+
# or this request passed raw: true. Retrieval paths use it to skip
|
|
58
|
+
# requesting flags, so a stored value is never deserialized or
|
|
59
|
+
# decompressed for a caller who asked for raw bytes.
|
|
60
|
+
def raw_request?(options)
|
|
61
|
+
raw_mode? || (options.is_a?(Hash) && options[:raw]) ? true : false
|
|
62
|
+
end
|
|
63
|
+
|
|
54
64
|
# Chokepoint method for error handling and ensuring liveness
|
|
55
65
|
def request(opkey, *args)
|
|
56
66
|
verify_state(opkey)
|
|
@@ -58,7 +68,11 @@ module Dalli
|
|
|
58
68
|
begin
|
|
59
69
|
request_completed = false
|
|
60
70
|
@connection_manager.start_request!
|
|
71
|
+
drain_deferred_quiet_responses(opkey)
|
|
61
72
|
response = send(opkey, *args)
|
|
73
|
+
# Quiet requests can leave replies on the socket; remember that so the
|
|
74
|
+
# end of the quiet block knows this server needs draining
|
|
75
|
+
@connection_manager.mark_quiet_responses_pending! if quiet? && opkey != :noop
|
|
62
76
|
|
|
63
77
|
# pipelined_get/pipelined_get_interleaved emit query but don't read the response(s)
|
|
64
78
|
@connection_manager.finish_request! unless %i[pipelined_get pipelined_get_interleaved].include?(opkey)
|
|
@@ -142,7 +156,15 @@ module Dalli
|
|
|
142
156
|
reconnect_on_pipeline_complete!
|
|
143
157
|
values = nil
|
|
144
158
|
|
|
145
|
-
|
|
159
|
+
# Only the read is socket I/O. The caller's block runs below, outside
|
|
160
|
+
# this rescue, so an Errno or Timeout::Error it raises reaches the
|
|
161
|
+
# caller instead of being mistaken for a network failure (which closed
|
|
162
|
+
# the connection and retried the whole get_multi, yielding keys twice).
|
|
163
|
+
begin
|
|
164
|
+
response_buffer.read
|
|
165
|
+
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
166
|
+
@connection_manager.error_on_request!(e)
|
|
167
|
+
end
|
|
146
168
|
|
|
147
169
|
status, cas, key, value = response_buffer.process_single_getk_response
|
|
148
170
|
# status is not nil only if we have a full response to parse
|
|
@@ -168,8 +190,6 @@ module Dalli
|
|
|
168
190
|
end
|
|
169
191
|
|
|
170
192
|
values || {}
|
|
171
|
-
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
172
|
-
@connection_manager.error_on_request!(e)
|
|
173
193
|
end
|
|
174
194
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
175
195
|
|
|
@@ -183,10 +203,13 @@ module Dalli
|
|
|
183
203
|
@connection_manager.abort_request!
|
|
184
204
|
return true unless connected?
|
|
185
205
|
|
|
186
|
-
# Closes the connection, which ensures that our connection
|
|
187
|
-
#
|
|
188
|
-
|
|
189
|
-
|
|
206
|
+
# Closes the connection, which ensures that our connection is in a
|
|
207
|
+
# clean state for future requests. The get_multi ran out of time
|
|
208
|
+
# overall rather than hitting a socket error, so it doesn't count
|
|
209
|
+
# toward socket_max_failures: two slow get_multis in a row would
|
|
210
|
+
# otherwise mark a healthy server down.
|
|
211
|
+
Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
|
|
212
|
+
@connection_manager.close
|
|
190
213
|
true
|
|
191
214
|
end
|
|
192
215
|
|
|
@@ -217,6 +240,14 @@ module Dalli
|
|
|
217
240
|
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
|
|
218
241
|
private_constant :ALLOWED_QUIET_OPS
|
|
219
242
|
|
|
243
|
+
# With defer_drain, quiet blocks leave their replies unread. Read them
|
|
244
|
+
# before any non-quiet request, which reads a reply of its own.
|
|
245
|
+
def drain_deferred_quiet_responses(opkey)
|
|
246
|
+
return unless @defer_drain && !quiet? && opkey != :noop && @connection_manager.quiet_responses_pending?
|
|
247
|
+
|
|
248
|
+
noop
|
|
249
|
+
end
|
|
250
|
+
|
|
220
251
|
def verify_allowed_quiet!(opkey)
|
|
221
252
|
return if ALLOWED_QUIET_OPS.include?(opkey)
|
|
222
253
|
|
|
@@ -314,6 +345,7 @@ module Dalli
|
|
|
314
345
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
315
346
|
# the expected state before pipeline_response_setup is called.
|
|
316
347
|
response_buffer.clear
|
|
348
|
+
response_buffer.raw = raw_request?(options)
|
|
317
349
|
|
|
318
350
|
# The terminating noop is sent by pipeline_response_setup
|
|
319
351
|
write(quiet_get_requests(keys, options, return_cas: return_cas))
|
|
@@ -325,6 +357,7 @@ module Dalli
|
|
|
325
357
|
def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
|
|
326
358
|
# Initialize the response buffer for draining during send phase
|
|
327
359
|
response_buffer.ensure_ready
|
|
360
|
+
response_buffer.raw = raw_request?(options)
|
|
328
361
|
|
|
329
362
|
keys.each_slice(chunk_size) do |chunk|
|
|
330
363
|
# Build and write this chunk of requests
|
|
@@ -33,9 +33,13 @@ module Dalli
|
|
|
33
33
|
@socket_type = socket_type
|
|
34
34
|
@options = DEFAULTS.merge(client_options)
|
|
35
35
|
@request_in_progress = false
|
|
36
|
+
@quiet_responses_pending = false
|
|
36
37
|
@sock = nil
|
|
37
38
|
@pid = nil
|
|
39
|
+
@write_buffer = []
|
|
40
|
+
@write_buffer_bytes = 0
|
|
38
41
|
|
|
42
|
+
@fail_count = 0
|
|
39
43
|
reset_down_info
|
|
40
44
|
end
|
|
41
45
|
|
|
@@ -51,9 +55,11 @@ module Dalli
|
|
|
51
55
|
Dalli.logger.debug { "Dalli::Server#connect #{name}" }
|
|
52
56
|
|
|
53
57
|
@sock = memcached_socket
|
|
54
|
-
|
|
58
|
+
# Writes are buffered in @write_buffer instead; see WRITE_BUFFER_FLUSH_BYTES
|
|
59
|
+
@sock.sync = true
|
|
55
60
|
@pid = Process.pid
|
|
56
61
|
@request_in_progress = false
|
|
62
|
+
@quiet_responses_pending = false
|
|
57
63
|
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
|
|
58
64
|
# SocketError = DNS resolution failure
|
|
59
65
|
error_on_request!(e)
|
|
@@ -83,6 +89,8 @@ module Dalli
|
|
|
83
89
|
def down!
|
|
84
90
|
close
|
|
85
91
|
log_down_detected
|
|
92
|
+
# Once down_retry_delay passes, the server gets a full set of attempts
|
|
93
|
+
@fail_count = 0
|
|
86
94
|
|
|
87
95
|
@error = $ERROR_INFO&.class&.name
|
|
88
96
|
@msg ||= $ERROR_INFO&.message
|
|
@@ -112,7 +120,7 @@ module Dalli
|
|
|
112
120
|
return unless @sock
|
|
113
121
|
|
|
114
122
|
begin
|
|
115
|
-
|
|
123
|
+
close_socket
|
|
116
124
|
rescue StandardError
|
|
117
125
|
nil
|
|
118
126
|
ensure
|
|
@@ -123,14 +131,38 @@ module Dalli
|
|
|
123
131
|
# @request_in_progress == true.
|
|
124
132
|
@sock = nil
|
|
125
133
|
@pid = nil
|
|
134
|
+
@quiet_responses_pending = false
|
|
135
|
+
discard_write_buffer
|
|
126
136
|
abort_request!
|
|
127
137
|
end
|
|
128
138
|
end
|
|
129
139
|
|
|
140
|
+
# A forked child shares the parent's connection, so it closes only its
|
|
141
|
+
# own file descriptor. Closing the TLS socket itself would send
|
|
142
|
+
# close_notify and end the parent's TLS session.
|
|
143
|
+
def close_socket
|
|
144
|
+
fork_detected? ? @sock.to_io.close : @sock.close
|
|
145
|
+
end
|
|
146
|
+
|
|
130
147
|
def connected?
|
|
131
148
|
!@sock.nil?
|
|
132
149
|
end
|
|
133
150
|
|
|
151
|
+
# True when a quiet request was written on this connection and its
|
|
152
|
+
# replies (memcached only suppresses some of them) haven't been
|
|
153
|
+
# consumed by a noop yet.
|
|
154
|
+
def quiet_responses_pending?
|
|
155
|
+
@quiet_responses_pending
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def mark_quiet_responses_pending!
|
|
159
|
+
@quiet_responses_pending = true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def clear_quiet_responses_pending!
|
|
163
|
+
@quiet_responses_pending = false
|
|
164
|
+
end
|
|
165
|
+
|
|
134
166
|
def request_in_progress?
|
|
135
167
|
@request_in_progress
|
|
136
168
|
end
|
|
@@ -145,6 +177,11 @@ module Dalli
|
|
|
145
177
|
raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless @request_in_progress
|
|
146
178
|
|
|
147
179
|
@request_in_progress = false
|
|
180
|
+
# A completed request is what proves the server healthy again, so the
|
|
181
|
+
# failure count resets here rather than on reconnect: a server that
|
|
182
|
+
# accepts connections but never answers would otherwise reset it on
|
|
183
|
+
# every retry and never reach socket_max_failures.
|
|
184
|
+
@fail_count = 0
|
|
148
185
|
end
|
|
149
186
|
|
|
150
187
|
def abort_request!
|
|
@@ -152,6 +189,7 @@ module Dalli
|
|
|
152
189
|
end
|
|
153
190
|
|
|
154
191
|
def read_line
|
|
192
|
+
flush_write_buffer
|
|
155
193
|
data = @sock.gets("\r\n")
|
|
156
194
|
error_on_request!('EOF in read_line') if data.nil?
|
|
157
195
|
data
|
|
@@ -161,14 +199,29 @@ module Dalli
|
|
|
161
199
|
|
|
162
200
|
# JRuby doesn't support IO#timeout=, so use custom readfull implementation
|
|
163
201
|
# CRuby 3.3+ has IO#timeout= which makes IO#read work with timeouts
|
|
202
|
+
# memcached can't store an item larger than 1 GiB (its -I maximum), so a
|
|
203
|
+
# reply claiming more (or a negative size) is malformed or hostile.
|
|
204
|
+
# IO#read allocates the full count up front, so check before reading.
|
|
205
|
+
MAX_READ_BYTES = (1024 * 1024 * 1024) + 2 # plus the value's trailing "\r\n"
|
|
206
|
+
|
|
207
|
+
def check_read_size!(count)
|
|
208
|
+
return if count.between?(0, MAX_READ_BYTES)
|
|
209
|
+
|
|
210
|
+
raise Dalli::DalliError, "Reply size #{count} from #{name} is out of range"
|
|
211
|
+
end
|
|
212
|
+
|
|
164
213
|
if RUBY_ENGINE == 'jruby'
|
|
165
214
|
def read(count)
|
|
215
|
+
check_read_size!(count)
|
|
216
|
+
flush_write_buffer
|
|
166
217
|
@sock.readfull(count)
|
|
167
218
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
168
219
|
error_on_request!(e)
|
|
169
220
|
end
|
|
170
221
|
else
|
|
171
222
|
def read(count)
|
|
223
|
+
check_read_size!(count)
|
|
224
|
+
flush_write_buffer
|
|
172
225
|
read_bytes(count)
|
|
173
226
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
174
227
|
error_on_request!(e)
|
|
@@ -179,22 +232,34 @@ module Dalli
|
|
|
179
232
|
# at the call site.
|
|
180
233
|
alias read_exact read
|
|
181
234
|
|
|
235
|
+
# Requests are buffered here rather than in the socket's own IO buffer
|
|
236
|
+
# (the socket is sync). Ruby flushes an IO's write buffer when any process
|
|
237
|
+
# holding it closes or finalizes the IO, so after a fork a child could
|
|
238
|
+
# send bytes the parent had buffered but not sent yet: a quiet write would
|
|
239
|
+
# run twice, or the parent's replies would shift onto the wrong requests.
|
|
240
|
+
# A forked child discards this buffer instead. Like IO's own buffer, it
|
|
241
|
+
# is sent once it grows past this size, or when a reply is read.
|
|
242
|
+
WRITE_BUFFER_FLUSH_BYTES = 64 * 1024
|
|
243
|
+
|
|
182
244
|
def write(bytes)
|
|
183
|
-
@
|
|
245
|
+
@write_buffer << bytes
|
|
246
|
+
@write_buffer_bytes += bytes.bytesize
|
|
247
|
+
flush_write_buffer if @write_buffer_bytes >= WRITE_BUFFER_FLUSH_BYTES
|
|
248
|
+
bytes.bytesize
|
|
184
249
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
|
|
185
250
|
error_on_request!(e)
|
|
186
251
|
end
|
|
187
252
|
|
|
188
253
|
def flush
|
|
189
|
-
|
|
254
|
+
flush_write_buffer
|
|
190
255
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
|
|
191
256
|
error_on_request!(e)
|
|
192
257
|
end
|
|
193
258
|
|
|
194
259
|
def flushed_write(bytes)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
260
|
+
@write_buffer << bytes
|
|
261
|
+
flush_write_buffer
|
|
262
|
+
bytes.bytesize
|
|
198
263
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
|
|
199
264
|
error_on_request!(e)
|
|
200
265
|
end
|
|
@@ -202,6 +267,7 @@ module Dalli
|
|
|
202
267
|
# Non-blocking read. Here to support the operation
|
|
203
268
|
# of the get_multi operation
|
|
204
269
|
def read_available(...)
|
|
270
|
+
flush_write_buffer
|
|
205
271
|
@sock.read_available(...)
|
|
206
272
|
end
|
|
207
273
|
|
|
@@ -228,8 +294,9 @@ module Dalli
|
|
|
228
294
|
raise Dalli::RetryableNetworkError, message
|
|
229
295
|
end
|
|
230
296
|
|
|
297
|
+
# Called on connect. Deliberately leaves @fail_count alone; see
|
|
298
|
+
# finish_request!.
|
|
231
299
|
def reset_down_info
|
|
232
|
-
@fail_count = 0
|
|
233
300
|
@down_at = nil
|
|
234
301
|
@last_down_at = nil
|
|
235
302
|
@msg = nil
|
|
@@ -254,7 +321,8 @@ module Dalli
|
|
|
254
321
|
def reconnect_on_fork
|
|
255
322
|
message = 'Fork detected, re-connecting child process...'
|
|
256
323
|
Dalli.logger.info { message }
|
|
257
|
-
#
|
|
324
|
+
# Drops anything the parent had buffered, closes the inherited socket
|
|
325
|
+
# without touching the parent's connection, and reconnects immediately
|
|
258
326
|
close
|
|
259
327
|
establish_connection
|
|
260
328
|
end
|
|
@@ -284,6 +352,37 @@ module Dalli
|
|
|
284
352
|
|
|
285
353
|
private
|
|
286
354
|
|
|
355
|
+
# One write per flush. A TLS socket sends each argument of
|
|
356
|
+
# write(*parts) as its own record and system call, so the parts are
|
|
357
|
+
# joined first.
|
|
358
|
+
def flush_write_buffer
|
|
359
|
+
return if @write_buffer.empty?
|
|
360
|
+
|
|
361
|
+
@sock.write(@write_buffer.size == 1 ? @write_buffer.first : joined_write_buffer)
|
|
362
|
+
discard_write_buffer
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Joined as bytes: requests can mix UTF-8 and binary strings, which
|
|
366
|
+
# String#<< refuses to combine when both hold non-ASCII bytes
|
|
367
|
+
if String.method_defined?(:append_as_bytes)
|
|
368
|
+
def joined_write_buffer
|
|
369
|
+
out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
|
|
370
|
+
@write_buffer.each { |part| out.append_as_bytes(part) }
|
|
371
|
+
out
|
|
372
|
+
end
|
|
373
|
+
else
|
|
374
|
+
def joined_write_buffer
|
|
375
|
+
out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
|
|
376
|
+
@write_buffer.each { |part| out << part.b }
|
|
377
|
+
out
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def discard_write_buffer
|
|
382
|
+
@write_buffer.clear
|
|
383
|
+
@write_buffer_bytes = 0
|
|
384
|
+
end
|
|
385
|
+
|
|
287
386
|
# Reads exactly `count` bytes. IO#read(count) on a blocking socket blocks
|
|
288
387
|
# until it has `count` bytes, accumulating across TCP chunks internally,
|
|
289
388
|
# and only hands back a shorter (or nil) buffer when the stream hits EOF.
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -38,20 +38,20 @@ module Dalli
|
|
|
38
38
|
RequestFormatter.plain_meta_get(key, skip_flags)
|
|
39
39
|
end
|
|
40
40
|
flushed_write(req)
|
|
41
|
-
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
41
|
+
response_processor.meta_get_with_value(cache_nils: cache_nils?(options), raw: raw_request?(options))
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def quiet_get_request(key, options = nil)
|
|
45
45
|
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
46
|
-
RequestFormatter.meta_get(key: key, return_cas: true, quiet: true, skip_flags:
|
|
46
|
+
RequestFormatter.meta_get(key: key, return_cas: true, quiet: true, skip_flags: raw_request?(options),
|
|
47
47
|
**routing_token_kwargs(options))
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# Same requests as quiet_get_request for each key, built in one pass
|
|
51
51
|
# and without the trailing noop.
|
|
52
52
|
def quiet_get_requests(keys, options = nil, return_cas: true)
|
|
53
|
-
RequestFormatter.multi_meta_get(keys, skip_flags:
|
|
54
|
-
**routing_token_kwargs(options))
|
|
53
|
+
RequestFormatter.multi_meta_get(keys, skip_flags: raw_request?(options), return_cas: return_cas,
|
|
54
|
+
terminate: false, **routing_token_kwargs(options))
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def gat(key, ttl, options = nil)
|
|
@@ -59,7 +59,7 @@ module Dalli
|
|
|
59
59
|
skip_flags = raw_mode? || (options && options[:raw])
|
|
60
60
|
req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags, **routing_token_kwargs(options))
|
|
61
61
|
flushed_write(req)
|
|
62
|
-
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
62
|
+
response_processor.meta_get_with_value(cache_nils: cache_nils?(options), raw: raw_request?(options))
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def touch(key, ttl)
|
|
@@ -72,9 +72,10 @@ module Dalli
|
|
|
72
72
|
# TODO: This is confusing, as there's a cas command in memcached
|
|
73
73
|
# and this isn't it. Maybe rename? Maybe eliminate?
|
|
74
74
|
def cas(key, options = nil)
|
|
75
|
-
req = RequestFormatter.meta_get(key: key, value: true, return_cas: true,
|
|
75
|
+
req = RequestFormatter.meta_get(key: key, value: true, return_cas: true, skip_flags: raw_request?(options),
|
|
76
|
+
**routing_token_kwargs(options))
|
|
76
77
|
flushed_write(req)
|
|
77
|
-
response_processor.meta_get_with_value_and_cas
|
|
78
|
+
response_processor.meta_get_with_value_and_cas(raw: raw_request?(options))
|
|
78
79
|
end
|
|
79
80
|
|
|
80
81
|
# Comprehensive meta get with support for all metadata flags.
|
|
@@ -103,7 +104,7 @@ module Dalli
|
|
|
103
104
|
# - :last_access - seconds since last access (only if return_last_access: true)
|
|
104
105
|
def meta_get(key, options = {})
|
|
105
106
|
req = RequestFormatter.meta_get(
|
|
106
|
-
key: key, value: true, return_cas: true,
|
|
107
|
+
key: key, value: true, return_cas: true, skip_flags: raw_request?(options),
|
|
107
108
|
vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
|
|
108
109
|
return_hit_status: options[:return_hit_status],
|
|
109
110
|
return_last_access: options[:return_last_access],
|
|
@@ -115,7 +116,7 @@ module Dalli
|
|
|
115
116
|
response_processor.meta_get_with_metadata(
|
|
116
117
|
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
117
118
|
return_last_access: options[:return_last_access],
|
|
118
|
-
return_ttl_remaining: options[:return_ttl_remaining]
|
|
119
|
+
return_ttl_remaining: options[:return_ttl_remaining], raw: raw_request?(options)
|
|
119
120
|
)
|
|
120
121
|
end
|
|
121
122
|
|
|
@@ -157,12 +158,17 @@ module Dalli
|
|
|
157
158
|
def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
|
|
158
159
|
(value, bitflags) = @value_marshaller.store(key, raw_value, options)
|
|
159
160
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
# eql?(0): only Integer 0 means "no CAS" here; anything else takes the general path
|
|
162
|
+
req = if mode == :set && !quiet && (cas.nil? || cas.eql?(0)) && !routing_tokens?(options)
|
|
163
|
+
RequestFormatter.plain_meta_set(key, value.bytesize, bitflags, ttl)
|
|
164
|
+
else
|
|
165
|
+
RequestFormatter.meta_set(key: key, value: value,
|
|
166
|
+
bitflags: bitflags, cas: cas,
|
|
167
|
+
ttl: ttl, mode: mode, quiet: quiet,
|
|
168
|
+
**routing_token_kwargs(options))
|
|
169
|
+
end
|
|
164
170
|
write("#{req}#{value}#{TERMINATOR}")
|
|
165
|
-
|
|
171
|
+
finish_write(quiet)
|
|
166
172
|
end
|
|
167
173
|
# rubocop:enable Metrics/ParameterLists
|
|
168
174
|
|
|
@@ -183,7 +189,7 @@ module Dalli
|
|
|
183
189
|
cas: cas, ttl: ttl, mode: mode, quiet: quiet?,
|
|
184
190
|
**routing_token_kwargs(options))
|
|
185
191
|
write("#{req}#{value}#{TERMINATOR}")
|
|
186
|
-
|
|
192
|
+
finish_write(quiet?)
|
|
187
193
|
end
|
|
188
194
|
|
|
189
195
|
# Delete Commands
|
|
@@ -191,10 +197,15 @@ module Dalli
|
|
|
191
197
|
# `options` supports the meta-delete keys :invalidate, :tombstone_ttl and
|
|
192
198
|
# :drop_value, plus :p_token/:l_token; see Dalli::Client#delete.
|
|
193
199
|
def delete(key, cas, options = nil)
|
|
194
|
-
|
|
195
|
-
|
|
200
|
+
# Fast path for a plain delete: no options, no CAS, not quiet
|
|
201
|
+
req = if options.nil? && (cas.nil? || cas.eql?(0)) && !quiet?
|
|
202
|
+
RequestFormatter.plain_meta_delete(key)
|
|
203
|
+
else
|
|
204
|
+
RequestFormatter.meta_delete(key: key, cas: cas, quiet: quiet?,
|
|
205
|
+
**tombstone_kwargs(options), **routing_token_kwargs(options))
|
|
206
|
+
end
|
|
196
207
|
write(req)
|
|
197
|
-
|
|
208
|
+
finish_write(quiet?)
|
|
198
209
|
response_processor.meta_delete unless quiet?
|
|
199
210
|
end
|
|
200
211
|
|
|
@@ -225,22 +236,34 @@ module Dalli
|
|
|
225
236
|
ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
|
|
226
237
|
write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
227
238
|
quiet: quiet?, **routing_token_kwargs(options)))
|
|
228
|
-
|
|
239
|
+
finish_write(quiet?)
|
|
229
240
|
response_processor.decr_incr unless quiet?
|
|
230
241
|
end
|
|
231
242
|
|
|
232
243
|
# Other Commands
|
|
233
244
|
def flush(delay = 0)
|
|
234
245
|
write(RequestFormatter.flush(delay: delay))
|
|
235
|
-
|
|
246
|
+
finish_write(quiet?)
|
|
236
247
|
response_processor.flush unless quiet?
|
|
237
248
|
end
|
|
238
249
|
|
|
239
250
|
# Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
|
|
240
251
|
# We need to read all the responses at once.
|
|
252
|
+
# A write that expects a reply is flushed so the reply can be read. A write
|
|
253
|
+
# in a quiet block stays buffered until the block's noop, unless
|
|
254
|
+
# defer_drain is on: then no noop follows at the end of the block, so it is
|
|
255
|
+
# flushed right away. (pipelined_set passes quiet: true outside a quiet
|
|
256
|
+
# block; its caller flushes with a terminating noop.)
|
|
257
|
+
def finish_write(quiet)
|
|
258
|
+
@connection_manager.flush if !quiet || (@defer_drain && quiet?)
|
|
259
|
+
end
|
|
260
|
+
|
|
241
261
|
def noop
|
|
242
262
|
write_noop
|
|
243
|
-
response_processor.consume_all_responses_until_mn
|
|
263
|
+
result = response_processor.consume_all_responses_until_mn
|
|
264
|
+
# Everything up to and including the MN has been read
|
|
265
|
+
@connection_manager.clear_quiet_responses_pending!
|
|
266
|
+
result
|
|
244
267
|
end
|
|
245
268
|
|
|
246
269
|
def stats(info = nil)
|
|
@@ -266,7 +289,7 @@ module Dalli
|
|
|
266
289
|
# response parsing to minimize per-key overhead. Avoids the PipelinedGetter
|
|
267
290
|
# machinery (IO.select, response buffering, server grouping).
|
|
268
291
|
def read_multi_req(keys, options = nil)
|
|
269
|
-
is_raw =
|
|
292
|
+
is_raw = raw_request?(options)
|
|
270
293
|
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw, **routing_token_kwargs(options))
|
|
271
294
|
flushed_write(buffer)
|
|
272
295
|
buffer.clear
|
|
@@ -282,7 +305,7 @@ module Dalli
|
|
|
282
305
|
# Shared by both the single-server fast path and PipelinedGetter's
|
|
283
306
|
# per-server-group request, so this one change covers both routes.
|
|
284
307
|
def read_multi_with_metadata_req(keys, options = nil)
|
|
285
|
-
is_raw =
|
|
308
|
+
is_raw = raw_request?(options)
|
|
286
309
|
buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw, return_cas: true,
|
|
287
310
|
**routing_token_kwargs(options))
|
|
288
311
|
flushed_write(buffer)
|
|
@@ -299,7 +322,9 @@ module Dalli
|
|
|
299
322
|
next unless line.start_with?('VA ')
|
|
300
323
|
|
|
301
324
|
tokens = line.chomp!(TERMINATOR).split
|
|
302
|
-
|
|
325
|
+
size = tokens[1].to_i
|
|
326
|
+
response_processor.check_value_size!(size)
|
|
327
|
+
value = @connection_manager.read(size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
303
328
|
stale = response_processor.stale_from_tokens(tokens)
|
|
304
329
|
cas = response_processor.cas_from_tokens(tokens)
|
|
305
330
|
bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
|
|
@@ -313,26 +338,27 @@ module Dalli
|
|
|
313
338
|
|
|
314
339
|
def read_multi_get_responses(is_raw)
|
|
315
340
|
hash = {}
|
|
316
|
-
key_index = is_raw ? 2 : 3
|
|
317
341
|
while (line = @connection_manager.read_line)
|
|
318
342
|
break if line.start_with?('MN')
|
|
319
343
|
next unless line.start_with?('VA ')
|
|
320
344
|
|
|
321
|
-
key, value = parse_multi_get_value(line,
|
|
345
|
+
key, value = parse_multi_get_value(line, is_raw)
|
|
322
346
|
hash[key] = value if key
|
|
323
347
|
end
|
|
324
348
|
hash
|
|
325
349
|
end
|
|
326
350
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
351
|
+
# Reads the "VA <size> [f<flags>] k<key> ..." line in place rather than
|
|
352
|
+
# splitting it into tokens
|
|
353
|
+
def parse_multi_get_value(line, is_raw)
|
|
354
|
+
processor = response_processor
|
|
355
|
+
size = processor.size_from_va_line(line)
|
|
356
|
+
processor.check_value_size!(size)
|
|
357
|
+
value = @connection_manager.read(size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
358
|
+
key = processor.key_from_va_line(line)
|
|
359
|
+
return unless key
|
|
332
360
|
|
|
333
|
-
|
|
334
|
-
key = KeyRegularizer.decode(key) if tokens.include?('b')
|
|
335
|
-
bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
|
|
361
|
+
bitflags = is_raw ? 0 : processor.bitflags_from_va_line(line)
|
|
336
362
|
[key, @value_marshaller.retrieve(value, bitflags)]
|
|
337
363
|
end
|
|
338
364
|
|
|
@@ -63,6 +63,19 @@ module Dalli
|
|
|
63
63
|
skip_flags ? "mg #{encoded_key(key)} v#{TERMINATOR}" : "mg #{encoded_key(key)} v f#{TERMINATOR}"
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Fast path for a plain set: mode :set, not quiet, no CAS and no routing
|
|
67
|
+
# tokens. Produces the same bytes as meta_set(key:, value:, bitflags:, ttl:)
|
|
68
|
+
# without the keyword-argument handling and incremental string building.
|
|
69
|
+
def plain_meta_set(key, value_bytesize, bitflags, ttl)
|
|
70
|
+
flags = bitflags ? " F#{bitflags}" : ''
|
|
71
|
+
flags += " T#{integer_flag(:ttl, ttl)}" if ttl
|
|
72
|
+
if KeyRegularizer.required?(key)
|
|
73
|
+
"ms #{KeyRegularizer.encode(key)} #{value_bytesize} c b#{flags} MS#{TERMINATOR}"
|
|
74
|
+
else
|
|
75
|
+
"ms #{key} #{value_bytesize} c#{flags} MS#{TERMINATOR}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
66
79
|
# Pass terminate: false to leave off the trailing noop, for callers
|
|
67
80
|
# (the pipelined get) that send it separately.
|
|
68
81
|
def multi_meta_get(keys, skip_flags: false, return_cas: false, terminate: true, p_token: nil, l_token: nil)
|
|
@@ -158,6 +171,11 @@ module Dalli
|
|
|
158
171
|
cmd << TERMINATOR
|
|
159
172
|
end
|
|
160
173
|
|
|
174
|
+
# Same bytes as meta_delete(key: key), without the keyword-argument handling
|
|
175
|
+
def plain_meta_delete(key)
|
|
176
|
+
"md #{encoded_key(key)}#{TERMINATOR}"
|
|
177
|
+
end
|
|
178
|
+
|
|
161
179
|
# Tombstone and routing-token flags apply to every key in the batch;
|
|
162
180
|
# see meta_delete.
|
|
163
181
|
def multi_meta_delete(keys, stale: false, ttl: nil, drop_value: false, p_token: nil, l_token: nil)
|
|
@@ -200,11 +218,11 @@ module Dalli
|
|
|
200
218
|
# specifications to a proxy or router inbetween a client and a
|
|
201
219
|
# memcached daemon."
|
|
202
220
|
#
|
|
203
|
-
# Empty / nil tokens are treated as no-ops.
|
|
204
|
-
# rejected with ArgumentError
|
|
205
|
-
# wire-protocol injection
|
|
206
|
-
#
|
|
207
|
-
#
|
|
221
|
+
# Empty / nil tokens are treated as no-ops. Whitespace and control
|
|
222
|
+
# characters are rejected with ArgumentError so a token can't be used
|
|
223
|
+
# for wire-protocol injection: CRLF would add a second command (e.g.
|
|
224
|
+
# "foo\r\nflush_all\r\n"), and a space would add meta flags to this one
|
|
225
|
+
# (e.g. "foo T1" changing an item's TTL on a read).
|
|
208
226
|
def routing_tokens(p_token: nil, l_token: nil)
|
|
209
227
|
# Only an empty *String* is a no-op. Checking respond_to?(:empty?)
|
|
210
228
|
# instead would also swallow p_token: [] / {} before the type check
|
|
@@ -261,15 +279,18 @@ module Dalli
|
|
|
261
279
|
|
|
262
280
|
private
|
|
263
281
|
|
|
264
|
-
# Disallowed bytes:
|
|
265
|
-
#
|
|
266
|
-
|
|
282
|
+
# Disallowed bytes: whitespace and control characters. CR, LF or NUL
|
|
283
|
+
# would let a token inject a second command; a space would let it add
|
|
284
|
+
# meta flags (T, N, I, q, ...) to the request it rides on.
|
|
285
|
+
ROUTING_TOKEN_FORBIDDEN = /[\x00-\x20\x7F]/
|
|
267
286
|
private_constant :ROUTING_TOKEN_FORBIDDEN
|
|
268
287
|
|
|
269
288
|
def validate_routing_token!(name, value)
|
|
270
289
|
return if value.nil?
|
|
271
290
|
raise ArgumentError, "#{name} must be a String, got #{value.class}" unless value.is_a?(String)
|
|
272
|
-
|
|
291
|
+
return unless value.match?(ROUTING_TOKEN_FORBIDDEN)
|
|
292
|
+
|
|
293
|
+
raise ArgumentError, "#{name} must not contain whitespace or control characters"
|
|
273
294
|
end
|
|
274
295
|
|
|
275
296
|
# Numeric flag values are written straight into the command line, so a
|
|
@@ -15,6 +15,9 @@ module Dalli
|
|
|
15
15
|
# when advancing through parsed responses.
|
|
16
16
|
##
|
|
17
17
|
class ResponseBuffer
|
|
18
|
+
# Whether the current pipeline was requested with raw: true
|
|
19
|
+
attr_writer :raw
|
|
20
|
+
|
|
18
21
|
def initialize(io_source, response_processor)
|
|
19
22
|
@io_source = io_source
|
|
20
23
|
@response_processor = response_processor
|
|
@@ -39,7 +42,7 @@ module Dalli
|
|
|
39
42
|
# Attempts to process a single response from the buffer,
|
|
40
43
|
# advancing the offset past the consumed bytes.
|
|
41
44
|
def process_single_getk_response
|
|
42
|
-
response = @response_processor.getk_response_from_buffer(@buffer, @offset)
|
|
45
|
+
response = @response_processor.getk_response_from_buffer(@buffer, @offset, raw: @raw || false)
|
|
43
46
|
@offset += response.pop
|
|
44
47
|
response
|
|
45
48
|
end
|