dalli 5.0.5 → 5.0.7

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.
@@ -24,34 +24,29 @@ module Dalli
24
24
 
25
25
  # Retrieval Commands
26
26
  def get(key, options = nil)
27
- encoded_key, base64 = KeyRegularizer.encode(key)
28
27
  # Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
29
28
  skip_flags = raw_mode? || (options && options[:raw])
30
- req = RequestFormatter.meta_get(key: encoded_key, base64: base64, skip_flags: skip_flags)
29
+ req = RequestFormatter.meta_get(key: key, skip_flags: skip_flags)
31
30
  flushed_write(req)
32
31
  response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
33
32
  end
34
33
 
35
34
  def quiet_get_request(key)
36
- encoded_key, base64 = KeyRegularizer.encode(key)
37
35
  # Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
38
- RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true,
39
- skip_flags: raw_mode?)
36
+ RequestFormatter.meta_get(key: key, return_cas: true, quiet: true, skip_flags: raw_mode?)
40
37
  end
41
38
 
42
39
  def gat(key, ttl, options = nil)
43
40
  ttl = TtlSanitizer.sanitize(ttl)
44
- encoded_key, base64 = KeyRegularizer.encode(key)
45
41
  skip_flags = raw_mode? || (options && options[:raw])
46
- req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64, skip_flags: skip_flags)
42
+ req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags)
47
43
  flushed_write(req)
48
44
  response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
49
45
  end
50
46
 
51
47
  def touch(key, ttl)
52
48
  ttl = TtlSanitizer.sanitize(ttl)
53
- encoded_key, base64 = KeyRegularizer.encode(key)
54
- req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
49
+ req = RequestFormatter.meta_get(key: key, ttl: ttl, value: false)
55
50
  flushed_write(req)
56
51
  response_processor.meta_get_without_value
57
52
  end
@@ -59,8 +54,7 @@ module Dalli
59
54
  # TODO: This is confusing, as there's a cas command in memcached
60
55
  # and this isn't it. Maybe rename? Maybe eliminate?
61
56
  def cas(key)
62
- encoded_key, base64 = KeyRegularizer.encode(key)
63
- req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
57
+ req = RequestFormatter.meta_get(key: key, value: true, return_cas: true)
64
58
  flushed_write(req)
65
59
  response_processor.meta_get_with_value_and_cas
66
60
  end
@@ -90,9 +84,8 @@ module Dalli
90
84
  # - :hit_before - true/false if previously accessed (only if return_hit_status: true)
91
85
  # - :last_access - seconds since last access (only if return_last_access: true)
92
86
  def meta_get(key, options = {})
93
- encoded_key, base64 = KeyRegularizer.encode(key)
94
87
  req = RequestFormatter.meta_get(
95
- key: encoded_key, value: true, return_cas: true, base64: base64,
88
+ key: key, value: true, return_cas: true,
96
89
  vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
97
90
  return_hit_status: options[:return_hit_status],
98
91
  return_last_access: options[:return_last_access], skip_lru_bump: options[:skip_lru_bump]
@@ -112,8 +105,7 @@ module Dalli
112
105
  # @param cas [Integer] optional CAS value for compare-and-swap
113
106
  # @return [Boolean] true if successful
114
107
  def delete_stale(key, cas = nil)
115
- encoded_key, base64 = KeyRegularizer.encode(key)
116
- req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
108
+ req = RequestFormatter.meta_delete(key: key, cas: cas, stale: true)
117
109
  flushed_write(req)
118
110
  response_processor.meta_delete
119
111
  end
@@ -140,14 +132,12 @@ module Dalli
140
132
  response_processor.meta_set_with_cas unless quiet?
141
133
  end
142
134
 
143
- # rubocop:disable Metrics/ParameterLists
144
135
  def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
145
136
  (value, bitflags) = @value_marshaller.store(key, raw_value, options)
146
137
  ttl = TtlSanitizer.sanitize(ttl) if ttl
147
- encoded_key, base64 = KeyRegularizer.encode(key)
148
- req = RequestFormatter.meta_set(key: encoded_key, value: value,
138
+ req = RequestFormatter.meta_set(key: key, value: value,
149
139
  bitflags: bitflags, cas: cas,
150
- ttl: ttl, mode: mode, quiet: quiet, base64: base64)
140
+ ttl: ttl, mode: mode, quiet: quiet)
151
141
  write("#{req}#{value}#{TERMINATOR}")
152
142
  @connection_manager.flush unless quiet
153
143
  end
@@ -166,8 +156,7 @@ module Dalli
166
156
  # rubocop:disable Metrics/ParameterLists
167
157
  def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, _options = {})
