dalli 3.0.5 → 3.1.2

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.

Potentially problematic release.


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

@@ -0,0 +1,177 @@
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
+ servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
23
+ end
24
+ rescue NetworkError => e
25
+ Dalli.logger.debug { e.inspect }
26
+ Dalli.logger.debug { 'retrying pipelined gets because of timeout' }
27
+ retry
28
+ end
29
+
30
+ def setup_requests(keys)
31
+ groups = groups_for_keys(keys)
32
+ make_getkq_requests(groups)
33
+
34
+ # TODO: How does this exit on a NetworkError
35
+ finish_queries(groups.keys)
36
+ end
37
+
38
+ ##
39
+ # Loop through the server-grouped sets of keys, writing
40
+ # the corresponding getkq requests to the appropriate servers
41
+ #
42
+ # It's worth noting that we could potentially reduce bytes
43
+ # on the wire by switching from getkq to getq, and using
44
+ # the opaque value to match requests to responses.
45
+ ##
46
+ def make_getkq_requests(groups)
47
+ groups.each do |server, keys_for_server|
48
+ server.request(:pipelined_get, keys_for_server)
49
+ rescue DalliError, NetworkError => e
50
+ Dalli.logger.debug { e.inspect }
51
+ Dalli.logger.debug { "unable to get keys for server #{server.name}" }
52
+ end
53
+ end
54
+
55
+ ##
56
+ # This loops through the servers that have keys in
57
+ # our set, sending the noop to terminate the set of queries.
58
+ ##
59
+ def finish_queries(servers)
60
+ deleted = []
61
+
62
+ servers.each do |server|
63
+ next unless server.alive?
64
+
65
+ begin
66
+ finish_query_for_server(server)
67
+ rescue Dalli::NetworkError
68
+ raise
69
+ rescue Dalli::DalliError
70
+ deleted.append(server)
71
+ end
72
+ end
73
+
74
+ servers.delete_if { |server| deleted.include?(server) }
75
+ rescue Dalli::NetworkError
76
+ abort_without_timeout(servers)
77
+ raise
78
+ end
79
+
80
+ def finish_query_for_server(server)
81
+ server.pipeline_response_setup
82
+ rescue Dalli::NetworkError
83
+ raise
84
+ rescue Dalli::DalliError => e
85
+ Dalli.logger.debug { e.inspect }
86
+ Dalli.logger.debug { "Results from server: #{server.name} will be missing from the results" }
87
+ raise
88
+ end
89
+
90
+ # Swallows Dalli::NetworkError
91
+ def abort_without_timeout(servers)
92
+ servers.each(&:pipeline_abort)
93
+ end
94
+
95
+ def fetch_responses(servers, start_time, timeout, &block)
96
+ # Remove any servers which are not connected
97
+ servers.delete_if { |s| !s.connected? }
98
+ return [] if servers.empty?
99
+
100
+ time_left = remaining_time(start_time, timeout)
101
+ readable_servers = servers_with_response(servers, time_left)
102
+ if readable_servers.empty?
103
+ abort_with_timeout(servers)
104
+ return []
105
+ end
106
+
107
+ # Loop through the servers with responses, and
108
+ # delete any from our list that are finished
109
+ readable_servers.each do |server|
110
+ servers.delete(server) if process_server(server, &block)
111
+ end
112
+ servers
113
+ rescue NetworkError
114
+ # Abort and raise if we encountered a network error. This triggers
115
+ # a retry at the top level.
116
+ abort_without_timeout(servers)
117
+ raise
118
+ end
119
+
120
+ def remaining_time(start, timeout)
121
+ elapsed = Time.now - start
122
+ return 0 if elapsed > timeout
123
+
124
+ timeout - elapsed
125
+ end
126
+
127
+ # Swallows Dalli::NetworkError
128
+ def abort_with_timeout(servers)
129
+ abort_without_timeout(servers)
130
+ servers.each do |server|
131
+ Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
132
+ end
133
+
134
+ true # Required to simplify caller
135
+ end
136
+
137
+ # Processes responses from a server. Returns true if there are no
138
+ # additional responses from this server.
139
+ def process_server(server)
140
+ server.pipeline_next_responses.each_pair do |key, value_list|
141
+ yield @key_manager.key_without_namespace(key), value_list
142
+ end
143
+
144
+ server.pipeline_complete?
145
+ end
146
+
147
+ def servers_with_response(servers, timeout)
148
+ return [] if servers.empty?
149
+
150
+ # TODO: - This is a bit challenging. Essentially the PipelinedGetter
151
+ # is a reactor, but without the benefit of a Fiber or separate thread.
152
+ # My suspicion is that we may want to try and push this down into the
153
+ # individual servers, but I'm not sure. For now, we keep the
154
+ # mapping between the alerted object (the socket) and the
155
+ # corrresponding server here.
156
+ server_map = servers.each_with_object({}) { |s, h| h[s.sock] = s }
157
+
158
+ readable, = IO.select(server_map.keys, nil, nil, timeout)
159
+ return [] if readable.nil?
160
+
161
+ readable.map { |sock| server_map[sock] }
162
+ end
163
+
164
+ def groups_for_keys(*keys)
165
+ keys.flatten!
166
+ keys.map! { |a| @key_manager.validate_key(a.to_s) }
167
+ groups = @ring.keys_grouped_by_server(keys)
168
+ if (unfound_keys = groups.delete(nil))
169
+ Dalli.logger.debug do
170
+ "unable to get keys for #{unfound_keys.length} keys "\
171
+ 'because no matching server was found'
172
+ end
173
+ end
174
+ groups
175
+ end
176
+ end
177
+ end
@@ -31,11 +31,14 @@ module Dalli
31
31
  deleteq: 0x14,
