dalli 3.1.3 → 3.2.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: '0905b56adf194401de7755ab22ebef6b57f6e673e8332f7cb99b3ef9ed1dde63'
4
- data.tar.gz: dac2014c748ef0c55fe171af0cec8dec807ae2468f758338c8968135acd47b92
3
+ metadata.gz: 56babb2362639cca3fd24225eea2a76a0d9a075a3b113b7f8ea09814a79bf59a
4
+ data.tar.gz: 309f6be3fce52b38608c4b22e5dce439e68944796fbe1f2240b2ed40760e8700
5
5
  SHA512:
6
- metadata.gz: 85d8382dfd13c2353a61c74525e17bd81e000308e8d476851ed25fdb6ef81e9aff4276eb0b1484e30c83001301a3500140cb46a595081e1b21f0801ea32d7e28
7
- data.tar.gz: 7f0adfc09e31d435bbb3f6cff2f65392a58f81697e7025ca71d9423f9a536d9c984ed7ba0742d0c0f5de0e865a7ebd2dc5548a0406c86d7f1fe0c8135c7af363
6
+ metadata.gz: 16847c7b39ff624deeb5fecd324d3f0bddafdcd6096c5f65cfa7a79e8b2369930b7078e8fc019bd49015c3415b43f33ea07cf85380b28d3c46199339bc9aeb2a
7
+ data.tar.gz: 890f87a779947ec269fb9889334eaa7b4424a654995b9b12b723dee7452f82de066b538465800c20bf9ed9bb2c5a81fbca3ae5ff382340f241f0525ad158df0c
data/History.md CHANGED
@@ -4,6 +4,32 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 3.2.0
8
+ ==========
9
+
10
+ - BREAKING CHANGE: Remove protocol_implementation client option (petergoldstein)
11
+ - Add protocol option with meta implementation (petergoldstein)
12
+
13
+ 3.1.6
14
+ ==========
15
+
16
+ - Fix bug with cas/cas! with "Not found" value (petergoldstein)
17
+ - Add Ruby 3.1 to CI (petergoldstein)
18
+ - Replace reject(&:nil?) with compact (petergoldstein)
19
+
20
+ 3.1.5
21
+ ==========
22
+
23
+ - Fix bug with get_cas key with "Not found" value (petergoldstein)
24
+ - Replace should return nil, not raise error, on miss (petergoldstein)
25
+
26
+ 3.1.4
27
+ ==========
28
+
29
+ - Improve response parsing performance (casperisfine)
30
+ - Reorganize binary protocol parsing a bit (petergoldstein)
31
+ - Fix handling of non-ASCII keys in get_multi (petergoldstein)
32
+
7
33
  3.1.3
8
34
  ==========
9
35
 
data/lib/dalli/client.rb CHANGED
@@ -43,8 +43,8 @@ module Dalli
43
43
  # #fetch operations.
44
44
  # - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
45
45
  # useful for injecting a FIPS compliant hash object.
46
- # - :protocol_implementation - defaults to Dalli::Protocol::Binary which uses the binary protocol. Allows you to
47
- # pass an alternative implementation using another protocol.
46
+ # - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
47
+ # to communicate with memcached.
48
48
  #
49
49
  def initialize(servers = nil, options = {})
50
50
  @servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
@@ -86,8 +86,6 @@ module Dalli
86
86
  # value and CAS will be passed to the block.
87
87
  def get_cas(key)
88
88
  (value, cas) = perform(:cas, key)
89
- # TODO: This is odd. Confirm this is working as expected.
90
- value = nil if !value || value == 'Not found'
91
89
  return [value, cas] unless block_given?
92
90
 
93
91
  yield value, cas
@@ -377,7 +375,6 @@ module Dalli
377
375
 
378
376
  def cas_core(key, always_set, ttl = nil, req_options = nil)
379
377
  (value, cas) = perform(:cas, key)
380
- value = nil if !value || value == 'Not found'
381
378
  return if value.nil? && !always_set
382
379
 
383
380
  newvalue = yield(value)
@@ -405,7 +402,12 @@ module Dalli
405
402
  end
406
403
 
407
404
  def protocol_implementation
408
- @protocol_implementation ||= @options.fetch(:protocol_implementation, Dalli::Protocol::Binary)
405
+ @protocol_implementation ||= case @options[:protocol]&.to_s
406
+ when 'meta'
407
+ Dalli::Protocol::Meta
408
+ else
409
+ Dalli::Protocol::Binary
410
+ end
409
411
  end
410
412
 
411
413
  ##
@@ -83,20 +83,20 @@ module Dalli
83
83
 
84
84
  response_buffer.read
85
85
 
86
- resp_header, key, value = pipeline_response
87
- # resp_header is not nil only if we have a full response to parse
86
+ status, cas, key, value = response_buffer.process_single_getk_response
87
+ # status is not nil only if we have a full response to parse
88
88
  # in the buffer