168
158
  ttl = TtlSanitizer.sanitize(ttl) if ttl
169
- encoded_key, base64 = KeyRegularizer.encode(key)
170
- req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
159
+ req = RequestFormatter.meta_set(key: key, value: value,
171
160
  cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
172
161
  write("#{req}#{value}#{TERMINATOR}")
173
162
  @connection_manager.flush unless quiet?
@@ -176,9 +165,7 @@ module Dalli
176
165
 
177
166
  # Delete Commands
178
167
  def delete(key, cas)
179
- encoded_key, base64 = KeyRegularizer.encode(key)
180
- req = RequestFormatter.meta_delete(key: encoded_key, cas: cas,
181
- base64: base64, quiet: quiet?)
168
+ req = RequestFormatter.meta_delete(key: key, cas: cas, quiet: quiet?)
182
169
  write(req)
183
170
  @connection_manager.flush unless quiet?
184
171
  response_processor.meta_delete unless quiet?
@@ -187,11 +174,15 @@ module Dalli
187
174
  # Pipelined delete - writes a quiet delete request without reading response.
188
175
  # Used by PipelinedDeleter for bulk operations.
189
176
  def pipelined_delete(key)
190
- encoded_key, base64 = KeyRegularizer.encode(key)
191
- req = RequestFormatter.meta_delete(key: encoded_key, base64: base64, quiet: true)
177
+ req = RequestFormatter.meta_delete(key: key, quiet: true)
192
178
  write(req)
193
179
  end
194
180
 
181
+ def finish_pipelined_delete(sent)
182
+ write_noop
183
+ sent - response_processor.pipelined_delete_non_deletions
184
+ end
185
+
195
186
  # Arithmetic Commands
196
187
  def decr(key, count, ttl, initial)
197
188
  decr_incr false, key, count, ttl, initial
@@ -203,9 +194,8 @@ module Dalli
203
194
 
204
195
  def decr_incr(incr, key, delta, ttl, initial)
205
196
  ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
206
- encoded_key, base64 = KeyRegularizer.encode(key)
207
- write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
208
- quiet: quiet?, base64: base64))
197
+ write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
198
+ quiet: quiet?))
209
199
  @connection_manager.flush unless quiet?
210
200
  response_processor.decr_incr unless quiet?
211
201
  end
@@ -248,23 +238,9 @@ module Dalli
248
238
  # machinery (IO.select, response buffering, server grouping).
249
239
  def read_multi_req(keys)
250
240
  is_raw = raw_mode?
251
- # Inline request formatting — avoids RequestFormatter.meta_get overhead per key.
252
- # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
253
- # Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
254
- post_get = is_raw ? " v k q s\r\n" : " v f k q s\r\n"
255
- buffer = ''.b
256
- keys.each do |key|
257
- encoded_key, base64 = KeyRegularizer.encode(key)
258
- if base64
259
- buffer << 'mg ' << encoded_key << ' b' << post_get
260
- else
261
- buffer << 'mg ' << encoded_key << post_get
262
- end
263
- end
264
- buffer << 'mn' << TERMINATOR
241
+ buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw)
265
242
  flushed_write(buffer)
266
243
  buffer.clear
267
-
268
244
  read_multi_get_responses(is_raw)
269
245
  end
270
246
 
@@ -287,52 +263,33 @@ module Dalli
287
263
  raw_key = tokens[key_index]
288
264
  return unless raw_key
289
265
 
