dalli 3.2.8 → 4.3.3

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.
@@ -189,16 +189,6 @@ module Dalli
189
189
  [resp_header.status, content]
190
190
  end
191
191
 
192
- def contains_header?(buf)
193
- return false unless buf
194
-
195
- buf.bytesize >= ResponseHeader::SIZE
196
- end
197
-
198
- def response_header_from_buffer(buf)
199
- ResponseHeader.new(buf)
200
- end
201
-
202
192
  ##
203
193
  # This method returns an array of values used in a pipelined
204
194
  # getk process. The first value is the number of bytes by
@@ -209,11 +199,11 @@ module Dalli
209
199
  # The remaining three values in the array are the ResponseHeader,
210
200
  # key, and value.
211
201
  ##
212
- def getk_response_from_buffer(buf)
202
+ def getk_response_from_buffer(buf, offset = 0)
213
203
  # There's no header in the buffer, so don't advance
214
- return [0, nil, nil, nil, nil] unless contains_header?(buf)
204
+ return [0, nil, nil, nil, nil] unless buf && buf.bytesize >= offset + ResponseHeader::SIZE
215
205
 
216
- resp_header = response_header_from_buffer(buf)
206
+ resp_header = ResponseHeader.new(buf.byteslice(offset, ResponseHeader::SIZE))
217
207
  body_len = resp_header.body_len
218
208
 
219
209
  # We have a complete response that has no body.
@@ -225,11 +215,11 @@ module Dalli
225
215
  resp_size = ResponseHeader::SIZE + body_len
226
216
  # The header is in the buffer, but the body is not. As we don't have
227
217
  # a complete response, don't advance the buffer
228
- return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size
218
+ return [0, nil, nil, nil, nil] unless buf.bytesize >= offset + resp_size
229
219
 
230
220
  # The full response is in our buffer, so parse it and return
231
221
  # the values
232
- body = buf.byteslice(ResponseHeader::SIZE, body_len)
222
+ body = buf.byteslice(offset + ResponseHeader::SIZE, body_len)
233
223
  key, value = unpack_response_body(resp_header, body, true)
234
224
  [resp_size, resp_header.ok?, resp_header.cas, key, value]
235
225
  end
@@ -22,6 +22,7 @@ module Dalli
22
22
  def get(key, options = nil)
23
23
  req = RequestFormatter.standard_request(opkey: :get, key: key)
24
24
  write(req)
25
+ @connection_manager.flush
25
26
  response_processor.get(cache_nils: cache_nils?(options))
26
27
  end
27
28
 
@@ -33,12 +34,14 @@ module Dalli
33
34
  ttl = TtlSanitizer.sanitize(ttl)
34
35
  req = RequestFormatter.standard_request(opkey: :gat, key: key, ttl: ttl)
35
36
  write(req)
37
+ @connection_manager.flush
36
38
  response_processor.get(cache_nils: cache_nils?(options))
37
39
  end
38
40
 
39
41
  def touch(key, ttl)
40
42
  ttl = TtlSanitizer.sanitize(ttl)
41
43
  write(RequestFormatter.standard_request(opkey: :touch, key: key, ttl: ttl))
44
+ @connection_manager.flush
42
45
  response_processor.generic_response
43
46
  end
44
47
 
@@ -47,6 +50,7 @@ module Dalli
47
50
  def cas(key)
48
51
  req = RequestFormatter.standard_request(opkey: :get, key: key)
49
52
  write(req)
53
+ @connection_manager.flush
50
54
  response_processor.data_cas_response
51
55
  end
52
56
 
@@ -56,6 +60,12 @@ module Dalli
56
60
  storage_req(opkey, key, value, ttl, cas, options)
57
61
  end
58
62
 
63
+ # Pipelined set - writes a quiet set request without reading response.
64
+ # Used by PipelinedSetter for bulk operations.
65
+ def pipelined_set(key, value, ttl, options)
66
+ storage_req(:setq, key, value, ttl, 0, options)
67
+ end
68
+
59
69
  def add(key, value, ttl, options)
60
70
  opkey = quiet? ? :addq : :add
61
71
  storage_req(opkey, key, value, ttl, 0, options)
@@ -75,6 +85,7 @@ module Dalli
75
85
  value: value, bitflags: bitflags,
76
86
  ttl: ttl, cas: cas)
77
87
  write(req)
88
+ @connection_manager.flush unless quiet?
78
89
  response_processor.storage_response unless quiet?
79
90
  end