32
32
  incrq: 0x15,
33
33
  decrq: 0x16,
34
+ flushq: 0x18,
35
+ appendq: 0x19,
36
+ prependq: 0x1A,
37
+ touch: 0x1C,
38
+ gat: 0x1D,
34
39
  auth_negotiation: 0x20,
35
40
  auth_request: 0x21,
36
- auth_continue: 0x22,
37
- touch: 0x1C,
38
- gat: 0x1D
41
+ auth_continue: 0x22
39
42
  }.freeze
40
43
 
41
44
  REQ_HEADER_FORMAT = 'CCnCCnNNQ'
@@ -56,6 +59,8 @@ module Dalli
56
59
 
57
60
  append: KEY_AND_VALUE,
58
61
  prepend: KEY_AND_VALUE,
62
+ appendq: KEY_AND_VALUE,
63
+ prependq: KEY_AND_VALUE,
59
64
  auth_request: KEY_AND_VALUE,
60
65
  auth_continue: KEY_AND_VALUE,
61
66
 
@@ -68,8 +73,11 @@ module Dalli
68
73
 
69
74
  incr: INCR_DECR,
70
75
  decr: INCR_DECR,
76
+ incrq: INCR_DECR,
77
+ decrq: INCR_DECR,
71
78
 
72
79
  flush: TTL_ONLY,
80
+ flushq: TTL_ONLY,
73
81
 
74
82
  noop: NO_BODY,
75
83
  auth_negotiation: NO_BODY,
@@ -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,57 @@ 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!(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)
64
+ raise Dalli::DalliError, "Response error #{resp_header.status}: #{RESPONSE_CODES[resp_header.status]}"
76
65
  end
77
66
 
78
- def raise_on_not_ok_status!(status)
79
- return if status.zero?
67
+ def generic_response(unpack: false, cache_nils: false)
68
+ resp_header, body = read_response
69
+
70
+ return false if resp_header.not_stored? # Not stored, normal status for add operation
71
+ return cache_nils ? ::Dalli::NOT_FOUND : nil if resp_header.not_found?
80
72
 
81
- raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
73
+ raise_on_not_ok!(resp_header)
74
+ return true unless body
75
+
76
+ unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last
82
77
  end
83
78
 
84
- def generic_response(unpack: false, cache_nils: false)
85
- status, extra_len, body, _, key_len = read_response
79
+ ##
80
+ # Response for a storage operation. Returns the cas on success. False
81
+ # if the value wasn't stored. And raises an error on all other error
82
+ # codes from memcached.
83
+ ##
84
+ def storage_response
85
+ resp_header, = read_response
86
+ return false if resp_header.not_stored? # Not stored, normal status for add operation
86
87
 
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
88
+ raise_on_not_ok!(resp_header)
89
+ resp_header.cas
90
+ end
89
91
 
90
- raise_on_not_ok_status!(status)
91
- return true unless body
92
+ def no_body_response
93
+ resp_header, = read_response
94
+ return false if resp_header.not_stored? # Not stored, possible status for append/prepend
92
95
 
93
- unpack_response_body(extra_len, key_len, body, unpack).last
96
+ raise_on_not_ok!(resp_header)
97
+ true
94
98
  end
95
99
 
96
- 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)
100
+ def data_cas_response(unpack: true)
101
+ resp_header, body = read_response
102
+ return [nil, resp_header.cas] if resp_header.not_found?
103
+ return [nil, false] if resp_header.not_stored?
100
104
 
101
- raise_on_not_ok_status!(status)
102
- return [nil, cas] unless body
105
+ raise_on_not_ok!(resp_header)
106
+ return [nil, resp_header.cas] unless body
103
107
 
104
- [unpack_response_body(extra_len, key_len, body, true).last, cas]
108
+ [unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last, resp_header.cas]
105
109
  end
106
110
 
107
111
  def cas_response
@@ -111,17 +115,17 @@ module Dalli
111
115
  def multi_with_keys_response
