dalli 5.0.8 → 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 +48 -18
- data/Gemfile +1 -1
- data/README.md +6 -1
- data/lib/dalli/client.rb +259 -68
- data/lib/dalli/pipelined_deleter.rb +29 -12
- data/lib/dalli/pipelined_getter.rb +57 -8
- data/lib/dalli/pipelined_setter.rb +15 -3
- data/lib/dalli/protocol/base.rb +48 -4
- data/lib/dalli/protocol/key_regularizer.rb +18 -1
- data/lib/dalli/protocol/meta.rb +85 -31
- data/lib/dalli/protocol/request_formatter.rb +110 -32
- data/lib/dalli/protocol/response_processor.rb +25 -6
- data/lib/dalli/socket.rb +4 -2
- data/lib/dalli/version.rb +2 -2
- data/lib/rack/session/dalli.rb +2 -1
- metadata +2 -2
data/lib/dalli/client.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'digest/md5'
|
|
4
4
|
|
|
5
|
+
# encoding: ascii
|
|
5
6
|
module Dalli
|
|
6
7
|
##
|
|
7
8
|
# Dalli::Client is the main class which developers will use to interact with
|
|
@@ -67,6 +68,7 @@ module Dalli
|
|
|
67
68
|
# Get the value associated with the key.
|
|
68
69
|
# If a value is not found, then +nil+ is returned.
|
|
69
70
|
def get(key, req_options = nil)
|
|
71
|
+
validate_routing_tokens!(req_options)
|
|
70
72
|
perform(:get, key, req_options)
|
|
71
73
|
end
|
|
72
74
|
|
|
@@ -74,8 +76,9 @@ module Dalli
|
|
|
74
76
|
# Gat (get and touch) fetch an item and simultaneously update its expiration time.
|
|
75
77
|
#
|
|
76
78
|
# If a value is not found, then +nil+ is returned.
|
|
77
|
-
def gat(key, ttl = nil)
|
|
78
|
-
|
|
79
|
+
def gat(key, ttl = nil, req_options = nil)
|
|
80
|
+
validate_routing_tokens!(req_options)
|
|
81
|
+
perform(:gat, key, ttl_or_default(ttl), req_options)
|
|
79
82
|
end
|
|
80
83
|
|
|
81
84
|
##
|
|
@@ -90,8 +93,9 @@ module Dalli
|
|
|
90
93
|
##
|
|
91
94
|
# Get the value and CAS ID associated with the key. If a block is provided,
|
|
92
95
|
# value and CAS will be passed to the block.
|
|
93
|
-
def get_cas(key)
|
|
94
|
-
(
|
|
96
|
+
def get_cas(key, req_options = nil)
|
|
97
|
+
validate_routing_tokens!(req_options)
|
|
98
|
+
(value, cas) = perform(:cas, key, req_options)
|
|
95
99
|
return [value, cas] unless block_given?
|
|
96
100
|
|
|
97
101
|
yield value, cas
|
|
@@ -105,17 +109,23 @@ module Dalli
|
|
|
105
109
|
# - :return_cas [Boolean] return the CAS value (default: true)
|
|
106
110
|
# - :return_hit_status [Boolean] return whether item was previously accessed
|
|
107
111
|
# - :return_last_access [Boolean] return seconds since last access
|
|
112
|
+
# - :return_ttl_remaining [Boolean] return seconds of TTL remaining (-1 if no TTL)
|
|
108
113
|
# - :skip_lru_bump [Boolean] don't bump LRU or update access stats
|
|
109
114
|
#
|
|
110
115
|
# @return [Hash] containing:
|
|
111
116
|
# - :value - the cached value (or nil on miss)
|
|
112
117
|
# - :cas - the CAS value
|
|
118
|
+
# - :miss - true when the key does not exist. Always present. Prefer it
|
|
119
|
+
# over a nil :value, which cannot distinguish a miss from a stored nil
|
|
120
|
+
# under cache_nils
|
|
113
121
|
# - :hit_before - true/false if previously accessed (only if return_hit_status: true)
|
|
114
122
|
# - :last_access - seconds since last access (only if return_last_access: true)
|
|
123
|
+
# - :ttl_remaining - seconds of TTL remaining, -1 when the item has no
|
|
124
|
+
# expiry (only if return_ttl_remaining: true)
|
|
115
125
|
#
|
|
116
126
|
# @example Get with hit status
|
|
117
127
|
# result = client.get_with_metadata('key', return_hit_status: true)
|
|
118
|
-
# # => { value: "data", cas: 123, hit_before: true }
|
|
128
|
+
# # => { value: "data", cas: 123, miss: false, hit_before: true }
|
|
119
129
|
#
|
|
120
130
|
# @example Get with all metadata without affecting LRU
|
|
121
131
|
# result = client.get_with_metadata('key',
|
|
@@ -126,6 +136,7 @@ module Dalli
|
|
|
126
136
|
# # => { value: "data", cas: 123, hit_before: true, last_access: 42 }
|
|
127
137
|
#
|
|
128
138
|
def get_with_metadata(key, options = {})
|
|
139
|
+
validate_routing_tokens!(options)
|
|
129
140
|
key = key.to_s
|
|
130
141
|
key = @key_manager.validate_key(key)
|
|
131
142
|
|
|
@@ -143,18 +154,81 @@ module Dalli
|
|
|
143
154
|
# Fetch multiple keys efficiently.
|
|
144
155
|
# If a block is given, yields key/value pairs one at a time.
|
|
145
156
|
# Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
|
|
146
|
-
#
|
|
147
|
-
|
|
157
|
+
#
|
|
158
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
159
|
+
#
|
|
160
|
+
# A transient network error is retried automatically. If a server remains
|
|
161
|
+
# unreachable after retrying, raises Dalli::NetworkError rather than
|
|
162
|
+
# silently omitting that server's keys from the result.
|
|
163
|
+
#
|
|
164
|
+
# @raise [Dalli::NetworkError] if a server is unreachable after retrying
|
|
165
|
+
# rubocop:disable Style/ExplicitBlockArgument
|
|
166
|
+
def get_multi(*keys, req_options: nil)
|
|
148
167
|
keys.flatten!
|
|
149
168
|
keys.compact!
|
|
150
169
|
return {} if keys.empty?
|
|
151
170
|
|
|
171
|
+
validate_routing_tokens!(req_options)
|
|
172
|
+
|
|
152
173
|
if block_given?
|
|
153
|
-
get_multi_yielding(keys) { |k, v| yield k, v }
|
|
174
|
+
get_multi_yielding(keys, req_options) { |k, v| yield k, v }
|
|
154
175
|
else
|
|
155
|
-
get_multi_hash(keys)
|
|
176
|
+
get_multi_hash(keys, req_options)
|
|
156
177
|
end
|
|
157
178
|
end
|
|
179
|
+
# rubocop:enable Style/ExplicitBlockArgument
|
|
180
|
+
|
|
181
|
+
##
|
|
182
|
+
# Fetch multiple keys efficiently, returning a stale-aware metadata Hash per
|
|
183
|
+
# key. If a block is given, yields key/metadata pairs one at a time.
|
|
184
|
+
#
|
|
185
|
+
# Like #get_multi and #get_multi_cas, keys that were not found are omitted
|
|
186
|
+
# from the result -- absence is the miss. A tombstoned item (see #delete
|
|
187
|
+
# with the meta protocol's invalidate flag) is *not* a miss: it is returned
|
|
188
|
+
# with stale: true, possibly with an empty value, which is the distinction
|
|
189
|
+
# stale-aware callers need.
|
|
190
|
+
#
|
|
191
|
+
# client.get_multi_with_metadata('a', 'b', 'absent')
|
|
192
|
+
# # => { 'a' => { value: 'v', cas: 12, stale: false, miss: false },
|
|
193
|
+
# # 'b' => { value: '', cas: 13, stale: true, miss: false } }
|
|
194
|
+
#
|
|
195
|
+
# Missing keys are `requested - result.keys`.
|
|
196
|
+
#
|
|
197
|
+
# Result key order matches request order only when every key lands on the
|
|
198
|
+
# same server; across multiple servers it follows per-server response
|
|
199
|
+
# order instead, the same as #get_multi.
|
|
200
|
+
#
|
|
201
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
202
|
+
#
|
|
203
|
+
# @param keys [Array<String>] the keys to fetch
|
|
204
|
+
# @param req_options [Hash, nil] routing-token options
|
|
205
|
+
# @return [Hash] key => { value:, cas:, stale:, miss: }
|
|
206
|
+
def get_multi_with_metadata(*keys, req_options: nil, &block)
|
|
207
|
+
keys.flatten!
|
|
208
|
+
keys.compact!
|
|
209
|
+
return {} if keys.empty?
|
|
210
|
+
|
|
211
|
+
validate_routing_tokens!(req_options)
|
|
212
|
+
|
|
213
|
+
results = Instrumentation.trace('get_multi_with_metadata',
|
|
214
|
+
multi_trace_attrs('get_multi_with_metadata', keys.size, keys)) do
|
|
215
|
+
if ring.servers.size == 1
|
|
216
|
+
single_server_get_multi_with_metadata(keys, req_options)
|
|
217
|
+
else
|
|
218
|
+
pipelined_getter.process_with_metadata(keys, req_options)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
if block
|
|
223
|
+
results.each(&block)
|
|
224
|
+
# Matches get_multi/get_multi_cas: nil when a block is given, so
|
|
225
|
+
# callers can't come to depend on a return value that block-form
|
|
226
|
+
# get_multi never provided.
|
|
227
|
+
return nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
results
|
|
231
|
+
end
|
|
158
232
|
|
|
159
233
|
##
|
|
160
234
|
# Fetch multiple keys efficiently, including available metadata such as CAS.
|
|
@@ -162,12 +236,16 @@ module Dalli
|
|
|
162
236
|
# [value, cas_id]
|
|
163
237
|
# If no block is given, returns a hash of
|
|
164
238
|
# { 'key' => [value, cas_id] }
|
|
165
|
-
|
|
239
|
+
#
|
|
240
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
241
|
+
def get_multi_cas(*keys, req_options: nil)
|
|
242
|
+
validate_routing_tokens!(req_options)
|
|
243
|
+
|
|
166
244
|
if block_given?
|
|
167
|
-
pipelined_getter.process(keys) { |*args| yield(*args) }
|
|
245
|
+
pipelined_getter.process(keys, req_options) { |*args| yield(*args) }
|
|
168
246
|
else
|
|
169
247
|
{}.tap do |hash|
|
|
170
|
-
pipelined_getter.process(keys) { |k, data| hash[k] = data }
|
|
248
|
+
pipelined_getter.process(keys, req_options) { |k, data| hash[k] = data }
|
|
171
249
|
end
|
|
172
250
|
end
|
|
173
251
|
end
|
|
@@ -223,8 +301,7 @@ module Dalli
|
|
|
223
301
|
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
|
|
224
302
|
raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
|
|
225
303
|
|
|
226
|
-
|
|
227
|
-
validate_integer!(:recache_threshold, recache_threshold)
|
|
304
|
+
validate_routing_tokens!(req_options)
|
|
228
305
|
key = key.to_s
|
|
229
306
|
key = @key_manager.validate_key(key)
|
|
230
307
|
|
|
@@ -300,16 +377,24 @@ module Dalli
|
|
|
300
377
|
# This method is more efficient than calling set() in a loop because
|
|
301
378
|
# it batches requests by server and uses quiet mode.
|
|
302
379
|
#
|
|
380
|
+
# A transient network error is retried automatically. If a server remains
|
|
381
|
+
# unreachable after retrying, raises Dalli::NetworkError; keys already
|
|
382
|
+
# sent to other servers before the error are not rolled back.
|
|
383
|
+
#
|
|
303
384
|
# @param hash [Hash] key-value pairs to set
|
|
304
385
|
# @param ttl [Integer] time-to-live in seconds (optional, uses default if not provided)
|
|
305
|
-
# @param req_options [Hash] options passed to each set operation
|
|
386
|
+
# @param req_options [Hash] options passed to each set operation; accepts
|
|
387
|
+
# :p_token/:l_token, applied to every key in the batch
|
|
306
388
|
# @return [void]
|
|
389
|
+
# @raise [Dalli::NetworkError] if a server is unreachable after retrying
|
|
307
390
|
#
|
|
308
391
|
# Example:
|
|
309
392
|
# client.set_multi({ 'key1' => 'value1', 'key2' => 'value2' }, 300)
|
|
310
393
|
def set_multi(hash, ttl = nil, req_options = nil)
|
|
311
394
|
return if hash.empty?
|
|
312
395
|
|
|
396
|
+
validate_routing_tokens!(req_options)
|
|
397
|
+
|
|
313
398
|
Instrumentation.trace('set_multi', multi_trace_attrs('set_multi', hash.size, hash.keys)) do
|
|
314
399
|
if ring.servers.size == 1
|
|
315
400
|
single_server_set_multi(hash, ttl_or_default(ttl), req_options)
|
|
@@ -323,6 +408,7 @@ module Dalli
|
|
|
323
408
|
# Set the key-value pair, verifying existing CAS.
|
|
324
409
|
# Returns the resulting CAS value if succeeded, and falsy otherwise.
|
|
325
410
|
def set_cas(key, value, cas, ttl = nil, req_options = nil)
|
|
411
|
+
validate_routing_tokens!(req_options)
|
|
326
412
|
perform(:set, key, value, ttl_or_default(ttl), cas, req_options)
|
|
327
413
|
end
|
|
328
414
|
|
|
@@ -330,6 +416,7 @@ module Dalli
|
|
|
330
416
|
# Conditionally add a key/value pair, if the key does not already exist
|
|
331
417
|
# on the server. Returns truthy if the operation succeeded.
|
|
332
418
|
def add(key, value, ttl = nil, req_options = nil)
|
|
419
|
+
validate_routing_tokens!(req_options)
|
|
333
420
|
perform(:add, key, value, ttl_or_default(ttl), req_options)
|
|
334
421
|
end
|
|
335
422
|
|
|
@@ -345,17 +432,49 @@ module Dalli
|
|
|
345
432
|
# key already exists on the server. Returns the new CAS value if the
|
|
346
433
|
# operation succeeded, or falsy otherwise.
|
|
347
434
|
def replace_cas(key, value, cas, ttl = nil, req_options = nil)
|
|
435
|
+
validate_routing_tokens!(req_options)
|
|
348
436
|
perform(:replace, key, value, ttl_or_default(ttl), cas, req_options)
|
|
349
437
|
end
|
|
350
438
|
|
|
351
439
|
# Delete a key/value pair, verifying existing CAS.
|
|
352
440
|
# Returns true if succeeded, and falsy otherwise.
|
|
353
|
-
|
|
354
|
-
|
|
441
|
+
# Delete a key, optionally with a CAS check.
|
|
442
|
+
#
|
|
443
|
+
# `req_options` accepts the same meta-delete options as #delete.
|
|
444
|
+
def delete_cas(key, cas = 0, req_options = nil)
|
|
445
|
+
validate_delete_options!(req_options)
|
|
446
|
+
validate_routing_tokens!(req_options)
|
|
447
|
+
perform(:delete, key, cas, req_options)
|
|
355
448
|
end
|
|
356
449
|
|
|
357
|
-
|
|
358
|
-
|
|
450
|
+
##
|
|
451
|
+
# Delete a key.
|
|
452
|
+
#
|
|
453
|
+
# `req_options` may include the memcached meta-delete options:
|
|
454
|
+
#
|
|
455
|
+
# - `:invalidate` (Boolean) — mark the item stale instead of removing it.
|
|
456
|
+
# This is the tombstone: readers see `stale: true` from
|
|
457
|
+
# #get_with_metadata and #get_multi_with_metadata, and the existing value
|
|
458
|
+
# is still readable unless `:drop_value` is also set. A tombstoned key is
|
|
459
|
+
# *not* a miss, which lets a reader tell "another process is repopulating
|
|
460
|
+
# this" apart from "this was never here".
|
|
461
|
+
# - `:tombstone_ttl` (Integer seconds) — how long the stale marker lives.
|
|
462
|
+
# Requires `:invalidate`; memcached only honors the TTL on a delete when
|
|
463
|
+
# it accompanies the invalidate flag, so passing it alone raises
|
|
464
|
+
# ArgumentError rather than sending a request the server would treat
|
|
465
|
+
# differently than intended. Once it elapses, reads see a miss.
|
|
466
|
+
# - `:drop_value` (Boolean) — remove the item's value but leave the item, so
|
|
467
|
+
# a tombstone need not retain the old payload. On its own it is not a
|
|
468
|
+
# tombstone: reads are an ordinary hit with an empty value.
|
|
469
|
+
# - `:p_token`/`:l_token` (String) — opaque routing tokens for an
|
|
470
|
+
# intermediate proxy or router; see #get.
|
|
471
|
+
#
|
|
472
|
+
# dc.delete('key', invalidate: true, tombstone_ttl: 30, drop_value: true)
|
|
473
|
+
#
|
|
474
|
+
# @param key [String] the key to delete
|
|
475
|
+
# @param req_options [Hash, nil] meta-delete options
|
|
476
|
+
def delete(key, req_options = nil)
|
|
477
|
+
delete_cas(key, 0, req_options)
|
|
359
478
|
end
|
|
360
479
|
|
|
361
480
|
##
|
|
@@ -363,22 +482,35 @@ module Dalli
|
|
|
363
482
|
# This method is more efficient than calling delete() in a loop because
|
|
364
483
|
# it batches requests by server and uses quiet mode.
|
|
365
484
|
#
|
|
485
|
+
# `req_options` accepts the same meta-delete options as #delete and applies
|
|
486
|
+
# them to every key in the batch.
|
|
487
|
+
#
|
|
366
488
|
# @param keys [Array<String>] keys to delete
|
|
367
|
-
# @
|
|
368
|
-
#
|
|
369
|
-
#
|
|
370
|
-
#
|
|
489
|
+
# @param req_options [Hash, nil] meta-delete options
|
|
490
|
+
# @return [Integer] the number of keys the server found and acted on. Only a
|
|
491
|
+
# key that did not exist decrements this count, so with `:invalidate` it
|
|
492
|
+
# reports how many keys were tombstoned rather than removed -- the action
|
|
493
|
+
# is whichever one the caller asked for. This is best-effort: a transient
|
|
494
|
+
# network error is retried automatically, and keys handled before the
|
|
495
|
+
# error are not recounted, so the result may under-report when a retry
|
|
496
|
+
# occurs. If a server remains unreachable after retrying, raises
|
|
497
|
+
# Dalli::NetworkError.
|
|
498
|
+
# @raise [Dalli::NetworkError] if a server is unreachable after retrying
|
|
371
499
|
#
|
|
372
500
|
# Example:
|
|
373
501
|
# client.delete_multi(['key1', 'key2', 'key3'])
|
|
374
|
-
|
|
502
|
+
# client.delete_multi(%w[key1 key2], invalidate: true, tombstone_ttl: 30)
|
|
503
|
+
def delete_multi(keys, req_options = nil)
|
|
375
504
|
return 0 if keys.empty?
|
|
376
505
|
|
|
506
|
+
validate_delete_options!(req_options)
|
|
507
|
+
validate_routing_tokens!(req_options)
|
|
508
|
+
|
|
377
509
|
Instrumentation.trace('delete_multi', multi_trace_attrs('delete_multi', keys.size, keys)) do
|
|
378
510
|
if ring.servers.size == 1
|
|
379
|
-
single_server_delete_multi(keys)
|
|
511
|
+
single_server_delete_multi(keys, req_options)
|
|
380
512
|
else
|
|
381
|
-
pipelined_deleter.process(keys)
|
|
513
|
+
pipelined_deleter.process(keys, req_options)
|
|
382
514
|
end
|
|
383
515
|
end
|
|
384
516
|
end
|
|
@@ -386,22 +518,22 @@ module Dalli
|
|
|
386
518
|
##
|
|
387
519
|
# Append value to the value already stored on the server for 'key'.
|
|
388
520
|
# Appending only works for values stored with :raw => true.
|
|
389
|
-
def append(key, value)
|
|
390
|
-
|
|
521
|
+
def append(key, value, req_options = nil)
|
|
522
|
+
validate_routing_tokens!(req_options)
|
|
523
|
+
perform(:append, key, value.to_s, req_options)
|
|
391
524
|
end
|
|
392
525
|
|
|
393
526
|
##
|
|
394
527
|
# Prepend value to the value already stored on the server for 'key'.
|
|
395
528
|
# Prepending only works for values stored with :raw => true.
|
|
396
|
-
def prepend(key, value)
|
|
397
|
-
|
|
529
|
+
def prepend(key, value, req_options = nil)
|
|
530
|
+
validate_routing_tokens!(req_options)
|
|
531
|
+
perform(:prepend, key, value.to_s, req_options)
|
|
398
532
|
end
|
|
399
533
|
|
|
400
534
|
##
|
|
401
535
|
# Incr adds the given amount to the counter on the memcached server.
|
|
402
536
|
# Amt must be a positive integer value.
|
|
403
|
-
# Default, if given, must be an Integer (or a String of decimal digits);
|
|
404
|
-
# anything else raises ArgumentError.
|
|
405
537
|
#
|
|
406
538
|
# If default is nil, the counter must already exist or the operation
|
|
407
539
|
# will fail and will return nil. Otherwise this method will return
|
|
@@ -412,18 +544,16 @@ module Dalli
|
|
|
412
544
|
# #cas.
|
|
413
545
|
#
|
|
414
546
|
# If the value already exists, it must have been set with raw: true
|
|
415
|
-
def incr(key, amt = 1, ttl = nil, default = nil)
|
|
547
|
+
def incr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
416
548
|
check_positive!(amt)
|
|
417
|
-
|
|
549
|
+
validate_routing_tokens!(req_options)
|
|
418
550
|
|
|
419
|
-
perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
|
|
551
|
+
perform(:incr, key, amt.to_i, ttl_or_default(ttl), default, req_options)
|
|
420
552
|
end
|
|
421
553
|
|
|
422
554
|
##
|
|
423
555
|
# Decr subtracts the given amount from the counter on the memcached server.
|
|
424
556
|
# Amt must be a positive integer value.
|
|
425
|
-
# Default, if given, must be an Integer (or a String of decimal digits);
|
|
426
|
-
# anything else raises ArgumentError.
|
|
427
557
|
#
|
|
428
558
|
# memcached counters are unsigned and cannot hold negative values. Calling
|
|
429
559
|
# decr on a counter which is 0 will just return 0.
|
|
@@ -437,11 +567,11 @@ module Dalli
|
|
|
437
567
|
# #cas.
|
|
438
568
|
#
|
|
439
569
|
# If the value already exists, it must have been set with raw: true
|
|
440
|
-
def decr(key, amt = 1, ttl = nil, default = nil)
|
|
570
|
+
def decr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
441
571
|
check_positive!(amt)
|
|
442
|
-
|
|
572
|
+
validate_routing_tokens!(req_options)
|
|
443
573
|
|
|
444
|
-
perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
|
|
574
|
+
perform(:decr, key, amt.to_i, ttl_or_default(ttl), default, req_options)
|
|
445
575
|
end
|
|
446
576
|
|
|
447
577
|
##
|
|
@@ -518,6 +648,30 @@ module Dalli
|
|
|
518
648
|
|
|
519
649
|
private
|
|
520
650
|
|
|
651
|
+
# Raised before the request reaches a server: RequestFormatter enforces the
|
|
652
|
+
# same rule, but reaching it means unwinding through Protocol::Base#request,
|
|
653
|
+
# which logs the failure as unexpected and closes the connection. A caller
|
|
654
|
+
# passing the wrong options should get a clean ArgumentError and keep its
|
|
655
|
+
# connection.
|
|
656
|
+
def validate_delete_options!(req_options)
|
|
657
|
+
return unless req_options.is_a?(Hash)
|
|
658
|
+
|
|
659
|
+
tombstone_ttl = req_options[:tombstone_ttl]
|
|
660
|
+
return unless tombstone_ttl
|
|
661
|
+
|
|
662
|
+
raise ArgumentError, 'tombstone_ttl requires invalidate: true' unless req_options[:invalidate]
|
|
663
|
+
|
|
664
|
+
# tombstone_kwargs coerces this with Integer(), deep inside the request
|
|
665
|
+
# path; validated here first so a bad value raises cleanly instead of
|
|
666
|
+
# unwinding through Protocol::Base#request, which would close the
|
|
667
|
+
# connection on the ArgumentError Integer() raises.
|
|
668
|
+
begin
|
|
669
|
+
Integer(tombstone_ttl)
|
|
670
|
+
rescue ArgumentError, TypeError
|
|
671
|
+
raise ArgumentError, "tombstone_ttl must be an integer, got #{tombstone_ttl.inspect}"
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
521
675
|
def record_hit_miss_metrics(span, key_count, hit_count)
|
|
522
676
|
return unless span
|
|
523
677
|
|
|
@@ -525,10 +679,10 @@ module Dalli
|
|
|
525
679
|
'db.memcached.miss_count' => key_count - hit_count)
|
|
526
680
|
end
|
|
527
681
|
|
|
528
|
-
def get_multi_yielding(keys)
|
|
682
|
+
def get_multi_yielding(keys, req_options = nil)
|
|
529
683
|
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
530
684
|
hit_count = 0
|
|
531
|
-
pipelined_getter.process(keys) do |k, data|
|
|
685
|
+
pipelined_getter.process(keys, req_options) do |k, data|
|
|
532
686
|
hit_count += 1
|
|
533
687
|
yield k, data.first
|
|
534
688
|
end
|
|
@@ -537,13 +691,13 @@ module Dalli
|
|
|
537
691
|
end
|
|
538
692
|
end
|
|
539
693
|
|
|
540
|
-
def get_multi_hash(keys)
|
|
694
|
+
def get_multi_hash(keys, req_options = nil)
|
|
541
695
|
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
542
696
|
hash = if ring.servers.size == 1
|
|
543
|
-
single_server_get_multi(keys)
|
|
697
|
+
single_server_get_multi(keys, req_options)
|
|
544
698
|
else
|
|
545
699
|
{}.tap do |h|
|
|
546
|
-
pipelined_getter.process(keys) { |k, data| h[k] = data.first }
|
|
700
|
+
pipelined_getter.process(keys, req_options) { |k, data| h[k] = data.first }
|
|
547
701
|
end
|
|
548
702
|
end
|
|
549
703
|
record_hit_miss_metrics(span, keys.size, hash.size)
|
|
@@ -556,11 +710,33 @@ module Dalli
|
|
|
556
710
|
server if server&.alive?
|
|
557
711
|
end
|
|
558
712
|
|
|
559
|
-
|
|
713
|
+
# The three single_server_* fast-path methods below share one contract,
|
|
714
|
+
# matching the pipelined multi-server path they stand in for: a transient
|
|
715
|
+
# RetryableNetworkError is retried (bounded implicitly by the server's own
|
|
716
|
+
# socket_max_failures, same as the pipelined path's retry), and a hard
|
|
717
|
+
# NetworkError -- the server genuinely unreachable, not just blipping --
|
|
718
|
+
# propagates to the caller rather than being swallowed into a silently
|
|
719
|
+
# wrong result. Only server_for_key/single_server finding no live server
|
|
720
|
+
# to route to at all is still silent, matching Ring#keys_grouped_by_server
|
|
721
|
+
# dropping a key it can't route on both the single- and multi-server paths.
|
|
722
|
+
def single_server_get_multi(keys, req_options = nil)
|
|
560
723
|
keys.map! { |k| @key_manager.validate_key(k.to_s) }
|
|
561
724
|
return {} unless (server = single_server)
|
|
562
725
|
|
|
563
|
-
result = server.request(:read_multi_req, keys)
|
|
726
|
+
result = server.request(:read_multi_req, keys, req_options)
|
|
727
|
+
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
728
|
+
result
|
|
729
|
+
rescue Dalli::RetryableNetworkError => e
|
|
730
|
+
Dalli.logger.debug { e.inspect }
|
|
731
|
+
Dalli.logger.debug { 'retrying single-server get_multi because of network error' }
|
|
732
|
+
retry
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def single_server_get_multi_with_metadata(keys, req_options = nil)
|
|
736
|
+
keys.map! { |k| @key_manager.validate_key(k.to_s) }
|
|
737
|
+
return {} unless (server = single_server)
|
|
738
|
+
|
|
739
|
+
result = server.request(:read_multi_with_metadata_req, keys, req_options)
|
|
564
740
|
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
565
741
|
result
|
|
566
742
|
rescue Dalli::NetworkError
|
|
@@ -572,24 +748,21 @@ module Dalli
|
|
|
572
748
|
return unless (server = single_server)
|
|
573
749
|
|
|
574
750
|
server.request(:write_multi_req, pairs, ttl, req_options)
|
|
575
|
-
rescue Dalli::
|
|
576
|
-
|
|
751
|
+
rescue Dalli::RetryableNetworkError => e
|
|
752
|
+
Dalli.logger.debug { e.inspect }
|
|
753
|
+
Dalli.logger.debug { 'retrying single-server set_multi because of network error' }
|
|
754
|
+
retry
|
|
577
755
|
end
|
|
578
756
|
|
|
579
|
-
def single_server_delete_multi(keys)
|
|
757
|
+
def single_server_delete_multi(keys, req_options = nil)
|
|
580
758
|
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
581
759
|
return 0 unless (server = single_server)
|
|
582
760
|
|
|
583
|
-
server.request(:delete_multi_req, validated_keys)
|
|
761
|
+
server.request(:delete_multi_req, validated_keys, req_options)
|
|
584
762
|
rescue Dalli::RetryableNetworkError => e
|
|
585
|
-
# Mirror the pipelined path: retry transient errors so a momentary blip
|
|
586
|
-
# still yields a real count. Bounded by the server's socket_max_failures,
|
|
587
|
-
# after which a hard NetworkError is raised and handled below.
|
|
588
763
|
Dalli.logger.debug { e.inspect }
|
|
589
764
|
Dalli.logger.debug { 'retrying single-server delete_multi because of network error' }
|
|
590
765
|
retry
|
|
591
|
-
rescue Dalli::NetworkError
|
|
592
|
-
0
|
|
593
766
|
end
|
|
594
767
|
|
|
595
768
|
def get_multi_attributes(keys)
|
|
@@ -623,21 +796,33 @@ module Dalli
|
|
|
623
796
|
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
|
624
797
|
end
|
|
625
798
|
|
|
626
|
-
#
|
|
627
|
-
#
|
|
628
|
-
#
|
|
629
|
-
#
|
|
630
|
-
#
|
|
631
|
-
|
|
632
|
-
|
|
799
|
+
# Validated here, before the request reaches Protocol::Base#request, rather
|
|
800
|
+
# than only at the RequestFormatter level. Reaching only the formatter's
|
|
801
|
+
# check means unwinding through Protocol::Base#request, which logs the
|
|
802
|
+
# failure as unexpected and closes the connection -- a caller passing a
|
|
803
|
+
# bad token should get a clean ArgumentError and keep its connection.
|
|
804
|
+
ROUTING_TOKEN_FORBIDDEN = /[\r\n\0]/
|
|
805
|
+
private_constant :ROUTING_TOKEN_FORBIDDEN
|
|
806
|
+
|
|
807
|
+
def validate_routing_tokens!(req_options)
|
|
808
|
+
return unless req_options.is_a?(Hash)
|
|
809
|
+
|
|
810
|
+
validate_routing_token!(:p_token, req_options[:p_token])
|
|
811
|
+
validate_routing_token!(:l_token, req_options[:l_token])
|
|
812
|
+
end
|
|
633
813
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
814
|
+
def validate_routing_token!(name, value)
|
|
815
|
+
# Only an empty *String* is a no-op; see the matching comment in
|
|
816
|
+
# RequestFormatter#routing_tokens for why respond_to?(:empty?) is wrong
|
|
817
|
+
# here (it would also excuse [] / {} from the type check below).
|
|
818
|
+
return if value.nil? || (value.is_a?(String) && value.empty?)
|
|
819
|
+
raise ArgumentError, "#{name} must be a String, got #{value.class}" unless value.is_a?(String)
|
|
820
|
+
raise ArgumentError, "#{name} must not contain CRLF or null bytes" if value.match?(ROUTING_TOKEN_FORBIDDEN)
|
|
637
821
|
end
|
|
638
822
|
|
|
639
823
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
640
|
-
(
|
|
824
|
+
validate_routing_tokens!(req_options)
|
|
825
|
+
(value, cas) = perform(:cas, key, req_options)
|
|
641
826
|
return if value.nil? && !always_set
|
|
642
827
|
|
|
643
828
|
newvalue = yield(value)
|
|
@@ -646,7 +831,13 @@ module Dalli
|
|
|
646
831
|
|
|
647
832
|
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
648
833
|
server = ring.server_for_key(key)
|
|
649
|
-
|
|
834
|
+
# req_options is the base, not the override: fetch_with_lock's own
|
|
835
|
+
# lock_ttl/recache_threshold parameters must always win, even if a
|
|
836
|
+
# caller's req_options happened to contain :vivify_ttl/:recache_ttl.
|
|
837
|
+
meta_options = req_options.is_a?(Hash) ? req_options.dup : {}
|
|
838
|
+
meta_options[:vivify_ttl] = lock_ttl
|
|
839
|
+
meta_options[:recache_ttl] = recache_threshold
|
|
840
|
+
result = server.request(:meta_get, key, meta_options)
|
|
650
841
|
|
|
651
842
|
return result[:value] unless result[:won_recache]
|
|
652
843
|
|
|
@@ -15,17 +15,24 @@ module Dalli
|
|
|
15
15
|
##
|
|
16
16
|
# Deletes multiple keys from memcached.
|
|
17
17
|
#
|
|
18
|
+
# `req_options` is applied to every delete in the batch (see
|
|
19
|
+
# Dalli::Client#delete for the supported meta-delete keys).
|
|
20
|
+
#
|
|
18
21
|
# @param keys [Array<String>] keys to delete
|
|
19
|
-
# @return [Integer] the number of keys
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
22
|
+
# @return [Integer] the number of keys the server found and acted on. Only
|
|
23
|
+
# a key that did not exist decrements this count, so with
|
|
24
|
+
# `req_options: {invalidate: true}` it reports how many keys were
|
|
25
|
+
# tombstoned rather than removed. This is best-effort: a transient
|
|
26
|
+
# network error is retried automatically, and keys handled before the
|
|
27
|
+
# error are not recounted, so the result may under-report when a retry
|
|
28
|
+
# occurs. If a server remains unreachable after retrying, raises
|
|
29
|
+
# Dalli::NetworkError.
|
|
23
30
|
##
|
|
24
|
-
def process(keys)
|
|
31
|
+
def process(keys, req_options = nil)
|
|
25
32
|
return 0 if keys.empty?
|
|
26
33
|
|
|
27
34
|
@ring.lock do
|
|
28
|
-
groups = setup_requests(keys)
|
|
35
|
+
groups = setup_requests(keys, req_options)
|
|
29
36
|
finish_requests(groups)
|
|
30
37
|
end
|
|
31
38
|
rescue Dalli::RetryableNetworkError => e
|
|
@@ -36,9 +43,9 @@ module Dalli
|
|
|
36
43
|
|
|
37
44
|
private
|
|
38
45
|
|
|
39
|
-
def setup_requests(keys)
|
|
46
|
+
def setup_requests(keys, req_options = nil)
|
|
40
47
|
groups = groups_for_keys(keys)
|
|
41
|
-
make_delete_requests(groups)
|
|
48
|
+
make_delete_requests(groups, req_options)
|
|
42
49
|
groups
|
|
43
50
|
end
|
|
44
51
|
|
|
@@ -46,12 +53,20 @@ module Dalli
|
|
|
46
53
|
# Loop through the server-grouped sets of keys, writing
|
|
47
54
|
# the corresponding quiet delete requests to the appropriate servers
|
|
48
55
|
##
|
|
49
|
-
|
|
56
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate: the
|
|
57
|
+
# top-level rescue in #process retries the whole pipelined delete on it. A
|
|
58
|
+
# combined `rescue DalliError, NetworkError` would silently swallow
|
|
59
|
+
# RetryableNetworkError too -- since NetworkError < DalliError -- dropping
|
|
60
|
+
# this server's keys on a transient hiccup instead of retrying. Only a
|
|
61
|
+
# non-network DalliError should be swallowed here.
|
|
62
|
+
def make_delete_requests(groups, req_options = nil)
|
|
50
63
|
groups.each do |server, keys_for_server|
|
|
51
64
|
keys_for_server.select! do |key|
|
|
52
|
-
server.request(:pipelined_delete, key)
|
|
65
|
+
server.request(:pipelined_delete, key, req_options)
|
|
53
66
|
true
|
|
54
|
-
rescue
|
|
67
|
+
rescue Dalli::NetworkError
|
|
68
|
+
raise
|
|
69
|
+
rescue DalliError => e
|
|
55
70
|
Dalli.logger.debug { e.inspect }
|
|
56
71
|
Dalli.logger.debug { "unable to delete key #{key} for server #{server.name}" }
|
|
57
72
|
false
|
|
@@ -66,7 +81,9 @@ module Dalli
|
|
|
66
81
|
def finish_requests(groups)
|
|
67
82
|
groups.sum do |server, keys_for_server|
|
|
68
83
|
server.request(:finish_pipelined_delete, keys_for_server.size)
|
|
69
|
-
rescue
|
|
84
|
+
rescue Dalli::NetworkError
|
|
85
|
+
raise
|
|
86
|
+
rescue DalliError => e
|
|
70
87
|
Dalli.logger.debug { e.inspect }
|
|
71
88
|
Dalli.logger.debug { "unable to complete pipelined delete on server #{server.name}" }
|
|
72
89
|
0
|