89
- while resp_header
89
+ until status.nil?
90
90
  # If the status is ok and key is nil, then this is the response
91
91
  # to the noop at the end of the pipeline
92
- finish_pipeline && break if resp_header.ok? && key.nil?
92
+ finish_pipeline && break if status && key.nil?
93
93
 
94
94
  # If the status is ok and the key is not nil, then this is a
95
95
  # getkq response with a value that we want to set in the response hash
96
- values[key] = [value, resp_header.cas] unless key.nil?
96
+ values[key] = [value, cas] unless key.nil?
97
97
 
98
98
  # Get the next response from the buffer
99
- resp_header, key, value = pipeline_response
99
+ status, cas, key, value = response_buffer.process_single_getk_response
100
100
  end
101
101
 
102
102
  values
@@ -147,6 +147,13 @@ module Dalli
147
147
 
148
148
  private
149
149
 
150
+ ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
151
+ def verify_allowed_quiet!(opkey)
152
+ return if ALLOWED_QUIET_OPS.include?(opkey)
153
+
154
+ raise Dalli::NotPermittedMultiOpError, "The operation #{opkey} is not allowed in a quiet block."
155
+ end
156
+
150
157
  ##
151
158
  # Checks to see if we can execute the specified operation. Checks
152
159
  # whether the connection is in use, and whether the command is allowed
@@ -207,10 +214,6 @@ module Dalli
207
214
  @response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
208
215
  end
209
216
 
210
- def pipeline_response
211
- response_buffer.process_single_getk_response
212
- end
213
-
214
217
  # Called after the noop response is received at the end of a set
215
218
  # of pipelined gets
216
219
  def finish_pipeline
@@ -94,7 +94,7 @@ module Dalli
94
94
  key_len = key.nil? ? 0 : key.bytesize
95
95
  value_len = value.nil? ? 0 : value.bytesize
96
96
  header = [REQUEST, OPCODES[opkey], key_len, extra_len, 0, 0, extra_len + key_len + value_len, opaque, cas]
97
- body = [bitflags, ttl, key, value].reject(&:nil?)
97
+ body = [bitflags, ttl, key, value].compact
98
98
  (header + body).pack(FORMAT[opkey])
99
99
  end
100
100
  # rubocop:enable Metrics/ParameterLists
@@ -46,11 +46,13 @@ module Dalli
46
46
  [resp_header, body]
47
47
  end
48
48
 
49
- def unpack_response_body(extra_len, key_len, body, unpack)
50
- bitflags = extra_len.positive? ? body.byteslice(0, extra_len).unpack1('N') : 0x0
51
- key = body.byteslice(extra_len, key_len) if key_len.positive?
52
- value = body.byteslice(extra_len + key_len, body.bytesize - (extra_len + key_len))
53
- value = unpack ? @value_marshaller.retrieve(value, bitflags) : value
49
+ def unpack_response_body(resp_header, body, parse_as_stored_value)
50
+ extra_len = resp_header.extra_len
51
+ key_len = resp_header.key_len
52
+ bitflags = extra_len.positive? ? body.unpack1('N') : 0x0
53
+ key = body.byteslice(extra_len, key_len).force_encoding('UTF-8') if key_len.positive?
54
+ value = body.byteslice((extra_len + key_len)..-1)
55
+ value = parse_as_stored_value ? @value_marshaller.retrieve(value, bitflags) : value
54
56
  [key, value]
55
57
  end
56
58
 
@@ -64,7 +66,7 @@ module Dalli
64
66
  raise Dalli::DalliError, "Response error #{resp_header.status}: #{RESPONSE_CODES[resp_header.status]}"
65
67
  end
66
68
 
67
- def generic_response(unpack: false, cache_nils: false)
69
+ def get(cache_nils: false)
68
70
  resp_header, body = read_response
69
71
 
70
72
  return false if resp_header.not_stored? # Not stored, normal status for add operation
@@ -73,7 +75,7 @@ module Dalli
73
75
  raise_on_not_ok!(resp_header)
74
76
  return true unless body
75
77
 
76
- unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last
78
+ unpack_response_body(resp_header, body, true).last
77
79
  end
78
80
 
79
81
  ##
@@ -83,13 +85,14 @@ module Dalli
83
85
  ##
84
86
  def storage_response
85
87
  resp_header, = read_response
88
+ return nil if resp_header.not_found?
86
89
  return false if resp_header.not_stored? # Not stored, normal status for add operation
87
90
 
88
91
  raise_on_not_ok!(resp_header)
89
92
  resp_header.cas
90
93
  end
91
94
 
92
- def delete_response
95
+ def delete
93
96
  resp_header, = read_response
94
97
  return false if resp_header.not_found? || resp_header.not_stored?
95
98
 
@@ -97,15 +100,7 @@ module Dalli
97
100
  true
98
101
  end
99
102
 