112
116
  hash = {}
113
117
  loop do
114
- status, extra_len, body, _, key_len = read_response
118
+ resp_header, body = read_response
115
119
  # This is the response to the terminating noop / end of stat
116
- return hash if status.zero? && key_len.zero?
120
+ return hash if resp_header.ok? && resp_header.key_len.zero?
117
121
 
118
122
  # Ignore any responses with non-zero status codes,
119
123
  # such as errors from set operations. That allows
120
124
  # this code to be used at the end of a multi
121
125
  # block to clear any error responses from inside the multi.
122
- next unless status.zero?
126
+ next unless resp_header.ok?
123
127
 
124
- key, value = unpack_response_body(extra_len, key_len, body, true)
128
+ key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
125
129
  hash[key] = value
126
130
  end
127
131
  end
@@ -132,16 +136,63 @@ module Dalli
132
136
  end
133
137
 
134
138
  def validate_auth_format(extra_len, count)
135
- return if extra_len.zero? && count.positive?
139
+ return if extra_len.zero?
136
140
 
137
141
  raise Dalli::NetworkError, "Unexpected message format: #{extra_len} #{count}"
138
142
  end
139
143
 
140
- def auth_response
141
- (extra_len, _type, status, count) = read_header.unpack(RESP_HEADER)
142
- validate_auth_format(extra_len, count)
143
- content = read(count)
144
- [status, content]
144
+ def auth_response(buf = read_header)
145
+ resp_header = ResponseHeader.new(buf)
146
+ body_len = resp_header.body_len
147
+ validate_auth_format(resp_header.extra_len, body_len)
148
+ content = read(body_len) if body_len.positive?
149
+ [resp_header.status, content]
150
+ end
151
+
152
+ def contains_header?(buf)
153
+ return false unless buf
154
+
155
+ buf.bytesize >= ResponseHeader::SIZE
156
+ end
157
+
158
+ def response_header_from_buffer(buf)
159
+ header = buf.slice(0, ResponseHeader::SIZE)
160
+ ResponseHeader.new(header)
161
+ end
162
+
163
+ ##
164
+ # This method returns an array of values used in a pipelined
165
+ # getk process. The first value is the number of bytes by
166
+ # which to advance the pointer in the buffer. If the
167
+ # complete response is found in the buffer, this will
168
+ # be the response size. Otherwise it is zero.
169
+ #
170
+ # The remaining three values in the array are the ResponseHeader,
171
+ # key, and value.
172
+ ##
173
+ def getk_response_from_buffer(buf)
174
+ # There's no header in the buffer, so don't advance
175
+ return [0, nil, nil, nil] unless contains_header?(buf)
176
+
177
+ resp_header = response_header_from_buffer(buf)
178
+ body_len = resp_header.body_len
179
+
180
+ # We have a complete response that has no body.
181
+ # This is either the response to the terminating
182
+ # noop or, if the status is not zero, an intermediate
183
+ # error response that needs to be discarded.
184
+ return [ResponseHeader::SIZE, resp_header, nil, nil] if body_len.zero?
185
+
186
+ resp_size = ResponseHeader::SIZE + body_len
187
+ # The header is in the buffer, but the body is not. As we don't have
188
+ # a complete response, don't advance the buffer
189
+ return [0, nil, nil, nil] unless buf.bytesize >= resp_size
190
+
191
+ # The full response is in our buffer, so parse it and return
192
+ # the values
193
+ body = buf.slice(ResponseHeader::SIZE, body_len)
194
+ key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
195
+ [resp_size, resp_header, key, value]
145
196
  end
146
197
  end
147
198
  end
@@ -11,9 +11,12 @@ module Dalli
11
11
  write(RequestFormatter.standard_request(opkey: :auth_negotiation))
12
12
 
13
13
  status, content = @response_processor.auth_response
14
- # TODO: Determine if this substitution is needed
15
- content.tr("\u0000", ' ')
16
- mechanisms = content.split
14
+ return [status, []] if content.nil?
15
+
16
+ # Substitute spaces for the \x00 returned by
17
+ # memcached as a separator for easier
18
+ content&.tr("\u0000", ' ')
19
+ mechanisms = content&.split
17
20
  [status, mechanisms]
18
21
  end
19
22
 
@@ -45,7 +48,7 @@ module Dalli
45
48
 
46
49
  return Dalli.logger.info("Dalli/SASL: #{content}") if status.zero?
47
50
 
48
- raise Dalli::DalliError, "Error authenticating: #{status}" unless status == 0x21
51
+ raise Dalli::DalliError, "Error authenticating: 0x#{status.to_s(16)}" unless status == 0x21
49
52
 
50
53
  raise NotImplementedError, 'No two-step authentication mechanisms supported'
51
54
  # (step, msg) = sasl.receive('challenge', content)