290
- key = KeyRegularizer.decode(raw_key[1..], tokens.include?('b'))
266
+ key = raw_key[1..]
267
+ key = KeyRegularizer.decode(key) if tokens.include?('b')
291
268
  bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
292
269
  [key, @value_marshaller.retrieve(value, bitflags)]
293
270
  end
294
271
 
295
- # rubocop:disable Metrics/AbcSize
296
-
297
272
  # Single-server fast path for set_multi. Inlines request formatting to
298
273
  # minimize per-key overhead. Avoids PipelinedSetter server grouping.
299
274
  def write_multi_req(pairs, ttl, req_options)
300
275
  ttl = TtlSanitizer.sanitize(ttl) if ttl
301
- buffer = ''.b
302
- pairs.each do |key, raw_value|
303
- (value, bitflags) = @value_marshaller.store(key, raw_value, req_options)
304
- encoded_key, base64 = KeyRegularizer.encode(key)
305
- # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
306
- buffer << "ms #{encoded_key} #{value.bytesize} c"
307
- buffer << ' b' if base64
308
- buffer << " F#{bitflags}" if bitflags
309
- buffer << " T#{ttl}" if ttl
310
- buffer << ' MS q' << TERMINATOR << value << TERMINATOR
276
+ entries = pairs.map do |key, raw_value|
277
+ [key, @value_marshaller.store(key, raw_value, req_options)]
311
278
  end
312
- buffer << RequestFormatter.meta_noop
279
+
280
+ buffer = RequestFormatter.multi_meta_set(entries, ttl: ttl)
313
281
  flushed_write(buffer)
314
282
  buffer.clear
315
283
  response_processor.consume_all_responses_until_mn
316
284
  end
317
- # rubocop:enable Metrics/AbcSize
318
285
 
319
286
  # Single-server fast path for delete_multi. Writes all quiet delete requests
320
287
  # terminated by a noop, then consumes all responses.
321
288
  def delete_multi_req(keys)
322
- buffer = ''.b
323
- keys.each do |key|
324
- encoded_key, base64 = KeyRegularizer.encode(key)
325
- # Inline format: "md <key> [b] q\r\n"
326
- if base64
327
- buffer << 'md ' << encoded_key << ' b q' << TERMINATOR
328
- else
329
- buffer << 'md ' << encoded_key << ' q' << TERMINATOR
330
- end
331
- end
332
- buffer << RequestFormatter.meta_noop
289
+ buffer = RequestFormatter.multi_meta_delete(keys)
333
290
  flushed_write(buffer)
334
291
  buffer.clear
335
- response_processor.consume_all_responses_until_mn
292
+ keys.size - response_processor.pipelined_delete_non_deletions
336
293
  end
337
294
 
338
295
  require_relative 'key_regularizer'
@@ -1,4 +1,4 @@
1
- # frozen_string_literal: false
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
4
  module Protocol
@@ -7,14 +7,13 @@ module Dalli
7
7
  # Class that encapsulates logic for formatting meta protocol requests
8
8
  # to memcached.
9
9
  ##
10
- class RequestFormatter
10
+ module RequestFormatter
11
+ extend self
12
+
11
13
  # Since these are string construction methods, we're going to disable these
12
14
  # Rubocop directives. We really can't make this construction much simpler,
13
15
  # and introducing an intermediate object seems like overkill.
14
16
  #
15
- # rubocop:disable Metrics/CyclomaticComplexity
16
- # rubocop:disable Metrics/ParameterLists
17
- # rubocop:disable Metrics/PerceivedComplexity
18
17
  #
19
18
  # Meta get flags:
20
19
  #
@@ -35,58 +34,98 @@ module Dalli
35
34
  # - Z: Client lost the recache race (another client is already regenerating)
36
35
  # - h0/h1: Hit status (0 = first access, 1 = previously accessed)
37
36
  # - l<N>: Seconds since last access
