dalli 5.0.7 → 5.1.1
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 +80 -1
- data/Gemfile +1 -1
- data/README.md +6 -1
- data/lib/dalli/client.rb +265 -56
- data/lib/dalli/pipelined_deleter.rb +29 -12
- data/lib/dalli/pipelined_getter.rb +84 -47
- data/lib/dalli/pipelined_setter.rb +15 -3
- data/lib/dalli/protocol/base.rb +73 -11
- data/lib/dalli/protocol/key_regularizer.rb +25 -1
- data/lib/dalli/protocol/meta.rb +101 -33
- data/lib/dalli/protocol/request_formatter.rb +118 -18
- data/lib/dalli/protocol/response_processor.rb +85 -16
- data/lib/dalli/ring.rb +31 -17
- data/lib/dalli/socket.rb +2 -4
- data/lib/dalli/version.rb +2 -2
- data/lib/rack/session/dalli.rb +1 -2
- metadata +1 -1
data/lib/dalli/client.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require 'digest/md5'
|
|
4
4
|
|
|
5
|
-
# encoding: ascii
|
|
6
5
|
module Dalli
|
|
7
6
|
##
|
|
8
7
|
# Dalli::Client is the main class which developers will use to interact with
|
|
@@ -68,6 +67,7 @@ module Dalli
|
|
|
68
67
|
# Get the value associated with the key.
|
|
69
68
|
# If a value is not found, then +nil+ is returned.
|
|
70
69
|
def get(key, req_options = nil)
|
|
70
|
+
validate_routing_tokens!(req_options)
|
|
71
71
|
perform(:get, key, req_options)
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -75,8 +75,9 @@ module Dalli
|
|
|
75
75
|
# Gat (get and touch) fetch an item and simultaneously update its expiration time.
|
|
76
76
|
#
|
|
77
77
|
# If a value is not found, then +nil+ is returned.
|
|
78
|
-
def gat(key, ttl = nil)
|
|
79
|
-
|
|
78
|
+
def gat(key, ttl = nil, req_options = nil)
|
|
79
|
+
validate_routing_tokens!(req_options)
|
|
80
|
+
perform(:gat, key, ttl_or_default(ttl), req_options)
|
|
80
81
|
end
|
|
81
82
|
|
|
82
83
|
##
|
|
@@ -91,8 +92,9 @@ module Dalli
|
|
|
91
92
|
##
|
|
92
93
|
# Get the value and CAS ID associated with the key. If a block is provided,
|
|
93
94
|
# value and CAS will be passed to the block.
|
|
94
|
-
def get_cas(key)
|
|
95
|
-
(
|
|
95
|
+
def get_cas(key, req_options = nil)
|
|
96
|
+
validate_routing_tokens!(req_options)
|
|
97
|
+
(value, cas) = perform(:cas, key, req_options)
|
|
96
98
|
return [value, cas] unless block_given?
|
|
97
99
|
|
|
98
100
|
yield value, cas
|
|
@@ -106,17 +108,23 @@ module Dalli
|
|
|
106
108
|
# - :return_cas [Boolean] return the CAS value (default: true)
|
|
107
109
|
# - :return_hit_status [Boolean] return whether item was previously accessed
|
|
108
110
|
# - :return_last_access [Boolean] return seconds since last access
|
|
111
|
+
# - :return_ttl_remaining [Boolean] return seconds of TTL remaining (-1 if no TTL)
|
|
109
112
|
# - :skip_lru_bump [Boolean] don't bump LRU or update access stats
|
|
110
113
|
#
|
|
111
114
|
# @return [Hash] containing:
|
|
112
115
|
# - :value - the cached value (or nil on miss)
|
|
113
116
|
# - :cas - the CAS value
|
|
117
|
+
# - :miss - true when the key does not exist. Always present. Prefer it
|
|
118
|
+
# over a nil :value, which cannot distinguish a miss from a stored nil
|
|
119
|
+
# under cache_nils
|
|
114
120
|
# - :hit_before - true/false if previously accessed (only if return_hit_status: true)
|
|
115
121
|
# - :last_access - seconds since last access (only if return_last_access: true)
|
|
122
|
+
# - :ttl_remaining - seconds of TTL remaining, -1 when the item has no
|
|
123
|
+
# expiry (only if return_ttl_remaining: true)
|
|
116
124
|
#
|
|
117
125
|
# @example Get with hit status
|
|
118
126
|
# result = client.get_with_metadata('key', return_hit_status: true)
|
|
119
|
-
# # => { value: "data", cas: 123, hit_before: true }
|
|
127
|
+
# # => { value: "data", cas: 123, miss: false, hit_before: true }
|
|
120
128
|
#
|
|
121
129
|
# @example Get with all metadata without affecting LRU
|
|
122
130
|
# result = client.get_with_metadata('key',
|
|
@@ -127,6 +135,7 @@ module Dalli
|
|
|
127
135
|
# # => { value: "data", cas: 123, hit_before: true, last_access: 42 }
|
|
128
136
|
#
|
|
129
137
|
def get_with_metadata(key, options = {})
|
|
138
|
+
validate_routing_tokens!(options)
|
|
130
139
|
key = key.to_s
|
|
131
140
|
key = @key_manager.validate_key(key)
|
|
132
141
|
|
|
@@ -144,19 +153,80 @@ module Dalli
|
|
|
144
153
|
# Fetch multiple keys efficiently.
|
|
145
154
|
# If a block is given, yields key/value pairs one at a time.
|
|
146
155
|
# Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
|
|
147
|
-
#
|
|
148
|
-
|
|
156
|
+
#
|
|
157
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
158
|
+
#
|
|
159
|
+
# A transient network error is retried automatically. If a server remains
|
|
160
|
+
# unreachable after retrying, raises Dalli::NetworkError rather than
|
|
161
|
+
# silently omitting that server's keys from the result.
|
|
162
|
+
#
|
|
163
|
+
# @raise [Dalli::NetworkError] if a server is unreachable after retrying
|
|
164
|
+
# rubocop:disable-next Style/ExplicitBlockArgument
|
|
165
|
+
def get_multi(*keys, req_options: nil)
|
|
149
166
|
keys.flatten!
|
|
150
167
|
keys.compact!
|
|
151
168
|
return {} if keys.empty?
|
|
152
169
|
|
|
170
|
+
validate_routing_tokens!(req_options)
|
|
171
|
+
|
|
153
172
|
if block_given?
|
|
154
|
-
get_multi_yielding(keys) { |k, v| yield k, v }
|
|
173
|
+
get_multi_yielding(keys, req_options) { |k, v| yield k, v }
|
|
155
174
|
else
|
|
156
|
-
get_multi_hash(keys)
|
|
175
|
+
get_multi_hash(keys, req_options)
|
|
157
176
|
end
|
|
158
177
|
end
|
|
159
|
-
|
|
178
|
+
|
|
179
|
+
##
|
|
180
|
+
# Fetch multiple keys efficiently, returning a stale-aware metadata Hash per
|
|
181
|
+
# key. If a block is given, yields key/metadata pairs one at a time.
|
|
182
|
+
#
|
|
183
|
+
# Like #get_multi and #get_multi_cas, keys that were not found are omitted
|
|
184
|
+
# from the result -- absence is the miss. A tombstoned item (see #delete
|
|
185
|
+
# with the meta protocol's invalidate flag) is *not* a miss: it is returned
|
|
186
|
+
# with stale: true, possibly with an empty value, which is the distinction
|
|
187
|
+
# stale-aware callers need.
|
|
188
|
+
#
|
|
189
|
+
# client.get_multi_with_metadata('a', 'b', 'absent')
|
|
190
|
+
# # => { 'a' => { value: 'v', cas: 12, stale: false, miss: false },
|
|
191
|
+
# # 'b' => { value: '', cas: 13, stale: true, miss: false } }
|
|
192
|
+
#
|
|
193
|
+
# Missing keys are `requested - result.keys`.
|
|
194
|
+
#
|
|
195
|
+
# Result key order matches request order only when every key lands on the
|
|
196
|
+
# same server; across multiple servers it follows per-server response
|
|
197
|
+
# order instead, the same as #get_multi.
|
|
198
|
+
#
|
|
199
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
200
|
+
#
|
|
201
|
+
# @param keys [Array<String>] the keys to fetch
|
|
202
|
+
# @param req_options [Hash, nil] routing-token options
|
|
203
|
+
# @return [Hash] key => { value:, cas:, stale:, miss: }
|
|
204
|
+
def get_multi_with_metadata(*keys, req_options: nil, &block)
|
|
205
|
+
keys.flatten!
|
|
206
|
+
keys.compact!
|
|
207
|
+
return {} if keys.empty?
|
|
208
|
+
|
|
209
|
+
validate_routing_tokens!(req_options)
|
|
210
|
+
|
|
211
|
+
results = Instrumentation.trace('get_multi_with_metadata',
|
|
212
|
+
multi_trace_attrs('get_multi_with_metadata', keys.size, keys)) do
|
|
213
|
+
if ring.servers.size == 1
|
|
214
|
+
single_server_get_multi_with_metadata(keys, req_options)
|
|
215
|
+
else
|
|
216
|
+
pipelined_getter.process_with_metadata(keys, req_options)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
if block
|
|
221
|
+
results.each(&block)
|
|
222
|
+
# Matches get_multi/get_multi_cas: nil when a block is given, so
|
|
223
|
+
# callers can't come to depend on a return value that block-form
|
|
224
|
+
# get_multi never provided.
|
|
225
|
+
return nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
results
|
|
229
|
+
end
|
|
160
230
|
|
|
161
231
|
##
|
|
162
232
|
# Fetch multiple keys efficiently, including available metadata such as CAS.
|
|
@@ -164,12 +234,16 @@ module Dalli
|
|
|
164
234
|
# [value, cas_id]
|
|
165
235
|
# If no block is given, returns a hash of
|
|
166
236
|
# { 'key' => [value, cas_id] }
|
|
167
|
-
|
|
237
|
+
#
|
|
238
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
239
|
+
def get_multi_cas(*keys, req_options: nil)
|
|
240
|
+
validate_routing_tokens!(req_options)
|
|
241
|
+
|
|
168
242
|
if block_given?
|
|
169
|
-
pipelined_getter.process(keys) { |*args| yield(*args) }
|
|
243
|
+
pipelined_getter.process(keys, req_options) { |*args| yield(*args) }
|
|
170
244
|
else
|
|
171
245
|
{}.tap do |hash|
|
|
172
|
-
pipelined_getter.process(keys) { |k, data| hash[k] = data }
|
|
246
|
+
pipelined_getter.process(keys, req_options) { |k, data| hash[k] = data }
|
|
173
247
|
end
|
|
174
248
|
end
|
|
175
249
|
end
|
|
@@ -227,6 +301,7 @@ module Dalli
|
|
|
227
301
|
|
|
228
302
|
validate_integer!(:lock_ttl, lock_ttl)
|
|
229
303
|
validate_integer!(:recache_threshold, recache_threshold)
|
|
304
|
+
validate_routing_tokens!(req_options)
|
|
230
305
|
key = key.to_s
|
|
231
306
|
key = @key_manager.validate_key(key)
|
|
232
307
|
|
|
@@ -302,16 +377,24 @@ module Dalli
|
|
|
302
377
|
# This method is more efficient than calling set() in a loop because
|
|
303
378
|
# it batches requests by server and uses quiet mode.
|
|
304
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
|
+
#
|
|
305
384
|
# @param hash [Hash] key-value pairs to set
|
|
306
385
|
# @param ttl [Integer] time-to-live in seconds (optional, uses default if not provided)
|
|
307
|
-
# @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
|
|
308
388
|
# @return [void]
|
|
389
|
+
# @raise [Dalli::NetworkError] if a server is unreachable after retrying
|
|
309
390
|
#
|
|
310
391
|
# Example:
|
|
311
392
|
# client.set_multi({ 'key1' => 'value1', 'key2' => 'value2' }, 300)
|
|
312
393
|
def set_multi(hash, ttl = nil, req_options = nil)
|
|
313
394
|
return if hash.empty?
|
|
314
395
|
|
|
396
|
+
validate_routing_tokens!(req_options)
|
|
397
|
+
|
|
315
398
|
Instrumentation.trace('set_multi', multi_trace_attrs('set_multi', hash.size, hash.keys)) do
|
|
316
399
|
if ring.servers.size == 1
|
|
317
400
|
single_server_set_multi(hash, ttl_or_default(ttl), req_options)
|
|
@@ -325,6 +408,7 @@ module Dalli
|
|
|
325
408
|
# Set the key-value pair, verifying existing CAS.
|
|
326
409
|
# Returns the resulting CAS value if succeeded, and falsy otherwise.
|
|
327
410
|
def set_cas(key, value, cas, ttl = nil, req_options = nil)
|
|
411
|
+
validate_routing_tokens!(req_options)
|
|
328
412
|
perform(:set, key, value, ttl_or_default(ttl), cas, req_options)
|
|
329
413
|
end
|
|
330
414
|
|
|
@@ -332,6 +416,7 @@ module Dalli
|
|
|
332
416
|
# Conditionally add a key/value pair, if the key does not already exist
|
|
333
417
|
# on the server. Returns truthy if the operation succeeded.
|
|
334
418
|
def add(key, value, ttl = nil, req_options = nil)
|
|
419
|
+
validate_routing_tokens!(req_options)
|
|
335
420
|
perform(:add, key, value, ttl_or_default(ttl), req_options)
|
|
336
421
|
end
|
|
337
422
|
|
|
@@ -347,17 +432,49 @@ module Dalli
|
|
|
347
432
|
# key already exists on the server. Returns the new CAS value if the
|
|
348
433
|
# operation succeeded, or falsy otherwise.
|
|
349
434
|
def replace_cas(key, value, cas, ttl = nil, req_options = nil)
|
|
435
|
+
validate_routing_tokens!(req_options)
|
|
350
436
|
perform(:replace, key, value, ttl_or_default(ttl), cas, req_options)
|
|
351
437
|
end
|
|
352
438
|
|
|
353
439
|
# Delete a key/value pair, verifying existing CAS.
|
|
354
440
|
# Returns true if succeeded, and falsy otherwise.
|
|
355
|
-
|
|
356
|
-
|
|
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)
|
|
357
448
|
end
|
|
358
449
|
|
|
359
|
-
|
|
360
|
-
|
|
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)
|
|
361
478
|
end
|
|
362
479
|
|
|
363
480
|
##
|
|
@@ -365,22 +482,35 @@ module Dalli
|
|
|
365
482
|
# This method is more efficient than calling delete() in a loop because
|
|
366
483
|
# it batches requests by server and uses quiet mode.
|
|
367
484
|
#
|
|
485
|
+
# `req_options` accepts the same meta-delete options as #delete and applies
|
|
486
|
+
# them to every key in the batch.
|
|
487
|
+
#
|
|
368
488
|
# @param keys [Array<String>] keys to delete
|
|
369
|
-
# @
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
#
|
|
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
|
|
373
499
|
#
|
|
374
500
|
# Example:
|
|
375
501
|
# client.delete_multi(['key1', 'key2', 'key3'])
|
|
376
|
-
|
|
502
|
+
# client.delete_multi(%w[key1 key2], invalidate: true, tombstone_ttl: 30)
|
|
503
|
+
def delete_multi(keys, req_options = nil)
|
|
377
504
|
return 0 if keys.empty?
|
|
378
505
|
|
|
506
|
+
validate_delete_options!(req_options)
|
|
507
|
+
validate_routing_tokens!(req_options)
|
|
508
|
+
|
|
379
509
|
Instrumentation.trace('delete_multi', multi_trace_attrs('delete_multi', keys.size, keys)) do
|
|
380
510
|
if ring.servers.size == 1
|
|
381
|
-
single_server_delete_multi(keys)
|
|
511
|
+
single_server_delete_multi(keys, req_options)
|
|
382
512
|
else
|
|
383
|
-
pipelined_deleter.process(keys)
|
|
513
|
+
pipelined_deleter.process(keys, req_options)
|
|
384
514
|
end
|
|
385
515
|
end
|
|
386
516
|
end
|
|
@@ -388,15 +518,17 @@ module Dalli
|
|
|
388
518
|
##
|
|
389
519
|
# Append value to the value already stored on the server for 'key'.
|
|
390
520
|
# Appending only works for values stored with :raw => true.
|
|
391
|
-
def append(key, value)
|
|
392
|
-
|
|
521
|
+
def append(key, value, req_options = nil)
|
|
522
|
+
validate_routing_tokens!(req_options)
|
|
523
|
+
perform(:append, key, value.to_s, req_options)
|
|
393
524
|
end
|
|
394
525
|
|
|
395
526
|
##
|
|
396
527
|
# Prepend value to the value already stored on the server for 'key'.
|
|
397
528
|
# Prepending only works for values stored with :raw => true.
|
|
398
|
-
def prepend(key, value)
|
|
399
|
-
|
|
529
|
+
def prepend(key, value, req_options = nil)
|
|
530
|
+
validate_routing_tokens!(req_options)
|
|
531
|
+
perform(:prepend, key, value.to_s, req_options)
|
|
400
532
|
end
|
|
401
533
|
|
|
402
534
|
##
|
|
@@ -414,11 +546,12 @@ module Dalli
|
|
|
414
546
|
# #cas.
|
|
415
547
|
#
|
|
416
548
|
# If the value already exists, it must have been set with raw: true
|
|
417
|
-
def incr(key, amt = 1, ttl = nil, default = nil)
|
|
549
|
+
def incr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
418
550
|
check_positive!(amt)
|
|
419
551
|
validate_integer!(:default, default)
|
|
552
|
+
validate_routing_tokens!(req_options)
|
|
420
553
|
|
|
421
|
-
perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
|
|
554
|
+
perform(:incr, key, amt.to_i, ttl_or_default(ttl), default, req_options)
|
|
422
555
|
end
|
|
423
556
|
|
|
424
557
|
##
|
|
@@ -439,11 +572,12 @@ module Dalli
|
|
|
439
572
|
# #cas.
|
|
440
573
|
#
|
|
441
574
|
# If the value already exists, it must have been set with raw: true
|
|
442
|
-
def decr(key, amt = 1, ttl = nil, default = nil)
|
|
575
|
+
def decr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
443
576
|
check_positive!(amt)
|
|
444
577
|
validate_integer!(:default, default)
|
|
578
|
+
validate_routing_tokens!(req_options)
|
|
445
579
|
|
|
446
|
-
perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
|
|
580
|
+
perform(:decr, key, amt.to_i, ttl_or_default(ttl), default, req_options)
|
|
447
581
|
end
|
|
448
582
|
|
|
449
583
|
##
|
|
@@ -504,6 +638,7 @@ module Dalli
|
|
|
504
638
|
alias reset close
|
|
505
639
|
|
|
506
640
|
CACHE_NILS = { cache_nils: true }.freeze
|
|
641
|
+
EMPTY_ATTRIBUTES = {}.freeze
|
|
507
642
|
|
|
508
643
|
def not_found?(val)
|
|
509
644
|
cache_nils ? val == ::Dalli::NOT_FOUND : val.nil?
|
|
@@ -520,6 +655,30 @@ module Dalli
|
|
|
520
655
|
|
|
521
656
|
private
|
|
522
657
|
|
|
658
|
+
# Raised before the request reaches a server: RequestFormatter enforces the
|
|
659
|
+
# same rule, but reaching it means unwinding through Protocol::Base#request,
|
|
660
|
+
# which logs the failure as unexpected and closes the connection. A caller
|
|
661
|
+
# passing the wrong options should get a clean ArgumentError and keep its
|
|
662
|
+
# connection.
|
|
663
|
+
def validate_delete_options!(req_options)
|
|
664
|
+
return unless req_options.is_a?(Hash)
|
|
665
|
+
|
|
666
|
+
tombstone_ttl = req_options[:tombstone_ttl]
|
|
667
|
+
return unless tombstone_ttl
|
|
668
|
+
|
|
669
|
+
raise ArgumentError, 'tombstone_ttl requires invalidate: true' unless req_options[:invalidate]
|
|
670
|
+
|
|
671
|
+
# tombstone_kwargs coerces this with Integer(), deep inside the request
|
|
672
|
+
# path; validated here first so a bad value raises cleanly instead of
|
|
673
|
+
# unwinding through Protocol::Base#request, which would close the
|
|
674
|
+
# connection on the ArgumentError Integer() raises.
|
|
675
|
+
begin
|
|
676
|
+
Integer(tombstone_ttl)
|
|
677
|
+
rescue ArgumentError, TypeError
|
|
678
|
+
raise ArgumentError, "tombstone_ttl must be an integer, got #{tombstone_ttl.inspect}"
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
|
|
523
682
|
def record_hit_miss_metrics(span, key_count, hit_count)
|
|
524
683
|
return unless span
|
|
525
684
|
|
|
@@ -527,10 +686,10 @@ module Dalli
|
|
|
527
686
|
'db.memcached.miss_count' => key_count - hit_count)
|
|
528
687
|
end
|
|
529
688
|
|
|
530
|
-
def get_multi_yielding(keys)
|
|
689
|
+
def get_multi_yielding(keys, req_options = nil)
|
|
531
690
|
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
532
691
|
hit_count = 0
|
|
533
|
-
pipelined_getter.process(keys) do |k, data|
|
|
692
|
+
pipelined_getter.process(keys, req_options, return_cas: false) do |k, data|
|
|
534
693
|
hit_count += 1
|
|
535
694
|
yield k, data.first
|
|
536
695
|
end
|
|
@@ -539,13 +698,13 @@ module Dalli
|
|
|
539
698
|
end
|
|
540
699
|
end
|
|
541
700
|
|
|
542
|
-
def get_multi_hash(keys)
|
|
701
|
+
def get_multi_hash(keys, req_options = nil)
|
|
543
702
|
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
544
703
|
hash = if ring.servers.size == 1
|
|
545
|
-
single_server_get_multi(keys)
|
|
704
|
+
single_server_get_multi(keys, req_options)
|
|
546
705
|
else
|
|
547
706
|
{}.tap do |h|
|
|
548
|
-
pipelined_getter.process(keys) { |k, data| h[k] = data.first }
|
|
707
|
+
pipelined_getter.process(keys, req_options, return_cas: false) { |k, data| h[k] = data.first }
|
|
549
708
|
end
|
|
550
709
|
end
|
|
551
710
|
record_hit_miss_metrics(span, keys.size, hash.size)
|
|
@@ -558,11 +717,33 @@ module Dalli
|
|
|
558
717
|
server if server&.alive?
|
|
559
718
|
end
|
|
560
719
|
|
|
561
|
-
|
|
720
|
+
# The three single_server_* fast-path methods below share one contract,
|
|
721
|
+
# matching the pipelined multi-server path they stand in for: a transient
|
|
722
|
+
# RetryableNetworkError is retried (bounded implicitly by the server's own
|
|
723
|
+
# socket_max_failures, same as the pipelined path's retry), and a hard
|
|
724
|
+
# NetworkError -- the server genuinely unreachable, not just blipping --
|
|
725
|
+
# propagates to the caller rather than being swallowed into a silently
|
|
726
|
+
# wrong result. Only server_for_key/single_server finding no live server
|
|
727
|
+
# to route to at all is still silent, matching Ring#keys_grouped_by_server
|
|
728
|
+
# dropping a key it can't route on both the single- and multi-server paths.
|
|
729
|
+
def single_server_get_multi(keys, req_options = nil)
|
|
730
|
+
keys.map! { |k| @key_manager.validate_key(k.to_s) }
|
|
731
|
+
return {} unless (server = single_server)
|
|
732
|
+
|
|
733
|
+
result = server.request(:read_multi_req, keys, req_options)
|
|
734
|
+
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
735
|
+
result
|
|
736
|
+
rescue Dalli::RetryableNetworkError => e
|
|
737
|
+
Dalli.logger.debug { e.inspect }
|
|
738
|
+
Dalli.logger.debug { 'retrying single-server get_multi because of network error' }
|
|
739
|
+
retry
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def single_server_get_multi_with_metadata(keys, req_options = nil)
|
|
562
743
|
keys.map! { |k| @key_manager.validate_key(k.to_s) }
|
|
563
744
|
return {} unless (server = single_server)
|
|
564
745
|
|
|
565
|
-
result = server.request(:
|
|
746
|
+
result = server.request(:read_multi_with_metadata_req, keys, req_options)
|
|
566
747
|
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
567
748
|
result
|
|
568
749
|
rescue Dalli::NetworkError
|
|
@@ -574,28 +755,26 @@ module Dalli
|
|
|
574
755
|
return unless (server = single_server)
|
|
575
756
|
|
|
576
757
|
server.request(:write_multi_req, pairs, ttl, req_options)
|
|
577
|
-
rescue Dalli::
|
|
578
|
-
|
|
758
|
+
rescue Dalli::RetryableNetworkError => e
|
|
759
|
+
Dalli.logger.debug { e.inspect }
|
|
760
|
+
Dalli.logger.debug { 'retrying single-server set_multi because of network error' }
|
|
761
|
+
retry
|
|
579
762
|
end
|
|
580
763
|
|
|
581
|
-
def single_server_delete_multi(keys)
|
|
764
|
+
def single_server_delete_multi(keys, req_options = nil)
|
|
582
765
|
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
583
766
|
return 0 unless (server = single_server)
|
|
584
767
|
|
|
585
|
-
server.request(:delete_multi_req, validated_keys)
|
|
768
|
+
server.request(:delete_multi_req, validated_keys, req_options)
|
|
586
769
|
rescue Dalli::RetryableNetworkError => e
|
|
587
|
-
# Mirror the pipelined path: retry transient errors so a momentary blip
|
|
588
|
-
# still yields a real count. Bounded by the server's socket_max_failures,
|
|
589
|
-
# after which a hard NetworkError is raised and handled below.
|
|
590
770
|
Dalli.logger.debug { e.inspect }
|
|
591
771
|
Dalli.logger.debug { 'retrying single-server delete_multi because of network error' }
|
|
592
772
|
retry
|
|
593
|
-
rescue Dalli::NetworkError
|
|
594
|
-
0
|
|
595
773
|
end
|
|
596
774
|
|
|
775
|
+
# Only built when tracing is on, since the Hash is thrown away otherwise
|
|
597
776
|
def get_multi_attributes(keys)
|
|
598
|
-
multi_trace_attrs('get_multi', keys.size, keys)
|
|
777
|
+
Instrumentation.enabled? ? multi_trace_attrs('get_multi', keys.size, keys) : EMPTY_ATTRIBUTES
|
|
599
778
|
end
|
|
600
779
|
|
|
601
780
|
def trace_attrs(operation, key, server)
|
|
@@ -627,9 +806,8 @@ module Dalli
|
|
|
627
806
|
|
|
628
807
|
# Numeric arguments that become meta protocol flags (GHSA-6wmv-xq9m-fmp7).
|
|
629
808
|
# RequestFormatter converts them to Integer as the wire-level backstop;
|
|
630
|
-
# checking here too
|
|
631
|
-
#
|
|
632
|
-
# closing the connection.
|
|
809
|
+
# checking here too gives the caller a clean ArgumentError for the same
|
|
810
|
+
# reason as routing tokens, described below.
|
|
633
811
|
def validate_integer!(name, value)
|
|
634
812
|
return if value.nil?
|
|
635
813
|
|
|
@@ -638,8 +816,33 @@ module Dalli
|
|
|
638
816
|
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
639
817
|
end
|
|
640
818
|
|
|
819
|
+
# Validated here, before the request reaches Protocol::Base#request, rather
|
|
820
|
+
# than only at the RequestFormatter level. Reaching only the formatter's
|
|
821
|
+
# check means unwinding through Protocol::Base#request, which logs the
|
|
822
|
+
# failure as unexpected and closes the connection -- a caller passing a
|
|
823
|
+
# bad token should get a clean ArgumentError and keep its connection.
|
|
824
|
+
ROUTING_TOKEN_FORBIDDEN = /[\r\n\0]/
|
|
825
|
+
private_constant :ROUTING_TOKEN_FORBIDDEN
|
|
826
|
+
|
|
827
|
+
def validate_routing_tokens!(req_options)
|
|
828
|
+
return unless req_options.is_a?(Hash)
|
|
829
|
+
|
|
830
|
+
validate_routing_token!(:p_token, req_options[:p_token])
|
|
831
|
+
validate_routing_token!(:l_token, req_options[:l_token])
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def validate_routing_token!(name, value)
|
|
835
|
+
# Only an empty *String* is a no-op; see the matching comment in
|
|
836
|
+
# RequestFormatter#routing_tokens for why respond_to?(:empty?) is wrong
|
|
837
|
+
# here (it would also excuse [] / {} from the type check below).
|
|
838
|
+
return if value.nil? || (value.is_a?(String) && value.empty?)
|
|
839
|
+
raise ArgumentError, "#{name} must be a String, got #{value.class}" unless value.is_a?(String)
|
|
840
|
+
raise ArgumentError, "#{name} must not contain CRLF or null bytes" if value.match?(ROUTING_TOKEN_FORBIDDEN)
|
|
841
|
+
end
|
|
842
|
+
|
|
641
843
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
642
|
-
(
|
|
844
|
+
validate_routing_tokens!(req_options)
|
|
845
|
+
(value, cas) = perform(:cas, key, req_options)
|
|
643
846
|
return if value.nil? && !always_set
|
|
644
847
|
|
|
645
848
|
newvalue = yield(value)
|
|
@@ -648,7 +851,13 @@ module Dalli
|
|
|
648
851
|
|
|
649
852
|
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
650
853
|
server = ring.server_for_key(key)
|
|
651
|
-
|
|
854
|
+
# req_options is the base, not the override: fetch_with_lock's own
|
|
855
|
+
# lock_ttl/recache_threshold parameters must always win, even if a
|
|
856
|
+
# caller's req_options happened to contain :vivify_ttl/:recache_ttl.
|
|
857
|
+
meta_options = req_options.is_a?(Hash) ? req_options.dup : {}
|
|
858
|
+
meta_options[:vivify_ttl] = lock_ttl
|
|
859
|
+
meta_options[:recache_ttl] = recache_threshold
|
|
860
|
+
result = server.request(:meta_get, key, meta_options)
|
|
652
861
|
|
|
653
862
|
return result[:value] unless result[:won_recache]
|
|
654
863
|
|
|
@@ -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
|