dalli 2.7.3 → 3.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/{History.md → CHANGELOG.md} +211 -0
- data/Gemfile +3 -6
- data/LICENSE +1 -1
- data/README.md +30 -208
- data/lib/dalli/cas/client.rb +2 -57
- data/lib/dalli/client.rb +254 -253
- data/lib/dalli/compressor.rb +13 -2
- data/lib/dalli/key_manager.rb +121 -0
- data/lib/dalli/options.rb +7 -7
- data/lib/dalli/pipelined_getter.rb +177 -0
- data/lib/dalli/protocol/base.rb +241 -0
- data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
- data/lib/dalli/protocol/binary/response_header.rb +36 -0
- data/lib/dalli/protocol/binary/response_processor.rb +239 -0
- data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
- data/lib/dalli/protocol/binary.rb +173 -0
- data/lib/dalli/protocol/connection_manager.rb +252 -0
- data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
- data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
- data/lib/dalli/protocol/meta/response_processor.rb +211 -0
- data/lib/dalli/protocol/meta.rb +178 -0
- data/lib/dalli/protocol/response_buffer.rb +54 -0
- data/lib/dalli/protocol/server_config_parser.rb +86 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
- data/lib/dalli/protocol/value_compressor.rb +85 -0
- data/lib/dalli/protocol/value_marshaller.rb +59 -0
- data/lib/dalli/protocol/value_serializer.rb +91 -0
- data/lib/dalli/protocol.rb +8 -0
- data/lib/dalli/ring.rb +97 -86
- data/lib/dalli/server.rb +4 -719
- data/lib/dalli/servers_arg_normalizer.rb +54 -0
- data/lib/dalli/socket.rb +123 -115
- data/lib/dalli/version.rb +5 -1
- data/lib/dalli.rb +45 -14
- data/lib/rack/session/dalli.rb +162 -42
- metadata +136 -63
- data/Performance.md +0 -42
- data/Rakefile +0 -43
- data/dalli.gemspec +0 -29
- data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -81
- data/lib/active_support/cache/dalli_store.rb +0 -372
- data/lib/dalli/railtie.rb +0 -7
- data/test/benchmark_test.rb +0 -243
- data/test/helper.rb +0 -56
- data/test/memcached_mock.rb +0 -201
- data/test/sasl/memcached.conf +0 -1
- data/test/sasl/sasldb +0 -1
- data/test/test_active_support.rb +0 -541
- data/test/test_cas_client.rb +0 -107
- data/test/test_compressor.rb +0 -52
- data/test/test_dalli.rb +0 -682
- data/test/test_encoding.rb +0 -32
- data/test/test_failover.rb +0 -137
- data/test/test_network.rb +0 -64
- data/test/test_rack_session.rb +0 -341
- data/test/test_ring.rb +0 -85
- data/test/test_sasl.rb +0 -105
- data/test/test_serializer.rb +0 -29
- data/test/test_server.rb +0 -110
@@ -0,0 +1,252 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
require 'socket'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
module Dalli
|
8
|
+
module Protocol
|
9
|
+
##
|
10
|
+
# Manages the socket connection to the server, including ensuring liveness
|
11
|
+
# and retries.
|
12
|
+
##
|
13
|
+
class ConnectionManager
|
14
|
+
DEFAULTS = {
|
15
|
+
# seconds between trying to contact a remote server
|
16
|
+
down_retry_delay: 30,
|
17
|
+
# connect/read/write timeout for socket operations
|
18
|
+
socket_timeout: 1,
|
19
|
+
# times a socket operation may fail before considering the server dead
|
20
|
+
socket_max_failures: 2,
|
21
|
+
# amount of time to sleep between retries when a failure occurs
|
22
|
+
socket_failure_delay: 0.1,
|
23
|
+
# Set keepalive
|
24
|
+
keepalive: true
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
attr_accessor :hostname, :port, :socket_type, :options
|
28
|
+
attr_reader :sock
|
29
|
+
|
30
|
+
def initialize(hostname, port, socket_type, client_options)
|
31
|
+
@hostname = hostname
|
32
|
+
@port = port
|
33
|
+
@socket_type = socket_type
|
34
|
+
@options = DEFAULTS.merge(client_options)
|
35
|
+
@request_in_progress = false
|
36
|
+
@sock = nil
|
37
|
+
@pid = nil
|
38
|
+
|
39
|
+
reset_down_info
|
40
|
+
end
|
41
|
+
|
42
|
+
def name
|
43
|
+
if socket_type == :unix
|
44
|
+
hostname
|
45
|
+
else
|
46
|
+
"#{hostname}:#{port}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def establish_connection
|
51
|
+
Dalli.logger.debug { "Dalli::Server#connect #{name}" }
|
52
|
+
|
53
|
+
@sock = memcached_socket
|
54
|
+
@pid = Process.pid
|
55
|
+
rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
|
56
|
+
# SocketError = DNS resolution failure
|
57
|
+
error_on_request!(e)
|
58
|
+
end
|
59
|
+
|
60
|
+
def reconnect_down_server?
|
61
|
+
return true unless @last_down_at
|
62
|
+
|
63
|
+
time_to_next_reconnect = @last_down_at + options[:down_retry_delay] - Time.now
|
64
|
+
return true unless time_to_next_reconnect.positive?
|
65
|
+
|
66
|
+
Dalli.logger.debug do
|
67
|
+
format('down_retry_delay not reached for %<name>s (%<time>.3f seconds left)', name: name,
|
68
|
+
time: time_to_next_reconnect)
|
69
|
+
end
|
70
|
+
false
|
71
|
+
end
|
72
|
+
|
73
|
+
def up!
|
74
|
+
log_up_detected
|
75
|
+
reset_down_info
|
76
|
+
end
|
77
|
+
|
78
|
+
# Marks the server instance as down. Updates the down_at state
|
79
|
+
# and raises an Dalli::NetworkError that includes the underlying
|
80
|
+
# error in the message. Calls close to clean up socket state
|
81
|
+
def down!
|
82
|
+
close
|
83
|
+
log_down_detected
|
84
|
+
|
85
|
+
@error = $ERROR_INFO&.class&.name
|
86
|
+
@msg ||= $ERROR_INFO&.message
|
87
|
+
raise_down_error
|
88
|
+
end
|
89
|
+
|
90
|
+
def raise_down_error
|
91
|
+
raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def socket_timeout
|
95
|
+
@socket_timeout ||= @options[:socket_timeout]
|
96
|
+
end
|
97
|
+
|
98
|
+
def confirm_ready!
|
99
|
+
error_on_request!(RuntimeError.new('Already writing to socket')) if request_in_progress?
|
100
|
+
close_on_fork if fork_detected?
|
101
|
+
end
|
102
|
+
|
103
|
+
def close
|
104
|
+
return unless @sock
|
105
|
+
|
106
|
+
begin
|
107
|
+
@sock.close
|
108
|
+
rescue StandardError
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
@sock = nil
|
112
|
+
@pid = nil
|
113
|
+
abort_request!
|
114
|
+
end
|
115
|
+
|
116
|
+
def connected?
|
117
|
+
!@sock.nil?
|
118
|
+
end
|
119
|
+
|
120
|
+
def request_in_progress?
|
121
|
+
@request_in_progress
|
122
|
+
end
|
123
|
+
|
124
|
+
def start_request!
|
125
|
+
@request_in_progress = true
|
126
|
+
end
|
127
|
+
|
128
|
+
def finish_request!
|
129
|
+
@request_in_progress = false
|
130
|
+
end
|
131
|
+
|
132
|
+
def abort_request!
|
133
|
+
@request_in_progress = false
|
134
|
+
end
|
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
|
+
|
146
|
+
def read(count)
|
147
|
+
start_request!
|
148
|
+
data = @sock.readfull(count)
|
149
|
+
finish_request!
|
150
|
+
data
|
151
|
+
rescue SystemCallError, Timeout::Error, EOFError => e
|
152
|
+
error_on_request!(e)
|
153
|
+
end
|
154
|
+
|
155
|
+
def write(bytes)
|
156
|
+
start_request!
|
157
|
+
result = @sock.write(bytes)
|
158
|
+
finish_request!
|
159
|
+
result
|
160
|
+
rescue SystemCallError, Timeout::Error => e
|
161
|
+
error_on_request!(e)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Non-blocking read. Should only be used in the context
|
165
|
+
# of a caller who has called start_request!, but not yet
|
166
|
+
# called finish_request!. Here to support the operation
|
167
|
+
# of the get_multi operation
|
168
|
+
def read_nonblock
|
169
|
+
@sock.read_available
|
170
|
+
end
|
171
|
+
|
172
|
+
def max_allowed_failures
|
173
|
+
@max_allowed_failures ||= @options[:socket_max_failures] || 2
|
174
|
+
end
|
175
|
+
|
176
|
+
def error_on_request!(err_or_string)
|
177
|
+
log_warn_message(err_or_string)
|
178
|
+
|
179
|
+
@fail_count += 1
|
180
|
+
if @fail_count >= max_allowed_failures
|
181
|
+
down!
|
182
|
+
else
|
183
|
+
# Closes the existing socket, setting up for a reconnect
|
184
|
+
# on next request
|
185
|
+
reconnect!('Socket operation failed, retrying...')
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def reconnect!(message)
|
190
|
+
close
|
191
|
+
sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
|
192
|
+
raise Dalli::NetworkError, message
|
193
|
+
end
|
194
|
+
|
195
|
+
def reset_down_info
|
196
|
+
@fail_count = 0
|
197
|
+
@down_at = nil
|
198
|
+
@last_down_at = nil
|
199
|
+
@msg = nil
|
200
|
+
@error = nil
|
201
|
+
end
|
202
|
+
|
203
|
+
def memcached_socket
|
204
|
+
if socket_type == :unix
|
205
|
+
Dalli::Socket::UNIX.open(hostname, options)
|
206
|
+
else
|
207
|
+
Dalli::Socket::TCP.open(hostname, port, options)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def log_warn_message(err_or_string)
|
212
|
+
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
213
|
+
Dalli.logger.warn do
|
214
|
+
detail = err_or_string.is_a?(String) ? err_or_string : "#{err_or_string.class}: #{err_or_string.message}"
|
215
|
+
"#{name} failed (count: #{@fail_count}) #{detail}"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def close_on_fork
|
220
|
+
message = 'Fork detected, re-connecting child process...'
|
221
|
+
Dalli.logger.info { message }
|
222
|
+
# Close socket on a fork, setting us up for reconnect
|
223
|
+
# on next request.
|
224
|
+
close
|
225
|
+
raise Dalli::NetworkError, message
|
226
|
+
end
|
227
|
+
|
228
|
+
def fork_detected?
|
229
|
+
@pid && @pid != Process.pid
|
230
|
+
end
|
231
|
+
|
232
|
+
def log_down_detected
|
233
|
+
@last_down_at = Time.now
|
234
|
+
|
235
|
+
if @down_at
|
236
|
+
time = Time.now - @down_at
|
237
|
+
Dalli.logger.debug { format('%<name>s is still down (for %<time>.3f seconds now)', name: name, time: time) }
|
238
|
+
else
|
239
|
+
@down_at = @last_down_at
|
240
|
+
Dalli.logger.warn("#{name} is down")
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def log_up_detected
|
245
|
+
return unless @down_at
|
246
|
+
|
247
|
+
time = Time.now - @down_at
|
248
|
+
Dalli.logger.warn { format('%<name>s is back (downtime was %<time>.3f seconds)', name: name, time: time) }
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
@@ -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,121 @@
|
|
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 << cas_string(cas)
|
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 << cas_string(cas)
|
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
|
+
# Always set a TTL if an initial value is specified
|
58
|
+
cmd << " N#{ttl || 0}" if ttl || initial
|
59
|
+
cmd << cas_string(cas)
|
60
|
+
cmd << ' q' if quiet
|
61
|
+
cmd << " M#{incr ? 'I' : 'D'}"
|
62
|
+
cmd + TERMINATOR
|
63
|
+
end
|
64
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
65
|
+
# rubocop:enable Metrics/MethodLength
|
66
|
+
# rubocop:enable Metrics/ParameterLists
|
67
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
68
|
+
|
69
|
+
def self.meta_noop
|
70
|
+
"mn#{TERMINATOR}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.version
|
74
|
+
"version#{TERMINATOR}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.flush(delay: nil, quiet: false)
|
78
|
+
cmd = +'flush_all'
|
79
|
+
cmd << " #{parse_to_64_bit_int(delay, 0)}" if delay
|
80
|
+
cmd << ' noreply' if quiet
|
81
|
+
cmd + TERMINATOR
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.stats(arg = nil)
|
85
|
+
cmd = +'stats'
|
86
|
+
cmd << " #{arg}" if arg
|
87
|
+
cmd + TERMINATOR
|
88
|
+
end
|
89
|
+
|
90
|
+
# rubocop:disable Metrics/MethodLength
|
91
|
+
def self.mode_to_token(mode)
|
92
|
+
case mode
|
93
|
+
when :add
|
94
|
+
'E'
|
95
|
+
when :replace
|
96
|
+
'R'
|
97
|
+
when :append
|
98
|
+
'A'
|
99
|
+
when :prepend
|
100
|
+
'P'
|
101
|
+
else
|
102
|
+
'S'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
# rubocop:enable Metrics/MethodLength
|
106
|
+
|
107
|
+
def self.cas_string(cas)
|
108
|
+
cas = parse_to_64_bit_int(cas, nil)
|
109
|
+
cas.nil? || cas.zero? ? '' : " C#{cas}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.parse_to_64_bit_int(val, default)
|
113
|
+
val.nil? ? nil : Integer(val)
|
114
|
+
rescue ArgumentError
|
115
|
+
# Sanitize to default if it isn't parsable as an integer
|
116
|
+
default
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
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..]
|
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
|