100
- def no_body_response
101
- resp_header, = read_response
102
- return false if resp_header.not_stored? # Not stored, possible status for append/prepend
103
-
104
- raise_on_not_ok!(resp_header)
105
- true
106
- end
107
-
108
- def data_cas_response(unpack: true)
103
+ def data_cas_response
109
104
  resp_header, body = read_response
110
105
  return [nil, resp_header.cas] if resp_header.not_found?
111
106
  return [nil, false] if resp_header.not_stored?
@@ -113,14 +108,16 @@ module Dalli
113
108
  raise_on_not_ok!(resp_header)
114
109
  return [nil, resp_header.cas] unless body
115
110
 
116
- [unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last, resp_header.cas]
111
+ [unpack_response_body(resp_header, body, true).last, resp_header.cas]
117
112
  end
118
113
 
119
- def cas_response
120
- data_cas_response.last
114
+ # Returns the new value for the key, if found and updated
115
+ def decr_incr
116
+ body = generic_response
117
+ body ? body.unpack1('Q>') : body
121
118
  end
122
119
 
123
- def multi_with_keys_response
120
+ def stats
124
121
  hash = {}
125
122
  loop do
126
123
  resp_header, body = read_response
@@ -133,14 +130,49 @@ module Dalli
133
130
  # block to clear any error responses from inside the multi.
134
131
  next unless resp_header.ok?
135
132
 
136
- key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
133
+ key, value = unpack_response_body(resp_header, body, true)
137
134
  hash[key] = value
138
135
  end
139
136
  end
140
137
 
141
- def decr_incr_response
142
- body = generic_response
143
- body ? body.unpack1('Q>') : body
138
+ def flush
139
+ no_body_response
140
+ end
141
+
142
+ def reset
143
+ generic_response
144
+ end
145
+
146
+ def version
147
+ generic_response
148
+ end
149
+
150
+ def consume_all_responses_until_noop
151
+ loop do
152
+ resp_header, = read_response
153
+ # This is the response to the terminating noop / end of stat
154
+ return true if resp_header.ok? && resp_header.key_len.zero?
155
+ end
156
+ end
157
+
158
+ def generic_response
159
+ resp_header, body = read_response
160
+
161
+ return false if resp_header.not_stored? # Not stored, normal status for add operation
162
+ return nil if resp_header.not_found?
163
+
164
+ raise_on_not_ok!(resp_header)
165
+ return true unless body
166
+
167
+ unpack_response_body(resp_header, body, false).last
168
+ end
169
+
170
+ def no_body_response
171
+ resp_header, = read_response
172
+ return false if resp_header.not_stored? # Not stored, possible status for append/prepend/delete
173
+
174
+ raise_on_not_ok!(resp_header)
175
+ true
144
176
  end
145
177
 
146
178
  def validate_auth_format(extra_len, count)
@@ -164,8 +196,7 @@ module Dalli
164
196
  end
165
197
 
166
198
  def response_header_from_buffer(buf)
167
- header = buf.slice(0, ResponseHeader::SIZE)
168
- ResponseHeader.new(header)
199
+ ResponseHeader.new(buf)
169
200
  end
170
201
 
171
202
  ##
@@ -180,7 +211,7 @@ module Dalli
180
211
  ##
181
212
  def getk_response_from_buffer(buf)
182
213
  # There's no header in the buffer, so don't advance
183
- return [0, nil, nil, nil] unless contains_header?(buf)
214
+ return [0, nil, nil, nil, nil] unless contains_header?(buf)
184
215
 
185
216
  resp_header = response_header_from_buffer(buf)
186
217
  body_len = resp_header.body_len
@@ -189,18 +220,18 @@ module Dalli
189
220
  # This is either the response to the terminating
190
221
  # noop or, if the status is not zero, an intermediate
191
222
  # error response that needs to be discarded.
192
- return [ResponseHeader::SIZE, resp_header, nil, nil] if body_len.zero?
223
+ return [ResponseHeader::SIZE, resp_header.ok?, resp_header.cas, nil, nil] if body_len.zero?
193
224
 
194
225
  resp_size = ResponseHeader::SIZE + body_len
195
226
  # The header is in the buffer, but the body is not. As we don't have
196
227
  # a complete response, don't advance the buffer
197
- return [0, nil, nil, nil] unless buf.bytesize >= resp_size
228
+ return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size
198
229
 
199
230
  # The full response is in our buffer, so parse it and return
200
231
  # the values
201
- body = buf.slice(ResponseHeader::SIZE, body_len)
202
- key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
203
- [resp_size, resp_header, key, value]
232
+ body = buf.byteslice(ResponseHeader::SIZE, body_len)
233
+ key, value = unpack_response_body(resp_header, body, true)
234
+ [resp_size, resp_header.ok?, resp_header.cas, key, value]
204
235
  end
205
236
  end
206
237
  end
@@ -18,18 +18,11 @@ module Dalli
18
18
 
19
19
  private
20
20
 
21
- ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
22
- def verify_allowed_quiet!(opkey)
23
- return if ALLOWED_QUIET_OPS.include?(opkey)
24
-
25
- raise Dalli::NotPermittedMultiOpError, "The operation #{opkey} is not allowed in a quiet block."
26
- end
27
-
28
21
  # Retrieval Commands
29
22
  def get(key, options = nil)
30
23
  req = RequestFormatter.standard_request(opkey: :get, key: key)
31
24
  write(req)
32
- response_processor.generic_response(unpack: true, cache_nils: cache_nils?(options))
25
+ response_processor.get(cache_nils: cache_nils?(options))
33
26
  end
34
27
 
35
28
  def quiet_get_request(key)
@@ -40,7 +33,7 @@ module Dalli
40
33
  ttl = TtlSanitizer.sanitize(ttl)
41
34
  req = RequestFormatter.standard_request(opkey: :gat, key: key, ttl: ttl)
42
35
  write(req)
43
- response_processor.generic_response(unpack: true, cache_nils: cache_nils?(options))
36
+ response_processor.get(cache_nils: cache_nils?(options))
44
37
  end
45
38
 
46
39
  def touch(key, ttl)
@@ -106,7 +99,7 @@ module Dalli
106
99
  opkey = quiet? ? :deleteq : :delete
107
100
  req = RequestFormatter.standard_request(opkey: opkey, key: key, cas: cas)
108
101
  write(req)
109
- response_processor.delete_response unless quiet?
102
+ response_processor.delete unless quiet?
110
103
  end
111
104
 
112
105
  # Arithmetic Commands
@@ -132,7 +125,7 @@ module Dalli
132
125
  initial ||= 0
133
126
  write(RequestFormatter.decr_incr_request(opkey: opkey, key: key,
134
127
  count: count, initial: initial, expiry: expiry))
135
- response_processor.decr_incr_response unless quiet?
128
+ response_processor.decr_incr unless quiet?
136
129
  end
137
130
 
138
131
  # Other Commands
@@ -146,23 +139,23 @@ module Dalli
146
139
  # We need to read all the responses at once.
147
140
  def noop
148
141
  write_noop
149
- response_processor.multi_with_keys_response
142
+ response_processor.consume_all_responses_until_noop
150
143
  end
151
144
 
152
145
  def stats(info = '')
153
146
  req = RequestFormatter.standard_request(opkey: :stat, key: info)
154
147
  write(req)
155
- response_processor.multi_with_keys_response
148
+ response_processor.stats
156
149
  end
157
150
 
158
151
  def reset_stats
159
152
  write(RequestFormatter.standard_request(opkey: :stat, key: 'reset'))
160
- response_processor.generic_response
153
+ response_processor.reset
161
154
  end
162
155
 
163
156
  def version
164
157
  write(RequestFormatter.standard_request(opkey: :version))
165
- response_processor.generic_response
158
+ response_processor.version
166
159
  end
167
160
 
168
161
  def write_noop
@@ -133,6 +133,16 @@ module Dalli
133
133
  @request_in_progress = false
134
134
  end
135
135
 
136
+ def read_line
137
+ start_request!
138
+ data = @sock.gets("\r\n")
139
+ error_on_request!('EOF in read_line') if data.nil?
140
+ finish_request!
141
+ data
142
+ rescue SystemCallError, Timeout::Error, EOFError => e
143
+ error_on_request!(e)
144
+ end
145
+
136
146
  def read(count)
137
147
  start_request!
138
148
  data = @sock.readfull(count)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Dalli