80
91
  # rubocop:enable Metrics/ParameterLists
@@ -91,6 +102,7 @@ module Dalli
91
102
 
92
103
  def write_append_prepend(opkey, key, value)
93
104
  write(RequestFormatter.standard_request(opkey: opkey, key: key, value: value))
105
+ @connection_manager.flush unless quiet?
94
106
  response_processor.no_body_response unless quiet?
95
107
  end
96
108
 
@@ -99,9 +111,17 @@ module Dalli
99
111
  opkey = quiet? ? :deleteq : :delete
100
112
  req = RequestFormatter.standard_request(opkey: opkey, key: key, cas: cas)
101
113
  write(req)
114
+ @connection_manager.flush unless quiet?
102
115
  response_processor.delete unless quiet?
103
116
  end
104
117
 
118
+ # Pipelined delete - writes a quiet delete request without reading response.
119
+ # Used by PipelinedDeleter for bulk operations.
120
+ def pipelined_delete(key)
121
+ req = RequestFormatter.standard_request(opkey: :deleteq, key: key, cas: 0)
122
+ write(req)
123
+ end
124
+
105
125
  # Arithmetic Commands
106
126
  def decr(key, count, ttl, initial)
107
127
  opkey = quiet? ? :decrq : :decr
@@ -119,12 +139,14 @@ module Dalli
119
139
  # if the key doesn't already exist, rather than
120
140
  # setting the initial value
121
141
  NOT_FOUND_EXPIRY = 0xFFFFFFFF
142
+ private_constant :NOT_FOUND_EXPIRY
122
143
 
123
144
  def decr_incr(opkey, key, count, ttl, initial)
124
145
  expiry = initial ? TtlSanitizer.sanitize(ttl) : NOT_FOUND_EXPIRY
125
146
  initial ||= 0
126
147
  write(RequestFormatter.decr_incr_request(opkey: opkey, key: key,
127
148
  count: count, initial: initial, expiry: expiry))
149
+ @connection_manager.flush unless quiet?
128
150
  response_processor.decr_incr unless quiet?
129
151
  end
130
152
 
@@ -132,6 +154,7 @@ module Dalli
132
154
  def flush(ttl = 0)
133
155
  opkey = quiet? ? :flushq : :flush
134
156
  write(RequestFormatter.standard_request(opkey: opkey, ttl: ttl))
157
+ @connection_manager.flush unless quiet?
135
158
  response_processor.no_body_response unless quiet?
136
159
  end
137
160
 
@@ -145,22 +168,26 @@ module Dalli
145
168
  def stats(info = '')
146
169
  req = RequestFormatter.standard_request(opkey: :stat, key: info)
147
170
  write(req)
171
+ @connection_manager.flush
148
172
  response_processor.stats
149
173
  end
150
174
 
151
175
  def reset_stats
152
176
  write(RequestFormatter.standard_request(opkey: :stat, key: 'reset'))
177
+ @connection_manager.flush
153
178
  response_processor.reset
154
179
  end
155
180
 
156
181
  def version
157
182
  write(RequestFormatter.standard_request(opkey: :version))
183
+ @connection_manager.flush
158
184
  response_processor.version
159
185
  end
160
186
 
161
187
  def write_noop
162
188
  req = RequestFormatter.standard_request(opkey: :noop)
163
189
  write(req)
190
+ @connection_manager.flush
164
191
  end
165
192
 
166
193
  require_relative 'binary/request_formatter'
@@ -53,6 +53,7 @@ module Dalli
53
53
  Dalli.logger.debug { "Dalli::Server#connect #{name}" }
54
54
 
55
55
  @sock = memcached_socket
56
+ @sock.sync = false # Enable buffered I/O for better performance
56
57
  @pid = PIDCache.pid
57
58
  @request_in_progress = false
58
59
  rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
@@ -100,13 +101,13 @@ module Dalli
100
101
 
101
102
  def confirm_ready!
102
103
  close if request_in_progress?
103
- close_on_fork if fork_detected?
104
+ reconnect_on_fork if fork_detected?
104
105
  end
105
106
 
106
107
  def confirm_in_progress!
107
108
  raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless request_in_progress?
108
109
 
109
- close_on_fork if fork_detected?
110
+ reconnect_on_fork if fork_detected?
110
111
  end
111
112
 
112
113
  def close
@@ -150,19 +151,25 @@ module Dalli
150
151
  data = @sock.gets("\r\n")
151
152
  error_on_request!('EOF in read_line') if data.nil?
152
153
  data
153
- rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
154
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
154
155
  error_on_request!(e)
155
156
  end
156
157
 
157
158
  def read(count)
158
159
  @sock.readfull(count)
159
- rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
160
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
160
161
  error_on_request!(e)
161
162
  end
162
163
 
163
164
  def write(bytes)
164
165
  @sock.write(bytes)
165
- rescue SystemCallError, *TIMEOUT_ERRORS => e
166
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
167
+ error_on_request!(e)
168
+ end
169
+
170
+ def flush
171
+ @sock.flush
172
+ rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS => e
166
173
  error_on_request!(e)
167
174
  end
168
175
 
@@ -192,7 +199,7 @@ module Dalli
192
199
  def reconnect!(message)
193
200
  close
194
201
  sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
195
- raise Dalli::NetworkError, message
202
+ raise Dalli::RetryableNetworkError, message
196
203
  end
197
204
 
198
205
  def reset_down_info
@@ -212,20 +219,18 @@ module Dalli
212
219
  end
213
220
 
214
221
  def log_warn_message(err_or_string)
215
- detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
216
222
  Dalli.logger.warn do
217
223
  detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
218
224
  "#{name} failed (count: #{@fail_count}) #{detail}"
219
225
  end
220
226
  end
221
227
 
222
- def close_on_fork
228
+ def reconnect_on_fork
223
229
  message = 'Fork detected, re-connecting child process...'
224
230
  Dalli.logger.info { message }
225
- # Close socket on a fork, setting us up for reconnect
226
- # on next request.
231
+ # Close socket on a fork and reconnect immediately
227
232
  close
228
- raise Dalli::NetworkError, message
233
+ establish_connection
229
234
  end
230
235
 
231
236
  def fork_detected?
@@ -10,7 +10,7 @@ module Dalli
10
10
  # memcached supports the use of base64 hashes for keys containing
11
11
  # whitespace or non-ASCII characters, provided the 'b' flag is included in the request.
12
12
  class KeyRegularizer
13
- WHITESPACE = /\s/.freeze
13
+ WHITESPACE = /\s/
14
14
 
15
15
  def self.encode(key)
16
16
  return [key, false] if key.ascii_only? && !WHITESPACE.match(key)
@@ -13,16 +13,45 @@ module Dalli
13
13
  # and introducing an intermediate object seems like overkill.
14
14
  #
15
15
  # rubocop:disable Metrics/CyclomaticComplexity
16
- # rubocop:disable Metrics/MethodLength
17
16
  # rubocop:disable Metrics/ParameterLists
18
17
  # rubocop:disable Metrics/PerceivedComplexity
19
- def self.meta_get(key:, value: true, return_cas: false, ttl: nil, base64: false, quiet: false)
18
+ #
19
+ # Meta get flags:
20
+ #
21
+ # Thundering herd protection:
22
+ # - vivify_ttl (N flag): On miss, create a stub item and return W flag. The TTL
23
+ # specifies how long the stub lives. Other clients see X (stale) and Z (lost race).
24
+ # - recache_ttl (R flag): If item's remaining TTL is below this threshold, return W
25
+ # flag to indicate this client should recache. Other clients get Z (lost race).
26
+ #
27
+ # Metadata flags:
28
+ # - return_hit_status (h flag): Return whether item has been hit before (0 or 1)
29
+ # - return_last_access (l flag): Return seconds since item was last accessed
30
+ # - skip_lru_bump (u flag): Don't bump item in LRU, don't update hit status or last access
31
+ #
32
+ # Response flags (parsed by response processor):
33
+ # - W: Client won the right to recache this item
34
+ # - X: Item is stale (another client is regenerating)
35
+ # - Z: Client lost the recache race (another client is already regenerating)
36
+ # - h0/h1: Hit status (0 = first access, 1 = previously accessed)
37
+ # - 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)
20
42
  cmd = "mg #{key}"
21
- cmd << ' v f' if value
43
+ # In raw mode (skip_flags: true), we don't request bitflags since they're not used.
44
+ # This saves 2 bytes per request and skips parsing on response.
45
+ cmd << (skip_flags ? ' v' : ' v f') if value
22
46
  cmd << ' c' if return_cas
23
47
  cmd << ' b' if base64
24
48
  cmd << " T#{ttl}" if ttl
25
49
  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
