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