6
+ module Protocol
7
+ class Meta
8
+ ##
9
+ # The meta protocol requires that keys be ASCII only, so Unicode keys are
10
+ # not supported. In addition, the use of whitespace in the key is not
11
+ # allowed.
12
+ # memcached supports the use of base64 hashes for keys containing
13
+ # whitespace or non-ASCII characters, provided the 'b' flag is included in the request.
14
+ class KeyRegularizer
15
+ WHITESPACE = /\s/.freeze
16
+
17
+ def self.encode(key)
18
+ return [key, false] if key.ascii_only? && !WHITESPACE.match(key)
19
+
20
+ [Base64.strict_encode64(key), true]
21
+ end
22
+
23
+ def self.decode(encoded_key, base64_encoded)
24
+ return encoded_key unless base64_encoded
25
+
26
+ Base64.strict_decode64(encoded_key).force_encoding('UTF-8')
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: false
2
+
3
+ module Dalli
4
+ module Protocol
5
+ class Meta
6
+ ##
7
+ # Class that encapsulates logic for formatting meta protocol requests
8
+ # to memcached.
9
+ ##
10
+ class RequestFormatter
11
+ # Since these are string construction methods, we're going to disable these
12
+ # Rubocop directives. We really can't make this construction much simpler,
13
+ # and introducing an intermediate object seems like overkill.
14
+ #
15
+ # rubocop:disable Metrics/CyclomaticComplexity
16
+ # rubocop:disable Metrics/MethodLength
17
+ # rubocop:disable Metrics/ParameterLists
18
+ # rubocop:disable Metrics/PerceivedComplexity
19
+ def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false)
20
+ cmd = "mg #{key}"
21
+ cmd << ' v f' if value
22
+ cmd << ' c' if return_cas
23
+ cmd << ' b' if base64
24
+ cmd << " T#{ttl}" if ttl
25
+ cmd << ' k q s' if quiet # Return the key in the response if quiet
26
+ cmd + TERMINATOR
27
+ end
28
+
29
+ def self.meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, base64: false, quiet: false)
30
+ cmd = "ms #{key} #{value.bytesize}"
31
+ cmd << ' c' unless %i[append prepend].include?(mode)
32
+ cmd << ' b' if base64
33
+ cmd << " F#{bitflags}" if bitflags
34
+ cmd << " C#{cas}" if cas && !cas.zero?
35
+ cmd << " T#{ttl}" if ttl
36
+ cmd << " M#{mode_to_token(mode)}"
37
+ cmd << ' q' if quiet
38
+ cmd << TERMINATOR
39
+ cmd << value
40
+ cmd + TERMINATOR
41
+ end
42
+
43
+ def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false)
44
+ cmd = "md #{key}"
45
+ cmd << ' b' if base64
46
+ cmd << " C#{cas}" if cas && !cas.zero?
47
+ cmd << " T#{ttl}" if ttl
48
+ cmd << ' q' if quiet
49
+ cmd + TERMINATOR
50
+ end
51
+
52
+ def self.meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, base64: false, quiet: false)
53
+ cmd = "ma #{key} v"
54
+ cmd << ' b' if base64
55
+ cmd << " D#{delta}" if delta
56
+ cmd << " J#{initial}" if initial
57
+ cmd << " C#{cas}" if cas && !cas.zero?
58
+ cmd << " N#{ttl}" if ttl
59
+ cmd << ' q' if quiet
60
+ cmd << " M#{incr ? 'I' : 'D'}"
61
+ cmd + TERMINATOR
62
+ end
63
+ # rubocop:enable Metrics/CyclomaticComplexity
64
+ # rubocop:enable Metrics/MethodLength
65
+ # rubocop:enable Metrics/ParameterLists
66
+ # rubocop:enable Metrics/PerceivedComplexity
67
+
68
+ def self.meta_noop
69
+ "mn#{TERMINATOR}"
70
+ end
71
+
72
+ def self.version
73
+ "version#{TERMINATOR}"
74
+ end
75
+
76
+ def self.flush(delay: nil, quiet: false)
77
+ cmd = +'flush_all'
78
+ cmd << " #{delay}" if delay
79
+ cmd << ' noreply' if quiet
80
+ cmd + TERMINATOR
81
+ end
82
+
83
+ def self.stats(arg = nil)
84
+ cmd = +'stats'
85
+ cmd << " #{arg}" if arg
86
+ cmd + TERMINATOR
87
+ end
88
+
89
+ # rubocop:disable Metrics/MethodLength
90
+ def self.mode_to_token(mode)
91
+ case mode
92
+ when :add
93
+ 'E'
94
+ when :replace
95
+ 'R'
96
+ when :append
97
+ 'A'
98
+ when :prepend
99
+ 'P'
100
+ else
101
+ 'S'
102
+ end
103
+ end
104
+ # rubocop:enable Metrics/MethodLength
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ module Protocol
5
+ class Meta
6
+ ##
7
+ # Class that encapsulates logic for processing meta protocol responses
8
+ # from memcached. Includes logic for pulling data from an IO source
9
+ # and parsing into local values. Handles errors on unexpected values.
10
+ ##
11
+ class ResponseProcessor
12
+ EN = 'EN'
13
+ END_TOKEN = 'END'
14
+ EX = 'EX'
15
+ HD = 'HD'
16
+ MN = 'MN'
17
+ NF = 'NF'
18
+ NS = 'NS'
19
+ OK = 'OK'
20
+ RESET = 'RESET'
21
+ STAT = 'STAT'
22
+ VA = 'VA'
23
+ VERSION = 'VERSION'
24
+
25
+ def initialize(io_source, value_marshaller)
26
+ @io_source = io_source
27
+ @value_marshaller = value_marshaller
28
+ end
29
+
30
+ def meta_get_with_value(cache_nils: false)
31
+ tokens = error_on_unexpected!([VA, EN, HD])
32
+ return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
33
+ return true unless tokens.first == VA
34
+
35
+ @value_marshaller.retrieve(read_line, bitflags_from_tokens(tokens))
36
+ end
37
+
38
+ def meta_get_with_value_and_cas
39
+ tokens = error_on_unexpected!([VA, EN, HD])
40
+ return [nil, 0] if tokens.first == EN
41
+
42
+ cas = cas_from_tokens(tokens)
43
+ return [nil, cas] unless tokens.first == VA
44
+
45
+ [@value_marshaller.retrieve(read_line, bitflags_from_tokens(tokens)), cas]
46
+ end
47
+
48
+ def meta_get_without_value
49
+ tokens = error_on_unexpected!([EN, HD])
50
+ tokens.first == EN ? nil : true
51
+ end
52
+
53
+ def meta_set_with_cas
54
+ tokens = error_on_unexpected!([HD, NS, NF, EX])
55
+ return false unless tokens.first == HD
56
+
57
+ cas_from_tokens(tokens)
58
+ end
59
+
60
+ def meta_set_append_prepend
61
+ tokens = error_on_unexpected!([HD, NS, NF, EX])
62
+ return false unless tokens.first == HD
63
+
64
+ true
65
+ end
66
+
67
+ def meta_delete
68
+ tokens = error_on_unexpected!([HD, NF, EX])
69
+ tokens.first == HD
70
+ end
71
+
72
+ def decr_incr
73
+ tokens = error_on_unexpected!([VA, NF, NS, EX])
74
+ return false if [NS, EX].include?(tokens.first)
75
+ return nil if tokens.first == NF
76
+
77
+ read_line.to_i
78
+ end
79
+
80
+ def stats
81
+ tokens = error_on_unexpected!([END_TOKEN, STAT])
82
+ values = {}
83
+ while tokens.first != END_TOKEN
84
+ values[tokens[1]] = tokens[2]
85
+ tokens = next_line_to_tokens
86
+ end
87
+ values
88
+ end
89
+
90
+ def flush
91
+ error_on_unexpected!([OK])
92
+
93
+ true
94
+ end
95
+
96
+ def reset
97
+ error_on_unexpected!([RESET])
98
+
99
+ true
100
+ end
101
+
102
+ def version
103
+ tokens = error_on_unexpected!([VERSION])
104
+ tokens.last
105
+ end
106
+
107
+ def consume_all_responses_until_mn
108
+ tokens = next_line_to_tokens
109
+
110
+ tokens = next_line_to_tokens while tokens.first != MN
111
+ true
112
+ end
113
+
114
+ def tokens_from_header_buffer(buf)
115
+ header = header_from_buffer(buf)
116
+ tokens = header.split
117
+ header_len = header.bytesize + TERMINATOR.length
118
+ body_len = body_len_from_tokens(tokens)
119
+ [tokens, header_len, body_len]
120
+ end
121
+
122
+ def full_response_from_buffer(tokens, body, resp_size)
123
+ value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
124
+ [resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
125
+ end
126
+
127
+ ##
128
+ # This method returns an array of values used in a pipelined
129
+ # getk process. The first value is the number of bytes by
130
+ # which to advance the pointer in the buffer. If the
131
+ # complete response is found in the buffer, this will
132
+ # be the response size. Otherwise it is zero.
133
+ #
134
+ # The remaining three values in the array are the ResponseHeader,
135
+ # key, and value.
136
+ ##
137
+ def getk_response_from_buffer(buf)
138
+ # There's no header in the buffer, so don't advance
139
+ return [0, nil, nil, nil, nil] unless contains_header?(buf)
140
+
141
+ tokens, header_len, body_len = tokens_from_header_buffer(buf)
142
+
143
+ # We have a complete response that has no body.
144
+ # This is either the response to the terminating
145
+ # noop or, if the status is not MN, an intermediate
146
+ # error response that needs to be discarded.
147
+ return [header_len, true, nil, nil, nil] if body_len.zero?
148
+
149
+ resp_size = header_len + body_len + TERMINATOR.length
150
+ # The header is in the buffer, but the body is not. As we don't have
151
+ # a complete response, don't advance the buffer
152
+ return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size
153
+
154
+ # The full response is in our buffer, so parse it and return
155
+ # the values
156
+ body = buf.slice(header_len, body_len)
157
+ full_response_from_buffer(tokens, body, resp_size)
158
+ end
159
+
160
+ def contains_header?(buf)
161
+ buf.include?(TERMINATOR)
162
+ end
163
+
164
+ def header_from_buffer(buf)
165
+ buf.split(TERMINATOR, 2).first
166
+ end
167
+
168
+ def error_on_unexpected!(expected_codes)
169
+ tokens = next_line_to_tokens
170
+ raise Dalli::DalliError, "Response error: #{tokens.first}" unless expected_codes.include?(tokens.first)
171
+
172
+ tokens
173
+ end
174
+
175
+ def bitflags_from_tokens(tokens)
176
+ value_from_tokens(tokens, 'f')&.to_i
177
+ end
178
+
179
+ def cas_from_tokens(tokens)
180
+ value_from_tokens(tokens, 'c')&.to_i
181
+ end
182
+
183
+ def key_from_tokens(tokens)
184
+ encoded_key = value_from_tokens(tokens, 'k')
185
+ base64_encoded = tokens.any?('b')
186
+ KeyRegularizer.decode(encoded_key, base64_encoded)
187
+ end
188
+
189
+ def body_len_from_tokens(tokens)
190
+ value_from_tokens(tokens, 's')&.to_i
191
+ end
192
+
193
+ def value_from_tokens(tokens, flag)
194
+ bitflags_token = tokens.find { |t| t.start_with?(flag) }
195
+ return 0 unless bitflags_token
196
+
197
+ bitflags_token[1..-1]
198
+ end
199
+
200
+ def read_line
201
+ @io_source.read_line&.chomp!(TERMINATOR)
202
+ end
203
+
204
+ def next_line_to_tokens
205
+ line = read_line
206
+ line&.split || []
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'socket'
5
+ require 'timeout'
6
+
7
+ module Dalli
8
+ module Protocol
9
+ ##
10
+ # Access point for a single Memcached server, accessed via Memcached's meta
11
+ # protocol. Contains logic for managing connection state to the server (retries, etc),
12
+ # formatting requests to the server, and unpacking responses.
13
+ ##
14
+ class Meta < Base
15
+ TERMINATOR = "\r\n"
16
+
17
+ def response_processor
18
+ @response_processor ||= ResponseProcessor.new(@connection_manager, @value_marshaller)
19
+ end
20
+
21
+ # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
22
+
23
+ private
24
+
25
+ # Retrieval Commands
26
+ def get(key, options = nil)
27
+ encoded_key, base64 = KeyRegularizer.encode(key)
28
+ req = RequestFormatter.meta_get(key: encoded_key, base64: base64)
29
+ write(req)
30
+ response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
31
+ end
32
+
33
+ def quiet_get_request(key)
34
+ encoded_key, base64 = KeyRegularizer.encode(key)
35
+ RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true)
36
+ end
37
+
38
+ def gat(key, ttl, options = nil)
39
+ ttl = TtlSanitizer.sanitize(ttl)
40
+ encoded_key, base64 = KeyRegularizer.encode(key)
41
+ req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64)
42
+ write(req)
43
+ response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
44
+ end
45
+
46
+ def touch(key, ttl)
47
+ encoded_key, base64 = KeyRegularizer.encode(key)
48
+ req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
49
+ write(req)
50
+ response_processor.meta_get_without_value
51
+ end
52
+
53
+ # TODO: This is confusing, as there's a cas command in memcached
54
+ # and this isn't it. Maybe rename? Maybe eliminate?
55
+ def cas(key)
56
+ encoded_key, base64 = KeyRegularizer.encode(key)
57
+ req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
58
+ write(req)
59
+ response_processor.meta_get_with_value_and_cas
60
+ end
61
+
62
+ # Storage Commands
63
+ def set(key, value, ttl, cas, options)
64
+ write_storage_req(:set, key, value, ttl, cas, options)
65
+ response_processor.meta_set_with_cas unless quiet?
66
+ end
67
+
68
+ def add(key, value, ttl, options)
69
+ write_storage_req(:add, key, value, ttl, nil, options)
70
+ response_processor.meta_set_with_cas unless quiet?
71
+ end
72
+
73
+ def replace(key, value, ttl, cas, options)
74
+ write_storage_req(:replace, key, value, ttl, cas, options)
75
+ response_processor.meta_set_with_cas unless quiet?
76
+ end
77
+
78
+ # rubocop:disable Metrics/ParameterLists
79
+ def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {})
80
+ (value, bitflags) = @value_marshaller.store(key, raw_value, options)
81
+ ttl = TtlSanitizer.sanitize(ttl) if ttl
82
+ encoded_key, base64 = KeyRegularizer.encode(key)
83
+ req = RequestFormatter.meta_set(key: encoded_key, value: value,
84
+ bitflags: bitflags, cas: cas,
85
+ ttl: ttl, mode: mode, quiet: quiet?, base64: base64)
86
+ write(req)
87
+ end
88
+ # rubocop:enable Metrics/ParameterLists
89
+
90
+ def append(key, value)
91
+ write_append_prepend_req(:append, key, value)
92
+ response_processor.meta_set_append_prepend unless quiet?
93
+ end
94
+
95
+ def prepend(key, value)
96
+ write_append_prepend_req(:prepend, key, value)
97
+ response_processor.meta_set_append_prepend unless quiet?
98
+ end
99
+
100
+ # rubocop:disable Metrics/ParameterLists
101
+ def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, _options = {})
102
+ ttl = TtlSanitizer.sanitize(ttl) if ttl
103
+ encoded_key, base64 = KeyRegularizer.encode(key)
104
+ req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
105
+ cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
106
+ write(req)
107
+ end
108
+ # rubocop:enable Metrics/ParameterLists
109
+
110
+ # Delete Commands
111
+ def delete(key, cas)
112
+ encoded_key, base64 = KeyRegularizer.encode(key)
113
+ req = RequestFormatter.meta_delete(key: encoded_key, cas: cas,
114
+ base64: base64, quiet: quiet?)
115
+ write(req)
116
+ response_processor.meta_delete unless quiet?
117
+ end
118
+
119
+ # Arithmetic Commands
120
+ def decr(key, count, ttl, initial)
121
+ decr_incr false, key, count, ttl, initial
122
+ end
123
+
124
+ def incr(key, count, ttl, initial)
125
+ decr_incr true, key, count, ttl, initial
126
+ end
127
+
128
+ def decr_incr(incr, key, delta, ttl, initial)
129
+ ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
130
+ encoded_key, base64 = KeyRegularizer.encode(key)
131
+ write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
132
+ quiet: quiet?, base64: base64))
133
+ response_processor.decr_incr unless quiet?
134
+ end
135
+
136
+ # Other Commands
137
+ def flush(delay = 0)
138
+ write(RequestFormatter.flush(delay: delay))
139
+ response_processor.flush unless quiet?
140
+ end
141
+
142
+ # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
143
+ # We need to read all the responses at once.
144
+ def noop
145
+ write_noop
146
+ response_processor.consume_all_responses_until_mn
147
+ end
148
+
149
+ def stats(info = nil)
150
+ write(RequestFormatter.stats(info))
151
+ response_processor.stats
152
+ end
153
+
154
+ def reset_stats
155
+ write(RequestFormatter.stats('reset'))
156
+ response_processor.reset
157
+ end
158
+
159
+ def version
160
+ write(RequestFormatter.version)
161
+ response_processor.version
162
+ end
163
+
164
+ def write_noop
165
+ write(RequestFormatter.meta_noop)
166
+ end
167
+
168
+ def authenticate_connection
169
+ raise Dalli::DalliError, 'Authentication not supported for the meta protocol.'
170
+ end
171
+
172
+ require_relative 'meta/key_regularizer'
173
+ require_relative 'meta/request_formatter'
174
+ require_relative 'meta/response_processor'
175
+ end
176
+ end
177
+ end
@@ -12,6 +12,7 @@ module Dalli
12
12
  def initialize(io_source, response_processor)