52
+ cmd << ' h' if return_hit_status # Return hit status (0 or 1)
53
+ cmd << ' l' if return_last_access # Return seconds since last access
54
+ cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
26
55
  cmd + TERMINATOR
27
56
  end
28
57
 
@@ -36,15 +65,17 @@ module Dalli
36
65
  cmd << " M#{mode_to_token(mode)}"
37
66
  cmd << ' q' if quiet
38
67
  cmd << TERMINATOR
39
- cmd << value
40
- cmd + TERMINATOR
41
68
  end
42
69
 
43
- def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false)
70
+ # Thundering herd protection flag:
71
+ # - stale (I flag): Instead of deleting the item, mark it as stale. Other clients
72
+ # 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)
44
74
  cmd = "md #{key}"
45
75
  cmd << ' b' if base64
46
76
  cmd << cas_string(cas)
47
77
  cmd << " T#{ttl}" if ttl
78
+ cmd << ' I' if stale # Mark stale instead of deleting
48
79
  cmd << ' q' if quiet
49
80
  cmd + TERMINATOR
50
81
  end
@@ -62,7 +93,6 @@ module Dalli
62
93
  cmd + TERMINATOR
63
94
  end
64
95
  # rubocop:enable Metrics/CyclomaticComplexity
65
- # rubocop:enable Metrics/MethodLength
66
96
  # rubocop:enable Metrics/ParameterLists
67
97
  # rubocop:enable Metrics/PerceivedComplexity
68
98
 
@@ -81,13 +111,16 @@ module Dalli
81
111
  cmd + TERMINATOR
82
112
  end
83
113
 
114
+ ALLOWED_STATS_ARGS = [nil, '', 'items', 'slabs', 'settings', 'reset'].freeze
115
+
84
116
  def self.stats(arg = nil)
117
+ raise ArgumentError, "Invalid stats argument: #{arg.inspect}" unless ALLOWED_STATS_ARGS.include?(arg)
118
+
85
119
  cmd = +'stats'
86
- cmd << " #{arg}" if arg
120
+ cmd << " #{arg}" if arg && !arg.empty?
87
121
  cmd + TERMINATOR
88
122
  end
89
123
 
90
- # rubocop:disable Metrics/MethodLength
91
124
  def self.mode_to_token(mode)
92
125
  case mode
93
126
  when :add
@@ -102,7 +135,6 @@ module Dalli
102
135
  'S'
103
136
  end
104
137
  end
105
- # rubocop:enable Metrics/MethodLength
106
138
 
107
139
  def self.cas_string(cas)
108
140
  cas = parse_to_64_bit_int(cas, nil)
@@ -21,6 +21,7 @@ module Dalli
21
21
  STAT = 'STAT'
22
22
  VA = 'VA'
23
23
  VERSION = 'VERSION'
24
+ SERVER_ERROR = 'SERVER_ERROR'
24
25
 
25
26
  def initialize(io_source, value_marshaller)
26
27
  @io_source = io_source
@@ -32,7 +33,7 @@ module Dalli
32
33
  return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
33
34
  return true unless tokens.first == VA
34
35
 
35
- @value_marshaller.retrieve(read_line, bitflags_from_tokens(tokens))
36
+ @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
36
37
  end
37
38
 
38
39
  def meta_get_with_value_and_cas
@@ -42,7 +43,7 @@ module Dalli
42
43
  cas = cas_from_tokens(tokens)
43
44
  return [nil, cas] unless tokens.first == VA
44
45
 
45
- [@value_marshaller.retrieve(read_line, bitflags_from_tokens(tokens)), cas]
46
+ [@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
46
47
  end
47
48
 
48
49
  def meta_get_without_value
@@ -50,6 +51,41 @@ module Dalli
50
51
  tokens.first == EN ? nil : true
51
52
  end
52
53
 
54
+ # Returns a hash with all requested metadata:
55
+ # - :value - the cached value (or nil if miss)
56
+ # - :cas - the CAS value (if return_cas was requested)
57
+ # - :won_recache - true if client won the right to recache (W flag)
58
+ # - :stale - true if the item is stale (X flag)
59
+ # - :lost_recache - true if another client is already recaching (Z flag)
60
+ # - :hit_before - true/false if item was previously accessed (h flag, if requested)
61
+ # - :last_access - seconds since last access (l flag, if requested)
62
+ #
63
+ # Used by meta_get for comprehensive metadata retrieval.
64
+ # Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
65
+ def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false)
66
+ tokens = error_on_unexpected!([VA, EN, HD])
67
+ result = build_metadata_result(tokens)
68
+ result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
69
+ result[:last_access] = last_access_from_tokens(tokens) if return_last_access
70
+ result[:value] = parse_value_from_tokens(tokens, cache_nils)
71
+ result
72
+ end
73
+
74
+ def build_metadata_result(tokens)
75
+ {
76
+ value: nil, cas: cas_from_tokens(tokens),
77
+ won_recache: tokens.include?('W'), stale: tokens.include?('X'),
78
+ lost_recache: tokens.include?('Z')
79
+ }
80
+ end
81
+
82
+ def parse_value_from_tokens(tokens, cache_nils)
83
+ return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
84
+ return unless tokens.first == VA
85
+
86
+ @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
87
+ end
88
+
53
89
  def meta_set_with_cas
