dalli 5.0.10 → 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 +47 -80
- data/Gemfile +1 -1
- data/README.md +7 -6
- data/lib/dalli/client.rb +272 -96
- data/lib/dalli/compressor.rb +5 -53
- data/lib/dalli/key_manager.rb +2 -23
- 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 +55 -28
- data/lib/dalli/protocol/connection_manager.rb +9 -90
- data/lib/dalli/protocol/key_regularizer.rb +18 -1
- data/lib/dalli/protocol/meta.rb +89 -37
- data/lib/dalli/protocol/request_formatter.rb +110 -32
- data/lib/dalli/protocol/response_processor.rb +38 -55
- data/lib/dalli/protocol/value_compressor.rb +3 -30
- data/lib/dalli/protocol/value_serializer.rb +1 -1
- data/lib/dalli/socket.rb +4 -2
- data/lib/dalli/version.rb +2 -2
- data/lib/dalli.rb +0 -1
- data/lib/rack/session/dalli.rb +2 -1
- metadata +2 -3
- data/lib/dalli/json_serializer.rb +0 -28
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,18 +136,15 @@ module Dalli
|
|
|
126
136
|
# # => { value: "data", cas: 123, hit_before: true, last_access: 42 }
|
|
127
137
|
#
|
|
128
138
|
def get_with_metadata(key, options = {})
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
139
|
+
validate_routing_tokens!(options)
|
|
140
|
+
key = key.to_s
|
|
141
|
+
key = @key_manager.validate_key(key)
|
|
132
142
|
|
|
133
|
-
server = ring.server_for_key(
|
|
134
|
-
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata',
|
|
135
|
-
server.request(:meta_get,
|
|
143
|
+
server = ring.server_for_key(key)
|
|
144
|
+
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', key, server)) do
|
|
145
|
+
server.request(:meta_get, key, options)
|
|
136
146
|
end
|
|
137
|
-
|
|
138
|
-
# is marked down is final, as in #perform; retrying it looped forever once
|
|
139
|
-
# down_retry_delay let the same server be tried again.
|
|
140
|
-
rescue RetryableNetworkError => e
|
|
147
|
+
rescue NetworkError => e
|
|
141
148
|
Dalli.logger.debug { e.inspect }
|
|
142
149
|
Dalli.logger.debug { 'retrying get_with_metadata with new server' }
|
|
143
150
|
retry
|
|
@@ -147,17 +154,80 @@ module Dalli
|
|
|
147
154
|
# Fetch multiple keys efficiently.
|
|
148
155
|
# If a block is given, yields key/value pairs one at a time.
|
|
149
156
|
# Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
|
|
150
|
-
#
|
|
151
|
-
|
|
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)
|
|
152
167
|
keys.flatten!
|
|
153
168
|
keys.compact!
|
|
154
169
|
return {} if keys.empty?
|
|
155
170
|
|
|
171
|
+
validate_routing_tokens!(req_options)
|
|
172
|
+
|
|
156
173
|
if block_given?
|
|
157
|
-
get_multi_yielding(keys) { |k, v| yield k, v }
|
|
174
|
+
get_multi_yielding(keys, req_options) { |k, v| yield k, v }
|
|
158
175
|
else
|
|
159
|
-
get_multi_hash(keys)
|
|
176
|
+
get_multi_hash(keys, req_options)
|
|
177
|
+
end
|
|
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
|
|
160
228
|
end
|
|
229
|
+
|
|
230
|
+
results
|
|
161
231
|
end
|
|
162
232
|
|
|
163
233
|
##
|
|
@@ -166,12 +236,16 @@ module Dalli
|
|
|
166
236
|
# [value, cas_id]
|
|
167
237
|
# If no block is given, returns a hash of
|
|
168
238
|
# { 'key' => [value, cas_id] }
|
|
169
|
-
|
|
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
|
+
|
|
170
244
|
if block_given?
|
|
171
|
-
pipelined_getter.process(keys) { |*args| yield(*args) }
|
|
245
|
+
pipelined_getter.process(keys, req_options) { |*args| yield(*args) }
|
|
172
246
|
else
|
|
173
247
|
{}.tap do |hash|
|
|
174
|
-
pipelined_getter.process(keys) { |k, data| hash[k] = data }
|
|
248
|
+
pipelined_getter.process(keys, req_options) { |k, data| hash[k] = data }
|
|
175
249
|
end
|
|
176
250
|
end
|
|
177
251
|
end
|
|
@@ -227,19 +301,15 @@ module Dalli
|
|
|
227
301
|
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
|
|
228
302
|
raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
|
|
229
303
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
validated_key = @key_manager.validate_key(key.to_s)
|
|
304
|
+
validate_routing_tokens!(req_options)
|
|
305
|
+
key = key.to_s
|
|
306
|
+
key = @key_manager.validate_key(key)
|
|
234
307
|
|
|
235
|
-
server = ring.server_for_key(
|
|
236
|
-
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock',
|
|
237
|
-
fetch_with_lock_request(
|
|
308
|
+
server = ring.server_for_key(key)
|
|
309
|
+
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', key, server)) do
|
|
310
|
+
fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
|
|
238
311
|
end
|
|
239
|
-
|
|
240
|
-
# is marked down is final, as in #perform; retrying it looped forever once
|
|
241
|
-
# down_retry_delay let the same server be tried again.
|
|
242
|
-
rescue RetryableNetworkError => e
|
|
312
|
+
rescue NetworkError => e
|
|
243
313
|
Dalli.logger.debug { e.inspect }
|
|
244
314
|
Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
|
|
245
315
|
retry
|
|
@@ -307,16 +377,24 @@ module Dalli
|
|
|
307
377
|
# This method is more efficient than calling set() in a loop because
|
|
308
378
|
# it batches requests by server and uses quiet mode.
|
|
309
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
|
+
#
|
|
310
384
|
# @param hash [Hash] key-value pairs to set
|
|
311
385
|
# @param ttl [Integer] time-to-live in seconds (optional, uses default if not provided)
|
|
312
|
-
# @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
|
|
313
388
|
# @return [void]
|
|
389
|
+
# @raise [Dalli::NetworkError] if a server is unreachable after retrying
|
|
314
390
|
#
|
|
315
391
|
# Example:
|
|
316
392
|
# client.set_multi({ 'key1' => 'value1', 'key2' => 'value2' }, 300)
|
|
317
393
|
def set_multi(hash, ttl = nil, req_options = nil)
|
|
318
394
|
return if hash.empty?
|
|
319
395
|
|
|
396
|
+
validate_routing_tokens!(req_options)
|
|
397
|
+
|
|
320
398
|
Instrumentation.trace('set_multi', multi_trace_attrs('set_multi', hash.size, hash.keys)) do
|
|
321
399
|
if ring.servers.size == 1
|
|
322
400
|
single_server_set_multi(hash, ttl_or_default(ttl), req_options)
|
|
@@ -330,6 +408,7 @@ module Dalli
|
|
|
330
408
|
# Set the key-value pair, verifying existing CAS.
|
|
331
409
|
# Returns the resulting CAS value if succeeded, and falsy otherwise.
|
|
332
410
|
def set_cas(key, value, cas, ttl = nil, req_options = nil)
|
|
411
|
+
validate_routing_tokens!(req_options)
|
|
333
412
|
perform(:set, key, value, ttl_or_default(ttl), cas, req_options)
|
|
334
413
|
end
|
|
335
414
|
|
|
@@ -337,6 +416,7 @@ module Dalli
|
|
|
337
416
|
# Conditionally add a key/value pair, if the key does not already exist
|
|
338
417
|
# on the server. Returns truthy if the operation succeeded.
|
|
339
418
|
def add(key, value, ttl = nil, req_options = nil)
|
|
419
|
+
validate_routing_tokens!(req_options)
|
|
340
420
|
perform(:add, key, value, ttl_or_default(ttl), req_options)
|
|
341
421
|
end
|
|
342
422
|
|
|
@@ -352,17 +432,49 @@ module Dalli
|
|
|
352
432
|
# key already exists on the server. Returns the new CAS value if the
|
|
353
433
|
# operation succeeded, or falsy otherwise.
|
|
354
434
|
def replace_cas(key, value, cas, ttl = nil, req_options = nil)
|
|
435
|
+
validate_routing_tokens!(req_options)
|
|
355
436
|
perform(:replace, key, value, ttl_or_default(ttl), cas, req_options)
|
|
356
437
|
end
|
|
357
438
|
|
|
358
439
|
# Delete a key/value pair, verifying existing CAS.
|
|
359
440
|
# Returns true if succeeded, and falsy otherwise.
|
|
360
|
-
|
|
361
|
-
|
|
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)
|
|
362
448
|
end
|
|
363
449
|
|
|
364
|
-
|
|
365
|
-
|
|
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)
|
|
366
478
|
end
|
|
367
479
|
|
|
368
480
|
##
|
|
@@ -370,22 +482,35 @@ module Dalli
|
|
|
370
482
|
# This method is more efficient than calling delete() in a loop because
|
|
371
483
|
# it batches requests by server and uses quiet mode.
|
|
372
484
|
#
|
|
485
|
+
# `req_options` accepts the same meta-delete options as #delete and applies
|
|
486
|
+
# them to every key in the batch.
|
|
487
|
+
#
|
|
373
488
|
# @param keys [Array<String>] keys to delete
|
|
374
|
-
# @
|
|
375
|
-
#
|
|
376
|
-
#
|
|
377
|
-
#
|
|
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
|
|
378
499
|
#
|
|
379
500
|
# Example:
|
|
380
501
|
# client.delete_multi(['key1', 'key2', 'key3'])
|
|
381
|
-
|
|
502
|
+
# client.delete_multi(%w[key1 key2], invalidate: true, tombstone_ttl: 30)
|
|
503
|
+
def delete_multi(keys, req_options = nil)
|
|
382
504
|
return 0 if keys.empty?
|
|
383
505
|
|
|
506
|
+
validate_delete_options!(req_options)
|
|
507
|
+
validate_routing_tokens!(req_options)
|
|
508
|
+
|
|
384
509
|
Instrumentation.trace('delete_multi', multi_trace_attrs('delete_multi', keys.size, keys)) do
|
|
385
510
|
if ring.servers.size == 1
|
|
386
|
-
single_server_delete_multi(keys)
|
|
511
|
+
single_server_delete_multi(keys, req_options)
|
|
387
512
|
else
|
|
388
|
-
pipelined_deleter.process(keys)
|
|
513
|
+
pipelined_deleter.process(keys, req_options)
|
|
389
514
|
end
|
|
390
515
|
end
|
|
391
516
|
end
|
|
@@ -393,22 +518,22 @@ module Dalli
|
|
|
393
518
|
##
|
|
394
519
|
# Append value to the value already stored on the server for 'key'.
|
|
395
520
|
# Appending only works for values stored with :raw => true.
|
|
396
|
-
def append(key, value)
|
|
397
|
-
|
|
521
|
+
def append(key, value, req_options = nil)
|
|
522
|
+
validate_routing_tokens!(req_options)
|
|
523
|
+
perform(:append, key, value.to_s, req_options)
|
|
398
524
|
end
|
|
399
525
|
|
|
400
526
|
##
|
|
401
527
|
# Prepend value to the value already stored on the server for 'key'.
|
|
402
528
|
# Prepending only works for values stored with :raw => true.
|
|
403
|
-
def prepend(key, value)
|
|
404
|
-
|
|
529
|
+
def prepend(key, value, req_options = nil)
|
|
530
|
+
validate_routing_tokens!(req_options)
|
|
531
|
+
perform(:prepend, key, value.to_s, req_options)
|
|
405
532
|
end
|
|
406
533
|
|
|
407
534
|
##
|
|
408
535
|
# Incr adds the given amount to the counter on the memcached server.
|
|
409
536
|
# Amt must be a positive integer value.
|
|
410
|
-
# Default, if given, must be an Integer (or a String of decimal digits);
|
|
411
|
-
# anything else raises ArgumentError.
|
|
412
537
|
#
|
|
413
538
|
# If default is nil, the counter must already exist or the operation
|
|
414
539
|
# will fail and will return nil. Otherwise this method will return
|
|
@@ -419,18 +544,16 @@ module Dalli
|
|
|
419
544
|
# #cas.
|
|
420
545
|
#
|
|
421
546
|
# If the value already exists, it must have been set with raw: true
|
|
422
|
-
def incr(key, amt = 1, ttl = nil, default = nil)
|
|
547
|
+
def incr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
423
548
|
check_positive!(amt)
|
|
424
|
-
|
|
549
|
+
validate_routing_tokens!(req_options)
|
|
425
550
|
|
|
426
|
-
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)
|
|
427
552
|
end
|
|
428
553
|
|
|
429
554
|
##
|
|
430
555
|
# Decr subtracts the given amount from the counter on the memcached server.
|
|
431
556
|
# Amt must be a positive integer value.
|
|
432
|
-
# Default, if given, must be an Integer (or a String of decimal digits);
|
|
433
|
-
# anything else raises ArgumentError.
|
|
434
557
|
#
|
|
435
558
|
# memcached counters are unsigned and cannot hold negative values. Calling
|
|
436
559
|
# decr on a counter which is 0 will just return 0.
|
|
@@ -444,11 +567,11 @@ module Dalli
|
|
|
444
567
|
# #cas.
|
|
445
568
|
#
|
|
446
569
|
# If the value already exists, it must have been set with raw: true
|
|
447
|
-
def decr(key, amt = 1, ttl = nil, default = nil)
|
|
570
|
+
def decr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
448
571
|
check_positive!(amt)
|
|
449
|
-
|
|
572
|
+
validate_routing_tokens!(req_options)
|
|
450
573
|
|
|
451
|
-
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)
|
|
452
575
|
end
|
|
453
576
|
|
|
454
577
|
##
|
|
@@ -525,6 +648,30 @@ module Dalli
|
|
|
525
648
|
|
|
526
649
|
private
|
|
527
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
|
+
|
|
528
675
|
def record_hit_miss_metrics(span, key_count, hit_count)
|
|
529
676
|
return unless span
|
|
530
677
|
|
|
@@ -532,10 +679,10 @@ module Dalli
|
|
|
532
679
|
'db.memcached.miss_count' => key_count - hit_count)
|
|
533
680
|
end
|
|
534
681
|
|
|
535
|
-
def get_multi_yielding(keys)
|
|
682
|
+
def get_multi_yielding(keys, req_options = nil)
|
|
536
683
|
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
537
684
|
hit_count = 0
|
|
538
|
-
pipelined_getter.process(keys) do |k, data|
|
|
685
|
+
pipelined_getter.process(keys, req_options) do |k, data|
|
|
539
686
|
hit_count += 1
|
|
540
687
|
yield k, data.first
|
|
541
688
|
end
|
|
@@ -544,13 +691,13 @@ module Dalli
|
|
|
544
691
|
end
|
|
545
692
|
end
|
|
546
693
|
|
|
547
|
-
def get_multi_hash(keys)
|
|
694
|
+
def get_multi_hash(keys, req_options = nil)
|
|
548
695
|
Instrumentation.trace_with_result('get_multi', get_multi_attributes(keys)) do |span|
|
|
549
696
|
hash = if ring.servers.size == 1
|
|
550
|
-
single_server_get_multi(keys)
|
|
697
|
+
single_server_get_multi(keys, req_options)
|
|
551
698
|
else
|
|
552
699
|
{}.tap do |h|
|
|
553
|
-
pipelined_getter.process(keys) { |k, data| h[k] = data.first }
|
|
700
|
+
pipelined_getter.process(keys, req_options) { |k, data| h[k] = data.first }
|
|
554
701
|
end
|
|
555
702
|
end
|
|
556
703
|
record_hit_miss_metrics(span, keys.size, hash.size)
|
|
@@ -563,11 +710,33 @@ module Dalli
|
|
|
563
710
|
server if server&.alive?
|
|
564
711
|
end
|
|
565
712
|
|
|
566
|
-
|
|
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)
|
|
723
|
+
keys.map! { |k| @key_manager.validate_key(k.to_s) }
|
|
724
|
+
return {} unless (server = single_server)
|
|
725
|
+
|
|
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)
|
|
567
736
|
keys.map! { |k| @key_manager.validate_key(k.to_s) }
|
|
568
737
|
return {} unless (server = single_server)
|
|
569
738
|
|
|
570
|
-
result = server.request(:
|
|
739
|
+
result = server.request(:read_multi_with_metadata_req, keys, req_options)
|
|
571
740
|
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
572
741
|
result
|
|
573
742
|
rescue Dalli::NetworkError
|
|
@@ -579,24 +748,21 @@ module Dalli
|
|
|
579
748
|
return unless (server = single_server)
|
|
580
749
|
|
|
581
750
|
server.request(:write_multi_req, pairs, ttl, req_options)
|
|
582
|
-
rescue Dalli::
|
|
583
|
-
|
|
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
|
|
584
755
|
end
|
|
585
756
|
|
|
586
|
-
def single_server_delete_multi(keys)
|
|
757
|
+
def single_server_delete_multi(keys, req_options = nil)
|
|
587
758
|
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
588
759
|
return 0 unless (server = single_server)
|
|
589
760
|
|
|
590
|
-
server.request(:delete_multi_req, validated_keys)
|
|
761
|
+
server.request(:delete_multi_req, validated_keys, req_options)
|
|
591
762
|
rescue Dalli::RetryableNetworkError => e
|
|
592
|
-
# Mirror the pipelined path: retry transient errors so a momentary blip
|
|
593
|
-
# still yields a real count. Bounded by the server's socket_max_failures,
|
|
594
|
-
# after which a hard NetworkError is raised and handled below.
|
|
595
763
|
Dalli.logger.debug { e.inspect }
|
|
596
764
|
Dalli.logger.debug { 'retrying single-server delete_multi because of network error' }
|
|
597
765
|
retry
|
|
598
|
-
rescue Dalli::NetworkError
|
|
599
|
-
0
|
|
600
766
|
end
|
|
601
767
|
|
|
602
768
|
def get_multi_attributes(keys)
|
|
@@ -630,21 +796,32 @@ module Dalli
|
|
|
630
796
|
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
|
631
797
|
end
|
|
632
798
|
|
|
633
|
-
#
|
|
634
|
-
#
|
|
635
|
-
#
|
|
636
|
-
#
|
|
637
|
-
#
|
|
638
|
-
|
|
639
|
-
|
|
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
|
|
640
806
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
|
813
|
+
|
|
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)
|
|
644
821
|
end
|
|
645
822
|
|
|
646
823
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
647
|
-
|
|
824
|
+
validate_routing_tokens!(req_options)
|
|
648
825
|
(value, cas) = perform(:cas, key, req_options)
|
|
649
826
|
return if value.nil? && !always_set
|
|
650
827
|
|
|
@@ -654,8 +831,9 @@ module Dalli
|
|
|
654
831
|
|
|
655
832
|
def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
|
|
656
833
|
server = ring.server_for_key(key)
|
|
657
|
-
# req_options
|
|
658
|
-
#
|
|
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.
|
|
659
837
|
meta_options = req_options.is_a?(Hash) ? req_options.dup : {}
|
|
660
838
|
meta_options[:vivify_ttl] = lock_ttl
|
|
661
839
|
meta_options[:recache_ttl] = recache_threshold
|
|
@@ -697,20 +875,18 @@ module Dalli
|
|
|
697
875
|
# rubocop:enable Naming/MethodParameterName
|
|
698
876
|
return yield if block_given?
|
|
699
877
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
# namespace a second time and send the retry to a different key.
|
|
703
|
-
validated_key = @key_manager.validate_key(key.to_s)
|
|
878
|
+
key = key.to_s
|
|
879
|
+
key = @key_manager.validate_key(key)
|
|
704
880
|
|
|
705
|
-
server = ring.server_for_key(
|
|
881
|
+
server = ring.server_for_key(key)
|
|
706
882
|
|
|
707
883
|
if Instrumentation.enabled?
|
|
708
884
|
op_name = op.name
|
|
709
|
-
Instrumentation.trace(op_name, trace_attrs(op_name,
|
|
710
|
-
server.request(op,
|
|
885
|
+
Instrumentation.trace(op_name, trace_attrs(op_name, key, server)) do
|
|
886
|
+
server.request(op, key, ...)
|
|
711
887
|
end
|
|
712
888
|
else
|
|
713
|
-
server.request(op,
|
|
889
|
+
server.request(op, key, ...)
|
|
714
890
|
end
|
|
715
891
|
rescue RetryableNetworkError => e
|
|
716
892
|
Dalli.logger.debug { e.inspect }
|