dalli 5.2.2 → 5.2.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd910ad2e0c6e5aa47b8591e9ba8c5806b6208fbab0ae0852637aecc48939051
4
- data.tar.gz: bd0db96211060011acb2135feb337d8acc88638b08c57b3fa875445695a09b59
3
+ metadata.gz: 626c8dbb98798247423221395d6d518a87ad9ce64ce268488ecd4eb7d8d7f347
4
+ data.tar.gz: 999126be42f4c80be3eb566d3f10dfbb8e10b46446aaaf0da546523c0e734eef
5
5
  SHA512:
6
- metadata.gz: 82da7aebd50661e1ab9af25dab7d66e5f70cb15260ff9ca4b3476e63eeb60cbb2ad1299718e56312f9a6a61d7a2b14d394fcc470325329f1152db42ac921ee6d
7
- data.tar.gz: a036421fac121aa342adc3beced711ce64a1bd241673a9314f1f1454a951515890732c4c9651e52baf65c94cd01c4b5cbb655c8dcca4fa4db03d656c3cfc4abe
6
+ metadata.gz: 34aaa230697fd4ac27118ec8a90608d30bdc8dabaf9fc5cfc1b8c4e56a20c701708f1b6b7052fd76c7363f25e238905648296865c41b770f434457005af7aac8
7
+ data.tar.gz: 7bf1bbe810632bd9733a9cb122900ff9e1b283ade2ef071cec13d609cf4e0b2580de06f1116e9a71685fe98aa7804022c719ef3b921e41e9eb76f2be98b5ab1a
data/CHANGELOG.md CHANGED
@@ -4,6 +4,74 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 5.2.4
8
+ ==========
9
+
10
+ Security:
11
+
12
+ - Keep a connection shared by several threads or fibers in step during a `quiet` block (GHSA-5xvx-r8rg-5qm2)
13
+ - A `quiet` (or `multi`) block is quiet only for the thread or fiber that runs it. memcached still replies to a quiet request that fails (for example an `add` of a key that exists), and another thread or fiber using the same client while the block was open read that reply as its own, so its later requests could return another key's value. A non-quiet request now reads any pending quiet replies first
14
+ - Don't let a `get_multi` that stopped early leave replies for a later request (GHSA-5xvx-r8rg-5qm2)
15
+ - A pipelined `get_multi` stopped by a non-network exception (its block raising or breaking, `Timeout.timeout`, or a value that can't be deserialized) left its unread replies in the response buffer, and a later `get_multi` large enough to use the interleaved path returned them as its own. The buffer is now cleared when a connection closes or a pipelined get starts, and unfinished pipelines are aborted
16
+ - Affects every supported line (the `get_multi` issue, 4.3 and later); fixed in 5.2.4, 5.1.6, 5.0.12, 4.3.9 and 3.2.15. On 5.2, clients with `defer_drain: true` weren't affected by the `quiet` issue
17
+
18
+ Bug fixes:
19
+
20
+ - A failed TLS handshake is now handled as a network error even when openssl wasn't loaded before Dalli
21
+ - The fix in 5.2.3 (#1215) took effect only when the application had already required openssl: otherwise Dalli's list of TLS errors was empty, so a failed handshake still raised `OpenSSL::SSL::SSLError` and left the socket open
22
+
23
+ 5.2.3
24
+ ==========
25
+
26
+ Security:
27
+
28
+ - Don't rely on memcached's auto-create for keys that need base64 encoding (GHSA-r47x-6p6r-mvcp)
29
+ - memcached stores an item made by auto-create (`ma` with `N`/`J`, or `mg` with `N`) for a base64-encoded key without its binary-key flag, and later replies name the item by its raw key bytes. A key containing spaces or CR/LF could then make a multi-get read the replies after it as different ones
30
+ - `incr` and `decr` with an initial value now create such counters with an add instead. Return values are unchanged
31
+ - The new `verify_reply_keys` option makes `get_multi`, `get_multi_cas` and `get_multi_with_metadata` ignore replies for keys they didn't request, which guards against items created by older Dalli versions or other clients until memcached is upgraded. It's off by default, since it costs a multi-get about 15%
32
+ - Affects the meta protocol since 3.2.0; fixed in 5.2.3, 5.1.5, 5.0.11, 4.3.8 and 3.2.14. memcached 1.6.45 and earlier still name these items by their raw key bytes
33
+
34
+ Bug fixes:
35
+
36
+ - `get_multi_with_metadata` and its `CacheResult` adapter now retry transient network errors and raise `Dalli::NetworkError` on terminal failures instead of returning misses or partial results (#1222, @jaideeppyne)
37
+ - A per-request `raw: true` write no longer compresses the value (#1213)
38
+ - Values at or above the compression threshold (4 KB by default) were compressed and flagged even with `raw: true`. Raw reads ignore flags, so reading them back with `raw: true` returned the compressed bytes
39
+ - Raw values are now stored exactly as given. Callers that pass `raw: true` without `compress: false` store large values uncompressed; Rails' `MemCacheStore` passes `compress: false` and isn't affected
40
+ - Fix `cas` and `cas!` edge cases, and make `flush` attempt every server (#1214)
41
+ - `cas!` on a missing key used an unconditional set, so a value another client created while the block ran was overwritten. It now adds the key and returns `false` if another client got there first
42
+ - With `cache_nils: true`, `cas` treated a stored `nil` as a miss and didn't call the block
43
+ - The docs now say what `cas` and `cas!` return on success: the new CAS value, not `true`. The return values are unchanged
44
+ - `flush` stopped at the first server that raised, so later servers weren't flushed. It now flushes every server it can reach, then raises the first error
45
+ - Handle failed TLS handshakes like other connection failures, and warn when TLS verification is off (#1215)
46
+ - A failed or timed-out handshake left the TCP socket open until GC and raised `OpenSSL::SSL::SSLError` directly, so the server was never marked down. The socket is now closed, and the error goes through the usual retry and `socket_max_failures` handling, raising `Dalli::NetworkError` (or `Dalli::RingError` once no server is available)
47
+ - Certificate and hostname checks come only from the `ssl_context` you pass, and a bare `OpenSSL::SSL::SSLContext.new` checks neither. Dalli now logs a warning once per process when `verify_mode` doesn't include `VERIFY_PEER` or `verify_hostname` is false; see the new TLS section in the README
48
+ - Connection and configuration fixes (#1216)
49
+ - `memcached://` URIs with a bracketed IPv6 host (`memcached://[::1]:11211`) now connect
50
+ - A server weight below 1 raises `ArgumentError`. Before, a ring where every weight was 0 raised `FloatDomainError`
51
+ - A reply line longer than 8 KiB without a CRLF, or one cut short by the connection closing, is now a network error that closes the connection. Before, the line had no length limit
52
+ - On CRuby, DNS resolution is now limited by `socket_timeout` (`resolv_timeout:`) as well as the connect
53
+ - `Dalli::Client.new` no longer modifies the options hash it's given, and accepts a frozen one (#1217)
54
+ - `value_max_bytes` now counts the key and memcached's per-item overhead (#1217)
55
+ - memcached's `-I` limit covers the whole item, not only the value. A value of exactly 1 MB passed Dalli's check and then failed in memcached with `Dalli::ServerError`, which `quiet` blocks and `set_multi` silently discarded
56
+ - Dalli now checks value + key + 63 bytes, the overhead measured on memcached 1.6, and raises `Dalli::ValueOverMaxSize` before sending. A custom `value_max_bytes` is now that much stricter
57
+ - A String `value_max_bytes` is converted to an Integer instead of raising on every set
58
+ - `Rack::Session::Dalli` fixes (#1217)
59
+ - It no longer requires the `connection_pool` gem unless `pool_size` or `pool_timeout` is given
60
+ - `pool_size` and `pool_timeout` are no longer passed to `Dalli::Client`, and the options hash isn't modified
61
+ - A failed session delete is logged as a warning, not only when `$VERBOSE` is set
62
+ - One value that can't be stored no longer discards other buffered writes (#1218)
63
+ - A value that raised `Dalli::MarshalError` or `Dalli::ValueOverMaxSize` closed the connection, which dropped writes still buffered from the same `quiet` block or `set_multi`. Multi-server `set_multi` did this without raising
64
+ - Both `set_multi` paths now store every other key, skip the bad one, and then raise. Single-server `set_multi` used to store nothing
65
+ - When single-server `set_multi` failed to marshal a value, the error log included every value in the batch. It now lists only the keys
66
+ - `Rack::Session::Dalli`'s 401 for a session deleted mid-request is now a valid Rack response (#1220)
67
+ - When a session is deleted while a request is using it (for example, by a logout in another request), the store still returns 401 instead of writing the session back. The 401 now has a `content-type` and expires the session cookie, so the browser stops sending the dead session ID
68
+ - The app's response body is now closed, as the Rack spec requires. Before, it was dropped without `close`
69
+ - The app has already run when the 401 is returned, so its side effects have happened. This is documented in the README
70
+
71
+ Improvements:
72
+
73
+ - Add a `connect_timeout` option to set a separate timeout for DNS resolution and TCP/UNIX socket connects (including reconnects); it defaults to `socket_timeout`. TLS handshakes continue to use `socket_timeout` (#1221, @underwoo16)
74
+
7
75
  5.2.2
8
76
  ==========
9
77
 
data/README.md CHANGED
@@ -59,6 +59,17 @@ Dalli::Client.new('localhost:11211', namespace: 'myapp', namespace_separator: '/
59
59
 
60
60
  The separator must be a single non-alphanumeric character. Valid examples: `:`, `/`, `|`, `.`, `-`, `_`, `#`
61
61
 
62
+ ### Maximum Item Size
63
+
64
+ memcached rejects items larger than its `-I` setting (1MB by default). Set `value_max_bytes` to the same value if you change it:
65
+
66
+ ```ruby
67
+ # memcached started with -I 2m
68
+ Dalli::Client.new('localhost:11211', value_max_bytes: 2 * 1024 * 1024)
69
+ ```
70
+
71
+ memcached's limit covers the whole item, so Dalli checks the stored (serialized and compressed) value plus the key plus 63 bytes of per-item overhead against `value_max_bytes`, and raises `Dalli::ValueOverMaxSize` before sending anything larger. With the defaults, the largest value you can store under a 10-byte key is 1,048,576 - 10 - 63 = 1,048,503 bytes.
72
+
62
73
  ### Deferred Draining
63
74
 
64
75
  By default, a `quiet` (or `multi`) block ends by waiting for the replies memcached still sends for its quiet requests, one round trip for each server the block wrote to. With `defer_drain: true`, the block returns without waiting:
@@ -112,6 +123,35 @@ The digest is MD5 by default. `digest_class:` takes any object that responds to
112
123
 
113
124
  See the [5.0-Upgrade.md](5.0-Upgrade.md) guide for upgrade information.
114
125
 
126
+ ### Counters and verify_reply_keys
127
+
128
+ memcached names an item made by auto-create for a base64-encoded key (one with whitespace, control or non-ASCII characters) by its raw key bytes in later replies. Since 5.2.3, Dalli creates counters for such keys with an add rather than auto-create. Items created before then, or by other clients, can still be named that way until memcached releases a fix (1.6.45 and earlier behave this way). If keys passed to `incr` or `decr` with an initial value can contain untrusted input, set `verify_reply_keys: true` until then: multi-gets then ignore replies for keys they didn't request, at a cost of about 15% per multi-get.
129
+
130
+ ## TLS
131
+
132
+ Pass an `OpenSSL::SSL::SSLContext` as `ssl_context:` to connect over TLS. Certificate and hostname verification come only from that context: Dalli adds no checks of its own. A bare `OpenSSL::SSL::SSLContext.new` verifies nothing, so it accepts any certificate from any server. Configure the context to verify both:
133
+
134
+ ```ruby
135
+ ssl_context = OpenSSL::SSL::SSLContext.new
136
+ ssl_context.set_params(
137
+ verify_mode: OpenSSL::SSL::VERIFY_PEER,
138
+ verify_hostname: true,
139
+ ca_file: '/path/to/ca.crt' # omit to use the system's trusted certificates
140
+ )
141
+
142
+ Dalli::Client.new('memcached.example.com:11211', ssl_context: ssl_context)
143
+ ```
144
+
145
+ Dalli logs a warning once per process when the context's `verify_mode` is `VERIFY_NONE`, or when it is `VERIFY_PEER` but `verify_hostname` is false.
146
+
147
+ A failed TLS handshake counts as a failed connection attempt, like a refused connection: once `socket_max_failures` attempts have failed, the server is marked down and requests fail over to the other servers.
148
+
149
+ ## Rack Sessions
150
+
151
+ `Rack::Session::Dalli` stores Rack sessions in memcached. If a session is deleted while a request is using it (for example, when the user logs out in another tab during a slow request), the store doesn't write the session back, so a logged-out session can't come back. It replaces the app's response with a `401 Wrong session ID` that expires the session cookie.
152
+
153
+ The app has already run by then, so whatever it did (database writes, emails, and so on) has happened, and only its response is lost. If a request needs a different outcome, check that the session still exists before making the changes.
154
+
115
155
  ## OpenTelemetry Tracing
116
156
 
117
157
  Dalli automatically instruments operations with [OpenTelemetry](https://opentelemetry.io/) when the SDK is present. No configuration is required - just add the OpenTelemetry gems to your application:
data/lib/dalli/client.rb CHANGED
@@ -35,15 +35,20 @@ module Dalli
35
35
  # to memcached. Default: true.
36
36
  # - :compression_min_size - the minimum size (in bytes) for which Dalli will compress values sent to Memcached.
37
37
  # Defaults to 4K.
38
+ # - :value_max_bytes - the largest item memcached will accept, i.e. its -I setting. Defaults to 1MB. Like
39
+ # memcached, Dalli counts the key and 63 bytes of item overhead against this limit along
40
+ # with the (serialized, compressed) value, so the largest value that can be stored is
41
+ # value_max_bytes - key bytesize - 63. Larger values raise Dalli::ValueOverMaxSize.
38
42
  # - :serializer - defaults to Marshal
39
43
  # - :compressor - defaults to Dalli::Compressor, a Zlib-based implementation
40
44
  # - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for
41
45
  # #fetch operations.
42
46
  # - :raw - If set, disables serialization and compression entirely at the client level.
43
47
  # Only String values are supported. This is useful when the caller handles its own
44
- # serialization (e.g., Rails' ActiveSupport::Cache). Note: this is different from
45
- # the per-request :raw option which converts values to strings but still uses the
46
- # serialization pipeline.
48
+ # serialization (e.g., Rails' ActiveSupport::Cache). The per-request :raw option
49
+ # (e.g. set(key, value, ttl, raw: true)) does the same for a single call: String
50
+ # values are stored as-is, uncompressed, and non-String values raise
51
+ # Dalli::MarshalError.
47
52
  # - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
48
53
  # useful for injecting a FIPS compliant hash object.
49
54
  # - :otel_db_statement - controls the +db.query.text+ span attribute when OpenTelemetry is loaded.
@@ -58,6 +63,17 @@ module Dalli
58
63
  # quiet requests (NF, NS, errors) wait on the connection until then, so a client that only makes
59
64
  # quiet requests should call #drain_deferred_responses periodically. See "Deferred Draining" in
60
65
  # the README.
66
+ # - :socket_timeout - timeout in seconds for reads and writes on a connection, and for the TLS
67
+ # handshake. Also used for connecting unless :connect_timeout is set.
68
+ # Default: 1.
69
+ # - :connect_timeout - timeout in seconds for DNS resolution and TCP/UNIX socket connects,
70
+ # including reconnects after a failure. Defaults to :socket_timeout.
71
+ # TLS handshakes use :socket_timeout.
72
+ # - :verify_reply_keys - defaults to false. When true, get_multi, get_multi_cas and get_multi_with_metadata only
73
+ # accept replies for the keys they requested; after a reply naming another key, the rest of
74
+ # that server's replies are treated as misses. memcached can name an item by its raw key
75
+ # bytes when the item was created by auto-create from a base64-encoded key, which Dalli no
76
+ # longer does. Costs a multi-get about 15%.
61
77
  #
62
78
  def initialize(servers = nil, options = {})
63
79
  @normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
@@ -226,9 +242,14 @@ module Dalli
226
242
  #
227
243
  # `req_options` accepts :p_token/:l_token, applied to every key in the batch.
228
244
  #
245
+ # A transient network error is retried automatically. If a server remains
246
+ # unreachable after retrying, raises Dalli::NetworkError rather than
247
+ # returning its keys as misses or yielding a partial result.
248
+ #
229
249
  # @param keys [Array<String>] the keys to fetch
230
250
  # @param req_options [Hash, nil] routing-token options
231
251
  # @return [Hash] key => { value:, cas:, stale:, miss: }
252
+ # @raise [Dalli::NetworkError] if a server is unreachable after retrying
232
253
  def get_multi_with_metadata(*keys, req_options: nil, &block)
233
254
  keys.flatten!
234
255
  keys.compact!
@@ -371,24 +392,26 @@ module Dalli
371
392
  # compare and swap values using optimistic locking.
372
393
  # Fetch the existing value for key.
373
394
  # If it exists, yield the value to the block.
374
- # Add the block's return value as the new value for the key.
375
- # Add will fail if someone else changed the value.
395
+ # Store the block's return value as the new value for the key.
396
+ # The store will fail if someone else changed the value.
376
397
  #
377
398
  # Returns:
378
- # - nil if the key did not exist.
399
+ # - nil if the key did not exist (or held nil and :cache_nils is false);
400
+ # the block is not called.
379
401
  # - false if the value was changed by someone else.
380
- # - true if the value was successfully updated.
402
+ # - the new CAS value (a positive Integer) if the value was successfully updated.
381
403
  def cas(key, ttl = nil, req_options = nil, &)
382
404
  cas_core(key, false, ttl, req_options, &)
383
405
  end
384
406
 
385
407
  ##
386
408
  # like #cas, but will yield to the block whether or not the value
387
- # already exists.
409
+ # already exists. If the key did not exist, the block receives nil.
388
410
  #
389
411
  # Returns:
390
- # - false if the value was changed by someone else.
391
- # - true if the value was successfully updated.
412
+ # - false if the value was changed by someone else, including another
413
+ # client creating a missing key while the block ran.
414
+ # - the new CAS value (a positive Integer) if the value was successfully updated.
392
415
  def cas!(key, ttl = nil, req_options = nil, &)
393
416
  cas_core(key, true, ttl, req_options, &)
394
417
  end
@@ -411,6 +434,11 @@ module Dalli
411
434
  # the defer_drain option, before the next non-quiet request), and
412
435
  # Dalli client methods invoked inside the block will not
413
436
  # have return values
437
+ #
438
+ # The block is quiet only for the thread or fiber that runs it. Another
439
+ # thread or fiber sharing this client can make requests meanwhile; a
440
+ # non-quiet one first sends the block's buffered requests and discards
441
+ # their replies, so it reads only its own.
414
442
  def quiet
415
443
  old = Thread.current[::Dalli::QUIET]
416
444
  Thread.current[::Dalli::QUIET] = true
@@ -650,9 +678,20 @@ module Dalli
650
678
  ##
651
679
  # Flush the memcached server, at 'delay' seconds in the future.
652
680
  # Delay defaults to zero seconds, which means an immediate flush.
681
+ # Every server is attempted. If any of them fails, the first error is
682
+ # raised once all the others have been flushed.
653
683
  ##
654
684
  def flush(delay = 0)
655
- ring.servers.map { |s| s.request(:flush, delay) }
685
+ first_error = nil
686
+ results = ring.servers.map do |s|
687
+ s.request(:flush, delay)
688
+ rescue StandardError => e
689
+ first_error ||= e
690
+ nil
691
+ end
692
+ raise first_error if first_error
693
+
694
+ results
656
695
  end
657
696
  alias flush_all flush
658
697
 
@@ -805,16 +844,26 @@ module Dalli
805
844
  Dalli.logger.debug { e.inspect }
806
845
  Dalli.logger.debug { 'retrying single-server get_multi because of network error' }
807
846
  retry
847
+ rescue Dalli::UnexpectedReplyKeyError => e
848
+ # The connection is closed and the keys are treated as missing, as the
849
+ # pipelined get_multi does for a server whose replies are out of step
850
+ Dalli.logger.warn { e.message }
851
+ {}
808
852
  end
809
853
 
810
854
  def single_server_get_multi_with_metadata(keys, req_options = nil)
811
- keys.map! { |k| @key_manager.validate_key(k.to_s) }
855
+ validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
812
856
  return {} unless (server = single_server)
813
857
 
814
- result = server.request(:read_multi_with_metadata_req, keys, req_options)
858
+ result = server.request(:read_multi_with_metadata_req, validated_keys, req_options)
815
859
  result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
816
860
  result
817
- rescue Dalli::NetworkError
861
+ rescue Dalli::RetryableNetworkError => e
862
+ Dalli.logger.debug { e.inspect }
863
+ Dalli.logger.debug { 'retrying single-server get_multi_with_metadata because of network error' }
864
+ retry
865
+ rescue Dalli::UnexpectedReplyKeyError => e
866
+ Dalli.logger.warn { e.message }
818
867
  {}
819
868
  end
820
869
 
@@ -913,12 +962,23 @@ module Dalli
913
962
  def cas_core(key, always_set, ttl = nil, req_options = nil)
914
963
  validate_routing_tokens!(req_options)
915
964
  (value, cas) = perform(:cas, key, req_options)
916
- return if value.nil? && !always_set
965
+ return if !always_set && cas_miss?(value, cas)
917
966
 
918
967
  newvalue = yield(value)
968
+ # A CAS of 0 means the key was missing. A set with CAS 0 is unconditional,
969
+ # so add instead: it fails if another client created the key meanwhile.
970
+ return perform(:add, key, newvalue, ttl_or_default(ttl), req_options) if cas.zero?
971
+
919
972
  perform(:set, key, newvalue, ttl_or_default(ttl), cas, req_options)
920
973
  end
921
974
 
975
+ # The CAS lookup returns [nil, 0] on a miss rather than NOT_FOUND, and a
976
+ # stored item always has a non-zero CAS. So under cache_nils a zero CAS
977
+ # marks the miss, and a stored nil is a hit, as in #not_found?.
978
+ def cas_miss?(value, cas)
979
+ cache_nils ? cas.zero? : value.nil?
980
+ end
981
+
922
982
  def fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options)
923
983
  server = ring.server_for_key(key)
924
984
  # req_options is the base, not the override: fetch_with_lock's own
@@ -986,7 +1046,10 @@ module Dalli
986
1046
  retry
987
1047
  end
988
1048
 
989
- def normalize_options(opts)
1049
+ # Works on a copy so the caller's hash is neither modified here nor able
1050
+ # to change this client's configuration later.
1051
+ def normalize_options(options)
1052
+ opts = options.dup
990
1053
  opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
991
1054
  opts
992
1055
  rescue NoMethodError
data/lib/dalli/options.rb CHANGED
@@ -43,7 +43,7 @@ module Dalli
43
43
  end
44
44
  end
45
45
 
46
- def pipeline_abort
46
+ def pipeline_abort(...)
47
47
  @lock.synchronize do
48
48
  super
49
49
  end
@@ -32,15 +32,21 @@ module Dalli
32
32
  return {} if keys.empty?
33
33
 
34
34
  @ring.lock do
35
- # Stores partial results collected during interleaved send phase
36
- @partial_results = {}
37
- servers = setup_requests(keys, req_options, return_cas: return_cas)
38
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
39
-
40
- # First yield any partial results collected during interleaved send
41
- yield_partial_results(&block)
42
-
43
- servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
35
+ # The servers with replies still to read, updated in place
36
+ servers = []
37
+ begin
38
+ # Stores partial results collected during interleaved send phase
39
+ @partial_results = {}
40
+ setup_requests(servers, keys, req_options, return_cas: return_cas)
41
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
42
+
43
+ # First yield any partial results collected during interleaved send
44
+ yield_partial_results(&block)
45
+
46
+ fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
47
+ ensure
48
+ abort_unfinished(servers)
49
+ end
44
50
  end
45
51
  rescue Dalli::RetryableNetworkError => e
46
52
  Dalli.logger.debug { e.inspect }
@@ -60,6 +66,9 @@ module Dalli
60
66
  #
61
67
  # `req_options` accepts :p_token/:l_token, applied to every key in the batch.
62
68
  #
69
+ # A transient network error is retried automatically. A terminal
70
+ # Dalli::NetworkError propagates instead of returning partial metadata.
71
+ #
63
72
  def process_with_metadata(keys, req_options = nil)
64
73
  return {} if keys.empty?
65
74
 
@@ -67,9 +76,9 @@ module Dalli
67
76
  results = {}
68
77
  groups_for_keys(keys).each do |server, keys_for_server|
69
78
  results.merge!(server.request(:read_multi_with_metadata_req, keys_for_server, req_options))
70
- rescue Dalli::RetryableNetworkError
79
+ rescue Dalli::NetworkError
71
80
  raise
72
- rescue DalliError, NetworkError => e
81
+ rescue DalliError => e
73
82
  Dalli.logger.debug { e.inspect }
74
83
  Dalli.logger.debug { "unable to get keys for server #{server.name}" }
75
84
  end
@@ -93,9 +102,9 @@ module Dalli
93
102
 
94
103
  # Sends each server's queries and its terminating noop before building
95
104
  # the next server's, so memcached is already answering the first servers
96
- # while the rest are prepared. Returns the servers with a pending response.
97
- def setup_requests(keys, req_options = nil, return_cas: true)
98
- started = []
105
+ # while the rest are prepared. Adds the servers with a pending response
106
+ # to started.
107
+ def setup_requests(started, keys, req_options = nil, return_cas: true)
99
108
  groups_for_keys(keys).each do |server, keys_for_server|
100
109
  make_getkq_request(server, keys_for_server, req_options, return_cas: return_cas)
101
110
  next unless server.connected?
@@ -108,7 +117,6 @@ module Dalli
108
117
  rescue Dalli::DalliError
109
118
  started.delete(server)
110
119
  end
111
- started
112
120
  end
113
121
 
114
122
  ##
@@ -152,29 +160,39 @@ module Dalli
152
160
  raise
153
161
  end
154
162
 
155
- # Swallows Dalli::NetworkError
163
+ # Swallows Dalli::NetworkError. Empties servers, so they aren't aborted
164
+ # again.
156
165
  def abort_without_timeout(servers)
157
166
  servers.each(&:pipeline_abort)
167
+ servers.clear
168
+ end
169
+
170
+ # When #process stops before reading every server's replies (the caller's
171
+ # block raises or breaks, a Timeout.timeout fires, a value can't be
172
+ # deserialized), the rest are still on the socket and some may be in the
173
+ # response buffer. Closing those connections keeps them from being read
174
+ # as a later request's replies. Aborts that #process already handled
175
+ # emptied servers.
176
+ def abort_unfinished(servers)
177
+ servers.each { |server| server.pipeline_abort(timed_out: false) }
178
+ servers.clear
158
179
  end
159
180
 
181
+ # Reads the replies available, removing finished servers from servers
160
182
  def fetch_responses(servers, start_time, timeout, &block)
161
183
  # Remove any servers which are not connected
162
184
  servers.select!(&:connected?)
163
- return [] if servers.empty?
185
+ return if servers.empty?
164
186
 
165
187
  time_left = remaining_time(start_time, timeout)
166
188
  readable_servers = servers_with_response(servers, time_left)
167
- if readable_servers.empty?
168
- abort_with_timeout(servers)
169
- return []
170
- end
189
+ return abort_with_timeout(servers) if readable_servers.empty?
171
190
 
172
191
  # Loop through the servers with responses, and
173
192
  # delete any from our list that are finished
174
193
  readable_servers.each do |server|
175
194
  servers.delete(server) if process_server(server, &block)
176
195
  end
177
- servers
178
196
  rescue NetworkError
179
197
  # Abort and raise if we encountered a network error. This triggers
180
198
  # a retry at the top level on RetryableNetworkError.
@@ -191,12 +209,10 @@ module Dalli
191
209
 
192
210
  # Swallows Dalli::NetworkError
193
211
  def abort_with_timeout(servers)
194
- abort_without_timeout(servers)
195
212
  servers.each do |server|
196
213
  Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
197
214
  end
198
-
199
- true # Required to simplify caller
215
+ abort_without_timeout(servers)
200
216
  end
201
217
 
202
218
  # Processes responses from a server. Returns true if there are no
@@ -18,6 +18,9 @@ module Dalli
18
18
  # A transient network error is retried automatically. If a server remains
19
19
  # unreachable after retrying, raises Dalli::NetworkError.
20
20
  #
21
+ # A value that can't be marshalled or is over value_max_bytes is skipped;
22
+ # the other keys are still stored, then the first such error is raised.
23
+ #
21
24
  # @param hash [Hash] key-value pairs to set
22
25
  # @param ttl [Integer] time-to-live in seconds
23
26
  # @param req_options [Hash] options passed to each set operation
@@ -26,10 +29,12 @@ module Dalli
26
29
  def process(hash, ttl, req_options)
27
30
  return if hash.empty?
28
31
 
32
+ @value_error = nil
29
33
  @ring.lock do
30
34
  servers = setup_requests(hash, ttl, req_options)
31
35
  finish_requests(servers)
32
36
  end
37
+ raise @value_error if @value_error
33
38
  rescue Dalli::RetryableNetworkError => e
34
39
  Dalli.logger.debug { e.inspect }
35
40
  Dalli.logger.debug { 'retrying pipelined sets because of network error' }
@@ -62,6 +67,10 @@ module Dalli
62
67
  server.request(:pipelined_set, key, value, ttl, req_options)
63
68
  rescue Dalli::NetworkError
64
69
  raise
70
+ rescue Dalli::MarshalError, Dalli::ValueOverMaxSize => e
71
+ # Nothing was written for this key, so the server's other keys are
72
+ # unaffected; raised by #process once the batch is complete
73
+ @value_error ||= e
65
74
  rescue DalliError => e
66
75
  Dalli.logger.debug { e.inspect }
67
76
  Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }