dalli 3.0.6 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: adcf2507fd177cbc44167154462aae102a6f34f9cd6a85ba29ea9a26e87efce6
4
- data.tar.gz: 57bd7da6c8c90fbcd99c4738bdb061df3630df0ff17ead1cc510d5d58f8677e2
3
+ metadata.gz: 5450b3f6caa26344dbcf9718ee2e12c98d593df098d5aec6f41655783675e2b8
4
+ data.tar.gz: fdc8ff913351a2ed85fb34c0aa50506f5014e29c3671cbf312eaf7255b526742
5
5
  SHA512:
6
- metadata.gz: cec1cceffc54713b77746ec455ddd3817b0dad37ceacb33d20f5f82a51015c7d82788ad78d043f4859d26a6c236bb57d8aac053770ea464827c5c22f07057d98
7
- data.tar.gz: 70e93ae5cca84bc211315359391bad55b2f31908604ee195f42e701b472ab469074f3ee9350618491cfbf9d34b53f4e41950998e8e34cd446d8c4aca63d4b0d4
6
+ metadata.gz: 226719cde318de90be8fb77160b926ddbc75002ab0632494c7de0d1917bc88a8fde01b0e827765db3baa5f439c42f840a298c546d56473109e65003509d452ff
7
+ data.tar.gz: 6b9ca93a048ae7a5074bd128432ceb1f19497e16c773a4a246ba1da13ecb1d7e9024c41f9e112ecea2244205afded043d893c8368fcf5c137b14f134b9f1548e
data/History.md CHANGED
@@ -1,6 +1,15 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ 3.1.0
5
+ ==========
6
+
7
+ - BREAKING CHANGE: Update Rack::Session::Dalli to inherit from Abstract::PersistedSecure. This will invalidate existing sessions (petergoldstein)
8
+ - BREAKING CHANGE: Use of unsupported operations in a multi block now raise an error. (petergoldstein)
9
+ - Extract PipelinedGetter from Dalli::Client (petergoldstein)
10
+ - Fix SSL socket so that it works with pipelined gets (petergoldstein)
11
+ - Additional refactoring to split classes (petergoldstein)
12
+
4
13
  3.0.6
5
14
  ==========
6
15
 
data/lib/dalli/client.rb CHANGED
@@ -63,12 +63,12 @@ module Dalli
63
63
  # pipelined as Dalli will use 'quiet' operations where possible.
64
64
  # Currently supports the set, add, replace and delete operations.
65
65
  def multi
66
- old = Thread.current[:dalli_multi]
67
- Thread.current[:dalli_multi] = true
66
+ old = Thread.current[::Dalli::MULTI_KEY]
67
+ Thread.current[::Dalli::MULTI_KEY] = true
68
68
  yield
69
69
  ensure
70
- @ring&.flush_multi_responses
71
- Thread.current[:dalli_multi] = old
70
+ @ring&.pipeline_consume_and_ignore_responses
71
+ Thread.current[::Dalli::MULTI_KEY] = old
72
72
  end
73
73
 
74
74
  ##
@@ -89,10 +89,10 @@ module Dalli
89
89
  return {} if keys.empty?
90
90
 
91
91
  if block_given?
92
- get_multi_yielder(keys) { |k, data| yield k, data.first }
92
+ pipelined_getter.process(keys) { |k, data| yield k, data.first }
93
93
  else
94
94
  {}.tap do |hash|
95
- get_multi_yielder(keys) { |k, data| hash[k] = data.first }
95
+ pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
96
96
  end
97
97
  end
98
98
  end
@@ -306,10 +306,10 @@ module Dalli
306
306
  # { 'key' => [value, cas_id] }
307
307
  def get_multi_cas(*keys)
308
308
  if block_given?
309
- get_multi_yielder(keys) { |*args| yield(*args) }
309
+ pipelined_getter.process(keys) { |*args| yield(*args) }
310
310
  else
311
311
  {}.tap do |hash|
312
- get_multi_yielder(keys) { |k, data| hash[k] = data }
312
+ pipelined_getter.process(keys) { |k, data| hash[k] = data }
313
313
  end
314
314
  end
315
315
  end
@@ -341,9 +341,7 @@ module Dalli
341
341
  # Close our connection to each server.
342
342
  # If you perform another operation after this, the connections will be re-established.
343
343
  def close
344
- return unless @ring
345
-
346
- @ring.servers.each(&:close)
344
+ @ring&.close
347
345
  @ring = nil
348
346
  end
349
347
  alias reset close
@@ -402,145 +400,14 @@ module Dalli
402
400
  end
403
401
 
404
402
  def normalize_options(opts)
405
- begin
406
- opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
407
- rescue NoMethodError
408
- raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
409
- end
403
+ opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
410
404
  opts
405
+ rescue NoMethodError
406
+ raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
411
407
  end
412
408
 
413
- # TODO: Look at extracting below into separate MultiYielder class
414
-
415
- ##
416
- # Yields, one at a time, keys and their values+attributes.
417
- #
418
- def get_multi_yielder(keys, &block)
419
- return {} if keys.empty?
420
-
421
- ring.lock do
422
- groups = groups_for_keys(keys)
423
- if (unfound_keys = groups.delete(nil))
424
- Dalli.logger.debug do
425
- "unable to get keys for #{unfound_keys.length} keys "\
426
- 'because no matching server was found'
427
- end
428
- end
429
- make_multi_get_requests(groups)
430
-
431
- servers = groups.keys
432
- return if servers.empty?
433
-
434
- # TODO: How does this exit on a NetworkError
435
- servers = perform_multi_response_start(servers)
436
-
437
- timeout = servers.first.options[:socket_timeout]
438
- start_time = Time.now
439
- loop do
440
- # remove any dead servers
441
- # TODO: Is this well behaved in a multi-threaded environment?
442
- # Accessing the server socket like this seems problematic
443
- servers.delete_if { |s| s.sock.nil? }
444
- break if servers.empty?
445
-
446
- servers = multi_yielder_loop(servers, start_time, timeout, &block)
447
- end
448
- end
449
- rescue NetworkError => e
450
- Dalli.logger.debug { e.inspect }
451
- Dalli.logger.debug { 'retrying multi yielder because of timeout' }
452
- retry
453
- end
454
-
455
- def make_multi_get_requests(groups)
456
- groups.each do |server, keys_for_server|
457
- server.request(:send_multiget, keys_for_server)
458
- rescue DalliError, NetworkError => e
459
- Dalli.logger.debug { e.inspect }
460
- Dalli.logger.debug { "unable to get keys for server #{server.name}" }
461
- end
462
- end
463
-
464
- # raises Dalli::NetworkError
465
- def perform_multi_response_start(servers)
466
- deleted = []
467
-
468
- servers.each do |server|
469
- next unless server.alive?
470
-
471
- begin
472
- server.multi_response_start
473
- rescue Dalli::NetworkError
474
- abort_multi_response(servers)
475
- raise
476
- rescue Dalli::DalliError => e
477
- Dalli.logger.debug { e.inspect }
478
- Dalli.logger.debug { 'results from this server will be missing' }
479
- deleted.append(server)
480
- end
481
- end
482
-
483
- servers.delete_if { |server| deleted.include?(server) }
484
- end
485
-
486
- # Swallows Dalli::NetworkError
487
- def abort_multi_response(servers)
488
- servers.each(&:multi_response_abort)
489
- end
490
-
491
- def multi_yielder_loop(servers, start_time, timeout, &block)
492
- time_left = remaining_time(start_time, timeout)
493
- readable_servers = servers_with_data(servers, time_left)
494
- if readable_servers.empty?
495
- abort_multi_connections_w_timeout(servers)
496
- return readable_servers
497
- end
498
-
499
- readable_servers.each do |server|
500
- servers.delete(server) if respond_to_readable_server(server, &block)
501
- end
502
- servers
503
- rescue NetworkError
504
- abort_multi_response(servers)
505
- raise
506
- end
507
-
508
- def remaining_time(start, timeout)
509
- elapsed = Time.now - start
510
- return 0 if elapsed > timeout
511
-
512
- timeout - elapsed
513
- end
514
-
515
- # Swallows Dalli::NetworkError
516
- def abort_multi_connections_w_timeout(servers)
517
- abort_multi_response(servers)
518
- servers.each do |server|
519
- Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
520
- end
521
-
522
- true # Required to simplify caller
523
- end
524
-
525
- def respond_to_readable_server(server)
526
- server.multi_response_nonblock.each_pair do |key, value_list|
527
- yield @key_manager.key_without_namespace(key), value_list
528
- end
529
-
530
- server.multi_response_completed?
531
- end
532
-
533
- def servers_with_data(servers, timeout)
534
- readable, = IO.select(servers.map(&:sock), nil, nil, timeout)
535
- return [] if readable.nil?
536
-
537
- readable.map(&:server)
538
- end
539
-
540
- def groups_for_keys(*keys)
541
- keys.flatten!
542
- keys.map! { |a| @key_manager.validate_key(a.to_s) }
543
- ring.keys_grouped_by_server(keys)
409
+ def pipelined_getter
410
+ PipelinedGetter.new(ring, @key_manager)
544
411
  end
545
412
  end
546
413
  end
data/lib/dalli/options.rb CHANGED
@@ -31,19 +31,19 @@ module Dalli
31
31
  end
32
32
  end
33
33
 
34
- def multi_response_start
34
+ def pipeline_response_start
35
35
  @lock.synchronize do
36
36
  super
37
37
  end
38
38
  end
39
39
 
40
- def multi_response_nonblock
40
+ def process_outstanding_pipeline_requests
41
41
  @lock.synchronize do
42
42
  super
43
43
  end
44
44
  end
45
45
 
46
- def multi_response_abort
46
+ def pipeline_response_abort
47
47
  @lock.synchronize do
48
48
  super
49
49
  end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ ##
5
+ # Contains logic for the pipelined gets implemented by the client.
6
+ ##
7
+ class PipelinedGetter
8
+ def initialize(ring, key_manager)
9
+ @ring = ring
10
+ @key_manager = key_manager
11
+ end
12
+
13
+ ##
14
+ # Yields, one at a time, keys and their values+attributes.
15
+ #
16
+ def process(keys, &block)
17
+ return {} if keys.empty?
18
+
19
+ @ring.lock do
20
+ servers = setup_requests(keys)
21
+ start_time = Time.now
22
+ loop do
23
+ # Remove any servers which are not connected
24
+ servers.delete_if { |s| !s.connected? }
25
+ break if servers.empty?
26
+
27
+ servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block)
28
+ end
29
+ end
30
+ rescue NetworkError => e
31
+ Dalli.logger.debug { e.inspect }
32
+ Dalli.logger.debug { 'retrying pipelined gets because of timeout' }
33
+ retry
34
+ end
35
+
36
+ def setup_requests(keys)
37
+ groups = groups_for_keys(keys)
38
+ make_getkq_requests(groups)
39
+
40
+ # TODO: How does this exit on a NetworkError
41
+ finish_queries(groups.keys)
42
+ end
43
+
44
+ ##
45
+ # Loop through the server-grouped sets of keys, writing
46
+ # the corresponding getkq requests to the appropriate servers
47
+ ##
48
+ def make_getkq_requests(groups)
49
+ groups.each do |server, keys_for_server|
50
+ server.request(:pipelined_get, keys_for_server)
51
+ rescue DalliError, NetworkError => e
52
+ Dalli.logger.debug { e.inspect }
53
+ Dalli.logger.debug { "unable to get keys for server #{server.name}" }
54
+ end
55
+ end
56
+
57
+ ##
58
+ # This loops through the servers that have keys in
59
+ # our set, sending the noop to terminate the set of queries.
60
+ ##
61
+ def finish_queries(servers)
62
+ deleted = []
63
+
64
+ servers.each do |server|
65
+ next unless server.alive?
66
+
67
+ begin
68
+ finish_query_for_server(server)
69
+ rescue Dalli::NetworkError
70
+ raise
71
+ rescue Dalli::DalliError
72
+ deleted.append(server)
73
+ end
74
+ end
75
+
76
+ servers.delete_if { |server| deleted.include?(server) }
77
+ rescue Dalli::NetworkError
78
+ abort_without_timeout(servers)
79
+ raise
80
+ end
81
+
82
+ def finish_query_for_server(server)
83
+ server.pipeline_response_start
84
+ rescue Dalli::NetworkError
85
+ raise
86
+ rescue Dalli::DalliError => e
87
+ Dalli.logger.debug { e.inspect }
88
+ Dalli.logger.debug { "Results from server: #{server.name} will be missing from the results" }
89
+ raise
90
+ end
91
+
92
+ # Swallows Dalli::NetworkError
93
+ def abort_without_timeout(servers)
94
+ servers.each(&:pipeline_response_abort)
95
+ end
96
+
97
+ def fetch_responses(servers, start_time, timeout, &block)
98
+ time_left = remaining_time(start_time, timeout)
99
+ readable_servers = servers_with_response(servers, time_left)
100
+ if readable_servers.empty?
101
+ abort_with_timeout(servers)
102
+ return []
103
+ end
104
+
105
+ # Loop through the servers with responses, and
106
+ # delete any from our list that are finished
107
+ readable_servers.each do |server|
108
+ servers.delete(server) if process_server(server, &block)
109
+ end
110
+ servers
111
+ rescue NetworkError
112
+ # Abort and raise if we encountered a network error. This triggers
113
+ # a retry at the top level.
114
+ abort_without_timeout(servers)
115
+ raise
116
+ end
117
+
118
+ def remaining_time(start, timeout)
119
+ elapsed = Time.now - start
120
+ return 0 if elapsed > timeout
121
+
122
+ timeout - elapsed
123
+ end
124
+
125
+ # Swallows Dalli::NetworkError
126
+ def abort_with_timeout(servers)
127
+ abort_without_timeout(servers)
128
+ servers.each do |server|
129
+ Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
130
+ end
131
+
132
+ true # Required to simplify caller
133
+ end
134
+
135
+ # Processes responses from a server. Returns true if there are no
136
+ # additional responses from this server.
137
+ def process_server(server)
138
+ server.process_outstanding_pipeline_requests.each_pair do |key, value_list|
139
+ yield @key_manager.key_without_namespace(key), value_list
140
+ end
141
+
142
+ server.pipeline_response_completed?
143
+ end
144
+
145
+ def servers_with_response(servers, timeout)
146
+ return [] if servers.empty?
147
+
148
+ # TODO: - This is a bit challenging. Essentially the PipelinedGetter
149
+ # is a reactor, but without the benefit of a Fiber or separate thread.
150
+ # My suspicion is that we may want to try and push this down into the
151
+ # individual servers, but I'm not sure. For now, we keep the
152
+ # mapping between the alerted object (the socket) and the
153
+ # corrresponding server here.
154
+ server_map = servers.each_with_object({}) { |s, h| h[s.sock] = s }
155
+
156
+ readable, = IO.select(server_map.keys, nil, nil, timeout)
157
+ return [] if readable.nil?
158
+
159
+ readable.map { |sock| server_map[sock] }
160
+ end
161
+
162
+ def groups_for_keys(*keys)
163
+ keys.flatten!
164
+ keys.map! { |a| @key_manager.validate_key(a.to_s) }
165
+ groups = @ring.keys_grouped_by_server(keys)
166
+ if (unfound_keys = groups.delete(nil))
167
+ Dalli.logger.debug do
168
+ "unable to get keys for #{unfound_keys.length} keys "\
169
+ 'because no matching server was found'
170
+ end
171
+ end
172
+ groups
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ module Protocol
5
+ class Binary
6
+ ##
7
+ # Class that encapsulates data parsed from a memcached response header.
8
+ ##
9
+ class ResponseHeader
10
+ SIZE = 24
11
+ FMT = '@2nCCnNNQ'
12
+
13
+ attr_reader :key_len, :extra_len, :data_type, :status, :body_len, :opaque, :cas
14
+
15
+ def initialize(buf)
16
+ raise ArgumentError, "Response buffer must be at least #{SIZE} bytes" unless buf.bytesize >= SIZE
17
+
18
+ @key_len, @extra_len, @data_type, @status, @body_len, @opaque, @cas = buf.unpack(FMT)
19
+ end
20
+
21
+ def ok?
22
+ status.zero?
23
+ end
24
+
25
+ def not_found?
26
+ status == 1
27
+ end
28
+
29
+ NOT_STORED_STATUSES = [2, 5].freeze
30
+ def not_stored?
31
+ NOT_STORED_STATUSES.include?(status)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -9,9 +9,6 @@ module Dalli
9
9
  # and parsing into local values. Handles errors on unexpected values.
10
10
  ##
11
11
  class ResponseProcessor
12
- RESP_HEADER = '@2nCCnNNQ'
13
- RESP_HEADER_SIZE = 24
14
-
15
12
  # Response codes taken from:
16
13
  # https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