38
- def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false,
39
- vivify_ttl: nil, recache_ttl: nil,
40
- return_hit_status: false, return_last_access: false, skip_lru_bump: false,
41
- skip_flags: false)
42
- cmd = "mg #{key}"
37
+ def meta_get(key:, value: true, return_cas: false, ttl: nil, quiet: false,
38
+ vivify_ttl: nil, recache_ttl: nil,
39
+ return_hit_status: false, return_last_access: false, skip_lru_bump: false,
40
+ skip_flags: false)
41
+ cmd = "mg #{encoded_key(key)}"
43
42
  # In raw mode (skip_flags: true), we don't request bitflags since they're not used.
44
43
  # This saves 2 bytes per request and skips parsing on response.
45
44
  cmd << (skip_flags ? ' v' : ' v f') if value
46
45
  cmd << ' c' if return_cas
47
- cmd << ' b' if base64
48
- cmd << " T#{ttl}" if ttl
46
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
49
47
  cmd << ' k q s' if quiet # Return the key in the response if quiet
50
- cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
51
- cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
48
+ cmd << " N#{integer_flag(:vivify_ttl, vivify_ttl)}" if vivify_ttl # Thundering herd: vivify on miss
49
+ # Thundering herd: win recache if TTL below threshold
50
+ cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
52
51
  cmd << ' h' if return_hit_status # Return hit status (0 or 1)
53
52
  cmd << ' l' if return_last_access # Return seconds since last access
54
53
  cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
55
54
  cmd << TERMINATOR
56
55
  end
57
56
 
58
- def self.meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, base64: false, quiet: false)
57
+ def multi_meta_get(keys, skip_flags: false)
58
+ # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
59
+ # Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
60
+ post_get = skip_flags ? " v k q s\r\n" : " v f k q s\r\n"
61
+ buffer = ''.b
62
+ keys.each do |key|
63
+ buffer << 'mg ' << encoded_key(key) << post_get
64
+ end
65
+ buffer << 'mn' << TERMINATOR
66
+ end
67
+
68
+ def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false)
69
+ base64 = KeyRegularizer.required?(key)
70
+ key = KeyRegularizer.encode(key) if base64
59
71
  cmd = "ms #{key} #{value.bytesize}"
60
- cmd << ' c' unless %i[append prepend].include?(mode)
72
+ # Skip the cas-return flag in quiet mode: the response is suppressed,
73
+ # so requesting it only adds bytes to the request.
74
+ cmd << ' c' if !quiet && !%i[append prepend].include?(mode)
61
75
  cmd << ' b' if base64
62
76
  cmd << " F#{bitflags}" if bitflags
63
77
  cmd << cas_string(cas)
64
- cmd << " T#{ttl}" if ttl
78
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
65
79
  cmd << " M#{mode_to_token(mode)}"
66
80
  cmd << ' q' if quiet
67
81
  cmd << TERMINATOR
68
82
  end
69
83
 
84
+ def multi_meta_set(entries, ttl: nil)
85
+ ttl = integer_flag(:ttl, ttl) if ttl
86
+ buffer = ''.b
87
+ entries.each do |key, pair|
88
+ value, bitflags = pair
89
+
90
+ base64 = KeyRegularizer.required?(key)
91
+ key = KeyRegularizer.encode(key) if base64
92
+
93
+ # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
94
+ buffer << "ms #{key} #{value.bytesize} c"
95
+ buffer << ' b' if base64
96
+ buffer << " F#{bitflags}" if bitflags
97
+ buffer << " T#{ttl}" if ttl
98
+ buffer << ' MS q' << TERMINATOR << value << TERMINATOR
99
+ end
100
+ buffer << META_NOOP
101
+ end
102
+
70
103
  # Thundering herd protection flag:
71
104
  # - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
72
105
  # using N/R flags will see the X flag and know the item is being regenerated.
73
- def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false, stale: false)
74
- cmd = "md #{key}"
75
- cmd << ' b' if base64
106
+ def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false)
107
+ cmd = "md #{encoded_key(key)}"
76
108
  cmd << cas_string(cas)
77
- cmd << " T#{ttl}" if ttl
109
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
78
110
  cmd << ' I' if stale # Mark stale instead of deleting