13
13
  @io_source = io_source
14
14
  @response_processor = response_processor
15
+ @buffer = nil
15
16
  end
16
17
 
17
18
  def read
@@ -21,9 +22,9 @@ module Dalli
21
22
  # Attempts to process a single response from the buffer. Starts
22
23
  # by advancing the buffer to the specified start position
23
24
  def process_single_getk_response
24
- bytes, resp_header, key, value = @response_processor.getk_response_from_buffer(@buffer)
25
+ bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer)
25
26
  advance(bytes)
26
- [resp_header, key, value]
27
+ [status, cas, key, value]
27
28
  end
28
29
 
29
30
  # Advances the internal response buffer by bytes_to_advance
@@ -31,13 +32,13 @@ module Dalli
31
32
  def advance(bytes_to_advance)
32
33
  return unless bytes_to_advance.positive?
33
34
 
34
- @buffer = @buffer[bytes_to_advance..-1]
35
+ @buffer = @buffer.byteslice(bytes_to_advance..-1)
35
36
  end
36
37
 
37
38
  # Resets the internal buffer to an empty state,
38
39
  # so that we're ready to read pipelined responses
39
40
  def reset
40
- @buffer = +''
41
+ @buffer = ''.b
41
42
  end
42
43
 
43
44
  # Clear the internal response buffer
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.1.3'
4
+ VERSION = '3.2.0'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
data/lib/dalli.rb CHANGED
@@ -65,6 +65,7 @@ require_relative 'dalli/protocol'
65
65
  require_relative 'dalli/protocol/base'