17
14
  RESPONSE_CODES = {
@@ -44,14 +41,9 @@ module Dalli
44
41
  end
45
42
 
46
43
  def read_response
47
- status, extra_len, key_len, body_len, cas = unpack_header(read_header)
48
- body = read(body_len) if body_len.positive?
49
- [status, extra_len, body, cas, key_len]
50
- end
51
-
52
- def unpack_header(header)
53
- (key_len, extra_len, _, status, body_len, _, cas) = header.unpack(RESP_HEADER)
54
- [status, extra_len, key_len, body_len, cas]
44
+ resp_header = ResponseHeader.new(read_header)
45
+ body = read(resp_header.body_len) if resp_header.body_len.positive?
46
+ [resp_header, body]
55
47
  end
56
48
 
57
49
  def unpack_response_body(extra_len, key_len, body, unpack)
@@ -63,45 +55,36 @@ module Dalli
63
55
  end
64
56
 
65
57
  def read_header
66
- read(RESP_HEADER_SIZE) || raise(Dalli::NetworkError, 'No response')
58
+ read(ResponseHeader::SIZE) || raise(Dalli::NetworkError, 'No response')
67
59
  end
68
60
 
69
- def not_found?(status)
70
- status == 1
71
- end
61
+ def raise_on_not_ok_status!(resp_header)
62
+ return if resp_header.ok?
72
63
 
73
- NOT_STORED_STATUSES = [2, 5].freeze
74
- def not_stored?(status)
75
- NOT_STORED_STATUSES.include?(status)
76
- end
77
-
78
- def raise_on_not_ok_status!(status)
79
- return if status.zero?
80
-
81
- raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
64
+ raise Dalli::DalliError, "Response error #{resp_header.status}: #{RESPONSE_CODES[resp_header.status]}"
82
65
  end
83
66
 
84
67
  def generic_response(unpack: false, cache_nils: false)
85
- status, extra_len, body, _, key_len = read_response
68
+ resp_header, body = read_response
86
69
 
87
- return cache_nils ? ::Dalli::NOT_FOUND : nil if not_found?(status)
88
- return false if not_stored?(status) # Not stored, normal status for add operation
70
+ return cache_nils ? ::Dalli::NOT_FOUND : nil if resp_header.not_found?
71
+ return false if resp_header.not_stored? # Not stored, normal status for add operation
89
72
 
90
- raise_on_not_ok_status!(status)
73
+ raise_on_not_ok_status!(resp_header)
91
74
  return true unless body
92
75
 
93
- unpack_response_body(extra_len, key_len, body, unpack).last
76
+ unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last
94
77
  end
95
78
 
96
79
  def data_cas_response
97
- status, extra_len, body, cas, key_len = read_response
98
- return [nil, cas] if not_found?(status)
99
- return [nil, false] if not_stored?(status)
80
+ resp_header, body = read_response
81
+ return [nil, resp_header.cas] if resp_header.not_found?
82
+ return [nil, false] if resp_header.not_stored?
100
83
 
101
- raise_on_not_ok_status!(status)
102
- return [nil, cas] unless body
84
+ raise_on_not_ok_status!(resp_header)
85
+ return [nil, resp_header.cas] unless body
103
86
 
104
- [unpack_response_body(extra_len, key_len, body, true).last, cas]
87
+ [unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true).last, resp_header.cas]
105
88
  end
106
89
 
107
90
  def cas_response
@@ -111,17 +94,17 @@ module Dalli
111
94
  def multi_with_keys_response
112
95
  hash = {}
113
96
  loop do
114
- status, extra_len, body, _, key_len = read_response
97
+ resp_header, body = read_response
115
98
  # This is the response to the terminating noop / end of stat
116
- return hash if status.zero? && key_len.zero?
99
+ return hash if resp_header.ok? && resp_header.key_len.zero?
117
100
 
118
101
  # Ignore any responses with non-zero status codes,
119
102
  # such as errors from set operations. That allows
120
103
  # this code to be used at the end of a multi
121
104
  # block to clear any error responses from inside the multi.
122
- next unless status.zero?
105
+ next unless resp_header.ok?
123
106
 
124
- key, value = unpack_response_body(extra_len, key_len, body, true)
107
+ key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
125
108
  hash[key] = value
126
109
  end
127
110
  end
@@ -137,11 +120,57 @@ module Dalli
137
120
  raise Dalli::NetworkError, "Unexpected message format: #{extra_len} #{count}"
138
121
  end
139
122
 
140
- def auth_response
141
- (_, extra_len, _, status, body_len,) = read_header.unpack(RESP_HEADER)
142
- validate_auth_format(extra_len, body_len)
123
+ def auth_response(buf = read_header)
124
+ resp_header = ResponseHeader.new(buf)
125
+ body_len = resp_header.body_len
126
+ validate_auth_format(resp_header.extra_len, body_len)
143
127
  content = read(body_len) if body_len.positive?
144
- [status, content]
128
+ [resp_header.status, content]
129
+ end
130
+
131
+ def contains_header?(buf)
132
+ return false unless buf
133
+
134
+ buf.bytesize >= ResponseHeader::SIZE
135
+ end
136
+
137
+ def response_header_from_buffer(buf)
138
+ header = buf.slice(0, ResponseHeader::SIZE)
139
+ ResponseHeader.new(header)
140
+ end
141
+
142
+ ##
143
+ # This method returns an array of values used in a pipelined
144
+ # getk process. The first value is the number of bytes by
145
+ # which to advance the pointer in the buffer. If the
146
+ # complete response is found in the buffer, this will
147
+ # be the response size. Otherwise it is zero.
148
+ #
149
+ # The remaining four values in the array are the status, key,
150
+ # value, and cas returned from the response.
151
+ ##
152
+ def getk_response_from_buffer(buf)
153
+ # There's no header in the buffer, so don't advance
154
+ return [0, 0, nil, nil, nil] unless contains_header?(buf)
155
+
156
+ resp_header = response_header_from_buffer(buf)
157
+ body_len = resp_header.body_len
158
+
159
+ # The response has no body - so we need to advance the
160
+ # buffer. This is either the response to the terminating
161
+ # noop or, if the status is not zero, an intermediate
162
+ # error response that needs to be discarded.
163
+ return [ResponseHeader::SIZE, resp_header.status, nil, nil, resp_header.cas] if body_len.zero?
164
+
165
+ # The header is in the buffer, but the body is not
166
+ resp_size = ResponseHeader::SIZE + body_len
167
+ return [0, resp_header.status, nil, nil, nil] unless buf.bytesize >= resp_size
168
+
169
+ # The full response is in our buffer, so parse it and return
170
+ # the values
171
+ body = buf.slice(ResponseHeader::SIZE, body_len)
172
+ key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
173
+ [resp_size, resp_header.status, key, value, resp_header.cas]
145
174
  end
146
175
  end
147
176
  end
@@ -6,6 +6,7 @@ require 'socket'
6
6
  require 'timeout'
7
7
 
8
8
  require_relative 'binary/request_formatter'
9
+ require_relative 'binary/response_header'
9
10
  require_relative 'binary/response_processor'
10
11
  require_relative 'binary/sasl_authentication'
11
12
 
@@ -32,9 +33,7 @@ module Dalli
32
33
  # times a socket operation may fail before considering the server dead
33
34
  socket_max_failures: 2,
34
35
  # amount of time to sleep between retries when a failure occurs
35
- socket_failure_delay: 0.1,
36
- username: nil,
37
- password: nil
36
+ socket_failure_delay: 0.1
38
37
  }.freeze
39
38
 
40
39
  def initialize(attribs, options = {})
@@ -42,6 +41,7 @@ module Dalli
42
41
  @options = DEFAULTS.merge(options)
43
42
  @value_marshaller = ValueMarshaller.new(@options)
44
43
  @response_processor = ResponseProcessor.new(self, @value_marshaller)
44
+ @response_buffer = ResponseBuffer.new(self, @response_processor)
45
45
 
46
46
  reset_down_info
47
47
  @sock = nil
@@ -49,6 +49,10 @@ module Dalli
49
49
  @request_in_progress = false
50
50
  end
51
51
 
52
+ def response_buffer
53
+ @response_buffer ||= ResponseBuffer.new(self, @response_processor)
54
+ end
55
+
52
56
  def name
53
57
  if socket_type == :unix
54
58
  hostname
@@ -58,8 +62,8 @@ module Dalli
58
62
  end
59
63
 
60
64
  # Chokepoint method for error handling and ensuring liveness
61
- def request(opcode, *args)
62
- verify_state
65
+ def request(opkey, *args)
66
+ verify_state(opkey)
63
67
  # The alive? call has the side effect of connecting the underlying
64
68
  # socket if it is not connected, or there's been a disconnect
65
69
  # because of timeout or other error. Method raises an error
@@ -67,7 +71,7 @@ module Dalli
67
71
  raise_memcached_down_err unless alive?
68
72
 
69
73
  begin
70
- send(opcode, *args)
74
+ send(opkey, *args)
71
75
  rescue Dalli::MarshalError => e
72
76
  log_marshall_err(args.first, e)
73
77
  raise
@@ -145,80 +149,79 @@ module Dalli
145
149
  # flushing responses for kv pairs that were found.
146
150
  #
147
151
  # Returns nothing.
148
- def multi_response_start
149
- verify_state
152
+ def pipeline_response_start
153
+ verify_state(:getkq)
150
154
  write_noop
151
- @multi_buffer = +''
152
- @position = 0
155
+ response_buffer.reset
153
156
  start_request!
154
157
  end
155
158
 
156
- # Did the last call to #multi_response_start complete successfully?
157
- def multi_response_completed?
158
- @multi_buffer.nil?
159
+ # Did the last call to #pipeline_response_start complete successfully?
160
+ def pipeline_response_completed?
161
+ response_buffer.completed?
162
+ end
163
+
164
+ def pipeline_response(bytes_to_advance = 0)
165
+ response_buffer.process_single_response(bytes_to_advance)
166
+ end
167
+
168
+ def reconnect_on_pipeline_complete!
169
+ reconnect! 'multi_response has completed' if pipeline_response_completed?
159
170
  end
160
171
 
161
172
  # Attempt to receive and parse as many key/value pairs as possible
162
- # from this server. After #multi_response_start, this should be invoked
173
+ # from this server. After #pipeline_response_start, this should be invoked
163
174
  # repeatedly whenever this server's socket is readable until
164
- # #multi_response_completed?.
175
+ # #pipeline_response_completed?.
165
176
  #
166
177
  # Returns a Hash of kv pairs received.
167
- def multi_response_nonblock
168
- reconnect! 'multi_response has completed' if @multi_buffer.nil?
169
-
170
- @multi_buffer << @sock.read_available
171
- buf = @multi_buffer
172
- pos = @position
178
+ def process_outstanding_pipeline_requests
179
+ reconnect_on_pipeline_complete!
173
180
  values = {}
174
181
 
175
- while buf.bytesize - pos >= ResponseProcessor::RESP_HEADER_SIZE
176
- header = buf.slice(pos, ResponseProcessor::RESP_HEADER_SIZE)
177
- _, extra_len, key_len, body_len, cas = @response_processor.unpack_header(header)
182
+ response_buffer.read
178
183
 
179
- # We've reached the noop at the end of the pipeline
180
- if key_len.zero?
181
- finish_multi_response
184
+ bytes_to_advance, status, key, value, cas = pipeline_response
185
+ # Loop while we have at least a complete header in the buffer
186
+ while bytes_to_advance.positive?
187
+ # If the status and key length are both zero, then this is the response
188
+ # to the noop at the end of the pipeline
189
+ if status.zero? && key.nil?
190
+ finish_pipeline
182
191
  break
183
192
  end
184
193
 
185
- # Break and read more unless we already have the entire response for this header
186
- resp_size = ResponseProcessor::RESP_HEADER_SIZE + body_len
187
- break unless buf.bytesize - pos >= resp_size
188
-
189
- body = buf.slice(pos + ResponseProcessor::RESP_HEADER_SIZE, body_len)
190
- begin
191
- key, value = @response_processor.unpack_response_body(extra_len, key_len, body, true)
192
- values[key] = [value, cas]
193
- rescue DalliError
194
- # TODO: Determine if we should be swallowing
195
- # this error
196
- end
194
+ # If the status is zero and the key len is positive, then this is a
195
+ # getkq response with a value that we want to set in the response hash
196
+ values[key] = [value, cas] unless key.nil?
197
197
 
198
- pos = pos + ResponseProcessor::RESP_HEADER_SIZE + body_len
198
+ # Get the next set of bytes from the buffer
199
+ bytes_to_advance, status, key, value, cas = pipeline_response(bytes_to_advance)
199
200
  end
200
- # TODO: We should be discarding the already processed buffer at this point
201
- @position = pos
202
201
 
203
202
  values
204
203
  rescue SystemCallError, Timeout::Error, EOFError => e
205
204
  failure!(e)
206
205
  end
207
206
 
208
- def finish_multi_response
209
- @multi_buffer = nil
210
- @position = nil
207
+ def read_nonblock
208
+ @sock.read_available
209
+ end
210
+
211
+ # Called after the noop response is received at the end of a set
212
+ # of pipelined gets
213
+ def finish_pipeline
214
+ response_buffer.clear
211
215
  finish_request!
212
216
  end
213
217
 
214
- # Abort an earlier #multi_response_start. Used to signal an external
218
+ # Abort an earlier #pipeline_response_start. Used to signal an external
215
219
  # timeout. The underlying socket is disconnected, and the exception is
216
220
  # swallowed.
217
221
  #
218
222
  # Returns nothing.
219
- def multi_response_abort
220
- @multi_buffer = nil
221
- @position = nil
223
+ def pipeline_response_abort
224
+ response_buffer.clear
222
225
  abort_request!