79
111
  cmd << ' q' if quiet
80
112
  cmd << TERMINATOR
81
113
  end
82
114
 
83
- def self.meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, base64: false, quiet: false)
84
- cmd = "ma #{key} v"
85
- cmd << ' b' if base64
86
- cmd << " D#{delta}" if delta
87
- cmd << " J#{initial}" if initial
115
+ def multi_meta_delete(keys)
116
+ buffer = ''.b
117
+ keys.each do |key|
118
+ buffer << 'md ' << encoded_key(key) << ' q' << TERMINATOR
119
+ end
120
+ buffer << META_NOOP
121
+ end
122
+
123
+ def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false)
124
+ cmd = "ma #{encoded_key(key)} v"
125
+ cmd << " D#{integer_flag(:delta, delta)}" if delta
126
+ cmd << " J#{integer_flag(:initial, initial)}" if initial
88
127
  # Always set a TTL if an initial value is specified
89
- cmd << " N#{ttl || 0}" if ttl || initial
128
+ cmd << " N#{ttl ? integer_flag(:ttl, ttl) : 0}" if ttl || initial
90
129
  cmd << cas_string(cas)
91
130
  cmd << ' q' if quiet
92
131
  cmd << " M#{incr ? 'I' : 'D'}"
@@ -97,15 +136,15 @@ module Dalli
97
136
  # rubocop:enable Metrics/PerceivedComplexity
98
137
 
99
138
  META_NOOP = "mn#{TERMINATOR}".freeze
100
- def self.meta_noop
139
+ def meta_noop
101
140
  META_NOOP
102
141
  end
103
142
 
104
- def self.version
143
+ def version
105
144
  "version#{TERMINATOR}"
106
145
  end
107
146
 
108
- def self.flush(delay: nil, quiet: false)
147
+ def flush(delay: nil, quiet: false)
109
148
  cmd = +'flush_all'
110
149
  cmd << " #{parse_to_64_bit_int(delay, 0)}" if delay
111
150
  cmd << ' noreply' if quiet
@@ -114,7 +153,7 @@ module Dalli
114
153
 
115
154
  ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
116
155
 
117
- def self.stats(arg = nil)
156
+ def stats(arg = nil)
118
157
  raise ArgumentError, "Invalid stats argument: #{arg.inspect}" unless ALLOWED_STATS_ARGS.include?(arg)
119
158
 
120
159
  cmd = +'stats'
@@ -122,7 +161,29 @@ module Dalli
122
161
  cmd << TERMINATOR
123
162
  end
124
163
 
125
- def self.mode_to_token(mode)
164
+ def encoded_key(key)
165
+ if KeyRegularizer.required?(key)
166
+ KeyRegularizer.encode(key) << ' b'
167
+ else
168
+ key
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ # Numeric flag values are written straight into the command line, so a
175
+ # value that isn't an integer -- e.g. a String carrying CRLF -- would let
176
+ # the caller inject further memcached commands (GHSA-6wmv-xq9m-fmp7).
177
+ # Converting to Integer means only digits ever reach the wire. Strings
178
+ # are parsed as base 10 so "010" means 10, as memcached would read it,
179
+ # rather than octal 8.
180
+ def integer_flag(name, value)
181
+ value.is_a?(String) ? Integer(value, 10) : Integer(value)
182
+ rescue ArgumentError, TypeError, FloatDomainError
183
+ raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
184
+ end
185
+
186
+ def mode_to_token(mode)
126
187
  case mode
127
188
  when :add
128
189
  'E'
@@ -137,12 +198,12 @@ module Dalli
137
198
  end
138
199
  end
139
200
 
140
- def self.cas_string(cas)
201
+ def cas_string(cas)
141
202
  cas = parse_to_64_bit_int(cas, nil)
142
203
  cas.nil? || cas.zero? ? '' : " C#{cas}"
143
204
  end
144
205
 
145
- def self.parse_to_64_bit_int(val, default)
206
+ def parse_to_64_bit_int(val, default)
146
207
  val.nil? ? nil : Integer(val)
