dalli 5.0.4 → 5.0.6

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,48 +24,38 @@ 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)
31
- write(req)
32
- @connection_manager.flush
29
+ req = RequestFormatter.meta_get(key: key, skip_flags: skip_flags)
30
+ flushed_write(req)
33
31
  response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
34
32
  end
35
33
 
36
34
  def quiet_get_request(key)
37
- encoded_key, base64 = KeyRegularizer.encode(key)
38
35
  # Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
39
- RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true,
40
- skip_flags: raw_mode?)
36
+ RequestFormatter.meta_get(key: key, return_cas: true, quiet: true, skip_flags: raw_mode?)
41
37
  end
42
38
 
43
39
  def gat(key, ttl, options = nil)
44
40
  ttl = TtlSanitizer.sanitize(ttl)
45
- encoded_key, base64 = KeyRegularizer.encode(key)
46
41
  skip_flags = raw_mode? || (options && options[:raw])
47
- req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64, skip_flags: skip_flags)
48
- write(req)
49
- @connection_manager.flush
42
+ req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags)
43
+ flushed_write(req)
50
44
  response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
51
45
  end
52
46
 
53
47
  def touch(key, ttl)
54
48
  ttl = TtlSanitizer.sanitize(ttl)
55
- encoded_key, base64 = KeyRegularizer.encode(key)
56
- req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
57
- write(req)
58
- @connection_manager.flush
49
+ req = RequestFormatter.meta_get(key: key, ttl: ttl, value: false)
50
+ flushed_write(req)
59
51
  response_processor.meta_get_without_value
60
52
  end
61
53
 
62
54
  # TODO: This is confusing, as there's a cas command in memcached
63
55
  # and this isn't it. Maybe rename? Maybe eliminate?
64
56
  def cas(key)
65
- encoded_key, base64 = KeyRegularizer.encode(key)
66
- req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
67
- write(req)
68
- @connection_manager.flush
57
+ req = RequestFormatter.meta_get(key: key, value: true, return_cas: true)
58
+ flushed_write(req)
69
59
  response_processor.meta_get_with_value_and_cas
70
60
  end
71
61
 
@@ -94,15 +84,13 @@ module Dalli
94
84
  # - :hit_before - true/false if previously accessed (only if return_hit_status: true)
95
85
  # - :last_access - seconds since last access (only if return_last_access: true)
96
86
  def meta_get(key, options = {})
97
- encoded_key, base64 = KeyRegularizer.encode(key)
98
87
  req = RequestFormatter.meta_get(
99
- key: encoded_key, value: true, return_cas: true, base64: base64,
88
+ key: key, value: true, return_cas: true,
100
89
  vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
101
90
  return_hit_status: options[:return_hit_status],
102
91
  return_last_access: options[:return_last_access], skip_lru_bump: options[:skip_lru_bump]
103
92
  )
104
- write(req)
105
- @connection_manager.flush
93
+ flushed_write(req)
106
94
  response_processor.meta_get_with_metadata(
107
95
  cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
108
96
  return_last_access: options[:return_last_access]
@@ -117,10 +105,8 @@ module Dalli
117
105
  # @param cas [Integer] optional CAS value for compare-and-swap
118
106
  # @return [Boolean] true if successful
119
107
  def delete_stale(key, cas = nil)
120
- encoded_key, base64 = KeyRegularizer.encode(key)
121
- req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
122
- write(req)
123
- @connection_manager.flush
108
+ req = RequestFormatter.meta_delete(key: key, cas: cas, stale: true)
109
+ flushed_write(req)
124
110
  response_processor.meta_delete
125
111
  end
126
112
 
@@ -146,17 +132,13 @@ module Dalli
146
132
  response_processor.meta_set_with_cas unless quiet?
147
133
  end
148
134
 
149
- # rubocop:disable Metrics/ParameterLists
150
135
  def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
151
136
  (value, bitflags) = @value_marshaller.store(key, raw_value, options)
152
137
  ttl = TtlSanitizer.sanitize(ttl) if ttl
153
- encoded_key, base64 = KeyRegularizer.encode(key)
154
- req = RequestFormatter.meta_set(key: encoded_key, value: value,
138
+ req = RequestFormatter.meta_set(key: key, value: value,
155
139
  bitflags: bitflags, cas: cas,
156
- ttl: ttl, mode: mode, quiet: quiet, base64: base64)
157
- write(req)
158
- write(value)
159
- write(TERMINATOR)
140
+ ttl: ttl, mode: mode, quiet: quiet)
141
+ write("#{req}#{value}#{TERMINATOR}")
160
142
  @connection_manager.flush unless quiet
161
143
  end
162
144
  # rubocop:enable Metrics/ParameterLists
@@ -174,21 +156,16 @@ module Dalli
174
156
  # rubocop:disable Metrics/ParameterLists
175
157
  def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, _options = {})
176
158
  ttl = TtlSanitizer.sanitize(ttl) if ttl
177
- encoded_key, base64 = KeyRegularizer.encode(key)
178
- req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
159
+ req = RequestFormatter.meta_set(key: key, value: value,
179
160
  cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
180
- write(req)
181
- write(value)
182
- write(TERMINATOR)
161
+ write("#{req}#{value}#{TERMINATOR}")
183
162
  @connection_manager.flush unless quiet?
184
163
  end
185
164
  # rubocop:enable Metrics/ParameterLists
186
165
 
187
166
  # Delete Commands
188
167
  def delete(key, cas)
189
- encoded_key, base64 = KeyRegularizer.encode(key)
190
- req = RequestFormatter.meta_delete(key: encoded_key, cas: cas,
191
- base64: base64, quiet: quiet?)
168
+ req = RequestFormatter.meta_delete(key: key, cas: cas, quiet: quiet?)
192
169
  write(req)
193
170
  @connection_manager.flush unless quiet?
194
171
  response_processor.meta_delete unless quiet?
@@ -197,11 +174,15 @@ module Dalli
197
174
  # Pipelined delete - writes a quiet delete request without reading response.
198
175
  # Used by PipelinedDeleter for bulk operations.
199
176
  def pipelined_delete(key)
200
- encoded_key, base64 = KeyRegularizer.encode(key)
201
- req = RequestFormatter.meta_delete(key: encoded_key, base64: base64, quiet: true)
177
+ req = RequestFormatter.meta_delete(key: key, quiet: true)
202
178
  write(req)
203
179
  end
204
180
 
181
+ def finish_pipelined_delete(sent)
182
+ write_noop
183
+ sent - response_processor.pipelined_delete_non_deletions
184
+ end
185
+
205
186
  # Arithmetic Commands
206
187
  def decr(key, count, ttl, initial)
207
188
  decr_incr false, key, count, ttl, initial
@@ -213,9 +194,8 @@ module Dalli
213
194
 
214
195
  def decr_incr(incr, key, delta, ttl, initial)
215
196
  ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
216
- encoded_key, base64 = KeyRegularizer.encode(key)
217
- write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
218
- quiet: quiet?, base64: base64))
197
+ write(RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial, incr: incr, ttl: ttl,
198
+ quiet: quiet?))
219
199
  @connection_manager.flush unless quiet?
220
200
  response_processor.decr_incr unless quiet?
221
201
  end
@@ -235,26 +215,22 @@ module Dalli
235
215
  end
236
216
 
237
217
  def stats(info = nil)
238
- write(RequestFormatter.stats(info))
239
- @connection_manager.flush
218
+ flushed_write(RequestFormatter.stats(info))
240
219
  response_processor.stats
241
220
  end
242
221
 
243
222
  def reset_stats
244
- write(RequestFormatter.stats('reset'))
245
- @connection_manager.flush
223
+ flushed_write(RequestFormatter.stats('reset'))
246
224
  response_processor.reset
247
225
  end
248
226
 
249
227
  def version
250
- write(RequestFormatter.version)
251
- @connection_manager.flush
228
+ flushed_write(RequestFormatter.version)
252
229
  response_processor.version
253
230
  end
254
231
 
255
232
  def write_noop
256
- write(RequestFormatter.meta_noop)
257
- @connection_manager.flush
233
+ flushed_write(RequestFormatter.meta_noop)
258
234
  end
259
235
 
260
236
  # Single-server fast path for get_multi. Inlines request formatting and
@@ -262,17 +238,9 @@ module Dalli
262
238
  # machinery (IO.select, response buffering, server grouping).
263
239
  def read_multi_req(keys)
264
240
  is_raw = raw_mode?
265
- # Inline request formatting — avoids RequestFormatter.meta_get overhead per key.
266
- # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
267
- # Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
268
- post_get = is_raw ? " v k q s\r\n" : " v f k q s\r\n"
269
- keys.each do |key|
270
- encoded_key, base64 = KeyRegularizer.encode(key)
271
- write(base64 ? "mg #{encoded_key} b#{post_get}" : "mg #{encoded_key}#{post_get}")
272
- end
273
- write("mn\r\n")
274
- @connection_manager.flush
275
-
241
+ buffer = RequestFormatter.multi_meta_get(keys, skip_flags: is_raw)
242
+ flushed_write(buffer)
243
+ buffer.clear
276
244
  read_multi_get_responses(is_raw)