223
226
  return true unless @sock
224
227
 
@@ -245,6 +248,10 @@ module Dalli
245
248
  failure!(e)
246
249
  end
247
250
 
251
+ def connected?
252
+ !@sock.nil?
253
+ end
254
+
248
255
  def socket_timeout
249
256
  @socket_timeout ||= @options[:socket_timeout]
250
257
  end
@@ -269,15 +276,23 @@ module Dalli
269
276
  @request_in_progress = false
270
277
  end
271
278
 
272
- def verify_state
279
+ def verify_state(opkey)
273
280
  failure!(RuntimeError.new('Already writing to socket')) if request_in_progress?
274
281
  reconnect_on_fork if fork_detected?
282
+ verify_allowed_multi!(opkey) if multi?
275
283
  end
276
284
 
277
285
  def fork_detected?
278
286
  @pid && @pid != Process.pid
279
287
  end
280
288
 
289
+ ALLOWED_MULTI_OPS = %i[add addq delete deleteq replace replaceq set setq noop].freeze
290
+ def verify_allowed_multi!(opkey)
291
+ return if ALLOWED_MULTI_OPS.include?(opkey)
292
+
293
+ raise Dalli::NotPermittedMultiOpError, "The operation #{opkey} is not allowed in a multi block."
294
+ end
295
+
281
296
  def reconnect_on_fork
282
297
  message = 'Fork detected, re-connecting child process...'
283
298
  Dalli.logger.info { message }
@@ -351,7 +366,7 @@ module Dalli
351
366
  end
352
367
 
353
368
  def multi?
354
- Thread.current[:dalli_multi]
369
+ Thread.current[::Dalli::MULTI_KEY]
355
370
  end
356
371
 
357
372
  def cache_nils?(opts)
@@ -366,12 +381,12 @@ module Dalli
366
381
  @response_processor.generic_response(unpack: true, cache_nils: cache_nils?(options))
367
382
  end
368
383
 
369
- def send_multiget(keys)
384
+ def pipelined_get(keys)
370
385
  req = +''
371
386
  keys.each do |key|
372
387
  req << RequestFormatter.standard_request(opkey: :getkq, key: key)
373
388
  end
374
- # Could send noop here instead of in multi_response_start
389
+ # Could send noop here instead of in pipeline_response_start
375
390
  write(req)
376
391
  end
377
392
 
@@ -520,9 +535,9 @@ module Dalli
520
535
 
521
536
  def memcached_socket
522
537
  if socket_type == :unix
523
- Dalli::Socket::UNIX.open(hostname, self, options)
538
+ Dalli::Socket::UNIX.open(hostname, options)
524
539
  else
525
- Dalli::Socket::TCP.open(hostname, port, self, options)
540
+ Dalli::Socket::TCP.open(hostname, port, options)
526
541
  end
527
542
  end
528
543
 
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'timeout'
5
+
6
+ module Dalli
7
+ module Protocol
8
+ ##
9
+ # Manages the buffer for responses from memcached.
10
+ ##
11
+ class ResponseBuffer
12
+ def initialize(io_source, response_processor)
13
+ @io_source = io_source
14
+ @response_processor = response_processor
15
+ end
16
+
17
+ def read
18
+ @buffer << @io_source.read_nonblock
19
+ end
20
+
21
+ # Attempts to process a single response from the buffer. Starts
22
+ # by advancing the buffer to the specified start position
23
+ def process_single_response(start_position = 0)
24
+ advance(start_position)
25
+ @response_processor.getk_response_from_buffer(@buffer)
26
+ end
27
+
28
+ # Advances the internal response buffer by bytes_to_advance
29
+ # bytes. The
30
+ def advance(bytes_to_advance)
31
+ @buffer = @buffer[bytes_to_advance..-1]
32
+ end
33
+
34
+ # Resets the internal buffer to an empty state,
35
+ # so that we're ready to read pipelined responses
36
+ def reset
37
+ @buffer = +''
38
+ end
39
+
40
+ # Clear the internal response buffer
41
+ def clear
42
+ @buffer = nil
43
+ end
44
+
45
+ def completed?
46
+ @buffer.nil?
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/dalli/ring.rb CHANGED
@@ -79,7 +79,7 @@ module Dalli
79
79
  end
80
80
  end
81
81
 
82
- def flush_multi_responses
82
+ def pipeline_consume_and_ignore_responses
83
83
  @servers.each do |s|
84
84
  s.request(:noop)
85
85
  rescue Dalli::NetworkError
@@ -92,6 +92,10 @@ module Dalli
92
92
  @servers.first.socket_timeout
93
93
  end
94
94
 
95
+ def close
96
+ @servers.each(&:close)
97
+ end
98
+
95
99
  private
96
100
 
97
101
  def threadsafe!
data/lib/dalli/socket.rb CHANGED
@@ -85,13 +85,13 @@ module Dalli
85
85
  ##
86
86
  class TCP < TCPSocket
87
87
  include Dalli::Socket::InstanceMethods
88
- attr_accessor :options, :server
88
+ # options - supports enhanced logging in the case of a timeout
89
+ attr_accessor :options
89
90
 
90
- def self.open(host, port, server, options = {})
91
+ def self.open(host, port, options = {})
91
92
  Timeout.timeout(options[:socket_timeout]) do
92
93
  sock = new(host, port)
93
94
  sock.options = { host: host, port: port }.merge(options)
94
- sock.server = server
95
95
  init_socket_options(sock, options)
96
96
 
97
97
  options[:ssl_context] ? wrapping_ssl_socket(sock, host, options[:ssl_context]) : sock
@@ -132,13 +132,15 @@ module Dalli
132
132
  ##
133
133
  class UNIX < UNIXSocket
134
134
  include Dalli::Socket::InstanceMethods
135
- attr_accessor :options, :server
136
135
 
137
- def self.open(path, server, options = {})
136
+ # options - supports enhanced logging in the case of a timeout
137
+ # server - used to support IO.select in the pipelined getter
138
+ attr_accessor :options
139
+
140
+ def self.open(path, options = {})
138
141
  Timeout.timeout(options[:socket_timeout]) do
139
142
  sock = new(path)
140
143
  sock.options = { path: path }.merge(options)