66
66
  require_relative 'dalli/protocol/binary'
67
67
  require_relative 'dalli/protocol/connection_manager'
68
+ require_relative 'dalli/protocol/meta'
68
69
  require_relative 'dalli/protocol/response_buffer'
69
70
  require_relative 'dalli/protocol/server_config_parser'
70
71
  require_relative 'dalli/protocol/ttl_sanitizer'
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.1.3
4
+ version: 3.2.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-12-14 00:00:00.000000000 Z
12
+ date: 2022-01-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: connection_pool
@@ -128,6 +128,10 @@ files:
128
128
  - lib/dalli/protocol/binary/response_processor.rb
129
129
  - lib/dalli/protocol/binary/sasl_authentication.rb
130
130
  - lib/dalli/protocol/connection_manager.rb
131
+ - lib/dalli/protocol/meta.rb
132
+ - lib/dalli/protocol/meta/key_regularizer.rb
133
+ - lib/dalli/protocol/meta/request_formatter.rb
134
+ - lib/dalli/protocol/meta/response_processor.rb
131
135
  - lib/dalli/protocol/response_buffer.rb
132
136
  - lib/dalli/protocol/server_config_parser.rb
133
137
  - lib/dalli/protocol/ttl_sanitizer.rb
@@ -160,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
164
  - !ruby/object:Gem::Version
161
165
  version: '0'
162
166
  requirements: []
163
- rubygems_version: 3.2.31
167
+ rubygems_version: 3.3.4
164
168
  signing_key:
165
169
  specification_version: 4
166
170
  summary: High performance memcached client for Ruby