277
245
  end
278
246
 
@@ -295,7 +263,8 @@ module Dalli
295
263
  raw_key = tokens[key_index]
296
264
  return unless raw_key
297
265
 
298
- key = KeyRegularizer.decode(raw_key[1..], tokens.include?('b'))
266
+ key = raw_key[1..]
267
+ key = KeyRegularizer.decode(key) if tokens.include?('b')
299
268
  bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
300
269
  [key, @value_marshaller.retrieve(value, bitflags)]
301
270
  end
@@ -304,33 +273,23 @@ module Dalli
304
273
  # minimize per-key overhead. Avoids PipelinedSetter server grouping.
305
274
  def write_multi_req(pairs, ttl, req_options)
306
275
  ttl = TtlSanitizer.sanitize(ttl) if ttl
307
- pairs.each do |key, raw_value|
308
- (value, bitflags) = @value_marshaller.store(key, raw_value, req_options)
309
- encoded_key, base64 = KeyRegularizer.encode(key)
310
- # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
311
- cmd = "ms #{encoded_key} #{value.bytesize} c"
312
- cmd << ' b' if base64
313
- cmd << " F#{bitflags}" if bitflags
314
- cmd << " T#{ttl}" if ttl
315
- cmd << " MS q\r\n"
316
- write(cmd)
317
- write(value)
318
- write(TERMINATOR)
276
+ entries = pairs.map do |key, raw_value|
277
+ [key, @value_marshaller.store(key, raw_value, req_options)]
319
278
  end
320
- write_noop
279
+
280
+ buffer = RequestFormatter.multi_meta_set(entries, ttl: ttl)
281
+ flushed_write(buffer)
282
+ buffer.clear
321
283
  response_processor.consume_all_responses_until_mn
322
284
  end
323
285
 
324
286
  # Single-server fast path for delete_multi. Writes all quiet delete requests
325
287
  # terminated by a noop, then consumes all responses.
326
288
  def delete_multi_req(keys)
327
- keys.each do |key|
328
- encoded_key, base64 = KeyRegularizer.encode(key)
329
- # Inline format: "md <key> [b] q\r\n"
330
- write(base64 ? "md #{encoded_key} b q\r\n" : "md #{encoded_key} q\r\n")
331
- end
332
- write_noop
333
- response_processor.consume_all_responses_until_mn
289
+ buffer = RequestFormatter.multi_meta_delete(keys)
290
+ flushed_write(buffer)
291
+ buffer.clear
292
+ keys.size - response_processor.pipelined_delete_non_deletions
334
293
  end
335
294
 
336
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,16 +34,15 @@ 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
46
  cmd << " T#{ttl}" if ttl
49
47
  cmd << ' k q s' if quiet # Return the key in the response if quiet
50
48
  cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
@@ -52,12 +50,27 @@ module Dalli
52
50
  cmd << ' h' if return_hit_status # Return hit status (0 or 1)
53
51
  cmd << ' l' if return_last_access # Return seconds since last access
54
52
  cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
55
- cmd + TERMINATOR
53
+ cmd << TERMINATOR
56
54
  end
57
55
 
58
- def self.meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, base64: false, quiet: false)
56
+ def multi_meta_get(keys, skip_flags: false)
57
+ # In raw mode: "mg <key> v k q s\r\n" (no f flag, key at index 2)
58
+ # Normal mode: "mg <key> v f k q s\r\n" (key at index 3)
59
+ post_get = skip_flags ? " v k q s\r\n" : " v f k q s\r\n"
60
+ buffer = ''.b
61
+ keys.each do |key|
62
+ buffer << 'mg ' << encoded_key(key) << post_get
63
+ end
64
+ buffer << 'mn' << TERMINATOR
65
+ end
66
+
67
+ def meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, quiet: false)
68
+ base64 = KeyRegularizer.required?(key)
69
+ key = KeyRegularizer.encode(key) if base64
59
70
  cmd = "ms #{key} #{value.bytesize}"
60
- cmd << ' c' unless %i[append prepend].include?(mode)
71
+ # Skip the cas-return flag in quiet mode: the response is suppressed,
72
+ # so requesting it only adds bytes to the request.
73
+ cmd << ' c' if !quiet && !%i[append prepend].include?(mode)
61
74
  cmd << ' b' if base64
62
75
  cmd << " F#{bitflags}" if bitflags
63
76
  cmd << cas_string(cas)
@@ -67,22 +80,46 @@ module Dalli
67
80
  cmd << TERMINATOR
68
81
  end
69
82
 