141
- sock.server = server
142
144
  sock
143
145
  end
144
146
  end
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '3.0.6'
4
+ VERSION = '3.1.0'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -24,10 +24,15 @@ module Dalli
24
24
  # payload too big for memcached
25
25
  class ValueOverMaxSize < DalliError; end
26
26
 
27
+ # operation is not permitted in a multi block
28
+ class NotPermittedMultiOpError < DalliError; end
29
+
27
30
  # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
28
31
  class NilObject; end # rubocop:disable Lint/EmptyClass
29
32
  NOT_FOUND = NilObject.new
30
33
 
34
+ MULTI_KEY = :dalli_multi
35
+
31
36
  def self.logger
32
37
  @logger ||= (rails_logger || default_logger)
33
38
  end
@@ -54,9 +59,11 @@ require_relative 'dalli/version'
54
59
  require_relative 'dalli/compressor'
55
60
  require_relative 'dalli/client'
56
61
  require_relative 'dalli/key_manager'
62
+ require_relative 'dalli/pipelined_getter'
57
63
  require_relative 'dalli/ring'
58
64
  require_relative 'dalli/protocol'
59
65
  require_relative 'dalli/protocol/binary'
66
+ require_relative 'dalli/protocol/response_buffer'
60
67
  require_relative 'dalli/protocol/server_config_parser'
61
68
  require_relative 'dalli/protocol/ttl_sanitizer'
62
69
  require_relative 'dalli/protocol/value_compressor'
@@ -8,14 +8,13 @@ require 'English'
8
8
  module Rack
9
9
  module Session
10
10
  # Rack::Session::Dalli provides memcached based session management.
11
- class Dalli < Abstract::Persisted
12
- attr_reader :pool
11
+ class Dalli < Abstract::PersistedSecure
12
+ attr_reader :data
13
13
 
14
14
  # Don't freeze this until we fix the specs/implementation
15
15
  # rubocop:disable Style/MutableConstant
16
16
  DEFAULT_DALLI_OPTIONS = {
17
- namespace: 'rack:session',
18
- memcache_server: 'localhost:11211'
17
+ namespace: 'rack:session'
19
18
  }
20
19
  # rubocop:enable Style/MutableConstant
21
20
 
@@ -33,25 +32,14 @@ module Rack
33
32
  # ENV['MEMCACHE_SERVERS'] and use that value if it is available, or fall
34
33
  # back to the same default behavior described above.
35
34
  #
36
- # Rack::Session::Dalli is intended to be a drop-in replacement for
37
- # Rack::Session::Memcache. It accepts additional options that control the
38
- # behavior of Rack::Session, Dalli::Client, and an optional
39
- # ConnectionPool. First and foremost, if you wish to instantiate your own
40
- # Dalli::Client (or ConnectionPool) and use that instead of letting
41
- # Rack::Session::Dalli instantiate it on your behalf, simply pass it in
42
- # as the `:cache` option. Please note that you will be responsible for
43
- # setting the namespace and any other options on Dalli::Client.
35
+ # Rack::Session::Dalli accepts the same options as Dalli::Client, so
36
+ # it's worth reviewing its documentation. Perhaps most importantly,
37
+ # if you don't specify a `:namespace` option, Rack::Session::Dalli
38
+ # will default to using 'rack:session'.
44
39
  #
45
- # Secondly, if you're not using the `:cache` option, Rack::Session::Dalli
46
- # accepts the same options as Dalli::Client, so it's worth reviewing its
47
- # documentation. Perhaps most importantly, if you don't specify a
48
- # `:namespace` option, Rack::Session::Dalli will default to using
49
- # "rack:session".
50
- #
51
- # Whether you are using the `:cache` option or not, it is not recommend
52
- # to set `:expires_in`. Instead, use `:expire_after`, which will control
53
- # both the expiration of the client cookie as well as the expiration of
54
- # the corresponding entry in memcached.
40
+ # It is not recommended to set `:expires_in`. Instead, use `:expire_after`,
41
+ # which will control both the expiration of the client cookie as well
42
+ # as the expiration of the corresponding entry in memcached.
55
43
  #
56
44
  # Rack::Session::Dalli also accepts a host of options that control how
57
45
  # the sessions and session cookies are managed, including the
@@ -78,87 +66,108 @@ module Rack
78
66
  super
79
67
 
80
68
  # Determine the default TTL for newly-created sessions
81
- @default_ttl = ttl @default_options[:expire_after]
82
-
83
- # Normalize and validate passed options
84
- mserv, mopts, popts = extract_dalli_options(options)
85
-
86
- @pool = ConnectionPool.new(popts || {}) { ::Dalli::Client.new(mserv, mopts) }
69
+ @default_ttl = ttl(@default_options[:expire_after])
70
+ @data = build_data_source(options)
87
71
  end
88
72
 
89
- def get_session(_env, sid)
90
- with_block([nil, {}]) do |dc|
91
- unless sid && !sid.empty? && (session = dc.get(sid))
92
- old_sid = sid
93
- sid = generate_sid_with(dc)
94
- session = {}
95
- unless dc.add(sid, session, @default_ttl)
96
- sid = old_sid
97
- redo # generate a new sid and try again
98
- end
99
- end
100
- [sid, session]
73
+ def find_session(_req, sid)
74
+ with_dalli_client([nil, {}]) do |dc|
75
+ existing_session = existing_session_for_sid(dc, sid)
76
+ return [sid, existing_session] unless existing_session.nil?
77
+
78
+ [create_sid_with_empty_session(dc), {}]
101
79
  end
102
80
  end
103
81
 
104
- def set_session(_env, session_id, new_session, options)
105
- return false unless session_id
82
+ def write_session(_req, sid, session, options)
83
+ return false unless sid
106
84
 
107
- with_block(false) do |dc|
108
- dc.set(session_id, new_session, ttl(options[:expire_after]))
109
- session_id
85
+ with_dalli_client(false) do |dc|
86
+ dc.set(memcached_key_from_sid(sid), session, ttl(options[:expire_after]))
87
+ sid
110
88
  end
111
89
  end
112
90
 