147
208
  rescue ArgumentError
148
209
  # Sanitize to default if it isn't parsable as an integer
@@ -5,16 +5,16 @@ require 'timeout'
5
5
 
6
6
  module Dalli
7
7
  module Protocol
8
+ # Compact the buffer when the consumed portion exceeds this
9
+ # threshold and represents more than half the buffer
10
+ COMPACT_THRESHOLD = 4096
11
+
8
12
  ##
9
13
  # Manages the buffer for responses from memcached.
10
14
  # Uses an offset-based approach to avoid string allocations
11
15
  # when advancing through parsed responses.
12
16
  ##
13
17
  class ResponseBuffer
14
- # Compact the buffer when the consumed portion exceeds this
15
- # threshold and represents more than half the buffer
16
- COMPACT_THRESHOLD = 4096
17
-
18
18
  def initialize(io_source, response_processor)
19
19
  @io_source = io_source
20
20
  @response_processor = response_processor
@@ -23,22 +23,32 @@ module Dalli
23
23
  end
24
24
 
25
25
  def read
26
- @buffer << @io_source.read_nonblock
26
+ remaining_bytes = @buffer.bytesize - @offset
27
+ if remaining_bytes.zero?
28
+ @offset = 0
29
+ @buffer = @io_source.read_available(@buffer)
30
+ else
31
+ if @offset > COMPACT_THRESHOLD && @offset > (@buffer.bytesize / 2)
32
+ @buffer.bytesplice(0, @offset, '')
33
+ @offset = 0
34
+ end
35
+ @buffer << @io_source.read_available
36
+ end
27
37
  end
28
38
 
29
39
  # Attempts to process a single response from the buffer,
30
40
  # advancing the offset past the consumed bytes.
31
41
  def process_single_getk_response
32
- bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer, @offset)
33
- @offset += bytes
34
- compact_if_needed
35
- [status, cas, key, value]
42
+ response = @response_processor.getk_response_from_buffer(@buffer, @offset)
43
+ @offset += response.pop
44
+ response
36
45
  end
37
46
 
38
47
  # Resets the internal buffer to an empty state,
39
48
  # so that we're ready to read pipelined responses
40
49
  def reset
41
- @buffer = ''.b
50
+ @buffer&.clear
51
+ @buffer ||= ''.b
42
52
  @offset = 0
43
53
  end
44
54
 
@@ -54,6 +64,7 @@ module Dalli
54
64
 
55
65
  # Clear the internal response buffer
56
66
  def clear
67
+ @buffer&.clear
57
68
  @buffer = nil
58
69
  @offset = 0
59
70
  end
@@ -61,18 +72,6 @@ module Dalli
61
72
  def in_progress?
62
73
  !@buffer.nil?
63
74
  end
64
-
65
- private
66
-
67
- # Only compact when we've consumed a significant portion of the buffer.
68
- # This avoids per-response string allocation while preventing unbounded
69
- # memory growth for large pipelines.
70
- def compact_if_needed
71
- return unless @offset > COMPACT_THRESHOLD && @offset > @buffer.bytesize / 2
72
-
73
- @buffer = @buffer.byteslice(@offset..)
74
- @offset = 0
75
- end
76
75
  end
77
76
  end
78
77
  end
@@ -157,9 +157,26 @@ module Dalli
157
157
  true
158
158
  end
159
159
 
160
+ # Consumes the responses to a batch of quiet (pipelined) delete
161
+ # requests, which are terminated by a noop (MN). In quiet mode
162
+ # memcached suppresses the success response for each deleted key, so
163
+ # every line received before the terminator corresponds to a key that
164
+ # was NOT deleted -- a miss (NF) or an error. Returns that count so
165
+ # callers can derive the number of successful deletes as
166
+ # (keys_sent - non_deletions).
167
+ def pipelined_delete_non_deletions
168
+ non_deletions = 0
169
+ tokens = next_line_to_tokens
170
+ until tokens.first == MN
171
+ non_deletions += 1
172
+ tokens = next_line_to_tokens
173
+ end
174
+ non_deletions
175
+ end
176
+
160
177
  def full_response_from_buffer(tokens, body, resp_size)