83
+ def multi_meta_set(entries, ttl: nil)
84
+ buffer = ''.b
85
+ entries.each do |key, pair|
86
+ value, bitflags = pair
87
+
88
+ base64 = KeyRegularizer.required?(key)
89
+ key = KeyRegularizer.encode(key) if base64
90
+
91
+ # Inline format: "ms <key> <size> c [b] F<flags> T<ttl> MS q\r\n"
92
+ buffer << "ms #{key} #{value.bytesize} c"
93
+ buffer << ' b' if base64
94
+ buffer << " F#{bitflags}" if bitflags
95
+ buffer << " T#{ttl}" if ttl
96
+ buffer << ' MS q' << TERMINATOR << value << TERMINATOR
97
+ end
98
+ buffer << META_NOOP
99
+ end
100
+
70
101
  # Thundering herd protection flag:
71
102
  # - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
72
103
  # 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
104
+ def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false)
105
+ cmd = "md #{encoded_key(key)}"
76
106
  cmd << cas_string(cas)
77
107
  cmd << " T#{ttl}" if ttl
78
108
  cmd << ' I' if stale # Mark stale instead of deleting
79
109
  cmd << ' q' if quiet
80
- cmd + TERMINATOR
110
+ cmd << TERMINATOR
81
111
  end
82
112
 
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
113
+ def multi_meta_delete(keys)
114
+ buffer = ''.b
115
+ keys.each do |key|
116
+ buffer << 'md ' << encoded_key(key) << ' q' << TERMINATOR
117
+ end
118
+ buffer << META_NOOP
119
+ end
120
+
121
+ def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false)
122
+ cmd = "ma #{encoded_key(key)} v"
86
123
  cmd << " D#{delta}" if delta
87
124
  cmd << " J#{initial}" if initial
88
125
  # Always set a TTL if an initial value is specified
@@ -90,38 +127,49 @@ module Dalli
90
127
  cmd << cas_string(cas)
91
128
  cmd << ' q' if quiet
92
129
  cmd << " M#{incr ? 'I' : 'D'}"
93
- cmd + TERMINATOR
130
+ cmd << TERMINATOR
94
131
  end
95
132
  # rubocop:enable Metrics/CyclomaticComplexity
96
133
  # rubocop:enable Metrics/ParameterLists
97
134
  # rubocop:enable Metrics/PerceivedComplexity
98
135
 
99
- def self.meta_noop
100
- "mn#{TERMINATOR}"
136
+ META_NOOP = "mn#{TERMINATOR}".freeze
137
+ def meta_noop
138
+ META_NOOP
101
139
  end
102
140
 
103
- def self.version
141
+ def version
104
142
  "version#{TERMINATOR}"
105
143
  end
106
144
 
107
- def self.flush(delay: nil, quiet: false)
145
+ def flush(delay: nil, quiet: false)
108
146
  cmd = +'flush_all'
109
147
  cmd << " #{parse_to_64_bit_int(delay, 0)}" if delay
110
148
  cmd << ' noreply' if quiet
111
- cmd + TERMINATOR
149
+ cmd << TERMINATOR
112
150
  end
113
151
 
114
152
  ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
115
153
 
116
- def self.stats(arg = nil)
154
+ def stats(arg = nil)
117
155
  raise ArgumentError, "Invalid stats argument: #{arg.inspect}" unless ALLOWED_STATS_ARGS.include?(arg)
118
156
 
119
157
  cmd = +'stats'
120
158
  cmd << " #{arg}" if arg && !arg.empty?
121
- cmd + TERMINATOR
159
+ cmd << TERMINATOR
160
+ end
161
+
162
+ def encoded_key(key)
163
+ if KeyRegularizer.required?(key)
164
+ KeyRegularizer.encode(key) << ' b'
165
+ else
166
+ key
167
+ end
122
168
  end
123
169
 
124
- def self.mode_to_token(mode)
170
+ private
171
+
172
+ def mode_to_token(mode)
125
173
  case mode
126
174
  when :add
127
175
  'E'
@@ -136,12 +184,12 @@ module Dalli
136
184
  end
137
185
  end
138
186
 
139
- def self.cas_string(cas)
187
+ def cas_string(cas)
140
188
  cas = parse_to_64_bit_int(cas, nil)
141
189
  cas.nil? || cas.zero? ? '' : " C#{cas}"
142
190
  end
143
191
 
144
- def self.parse_to_64_bit_int(val, default)
192
+ def parse_to_64_bit_int(val, default)
145
193
  val.nil? ? nil : Integer(val)
146
194
  rescue ArgumentError
147
195
  # 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