113
- def destroy_session(_env, session_id, options)
114
- with_block do |dc|
115
- dc.delete(session_id)
91
+ def delete_session(_req, sid, options)
92
+ with_dalli_client do |dc|
93
+ dc.delete(memcached_key_from_sid(sid))
116
94
  generate_sid_with(dc) unless options[:drop]
117
95
  end
118
96
  end
119
97
 
120
- def find_session(req, sid)
121
- get_session req.env, sid
98
+ private
99
+
100
+ def memcached_key_from_sid(sid)
101
+ sid.private_id
122
102
  end
123
103
 
124
- def write_session(req, sid, session, options)
125
- set_session req.env, sid, session, options
104
+ def existing_session_for_sid(client, sid)
105
+ return nil unless sid && !sid.empty?
106
+
107
+ client.get(memcached_key_from_sid(sid))
126
108
  end
127
109
 
128
- def delete_session(req, sid, options)
129
- destroy_session req.env, sid, options
110
+ def create_sid_with_empty_session(client)
111
+ loop do
112
+ sid = generate_sid_with(client)
113
+
114
+ break sid if client.add(memcached_key_from_sid(sid), {}, @default_ttl)
115
+ end
130
116
  end
131
117
 
132
- private
118
+ def generate_sid_with(client)
119
+ loop do
120
+ raw_sid = generate_sid
121
+ sid = raw_sid.is_a?(String) ? Rack::Session::SessionId.new(raw_sid) : raw_sid
122
+ break sid unless client.get(memcached_key_from_sid(sid))
123
+ end
124
+ end
125
+
126
+ def build_data_source(options)
127
+ server_configurations, client_options, pool_options = extract_dalli_options(options)
128
+
129
+ if pool_options.empty?
130
+ ::Dalli::Client.new(server_configurations, client_options)
131
+ else
132
+ ensure_connection_pool_added!
133
+ ConnectionPool.new(pool_options) do
134
+ ::Dalli::Client.new(server_configurations, client_options.merge(threadsafe: false))
135
+ end
136
+ end
137
+ end
133
138
 
134
139
  def extract_dalli_options(options)
135
140
  raise 'Rack::Session::Dalli no longer supports the :cache option.' if options[:cache]
136
141
 
137
- # Filter out Rack::Session-specific options and apply our defaults
142
+ client_options = retrieve_client_options(options)
143
+ server_configurations = client_options.delete(:memcache_server)
144
+
145
+ [server_configurations, client_options, retrieve_pool_options(options)]
146
+ end
147
+
148
+ def retrieve_client_options(options)
138
149
  # Filter out Rack::Session-specific options and apply our defaults
139
150
  filtered_opts = options.reject { |k, _| DEFAULT_OPTIONS.key? k }
140
- mopts = DEFAULT_DALLI_OPTIONS.merge(filtered_opts)
141
- mserv = mopts.delete :memcache_server
142
-
143
- popts = {}
144
- if mopts[:pool_size] || mopts[:pool_timeout]
145
- popts[:size] = mopts.delete :pool_size if mopts[:pool_size]
146
- popts[:timeout] = mopts.delete :pool_timeout if mopts[:pool_timeout]
147
- mopts[:threadsafe] = true
148
- end
149
-
150
- [mserv, mopts, popts]
151
+ DEFAULT_DALLI_OPTIONS.merge(filtered_opts)
151
152
  end
152
153
 
153
- def generate_sid_with(client)
154
- loop do
155
- sid = generate_sid
156
- break sid unless client.get(sid)
154
+ def retrieve_pool_options(options)
155
+ {}.tap do |pool_options|
156
+ pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
157
+ pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
157
158
  end
158
159
  end
159
160
 
160
- def with_block(default = nil, &block)
161
- @pool.with(&block)
161
+ def ensure_connection_pool_added!
162
+ require 'connection_pool'
163
+ rescue LoadError => e
164
+ warn "You don't have connection_pool installed in your application. "\
165
+ 'Please add it to your Gemfile and run bundle install'
166
+ raise e
167
+ end
168
+
169
+ def with_dalli_client(result_on_error = nil, &block)
170
+ @data.with(&block)
162
171
  rescue ::Dalli::DalliError, Errno::ECONNREFUSED
163
172
  raise if /undefined class/.match?($ERROR_INFO.message)
164
173
 
@@ -166,7 +175,7 @@ module Rack
166
175
  warn "#{self} is unable to find memcached server."
167
176
  warn $ERROR_INFO.inspect
168
177
  end
169
- default
178
+ result_on_error
170
179
  end
171
180
 
172
181
  def ttl(expire_after)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dalli
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.6
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-11-30 00:00:00.000000000 Z
12
+ date: 2021-12-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: connection_pool
@@ -29,16 +29,22 @@ dependencies:
29
29
  name: rack
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '2.0'
32
35
  - - ">="
33
36
  - !ruby/object:Gem::Version
34
- version: '0'
37
+ version: 2.2.0
35
38
  type: :development
36
39
  prerelease: false
37
40
  version_requirements: !ruby/object:Gem::Requirement
38
41
  requirements:
42
+ - - "~>"
43
+ - !ruby/object:Gem::Version
44
+ version: '2.0'
39
45
  - - ">="
40
46
  - !ruby/object:Gem::Version
41
- version: '0'
47
+ version: 2.2.0
42
48
  - !ruby/object:Gem::Dependency
43
49
  name: rubocop
44
50
  requirement: !ruby/object:Gem::Requirement
@@ -113,11 +119,14 @@ files:
113
119
  - lib/dalli/compressor.rb
114
120
  - lib/dalli/key_manager.rb
115
121
  - lib/dalli/options.rb
122
+ - lib/dalli/pipelined_getter.rb
116
123
  - lib/dalli/protocol.rb
117
124
  - lib/dalli/protocol/binary.rb
118
125
  - lib/dalli/protocol/binary/request_formatter.rb
126
+ - lib/dalli/protocol/binary/response_header.rb
119
127
  - lib/dalli/protocol/binary/response_processor.rb
120
128
  - lib/dalli/protocol/binary/sasl_authentication.rb
129
+ - lib/dalli/protocol/response_buffer.rb
121
130
  - lib/dalli/protocol/server_config_parser.rb
122
131
  - lib/dalli/protocol/ttl_sanitizer.rb
123
132
  - lib/dalli/protocol/value_compressor.rb