161
178
  value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
162
- [resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
179
+ [tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value, resp_size]
163
180
  end
164
181
 
165
182
  ##
@@ -174,24 +191,26 @@ module Dalli
174
191
  ##
175
192
  def getk_response_from_buffer(buf, offset = 0)
176
193
  # Find the header terminator starting from offset
177
- term_idx = buf.index(TERMINATOR, offset)
178
- return [0, nil, nil, nil, nil] unless term_idx
194
+ term_idx = buf.byteindex(TERMINATOR, offset)
195
+ return [0] unless term_idx
179
196
 
180
197
  header = buf.byteslice(offset, term_idx - offset)
181
198
  tokens = header.split
182
199
  header_len = header.bytesize + TERMINATOR.length
200
+
201
+ # The body len is removed from the tokens array
183
202
  body_len = body_len_from_tokens(tokens)
184
203
 
185
204
  # We have a complete response that has no body.
186
205
  # This is either the response to the terminating
187
206
  # noop or, if the status is not MN, an intermediate
188
207
  # error response that needs to be discarded.
189
- return [header_len, true, nil, nil, nil] if body_len.zero?
208
+ return [true, header_len] if body_len.zero?
190
209
 
191
210
  resp_size = header_len + body_len + TERMINATOR.length
192
211
  # The header is in the buffer, but the body is not. As we don't have
193
212
  # a complete response, don't advance the buffer
194
- return [0, nil, nil, nil, nil] unless buf.bytesize >= offset + resp_size
213
+ return [0] unless buf.bytesize >= offset + resp_size
195
214
 
196
215
  # The full response is in our buffer, so parse it and return
197
216
  # the values
@@ -210,17 +229,20 @@ module Dalli
210
229
  end
211
230
 
212
231
  def bitflags_from_tokens(tokens)
213
- value_from_tokens(tokens, 'f')&.to_i
232
+ value_from_tokens(tokens, 'f').to_i
214
233
  end
215
234
 
216
235
  def cas_from_tokens(tokens)
217
- value_from_tokens(tokens, 'c')&.to_i
236
+ value_from_tokens(tokens, 'c').to_i
218
237
  end
219
238
 
220
239
  def key_from_tokens(tokens)
221
240
  encoded_key = value_from_tokens(tokens, 'k')
222
- base64_encoded = tokens.any?('b')
223
- KeyRegularizer.decode(encoded_key, base64_encoded)
241
+ if tokens.delete('b')
242
+ KeyRegularizer.decode(encoded_key)
243
+ else
244
+ encoded_key
245
+ end
224
246
  end
225
247
 
226
248
  # Returns true if item was previously hit, false if first access, nil if not requested
@@ -235,18 +257,22 @@ module Dalli
235
257
  # Returns seconds since last access, or nil if not requested
236
258
  # The l flag returns l<seconds>
237
259
  def last_access_from_tokens(tokens)
238
- value_from_tokens(tokens, 'l')&.to_i
260
+ value_from_tokens(tokens, 'l').to_i
239
261
  end
240
262
 
241
263
  def body_len_from_tokens(tokens)
242
- value_from_tokens(tokens, 's')&.to_i
264
+ value_from_tokens(tokens, 's').to_i
243
265
  end
244
266
 
245
267
  def value_from_tokens(tokens, flag)
246
- bitflags_token = tokens.find { |t| t.start_with?(flag) }
247
- return 0 unless bitflags_token
248
-
249
- bitflags_token[1..]
268
+ # NB: as an optimization, we're mutating the matching token in place
269
+ # so there is a baked assumption that we're only accessing each token at most once.
270
+ index = tokens.find_index { |t| t.start_with?(flag) }
271
+ if index
272
+ tokens.delete_at(index).delete_prefix!(flag)
273
+ else
274
+ 0
275
+ end
250
276
  end
251
277
 
252
278
  def read_line