54
90
  tokens = error_on_unexpected!([HD, NS, NF, EX])
55
91
  return false unless tokens.first == HD
@@ -111,14 +147,6 @@ module Dalli
111
147
  true
112
148
  end
113
149
 
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
150
  def full_response_from_buffer(tokens, body, resp_size)
123
151
  value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
124
152
  [resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
@@ -134,11 +162,15 @@ module Dalli
134
162
  # The remaining three values in the array are the ResponseHeader,
135
163
  # key, and value.
136
164
  ##
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)
165
+ def getk_response_from_buffer(buf, offset = 0)
166
+ # Find the header terminator starting from offset
167
+ term_idx = buf.index(TERMINATOR, offset)
168
+ return [0, nil, nil, nil, nil] unless term_idx
140
169
 
141
- tokens, header_len, body_len = tokens_from_header_buffer(buf)
170
+ header = buf.byteslice(offset, term_idx - offset)
171
+ tokens = header.split
172
+ header_len = header.bytesize + TERMINATOR.length
173
+ body_len = body_len_from_tokens(tokens)
142
174
 
143
175
  # We have a complete response that has no body.
144
176
  # This is either the response to the terminating
@@ -149,27 +181,22 @@ module Dalli
149
181
  resp_size = header_len + body_len + TERMINATOR.length
150
182
  # The header is in the buffer, but the body is not. As we don't have
151
183
  # a complete response, don't advance the buffer
152
- return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size
184
+ return [0, nil, nil, nil, nil] unless buf.bytesize >= offset + resp_size
153
185
 
154
186
  # The full response is in our buffer, so parse it and return
155
187
  # the values
156
- body = buf.slice(header_len, body_len)
188
+ body = buf.byteslice(offset + header_len, body_len)
157
189
  full_response_from_buffer(tokens, body, resp_size)
158
190
  end
159
191
 
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
192
  def error_on_unexpected!(expected_codes)
169
193
  tokens = next_line_to_tokens
170
- raise Dalli::DalliError, "Response error: #{tokens.first}" unless expected_codes.include?(tokens.first)
171
194
 
172
- tokens
195
+ return tokens if expected_codes.include?(tokens.first)
196
+
197
+ raise Dalli::ServerError, tokens.join(' ').to_s if tokens.first == SERVER_ERROR
198
+
199
+ raise Dalli::DalliError, "Response error: #{tokens.first}"
173
200
  end
174
201
 
175
202
  def bitflags_from_tokens(tokens)
@@ -186,6 +213,21 @@ module Dalli
186
213
  KeyRegularizer.decode(encoded_key, base64_encoded)
187
214
  end
188
215
 
216
+ # Returns true if item was previously hit, false if first access, nil if not requested
217
+ # The h flag returns h0 (first access) or h1 (previously accessed)
218
+ def hit_status_from_tokens(tokens)
219
+ hit_token = tokens.find { |t| t.start_with?('h') && t.length == 2 }
220
+ return nil unless hit_token
221
+
222
+ hit_token[1] == '1'
223
+ end
224
+
225
+ # Returns seconds since last access, or nil if not requested
226
+ # The l flag returns l<seconds>
227
+ def last_access_from_tokens(tokens)
228
+ value_from_tokens(tokens, 'l')&.to_i
229
+ end
230
+
189
231
  def body_len_from_tokens(tokens)
190
232
  value_from_tokens(tokens, 's')&.to_i
191
233
  end
@@ -205,6 +247,10 @@ module Dalli
205
247
  line = read_line
206
248
  line&.split || []
207
249
  end
250
+
251
+ def read_data(data_size)
252
+ @io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
253
+ end
208
254
  end
209
255
  end
210
256
  end