dalli 5.2.5 → 5.3.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 +55 -0
- data/README.md +30 -0
- data/lib/dalli/client.rb +189 -76
- data/lib/dalli/key_grouping.rb +50 -0
- data/lib/dalli/key_manager.rb +30 -0
- data/lib/dalli/pipelined_deleter.rb +7 -16
- data/lib/dalli/pipelined_getter.rb +64 -58
- data/lib/dalli/pipelined_setter.rb +6 -15
- data/lib/dalli/protocol/base.rb +117 -41
- data/lib/dalli/protocol/connection_manager.rb +106 -2
- data/lib/dalli/protocol/key_regularizer.rb +5 -0
- data/lib/dalli/protocol/meta.rb +47 -9
- data/lib/dalli/protocol/request_formatter.rb +68 -20
- data/lib/dalli/protocol/response_processor.rb +49 -6
- data/lib/dalli/protocol/ttl_sanitizer.rb +13 -4
- data/lib/dalli/ring.rb +4 -0
- data/lib/dalli/servers_arg_normalizer.rb +19 -1
- data/lib/dalli/socket.rb +17 -2
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +14 -1
- data/lib/rack/session/dalli.rb +12 -3
- metadata +2 -1
data/lib/dalli/client.rb
CHANGED
|
@@ -77,8 +77,8 @@ module Dalli
|
|
|
77
77
|
#
|
|
78
78
|
def initialize(servers = nil, options = {})
|
|
79
79
|
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
|
80
|
+
warn_removed_options(options)
|
|
80
81
|
@options = normalize_options(options)
|
|
81
|
-
warn_removed_options(@options)
|
|
82
82
|
@key_manager = ::Dalli::KeyManager.new(@options)
|
|
83
83
|
@ring = nil
|
|
84
84
|
end
|
|
@@ -134,6 +134,10 @@ module Dalli
|
|
|
134
134
|
# - :return_last_access [Boolean] return seconds since last access
|
|
135
135
|
# - :return_ttl_remaining [Boolean] return seconds of TTL remaining (-1 if no TTL)
|
|
136
136
|
# - :skip_lru_bump [Boolean] don't bump LRU or update access stats
|
|
137
|
+
# - :vivify_ttl [Integer] on a miss, create a stub that lives this long
|
|
138
|
+
# (sanitized like any other TTL) and report won_recache
|
|
139
|
+
# - :recache_ttl [Integer] report won_recache when the item's remaining
|
|
140
|
+
# TTL is below this
|
|
137
141
|
#
|
|
138
142
|
# @return [Hash] containing:
|
|
139
143
|
# - :value - the cached value (or nil on miss)
|
|
@@ -160,6 +164,8 @@ module Dalli
|
|
|
160
164
|
#
|
|
161
165
|
def get_with_metadata(key, options = {})
|
|
162
166
|
validate_routing_tokens!(options)
|
|
167
|
+
validate_ttl!(:vivify_ttl, options[:vivify_ttl])
|
|
168
|
+
validate_integer!(:recache_ttl, options[:recache_ttl])
|
|
163
169
|
# A retry re-runs this method with its original arguments, so `key` must
|
|
164
170
|
# stay as the caller gave it; reassigning it would add the namespace again.
|
|
165
171
|
validated_key = @key_manager.validate_key(key.to_s)
|
|
@@ -200,11 +206,17 @@ module Dalli
|
|
|
200
206
|
#
|
|
201
207
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
202
208
|
#
|
|
203
|
-
# A transient network error is retried automatically. If a server
|
|
204
|
-
# unreachable after retrying, raises
|
|
205
|
-
#
|
|
209
|
+
# A transient network error is retried automatically. If a server stops
|
|
210
|
+
# during the call and is still unreachable after retrying, raises
|
|
211
|
+
# Dalli::NetworkError only with `failover: false`; with failover (the
|
|
212
|
+
# default) its keys go to another server, or are left out when no server
|
|
213
|
+
# is left. See "Network Errors and Retries" in the README.
|
|
206
214
|
#
|
|
207
|
-
#
|
|
215
|
+
# A retry starts the batch again, so a block can be yielded a key it was
|
|
216
|
+
# already yielded before the error.
|
|
217
|
+
#
|
|
218
|
+
# @raise [Dalli::NetworkError] if a server stops during the call and is
|
|
219
|
+
# still unreachable after retrying, without failover
|
|
208
220
|
# rubocop:disable-next Style/ExplicitBlockArgument
|
|
209
221
|
def get_multi(*keys, req_options: nil)
|
|
210
222
|
keys.flatten!
|
|
@@ -242,14 +254,18 @@ module Dalli
|
|
|
242
254
|
#
|
|
243
255
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
244
256
|
#
|
|
245
|
-
# A transient network error is retried automatically. If a server
|
|
246
|
-
# unreachable after retrying, raises
|
|
247
|
-
#
|
|
257
|
+
# A transient network error is retried automatically. If a server stops
|
|
258
|
+
# during the call and is still unreachable after retrying, raises
|
|
259
|
+
# Dalli::NetworkError only with `failover: false`; with failover (the
|
|
260
|
+
# default) its keys go to another server, or are left out when no server
|
|
261
|
+
# is left. See "Network Errors and Retries" in the README. The block is
|
|
262
|
+
# called only once every server has replied, so never twice for a key.
|
|
248
263
|
#
|
|
249
264
|
# @param keys [Array<String>] the keys to fetch
|
|
250
265
|
# @param req_options [Hash, nil] routing-token options
|
|
251
266
|
# @return [Hash] key => { value:, cas:, stale:, miss: }
|
|
252
|
-
# @raise [Dalli::NetworkError] if a server
|
|
267
|
+
# @raise [Dalli::NetworkError] if a server stops during the call and is
|
|
268
|
+
# still unreachable after retrying, without failover
|
|
253
269
|
def get_multi_with_metadata(*keys, req_options: nil, &block)
|
|
254
270
|
keys.flatten!
|
|
255
271
|
keys.compact!
|
|
@@ -306,6 +322,9 @@ module Dalli
|
|
|
306
322
|
# { 'key' => [value, cas_id] }
|
|
307
323
|
#
|
|
308
324
|
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
325
|
+
#
|
|
326
|
+
# Network errors are handled as in #get_multi, and as there a retry can
|
|
327
|
+
# yield a key a second time.
|
|
309
328
|
def get_multi_cas(*keys, req_options: nil)
|
|
310
329
|
validate_routing_tokens!(req_options)
|
|
311
330
|
|
|
@@ -328,11 +347,12 @@ module Dalli
|
|
|
328
347
|
# written to the cache and returned.
|
|
329
348
|
def fetch(key, ttl = nil, req_options = nil)
|
|
330
349
|
req_options = req_options.nil? ? CACHE_NILS : req_options.merge(CACHE_NILS) if cache_nils
|
|
350
|
+
ttl = ttl_or_default(ttl)
|
|
331
351
|
val = get(key, req_options)
|
|
332
352
|
return val unless block_given? && not_found?(val)
|
|
333
353
|
|
|
334
354
|
new_val = yield
|
|
335
|
-
add(key, new_val,
|
|
355
|
+
add(key, new_val, ttl, req_options)
|
|
336
356
|
new_val
|
|
337
357
|
end
|
|
338
358
|
|
|
@@ -369,7 +389,10 @@ module Dalli
|
|
|
369
389
|
def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
|
|
370
390
|
raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
|
|
371
391
|
|
|
372
|
-
|
|
392
|
+
# ttl is only used once the block has run; checked now so a bad one
|
|
393
|
+
# raises before the lock is taken
|
|
394
|
+
ttl_or_default(ttl)
|
|
395
|
+
validate_ttl!(:lock_ttl, lock_ttl)
|
|
373
396
|
validate_integer!(:recache_threshold, recache_threshold)
|
|
374
397
|
validate_routing_tokens!(req_options)
|
|
375
398
|
# Left unchanged for the retry below; see get_with_metadata
|
|
@@ -463,8 +486,10 @@ module Dalli
|
|
|
463
486
|
nil
|
|
464
487
|
end
|
|
465
488
|
|
|
489
|
+
# set_cas without its CAS check: the CAS here is always 0
|
|
466
490
|
def set(key, value, ttl = nil, req_options = nil)
|
|
467
|
-
|
|
491
|
+
validate_routing_tokens!(req_options)
|
|
492
|
+
perform(:set, key, value, ttl_or_default(ttl), 0, req_options)
|
|
468
493
|
end
|
|
469
494
|
|
|
470
495
|
##
|
|
@@ -472,16 +497,20 @@ module Dalli
|
|
|
472
497
|
# This method is more efficient than calling set() in a loop because
|
|
473
498
|
# it batches requests by server and uses quiet mode.
|
|
474
499
|
#
|
|
475
|
-
# A transient network error is retried automatically. If a server
|
|
476
|
-
# unreachable after retrying, raises
|
|
477
|
-
#
|
|
500
|
+
# A transient network error is retried automatically. If a server stops
|
|
501
|
+
# during the call and is still unreachable after retrying, raises
|
|
502
|
+
# Dalli::NetworkError only with `failover: false`; with failover (the
|
|
503
|
+
# default) its keys go to another server, or are left out when no server
|
|
504
|
+
# is left. See "Network Errors and Retries" in the README. Keys already
|
|
505
|
+
# written to other servers before an error are not rolled back.
|
|
478
506
|
#
|
|
479
507
|
# @param hash [Hash] key-value pairs to set
|
|
480
508
|
# @param ttl [Integer] time-to-live in seconds (optional, uses default if not provided)
|
|
481
509
|
# @param req_options [Hash] options passed to each set operation; accepts
|
|
482
510
|
# :p_token/:l_token, applied to every key in the batch
|
|
483
511
|
# @return [void]
|
|
484
|
-
# @raise [Dalli::NetworkError] if a server
|
|
512
|
+
# @raise [Dalli::NetworkError] if a server stops during the call and is
|
|
513
|
+
# still unreachable after retrying, without failover
|
|
485
514
|
#
|
|
486
515
|
# Example:
|
|
487
516
|
# client.set_multi({ 'key1' => 'value1', 'key2' => 'value2' }, 300)
|
|
@@ -502,7 +531,11 @@ module Dalli
|
|
|
502
531
|
##
|
|
503
532
|
# Set the key-value pair, verifying existing CAS.
|
|
504
533
|
# Returns the resulting CAS value if succeeded, and falsy otherwise.
|
|
534
|
+
# The CAS must be an Integer (or a String of decimal digits) between 0 and
|
|
535
|
+
# 2**64 - 1, or nil; anything else raises ArgumentError. A CAS of 0 or nil
|
|
536
|
+
# means an unconditional set.
|
|
505
537
|
def set_cas(key, value, cas, ttl = nil, req_options = nil)
|
|
538
|
+
validate_uint64!(:cas, cas)
|
|
506
539
|
validate_routing_tokens!(req_options)
|
|
507
540
|
perform(:set, key, value, ttl_or_default(ttl), cas, req_options)
|
|
508
541
|
end
|
|
@@ -525,18 +558,22 @@ module Dalli
|
|
|
525
558
|
##
|
|
526
559
|
# Conditionally add a key/value pair, verifying existing CAS, only if the
|
|
527
560
|
# key already exists on the server. Returns the new CAS value if the
|
|
528
|
-
# operation succeeded, or falsy otherwise.
|
|
561
|
+
# operation succeeded, or falsy otherwise. The CAS is validated as in
|
|
562
|
+
# #set_cas.
|
|
529
563
|
def replace_cas(key, value, cas, ttl = nil, req_options = nil)
|
|
564
|
+
validate_uint64!(:cas, cas)
|
|
530
565
|
validate_routing_tokens!(req_options)
|
|
531
566
|
perform(:replace, key, value, ttl_or_default(ttl), cas, req_options)
|
|
532
567
|
end
|
|
533
568
|
|
|
534
569
|
# Delete a key/value pair, verifying existing CAS.
|
|
535
570
|
# Returns true if succeeded, and falsy otherwise.
|
|
536
|
-
# Delete a key, optionally with a CAS check.
|
|
571
|
+
# Delete a key, optionally with a CAS check. The CAS is validated as in
|
|
572
|
+
# #set_cas.
|
|
537
573
|
#
|
|
538
574
|
# `req_options` accepts the same meta-delete options as #delete.
|
|
539
575
|
def delete_cas(key, cas = 0, req_options = nil)
|
|
576
|
+
validate_uint64!(:cas, cas)
|
|
540
577
|
validate_delete_options!(req_options)
|
|
541
578
|
validate_routing_tokens!(req_options)
|
|
542
579
|
perform(:delete, key, cas, req_options)
|
|
@@ -588,9 +625,10 @@ module Dalli
|
|
|
588
625
|
# is whichever one the caller asked for. This is best-effort: a transient
|
|
589
626
|
# network error is retried automatically, and keys handled before the
|
|
590
627
|
# error are not recounted, so the result may under-report when a retry
|
|
591
|
-
# occurs.
|
|
592
|
-
#
|
|
593
|
-
# @raise [Dalli::NetworkError] if a server
|
|
628
|
+
# occurs. A server that stops during the call is handled as in
|
|
629
|
+
# #set_multi.
|
|
630
|
+
# @raise [Dalli::NetworkError] if a server stops during the call and is
|
|
631
|
+
# still unreachable after retrying, without failover
|
|
594
632
|
#
|
|
595
633
|
# Example:
|
|
596
634
|
# client.delete_multi(['key1', 'key2', 'key3'])
|
|
@@ -628,9 +666,8 @@ module Dalli
|
|
|
628
666
|
|
|
629
667
|
##
|
|
630
668
|
# Incr adds the given amount to the counter on the memcached server.
|
|
631
|
-
# Amt must be
|
|
632
|
-
#
|
|
633
|
-
# anything else raises ArgumentError.
|
|
669
|
+
# Amt, and default if given, must be Integers (or Strings of decimal
|
|
670
|
+
# digits) between 0 and 2**64 - 1; anything else raises ArgumentError.
|
|
634
671
|
#
|
|
635
672
|
# If default is nil, the counter must already exist or the operation
|
|
636
673
|
# will fail and will return nil. Otherwise this method will return
|
|
@@ -642,18 +679,17 @@ module Dalli
|
|
|
642
679
|
#
|
|
643
680
|
# If the value already exists, it must have been set with raw: true
|
|
644
681
|
def incr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
645
|
-
|
|
646
|
-
|
|
682
|
+
amt = validate_uint64!(:amt, amt)
|
|
683
|
+
default = validate_uint64!(:default, default)
|
|
647
684
|
validate_routing_tokens!(req_options)
|
|
648
685
|
|
|
649
|
-
perform(:incr, key, amt
|
|
686
|
+
perform(:incr, key, amt, ttl_or_default(ttl), default, req_options)
|
|
650
687
|
end
|
|
651
688
|
|
|
652
689
|
##
|
|
653
690
|
# Decr subtracts the given amount from the counter on the memcached server.
|
|
654
|
-
# Amt must be
|
|
655
|
-
#
|
|
656
|
-
# anything else raises ArgumentError.
|
|
691
|
+
# Amt, and default if given, must be Integers (or Strings of decimal
|
|
692
|
+
# digits) between 0 and 2**64 - 1; anything else raises ArgumentError.
|
|
657
693
|
#
|
|
658
694
|
# memcached counters are unsigned and cannot hold negative values. Calling
|
|
659
695
|
# decr on a counter which is 0 will just return 0.
|
|
@@ -668,20 +704,23 @@ module Dalli
|
|
|
668
704
|
#
|
|
669
705
|
# If the value already exists, it must have been set with raw: true
|
|
670
706
|
def decr(key, amt = 1, ttl = nil, default = nil, req_options = nil)
|
|
671
|
-
|
|
672
|
-
|
|
707
|
+
amt = validate_uint64!(:amt, amt)
|
|
708
|
+
default = validate_uint64!(:default, default)
|
|
673
709
|
validate_routing_tokens!(req_options)
|
|
674
710
|
|
|
675
|
-
perform(:decr, key, amt
|
|
711
|
+
perform(:decr, key, amt, ttl_or_default(ttl), default, req_options)
|
|
676
712
|
end
|
|
677
713
|
|
|
678
714
|
##
|
|
679
715
|
# Flush the memcached server, at 'delay' seconds in the future.
|
|
680
716
|
# Delay defaults to zero seconds, which means an immediate flush.
|
|
717
|
+
# Delay must be a non-negative Integer (or a String of decimal digits);
|
|
718
|
+
# anything else raises ArgumentError.
|
|
681
719
|
# Every server is attempted. If any of them fails, the first error is
|
|
682
720
|
# raised once all the others have been flushed.
|
|
683
721
|
##
|
|
684
722
|
def flush(delay = 0)
|
|
723
|
+
validate_uint64!(:delay, delay)
|
|
685
724
|
first_error = nil
|
|
686
725
|
results = ring.servers.map do |s|
|
|
687
726
|
s.request(:flush, delay)
|
|
@@ -774,15 +813,7 @@ module Dalli
|
|
|
774
813
|
|
|
775
814
|
raise ArgumentError, 'tombstone_ttl requires invalidate: true' unless req_options[:invalidate]
|
|
776
815
|
|
|
777
|
-
|
|
778
|
-
# path; validated here first so a bad value raises cleanly instead of
|
|
779
|
-
# unwinding through Protocol::Base#request, which would close the
|
|
780
|
-
# connection on the ArgumentError Integer() raises.
|
|
781
|
-
begin
|
|
782
|
-
Integer(tombstone_ttl)
|
|
783
|
-
rescue ArgumentError, TypeError
|
|
784
|
-
raise ArgumentError, "tombstone_ttl must be an integer, got #{tombstone_ttl.inspect}"
|
|
785
|
-
end
|
|
816
|
+
validate_ttl!(:tombstone_ttl, tombstone_ttl)
|
|
786
817
|
end
|
|
787
818
|
|
|
788
819
|
def record_hit_miss_metrics(span, key_count, hit_count)
|
|
@@ -818,9 +849,20 @@ module Dalli
|
|
|
818
849
|
end
|
|
819
850
|
end
|
|
820
851
|
|
|
821
|
-
|
|
852
|
+
# retried is true once a request to the server has failed with a
|
|
853
|
+
# RetryableNetworkError. Without failover, finding the server down then
|
|
854
|
+
# means that request failed, which raises, rather than the server being
|
|
855
|
+
# down before the operation began, which returns nil. With failover it
|
|
856
|
+
# returns nil either way, as the multi-server path leaves keys out when
|
|
857
|
+
# no server is left.
|
|
858
|
+
def single_server(retried: false)
|
|
822
859
|
server = ring.servers.first
|
|
823
|
-
server if server&.alive?
|
|
860
|
+
return server if server&.alive?
|
|
861
|
+
if retried && !ring.failover?
|
|
862
|
+
raise Dalli::NetworkError, "#{server&.name} failed during this request and is still unreachable"
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
nil
|
|
824
866
|
end
|
|
825
867
|
|
|
826
868
|
# The three single_server_* fast-path methods below share one contract,
|
|
@@ -829,20 +871,22 @@ module Dalli
|
|
|
829
871
|
# socket_max_failures, same as the pipelined path's retry), and a hard
|
|
830
872
|
# NetworkError -- the server genuinely unreachable, not just blipping --
|
|
831
873
|
# propagates to the caller rather than being swallowed into a silently
|
|
832
|
-
# wrong result
|
|
833
|
-
#
|
|
834
|
-
#
|
|
874
|
+
# wrong result, including (without failover) the server going down before
|
|
875
|
+
# the retry, which single_server raises for. Otherwise finding no live
|
|
876
|
+
# server is still silent, matching Ring#keys_grouped_by_server dropping a
|
|
877
|
+
# key it can't route on both the single- and multi-server paths. Each
|
|
878
|
+
# method sets retried before it retries; a local keeps its value across
|
|
879
|
+
# a retry.
|
|
835
880
|
def single_server_get_multi(keys, req_options = nil)
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
return {} unless (server = single_server)
|
|
881
|
+
retried ||= false
|
|
882
|
+
caller_keys = @key_manager.caller_keys_by_wire_key(keys)
|
|
883
|
+
return {} unless (server = single_server(retried: retried))
|
|
839
884
|
|
|
840
|
-
|
|
841
|
-
result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
|
|
842
|
-
result
|
|
885
|
+
results_for_caller_keys(server.request(:read_multi_req, caller_keys.keys, req_options), caller_keys)
|
|
843
886
|
rescue Dalli::RetryableNetworkError => e
|
|
844
887
|
Dalli.logger.debug { e.inspect }
|
|
845
888
|
Dalli.logger.debug { 'retrying single-server get_multi because of network error' }
|
|
889
|
+
retried = true
|
|
846
890
|
retry
|
|
847
891
|
rescue Dalli::UnexpectedReplyKeyError => e
|
|
848
892
|
# The connection is closed and the keys are treated as missing, as the
|
|
@@ -852,40 +896,54 @@ module Dalli
|
|
|
852
896
|
end
|
|
853
897
|
|
|
854
898
|
def single_server_get_multi_with_metadata(keys, req_options = nil)
|
|
855
|
-
|
|
856
|
-
|
|
899
|
+
retried ||= false
|
|
900
|
+
caller_keys = @key_manager.caller_keys_by_wire_key(keys)
|
|
901
|
+
return {} unless (server = single_server(retried: retried))
|
|
857
902
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
result
|
|
903
|
+
results_for_caller_keys(server.request(:read_multi_with_metadata_req, caller_keys.keys, req_options),
|
|
904
|
+
caller_keys)
|
|
861
905
|
rescue Dalli::RetryableNetworkError => e
|
|
862
906
|
Dalli.logger.debug { e.inspect }
|
|
863
907
|
Dalli.logger.debug { 'retrying single-server get_multi_with_metadata because of network error' }
|
|
908
|
+
retried = true
|
|
864
909
|
retry
|
|
865
910
|
rescue Dalli::UnexpectedReplyKeyError => e
|
|
866
911
|
Dalli.logger.warn { e.message }
|
|
867
912
|
{}
|
|
868
913
|
end
|
|
869
914
|
|
|
915
|
+
# Replies name the key as sent; returns them under the caller's keys,
|
|
916
|
+
# skipping any key that wasn't requested
|
|
917
|
+
def results_for_caller_keys(results, caller_keys)
|
|
918
|
+
results.each_with_object({}) do |(key, value), hash|
|
|
919
|
+
caller_key = @key_manager.caller_key(caller_keys, key)
|
|
920
|
+
hash[caller_key] = value if caller_key
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
|
|
870
924
|
def single_server_set_multi(hash, ttl, req_options)
|
|
925
|
+
retried ||= false
|
|
871
926
|
pairs = hash.transform_keys { |k| @key_manager.validate_key(k.to_s) }
|
|
872
|
-
return unless (server = single_server)
|
|
927
|
+
return unless (server = single_server(retried: retried))
|
|
873
928
|
|
|
874
929
|
server.request(:write_multi_req, pairs, ttl, req_options)
|
|
875
930
|
rescue Dalli::RetryableNetworkError => e
|
|
876
931
|
Dalli.logger.debug { e.inspect }
|
|
877
932
|
Dalli.logger.debug { 'retrying single-server set_multi because of network error' }
|
|
933
|
+
retried = true
|
|
878
934
|
retry
|
|
879
935
|
end
|
|
880
936
|
|
|
881
937
|
def single_server_delete_multi(keys, req_options = nil)
|
|
938
|
+
retried ||= false
|
|
882
939
|
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
883
|
-
return 0 unless (server = single_server)
|
|
940
|
+
return 0 unless (server = single_server(retried: retried))
|
|
884
941
|
|
|
885
942
|
server.request(:delete_multi_req, validated_keys, req_options)
|
|
886
943
|
rescue Dalli::RetryableNetworkError => e
|
|
887
944
|
Dalli.logger.debug { e.inspect }
|
|
888
945
|
Dalli.logger.debug { 'retrying single-server delete_multi because of network error' }
|
|
946
|
+
retried = true
|
|
889
947
|
retry
|
|
890
948
|
end
|
|
891
949
|
|
|
@@ -917,10 +975,6 @@ module Dalli
|
|
|
917
975
|
attrs
|
|
918
976
|
end
|
|
919
977
|
|
|
920
|
-
def check_positive!(amt)
|
|
921
|
-
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
|
922
|
-
end
|
|
923
|
-
|
|
924
978
|
# Numeric arguments that become meta protocol flags (GHSA-6wmv-xq9m-fmp7).
|
|
925
979
|
# RequestFormatter converts them to Integer as the wire-level backstop;
|
|
926
980
|
# checking here too gives the caller a clean ArgumentError for the same
|
|
@@ -933,6 +987,27 @@ module Dalli
|
|
|
933
987
|
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
934
988
|
end
|
|
935
989
|
|
|
990
|
+
MAX_UINT64 = (2**64) - 1
|
|
991
|
+
private_constant :MAX_UINT64
|
|
992
|
+
|
|
993
|
+
# CAS values, incr/decr amounts and initial values, and the flush delay
|
|
994
|
+
# are read by memcached as unsigned 64-bit integers. RequestFormatter
|
|
995
|
+
# raises on anything else as the wire-level backstop; checking here first
|
|
996
|
+
# keeps the connection open, for the reason given for routing tokens
|
|
997
|
+
# below. Accepts an Integer or a String of decimal digits, and returns it
|
|
998
|
+
# as an Integer.
|
|
999
|
+
def validate_uint64!(name, value)
|
|
1000
|
+
return if value.nil?
|
|
1001
|
+
|
|
1002
|
+
int = case value
|
|
1003
|
+
when Integer then value
|
|
1004
|
+
when String then Integer(value, 10, exception: false)
|
|
1005
|
+
end
|
|
1006
|
+
return int if int && int >= 0 && int <= MAX_UINT64
|
|
1007
|
+
|
|
1008
|
+
raise ArgumentError, "#{name} must be an Integer between 0 and #{MAX_UINT64}, got #{value.inspect}"
|
|
1009
|
+
end
|
|
1010
|
+
|
|
936
1011
|
# Validated here, before the request reaches Protocol::Base#request, rather
|
|
937
1012
|
# than only at the RequestFormatter level. Reaching only the formatter's
|
|
938
1013
|
# check means unwinding through Protocol::Base#request, which logs the
|
|
@@ -961,15 +1036,16 @@ module Dalli
|
|
|
961
1036
|
|
|
962
1037
|
def cas_core(key, always_set, ttl = nil, req_options = nil)
|
|
963
1038
|
validate_routing_tokens!(req_options)
|
|
1039
|
+
ttl = ttl_or_default(ttl)
|
|
964
1040
|
(value, cas) = perform(:cas, key, req_options)
|
|
965
1041
|
return if !always_set && cas_miss?(value, cas)
|
|
966
1042
|
|
|
967
1043
|
newvalue = yield(value)
|
|
968
1044
|
# A CAS of 0 means the key was missing. A set with CAS 0 is unconditional,
|
|
969
1045
|
# so add instead: it fails if another client created the key meanwhile.
|
|
970
|
-
return perform(:add, key, newvalue,
|
|
1046
|
+
return perform(:add, key, newvalue, ttl, req_options) if cas.zero?
|
|
971
1047
|
|
|
972
|
-
perform(:set, key, newvalue,
|
|
1048
|
+
perform(:set, key, newvalue, ttl, cas, req_options)
|
|
973
1049
|
end
|
|
974
1050
|
|
|
975
1051
|
# The CAS lookup returns [nil, 0] on a miss rather than NOT_FOUND, and a
|
|
@@ -998,12 +1074,47 @@ module Dalli
|
|
|
998
1074
|
|
|
999
1075
|
##
|
|
1000
1076
|
# Uses the argument TTL or the client-wide default. Ensures
|
|
1001
|
-
# that the value is an integer
|
|
1077
|
+
# that the value is an integer memcached accepts as an expiration
|
|
1002
1078
|
##
|
|
1003
1079
|
def ttl_or_default(ttl)
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1080
|
+
ttl ||= @options[:expires_in] || 0
|
|
1081
|
+
# validate_ttl!'s fast path, without the call
|
|
1082
|
+
return ttl if ttl.is_a?(Integer) && ttl >= Protocol::TtlSanitizer::MIN_EXPIRATION_TIME &&
|
|
1083
|
+
ttl <= Protocol::TtlSanitizer::MAX_ACCEPTABLE_EXPIRATION_INTERVAL
|
|
1084
|
+
|
|
1085
|
+
validate_ttl!(:ttl, ttl)
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1088
|
+
# Returns the TTL as an Integer, sanitized (see TtlSanitizer), raising
|
|
1089
|
+
# ArgumentError unless memcached can use it. An interval over 30 days comes
|
|
1090
|
+
# back as the Unix time it becomes, so the request path doesn't convert it
|
|
1091
|
+
# again. A String must be a whole number in decimal, as for the other
|
|
1092
|
+
# numeric arguments; anything else is converted with to_i, so a Float or
|
|
1093
|
+
# an ActiveSupport::Duration still works. Once sanitized it must fit memcached's signed 32-bit expiration time. A
|
|
1094
|
+
# negative TTL is allowed: memcached stores the item as already expired.
|
|
1095
|
+
# memcached rejects a TTL outside that with CLIENT_ERROR, which a quiet
|
|
1096
|
+
# block or set_multi silently discards, so it is checked here, before the
|
|
1097
|
+
# request is written.
|
|
1098
|
+
def validate_ttl!(name, value)
|
|
1099
|
+
return if value.nil?
|
|
1100
|
+
# The usual case: an Integer TTL that TtlSanitizer leaves unchanged
|
|
1101
|
+
return value if value.is_a?(Integer) && value >= Protocol::TtlSanitizer::MIN_EXPIRATION_TIME &&
|
|
1102
|
+
value <= Protocol::TtlSanitizer::MAX_ACCEPTABLE_EXPIRATION_INTERVAL
|
|
1103
|
+
|
|
1104
|
+
ttl = ttl_to_i(name, value)
|
|
1105
|
+
min = Protocol::TtlSanitizer::MIN_EXPIRATION_TIME
|
|
1106
|
+
max = Protocol::TtlSanitizer::MAX_EXPIRATION_TIME
|
|
1107
|
+
ttl = Protocol::TtlSanitizer.sanitize(ttl)
|
|
1108
|
+
return ttl if ttl.between?(min, max)
|
|
1109
|
+
|
|
1110
|
+
raise ArgumentError, "#{name} must be a number of seconds no smaller than #{min} or a Unix time " \
|
|
1111
|
+
"no later than #{max}, got #{value.inspect}"
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
def ttl_to_i(name, value)
|
|
1115
|
+
value.is_a?(String) ? Integer(value, 10) : value.to_i
|
|
1116
|
+
rescue ArgumentError, NoMethodError, FloatDomainError
|
|
1117
|
+
raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
|
|
1007
1118
|
end
|
|
1008
1119
|
|
|
1009
1120
|
def ring
|
|
@@ -1018,7 +1129,10 @@ module Dalli
|
|
|
1018
1129
|
#
|
|
1019
1130
|
# This method also forces retries on network errors - when
|
|
1020
1131
|
# a particular memcached instance becomes unreachable, or the
|
|
1021
|
-
# operation times out.
|
|
1132
|
+
# operation times out. Once some of the request has been sent, only
|
|
1133
|
+
# one that is safe to apply twice is retried; for the others
|
|
1134
|
+
# (incr, decr, append, prepend, add, replace and CAS writes)
|
|
1135
|
+
# Protocol::Base#request raises a NetworkError, which isn't retried.
|
|
1022
1136
|
##
|
|
1023
1137
|
# rubocop:disable Naming/MethodParameterName
|
|
1024
1138
|
def perform(op, key, ...)
|
|
@@ -1047,13 +1161,12 @@ module Dalli
|
|
|
1047
1161
|
end
|
|
1048
1162
|
|
|
1049
1163
|
# Works on a copy so the caller's hash is neither modified here nor able
|
|
1050
|
-
# to change this client's configuration later.
|
|
1164
|
+
# to change this client's configuration later. Credentials are dropped:
|
|
1165
|
+
# Dalli 5 doesn't use them, and kept they could show up in #inspect.
|
|
1051
1166
|
def normalize_options(options)
|
|
1052
|
-
opts = options.
|
|
1053
|
-
opts[:expires_in] = opts[:expires_in]
|
|
1167
|
+
opts = options.except(:username, :password)
|
|
1168
|
+
opts[:expires_in] = ttl_to_i(:expires_in, opts[:expires_in]) if opts[:expires_in]
|
|
1054
1169
|
opts
|
|
1055
|
-
rescue NoMethodError
|
|
1056
|
-
raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
|
|
1057
1170
|
end
|
|
1058
1171
|
|
|
1059
1172
|
REMOVED_OPTIONS = {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# Groups a multi-key operation's keys by server, for PipelinedGetter,
|
|
6
|
+
# PipelinedSetter and PipelinedDeleter. Each of them is created for one
|
|
7
|
+
# operation, and retries the whole operation after a RetryableNetworkError.
|
|
8
|
+
#
|
|
9
|
+
# A key with no server to go to is left out, as it always has been: a
|
|
10
|
+
# server that was down before the operation started is skipped, and with
|
|
11
|
+
# failover its keys go to another server. Without failover a retry is
|
|
12
|
+
# different. The server was there for the first attempt and failed while
|
|
13
|
+
# serving it, so if the retry finds it unreachable, the operation raises
|
|
14
|
+
# Dalli::NetworkError rather than returning that server's keys as misses or
|
|
15
|
+
# leaving them unwritten. With failover the keys go to another server, or
|
|
16
|
+
# are left out when none is left, as for a server down from the start.
|
|
17
|
+
##
|
|
18
|
+
module KeyGrouping
|
|
19
|
+
NO_KEYS = [].freeze
|
|
20
|
+
private_constant :NO_KEYS
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# action names the operation in the debug log ('get', 'set', 'delete')
|
|
25
|
+
def groups_for_keys(keys, action)
|
|
26
|
+
groups = @ring.keys_grouped_by_server(keys)
|
|
27
|
+
unrouted = groups.delete(nil)
|
|
28
|
+
check_unrouted!(unrouted) unless @ring.failover?
|
|
29
|
+
if unrouted
|
|
30
|
+
Dalli.logger.debug do
|
|
31
|
+
"unable to #{action} #{unrouted.length} keys because no matching server was found"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
groups
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The first attempt records the keys that already had no server; a retry
|
|
38
|
+
# raises for any other key that has none now
|
|
39
|
+
def check_unrouted!(unrouted)
|
|
40
|
+
return @unrouted_on_first_attempt = unrouted || NO_KEYS unless @unrouted_on_first_attempt
|
|
41
|
+
return unless unrouted
|
|
42
|
+
|
|
43
|
+
lost = unrouted - @unrouted_on_first_attempt
|
|
44
|
+
return if lost.empty?
|
|
45
|
+
|
|
46
|
+
raise Dalli::NetworkError, "#{lost.length} keys had a server that failed during this request " \
|
|
47
|
+
'and is still unreachable after retrying'
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/dalli/key_manager.rb
CHANGED
|
@@ -71,6 +71,31 @@ module Dalli
|
|
|
71
71
|
"#{evaluate_namespace}#{namespace_separator}#{key}"
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
##
|
|
75
|
+
# Validates the keys for a multi-get. Returns a Hash from each key as
|
|
76
|
+
# sent to the caller's key (as a String) it came from, for #caller_key.
|
|
77
|
+
# A reply names the key as sent: namespaced, digested when too long, and
|
|
78
|
+
# decoded from base64 as UTF-8 when it was encoded, so it can't be turned
|
|
79
|
+
# back into the caller's key on its own. Keys are compared as bytes, and
|
|
80
|
+
# a key given more than once is sent once.
|
|
81
|
+
##
|
|
82
|
+
def caller_keys_by_wire_key(keys)
|
|
83
|
+
keys.each_with_object({}) do |key, caller_keys|
|
|
84
|
+
key = key.to_s
|
|
85
|
+
caller_keys[bytes_form(validate_key(key))] ||= key
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# The caller's key for a multi-get reply's key, using the Hash from
|
|
91
|
+
# #caller_keys_by_wire_key. Returns nil for a key that wasn't requested.
|
|
92
|
+
##
|
|
93
|
+
def caller_key(caller_keys, reply_key)
|
|
94
|
+
# A non-ASCII key in the Hash is binary, and a reply's key decoded from
|
|
95
|
+
# base64 is UTF-8; ASCII keys, the usual case, are found as is
|
|
96
|
+
caller_keys[reply_key] || caller_keys[reply_key.b]
|
|
97
|
+
end
|
|
98
|
+
|
|
74
99
|
def key_without_namespace(key)
|
|
75
100
|
return key if namespace.nil?
|
|
76
101
|
|
|
@@ -146,6 +171,11 @@ module Dalli
|
|
|
146
171
|
((key.bytesize + 2) / 3) * 4
|
|
147
172
|
end
|
|
148
173
|
|
|
174
|
+
# ASCII keys compare equal in any encoding as is
|
|
175
|
+
def bytes_form(key)
|
|
176
|
+
key.ascii_only? ? key : key.b
|
|
177
|
+
end
|
|
178
|
+
|
|
149
179
|
def prefix_length(digest)
|
|
150
180
|
return TRUNCATED_KEY_TARGET_SIZE - (TRUNCATED_KEY_SEPARATOR.length + digest.length) if namespace.nil?
|
|
151
181
|
|
|
@@ -7,6 +7,8 @@ module Dalli
|
|
|
7
7
|
# and using quiet mode to minimize round trips.
|
|
8
8
|
##
|
|
9
9
|
class PipelinedDeleter
|
|
10
|
+
include KeyGrouping
|
|
11
|
+
|
|
10
12
|
def initialize(ring, key_manager)
|
|
11
13
|
@ring = ring
|
|
12
14
|
@key_manager = key_manager
|
|
@@ -25,8 +27,9 @@ module Dalli
|
|
|
25
27
|
# tombstoned rather than removed. This is best-effort: a transient
|
|
26
28
|
# network error is retried automatically, and keys handled before the
|
|
27
29
|
# error are not recounted, so the result may under-report when a retry
|
|
28
|
-
# occurs. If a server
|
|
29
|
-
# Dalli::NetworkError
|
|
30
|
+
# occurs. If a server stops during the call and is still unreachable
|
|
31
|
+
# after retrying, raises Dalli::NetworkError without failover (see
|
|
32
|
+
# KeyGrouping).
|
|
30
33
|
##
|
|
31
34
|
def process(keys, req_options = nil)
|
|
32
35
|
return 0 if keys.empty?
|
|
@@ -44,7 +47,8 @@ module Dalli
|
|
|
44
47
|
private
|
|
45
48
|
|
|
46
49
|
def setup_requests(keys, req_options = nil)
|
|
47
|
-
|
|
50
|
+
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
51
|
+
groups = groups_for_keys(validated_keys, 'delete')
|
|
48
52
|
make_delete_requests(groups, req_options)
|
|
49
53
|
groups
|
|
50
54
|
end
|
|
@@ -89,18 +93,5 @@ module Dalli
|
|
|
89
93
|
0
|
|
90
94
|
end
|
|
91
95
|
end
|
|
92
|
-
|
|
93
|
-
def groups_for_keys(keys)
|
|
94
|
-
validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
|
|
95
|
-
groups = @ring.keys_grouped_by_server(validated_keys)
|
|
96
|
-
|
|
97
|
-
if (unfound_keys = groups.delete(nil))
|
|
98
|
-
Dalli.logger.debug do
|
|
99
|
-
"unable to delete #{unfound_keys.length} keys because no matching server was found"
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
groups
|
|
104
|
-
end
|
|
105
96
|
end
|
|
106
97
|
end
|