dalli 3.0.6 → 3.1.3
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 +4 -4
- data/History.md +29 -0
- data/lib/dalli/client.rb +163 -261
- data/lib/dalli/options.rb +3 -3
- data/lib/dalli/pipelined_getter.rb +177 -0
- data/lib/dalli/protocol/base.rb +238 -0
- data/lib/dalli/protocol/binary/request_formatter.rb +11 -3
- data/lib/dalli/protocol/binary/response_header.rb +36 -0
- data/lib/dalli/protocol/binary/response_processor.rb +101 -42
- data/lib/dalli/protocol/binary/sasl_authentication.rb +1 -1
- data/lib/dalli/protocol/binary.rb +81 -445
- data/lib/dalli/protocol/connection_manager.rb +242 -0
- data/lib/dalli/protocol/response_buffer.rb +53 -0
- data/lib/dalli/protocol/server_config_parser.rb +7 -7
- data/lib/dalli/ring.rb +5 -1
- data/lib/dalli/socket.rb +8 -6
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +9 -0
- data/lib/rack/session/dalli.rb +83 -74
- metadata +15 -4
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dalli
|
4
|
+
##
|
5
|
+
# Contains logic for the pipelined gets implemented by the client.
|
6
|
+
##
|
7
|
+
class PipelinedGetter
|
8
|
+
def initialize(ring, key_manager)
|
9
|
+
@ring = ring
|
10
|
+
@key_manager = key_manager
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Yields, one at a time, keys and their values+attributes.
|
15
|
+
#
|
16
|
+
def process(keys, &block)
|
17
|
+
return {} if keys.empty?
|
18
|
+
|
19
|
+
@ring.lock do
|
20
|
+
servers = setup_requests(keys)
|
21
|
+
start_time = Time.now
|
22
|
+
servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block) until servers.empty?
|
23
|
+
end
|
24
|
+
rescue NetworkError => e
|
25
|
+
Dalli.logger.debug { e.inspect }
|
26
|
+
Dalli.logger.debug { 'retrying pipelined gets because of timeout' }
|
27
|
+
retry
|
28
|
+
end
|
29
|
+
|
30
|
+
def setup_requests(keys)
|
31
|
+
groups = groups_for_keys(keys)
|
32
|
+
make_getkq_requests(groups)
|
33
|
+
|
34
|
+
# TODO: How does this exit on a NetworkError
|
35
|
+
finish_queries(groups.keys)
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Loop through the server-grouped sets of keys, writing
|
40
|
+
# the corresponding getkq requests to the appropriate servers
|
41
|
+
#
|
42
|
+
# It's worth noting that we could potentially reduce bytes
|
43
|
+
# on the wire by switching from getkq to getq, and using
|
44
|
+
# the opaque value to match requests to responses.
|
45
|
+
##
|
46
|
+
def make_getkq_requests(groups)
|
47
|
+
groups.each do |server, keys_for_server|
|
48
|
+
server.request(:pipelined_get, keys_for_server)
|
49
|
+
rescue DalliError, NetworkError => e
|
50
|
+
Dalli.logger.debug { e.inspect }
|
51
|
+
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# This loops through the servers that have keys in
|
57
|
+
# our set, sending the noop to terminate the set of queries.
|
58
|
+
##
|
59
|
+
def finish_queries(servers)
|
60
|
+
deleted = []
|
61
|
+
|
62
|
+
servers.each do |server|
|
63
|
+
next unless server.alive?
|
64
|
+
|
65
|
+
begin
|
66
|
+
finish_query_for_server(server)
|
67
|
+
rescue Dalli::NetworkError
|
68
|
+
raise
|
69
|
+
rescue Dalli::DalliError
|
70
|
+
deleted.append(server)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
servers.delete_if { |server| deleted.include?(server) }
|
75
|
+
rescue Dalli::NetworkError
|
76
|
+
abort_without_timeout(servers)
|
77
|
+
raise
|
78
|
+
end
|
79
|
+
|
80
|
+
def finish_query_for_server(server)
|
81
|
+
server.pipeline_response_setup
|
82
|
+
rescue Dalli::NetworkError
|
83
|
+
raise
|
84
|
+
rescue Dalli::DalliError => e
|
85
|
+
Dalli.logger.debug { e.inspect }
|
86
|
+
Dalli.logger.debug { "Results from server: #{server.name} will be missing from the results" }
|
87
|
+
raise
|
88
|
+
end
|
89
|
+
|
90
|
+
# Swallows Dalli::NetworkError
|
91
|
+
def abort_without_timeout(servers)
|
92
|
+
servers.each(&:pipeline_abort)
|
93
|
+
end
|
94
|
+
|
95
|
+
def fetch_responses(servers, start_time, timeout, &block)
|
96
|
+
# Remove any servers which are not connected
|
97
|
+
servers.delete_if { |s| !s.connected? }
|
98
|
+
return [] if servers.empty?
|
99
|
+
|
100
|
+
time_left = remaining_time(start_time, timeout)
|
101
|
+
readable_servers = servers_with_response(servers, time_left)
|
102
|
+
if readable_servers.empty?
|
103
|
+
abort_with_timeout(servers)
|
104
|
+
return []
|
105
|
+
end
|
106
|
+
|
107
|
+
# Loop through the servers with responses, and
|
108
|
+
# delete any from our list that are finished
|
109
|
+
readable_servers.each do |server|
|
110
|
+
servers.delete(server) if process_server(server, &block)
|
111
|
+
end
|
112
|
+
servers
|
113
|
+
rescue NetworkError
|
114
|
+
# Abort and raise if we encountered a network error. This triggers
|
115
|
+
# a retry at the top level.
|
116
|
+
abort_without_timeout(servers)
|
117
|
+
raise
|
118
|
+
end
|
119
|
+
|
120
|
+
def remaining_time(start, timeout)
|
121
|
+
elapsed = Time.now - start
|
122
|
+
return 0 if elapsed > timeout
|
123
|
+
|
124
|
+
timeout - elapsed
|
125
|
+
end
|
126
|
+
|
127
|
+
# Swallows Dalli::NetworkError
|
128
|
+
def abort_with_timeout(servers)
|
129
|
+
abort_without_timeout(servers)
|
130
|
+
servers.each do |server|
|
131
|
+
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
132
|
+
end
|
133
|
+
|
134
|
+
true # Required to simplify caller
|
135
|
+
end
|
136
|
+
|
137
|
+
# Processes responses from a server. Returns true if there are no
|
138
|
+
# additional responses from this server.
|
139
|
+
def process_server(server)
|
140
|
+
server.pipeline_next_responses.each_pair do |key, value_list|
|
141
|
+
yield @key_manager.key_without_namespace(key), value_list
|
142
|
+
end
|
143
|
+
|
144
|
+
server.pipeline_complete?
|
145
|
+
end
|
146
|
+
|
147
|
+
def servers_with_response(servers, timeout)
|
148
|
+
return [] if servers.empty?
|
149
|
+
|
150
|
+
# TODO: - This is a bit challenging. Essentially the PipelinedGetter
|
151
|
+
# is a reactor, but without the benefit of a Fiber or separate thread.
|
152
|
+
# My suspicion is that we may want to try and push this down into the
|
153
|
+
# individual servers, but I'm not sure. For now, we keep the
|
154
|
+
# mapping between the alerted object (the socket) and the
|
155
|
+
# corrresponding server here.
|
156
|
+
server_map = servers.each_with_object({}) { |s, h| h[s.sock] = s }
|
157
|
+
|
158
|
+
readable, = IO.select(server_map.keys, nil, nil, timeout)
|
159
|
+
return [] if readable.nil?
|
160
|
+
|
161
|
+
readable.map { |sock| server_map[sock] }
|
162
|
+
end
|
163
|
+
|
164
|
+
def groups_for_keys(*keys)
|
165
|
+
keys.flatten!
|
166
|
+
keys.map! { |a| @key_manager.validate_key(a.to_s) }
|
167
|
+
groups = @ring.keys_grouped_by_server(keys)
|
168
|
+
if (unfound_keys = groups.delete(nil))
|
169
|
+
Dalli.logger.debug do
|
170
|
+
"unable to get keys for #{unfound_keys.length} keys "\
|
171
|
+
'because no matching server was found'
|
172
|
+
end
|
173
|
+
end
|
174
|
+
groups
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'socket'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
module Dalli
|
8
|
+
module Protocol
|
9
|
+
##
|
10
|
+
# Base class for a single Memcached server, containing logic common to all
|
11
|
+
# protocols. Contains logic for managing connection state to the server and value
|
12
|
+
# handling.
|
13
|
+
##
|
14
|
+
class Base
|
15
|
+
extend Forwardable
|
16
|
+
|
17
|
+
attr_accessor :weight, :options
|
18
|
+
|
19
|
+
def_delegators :@value_marshaller, :serializer, :compressor, :compression_min_size, :compress_by_default?
|
20
|
+
def_delegators :@connection_manager, :name, :sock, :hostname, :port, :close, :connected?, :socket_timeout,
|
21
|
+
:socket_type, :up!, :down!, :write, :reconnect_down_server?, :raise_down_error
|
22
|
+
|
23
|
+
def initialize(attribs, client_options = {})
|
24
|
+
hostname, port, socket_type, @weight, user_creds = ServerConfigParser.parse(attribs)
|
25
|
+
@options = client_options.merge(user_creds)
|
26
|
+
@value_marshaller = ValueMarshaller.new(@options)
|
27
|
+
@connection_manager = ConnectionManager.new(hostname, port, socket_type, @options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Chokepoint method for error handling and ensuring liveness
|
31
|
+
def request(opkey, *args)
|
32
|
+
verify_state(opkey)
|
33
|
+
|
34
|
+
begin
|
35
|
+
send(opkey, *args)
|
36
|
+
rescue Dalli::MarshalError => e
|
37
|
+
log_marshal_err(args.first, e)
|
38
|
+
raise
|
39
|
+
rescue Dalli::DalliError
|
40
|
+
raise
|
41
|
+
rescue StandardError => e
|
42
|
+
log_unexpected_err(e)
|
43
|
+
down!
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Boolean method used by clients of this class to determine if this
|
49
|
+
# particular memcached instance is available for use.
|
50
|
+
def alive?
|
51
|
+
ensure_connected!
|
52
|
+
rescue Dalli::NetworkError
|
53
|
+
# ensure_connected! raises a NetworkError if connection fails. We
|
54
|
+
# want to capture that error and convert it to a boolean value here.
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def lock!; end
|
59
|
+
|
60
|
+
def unlock!; end
|
61
|
+
|
62
|
+
# Start reading key/value pairs from this connection. This is usually called
|
63
|
+
# after a series of GETKQ commands. A NOOP is sent, and the server begins
|
64
|
+
# flushing responses for kv pairs that were found.
|
65
|
+
#
|
66
|
+
# Returns nothing.
|
67
|
+
def pipeline_response_setup
|
68
|
+
verify_state(:getkq)
|
69
|
+
write_noop
|
70
|
+
response_buffer.reset
|
71
|
+
@connection_manager.start_request!
|
72
|
+
end
|
73
|
+
|
74
|
+
# Attempt to receive and parse as many key/value pairs as possible
|
75
|
+
# from this server. After #pipeline_response_setup, this should be invoked
|
76
|
+
# repeatedly whenever this server's socket is readable until
|
77
|
+
# #pipeline_complete?.
|
78
|
+
#
|
79
|
+
# Returns a Hash of kv pairs received.
|
80
|
+
def pipeline_next_responses
|
81
|
+
reconnect_on_pipeline_complete!
|
82
|
+
values = {}
|
83
|
+
|
84
|
+
response_buffer.read
|
85
|
+
|
86
|
+
resp_header, key, value = pipeline_response
|
87
|
+
# resp_header is not nil only if we have a full response to parse
|
88
|
+
# in the buffer
|
89
|
+
while resp_header
|
90
|
+
# If the status is ok and key is nil, then this is the response
|
91
|
+
# to the noop at the end of the pipeline
|
92
|
+
finish_pipeline && break if resp_header.ok? && key.nil?
|
93
|
+
|
94
|
+
# If the status is ok and the key is not nil, then this is a
|
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?
|
97
|
+
|
98
|
+
# Get the next response from the buffer
|
99
|
+
resp_header, key, value = pipeline_response
|
100
|
+
end
|
101
|
+
|
102
|
+
values
|
103
|
+
rescue SystemCallError, Timeout::Error, EOFError => e
|
104
|
+
@connection_manager.error_on_request!(e)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Abort current pipelined get. Generally used to signal an external
|
108
|
+
# timeout during pipelined get. The underlying socket is
|
109
|
+
# disconnected, and the exception is swallowed.
|
110
|
+
#
|
111
|
+
# Returns nothing.
|
112
|
+
def pipeline_abort
|
113
|
+
response_buffer.clear
|
114
|
+
@connection_manager.abort_request!
|
115
|
+
return true unless connected?
|
116
|
+
|
117
|
+
# Closes the connection, which ensures that our connection
|
118
|
+
# is in a clean state for future requests
|
119
|
+
@connection_manager.error_on_request!('External timeout')
|
120
|
+
rescue NetworkError
|
121
|
+
true
|
122
|
+
end
|
123
|
+
|
124
|
+
# Did the last call to #pipeline_response_setup complete successfully?
|
125
|
+
def pipeline_complete?
|
126
|
+
!response_buffer.in_progress?
|
127
|
+
end
|
128
|
+
|
129
|
+
def username
|
130
|
+
@options[:username] || ENV['MEMCACHE_USERNAME']
|
131
|
+
end
|
132
|
+
|
133
|
+
def password
|
134
|
+
@options[:password] || ENV['MEMCACHE_PASSWORD']
|
135
|
+
end
|
136
|
+
|
137
|
+
def require_auth?
|
138
|
+
!username.nil?
|
139
|
+
end
|
140
|
+
|
141
|
+
def quiet?
|
142
|
+
Thread.current[::Dalli::QUIET]
|
143
|
+
end
|
144
|
+
alias multi? quiet?
|
145
|
+
|
146
|
+
# NOTE: Additional public methods should be overridden in Dalli::Threadsafe
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
##
|
151
|
+
# Checks to see if we can execute the specified operation. Checks
|
152
|
+
# whether the connection is in use, and whether the command is allowed
|
153
|
+
##
|
154
|
+
def verify_state(opkey)
|
155
|
+
@connection_manager.confirm_ready!
|
156
|
+
verify_allowed_quiet!(opkey) if quiet?
|
157
|
+
|
158
|
+
# The ensure_connected call has the side effect of connecting the
|
159
|
+
# underlying socket if it is not connected, or there's been a disconnect
|
160
|
+
# because of timeout or other error. Method raises an error
|
161
|
+
# if it can't connect
|
162
|
+
raise_down_error unless ensure_connected!
|
163
|
+
end
|
164
|
+
|
165
|
+
# The socket connection to the underlying server is initialized as a side
|
166
|
+
# effect of this call. In fact, this is the ONLY place where that
|
167
|
+
# socket connection is initialized.
|
168
|
+
#
|
169
|
+
# Both this method and connect need to be in this class so we can do auth
|
170
|
+
# as required
|
171
|
+
#
|
172
|
+
# Since this is invoked exclusively in verify_state!, we don't need to worry about
|
173
|
+
# thread safety. Using it elsewhere may require revisiting that assumption.
|
174
|
+
def ensure_connected!
|
175
|
+
return true if connected?
|
176
|
+
return false unless reconnect_down_server?
|
177
|
+
|
178
|
+
connect # This call needs to be in this class so we can do auth
|
179
|
+
connected?
|
180
|
+
end
|
181
|
+
|
182
|
+
def cache_nils?(opts)
|
183
|
+
return false unless opts.is_a?(Hash)
|
184
|
+
|
185
|
+
opts[:cache_nils] ? true : false
|
186
|
+
end
|
187
|
+
|
188
|
+
def connect
|
189
|
+
@connection_manager.establish_connection
|
190
|
+
authenticate_connection if require_auth?
|
191
|
+
@version = version # Connect socket if not authed
|
192
|
+
up!
|
193
|
+
rescue Dalli::DalliError
|
194
|
+
raise
|
195
|
+
end
|
196
|
+
|
197
|
+
def pipelined_get(keys)
|
198
|
+
req = +''
|
199
|
+
keys.each do |key|
|
200
|
+
req << quiet_get_request(key)
|
201
|
+
end
|
202
|
+
# Could send noop here instead of in pipeline_response_setup
|
203
|
+
write(req)
|
204
|
+
end
|
205
|
+
|
206
|
+
def response_buffer
|
207
|
+
@response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
|
208
|
+
end
|
209
|
+
|
210
|
+
def pipeline_response
|
211
|
+
response_buffer.process_single_getk_response
|
212
|
+
end
|
213
|
+
|
214
|
+
# Called after the noop response is received at the end of a set
|
215
|
+
# of pipelined gets
|
216
|
+
def finish_pipeline
|
217
|
+
response_buffer.clear
|
218
|
+
@connection_manager.finish_request!
|
219
|
+
|
220
|
+
true # to simplify response
|
221
|
+
end
|
222
|
+
|
223
|
+
def reconnect_on_pipeline_complete!
|
224
|
+
@connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
|
225
|
+
end
|
226
|
+
|
227
|
+
def log_marshal_err(key, err)
|
228
|
+
Dalli.logger.error "Marshalling error for key '#{key}': #{err.message}"
|
229
|
+
Dalli.logger.error 'You are trying to cache a Ruby object which cannot be serialized to memcached.'
|
230
|
+
end
|
231
|
+
|
232
|
+
def log_unexpected_err(err)
|
233
|
+
Dalli.logger.error "Unexpected exception during Dalli request: #{err.class.name}: #{err.message}"
|
234
|
+
Dalli.logger.error err.backtrace.join("\n\t")
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -31,11 +31,14 @@ module Dalli
|
|
31
31
|
deleteq: 0x14,
|
32
32
|
incrq: 0x15,
|
33
33
|
decrq: 0x16,
|
34
|
+
flushq: 0x18,
|
35
|
+
appendq: 0x19,
|
36
|
+
prependq: 0x1A,
|
37
|
+
touch: 0x1C,
|
38
|
+
gat: 0x1D,
|
34
39
|
auth_negotiation: 0x20,
|
35
40
|
auth_request: 0x21,
|
36
|
-
auth_continue: 0x22
|
37
|
-
touch: 0x1C,
|
38
|
-
gat: 0x1D
|
41
|
+
auth_continue: 0x22
|
39
42
|
}.freeze
|
40
43
|
|
41
44
|
REQ_HEADER_FORMAT = 'CCnCCnNNQ'
|
@@ -56,6 +59,8 @@ module Dalli
|
|
56
59
|
|
57
60
|
append: KEY_AND_VALUE,
|
58
61
|
prepend: KEY_AND_VALUE,
|
62
|
+
appendq: KEY_AND_VALUE,
|
63
|
+
prependq: KEY_AND_VALUE,
|
59
64
|
auth_request: KEY_AND_VALUE,
|
60
65
|
auth_continue: KEY_AND_VALUE,
|
61
66
|
|
@@ -68,8 +73,11 @@ module Dalli
|
|
68
73
|
|
69
74
|
incr: INCR_DECR,
|
70
75
|
decr: INCR_DECR,
|
76
|
+
incrq: INCR_DECR,
|
77
|
+
decrq: INCR_DECR,
|
71
78
|
|
72
79
|
flush: TTL_ONLY,
|
80
|
+
flushq: TTL_ONLY,
|
73
81
|
|
74
82
|
noop: NO_BODY,
|
75
83
|
auth_negotiation: NO_BODY,
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dalli
|
4
|
+
module Protocol
|
5
|
+
class Binary
|
6
|
+
##
|
7
|
+
# Class that encapsulates data parsed from a memcached response header.
|
8
|
+
##
|
9
|
+
class ResponseHeader
|
10
|
+
SIZE = 24
|
11
|
+
FMT = '@2nCCnNNQ'
|
12
|
+
|
13
|
+
attr_reader :key_len, :extra_len, :data_type, :status, :body_len, :opaque, :cas
|
14
|
+
|
15
|
+
def initialize(buf)
|
16
|
+
raise ArgumentError, "Response buffer must be at least #{SIZE} bytes" unless buf.bytesize >= SIZE
|
17
|
+
|
18
|
+
@key_len, @extra_len, @data_type, @status, @body_len, @opaque, @cas = buf.unpack(FMT)
|
19
|
+
end
|
20
|
+
|
21
|
+
def ok?
|
22
|
+
status.zero?
|
23
|
+
end
|
24
|
+
|
25
|
+
def not_found?
|
26
|
+
status == 1
|
27
|
+
end
|
28
|
+
|
29
|
+
NOT_STORED_STATUSES = [2, 5].freeze
|
30
|
+
def not_stored?
|
31
|
+
NOT_STORED_STATUSES.include?(status)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -9,9 +9,6 @@ module Dalli
|
|
9
9
|
# and parsing into local values. Handles errors on unexpected values.
|
10
10
|
##
|
11
11
|
class ResponseProcessor
|
12
|
-
RESP_HEADER = '@2nCCnNNQ'
|
13
|
-
RESP_HEADER_SIZE = 24
|
14
|
-
|
15
12
|
# Response codes taken from:
|
16
13
|
# https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
|
17
14
|
RESPONSE_CODES = {
|
@@ -44,14 +41,9 @@ module Dalli
|
|
44
41
|
end
|
45
42
|
|
46
43
|
def read_response
|
47
|
-
|
48
|
-
body = read(body_len) if body_len.positive?
|
49
|
-
[
|
50
|
-
end
|
51
|
-
|
52
|
-
def unpack_header(header)
|
53
|
-
(key_len, extra_len, _, status, body_len, _, cas) = header.unpack(RESP_HEADER)
|
54
|
-
[status, extra_len, key_len, body_len, cas]
|
44
|
+
resp_header = ResponseHeader.new(read_header)
|
45
|
+
body = read(resp_header.body_len) if resp_header.body_len.positive?
|
46
|
+
[resp_header, body]
|
55
47
|
end
|
56
48
|
|
57
49
|
def unpack_response_body(extra_len, key_len, body, unpack)
|
@@ -63,45 +55,65 @@ module Dalli
|
|
63
55
|
end
|
64
56
|
|
65
57
|
def read_header
|
66
|
-
read(
|
58
|
+
read(ResponseHeader::SIZE) || raise(Dalli::NetworkError, 'No response')
|
67
59
|
end
|
68
60
|
|
69
|
-
def
|
70
|
-
|
61
|
+
def raise_on_not_ok!(resp_header)
|
62
|
+
return if resp_header.ok?
|
63
|
+
|
64
|
+
raise Dalli::DalliError, "Response error #{resp_header.status}: #{RESPONSE_CODES[resp_header.status]}"
|
71
65
|
end
|
72
66
|
|
73
|
-
|
74
|
-
|
75
|
-
|
67
|
+
def generic_response(unpack: false, cache_nils: false)
|
68
|
+
resp_header, body = read_response
|
69
|
+
|
70
|
+
return false if resp_header.not_stored? # Not stored, normal status for add operation
|
71
|
+
return cache_nils ? ::Dalli::NOT_FOUND : nil if resp_header.not_found?
|
72
|
+
|
73
|
+
raise_on_not_ok!(resp_header)
|
74
|
+
return true unless body
|
75
|
+
|
76
|
+
unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last
|
76
77
|
end
|
77
78
|
|
78
|
-
|
79
|
-
|
79
|
+
##
|
80
|
+
# Response for a storage operation. Returns the cas on success. False
|
81
|
+
# if the value wasn't stored. And raises an error on all other error
|
82
|
+
# codes from memcached.
|
83
|
+
##
|
84
|
+
def storage_response
|
85
|
+
resp_header, = read_response
|
86
|
+
return false if resp_header.not_stored? # Not stored, normal status for add operation
|
80
87
|
|
81
|
-
|
88
|
+
raise_on_not_ok!(resp_header)
|
89
|
+
resp_header.cas
|
82
90
|
end
|
83
91
|
|
84
|
-
def
|
85
|
-
|
92
|
+
def delete_response
|
93
|
+
resp_header, = read_response
|
94
|
+
return false if resp_header.not_found? || resp_header.not_stored?
|
86
95
|
|
87
|
-
|
88
|
-
|
96
|
+
raise_on_not_ok!(resp_header)
|
97
|
+
true
|
98
|
+
end
|
89
99
|
|
90
|
-
|
91
|
-
|
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
|
92
103
|
|
93
|
-
|
104
|
+
raise_on_not_ok!(resp_header)
|
105
|
+
true
|
94
106
|
end
|
95
107
|
|
96
|
-
def data_cas_response
|
97
|
-
|
98
|
-
return [nil, cas] if not_found?
|
99
|
-
return [nil, false] if not_stored?
|
108
|
+
def data_cas_response(unpack: true)
|
109
|
+
resp_header, body = read_response
|
110
|
+
return [nil, resp_header.cas] if resp_header.not_found?
|
111
|
+
return [nil, false] if resp_header.not_stored?
|
100
112
|
|
101
|
-
|
102
|
-
return [nil, cas] unless body
|
113
|
+
raise_on_not_ok!(resp_header)
|
114
|
+
return [nil, resp_header.cas] unless body
|
103
115
|
|
104
|
-
[unpack_response_body(extra_len, key_len, body,
|
116
|
+
[unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last, resp_header.cas]
|
105
117
|
end
|
106
118
|
|
107
119
|
def cas_response
|
@@ -111,17 +123,17 @@ module Dalli
|
|
111
123
|
def multi_with_keys_response
|
112
124
|
hash = {}
|
113
125
|
loop do
|
114
|
-
|
126
|
+
resp_header, body = read_response
|
115
127
|
# This is the response to the terminating noop / end of stat
|
116
|
-
return hash if
|
128
|
+
return hash if resp_header.ok? && resp_header.key_len.zero?
|
117
129
|
|
118
130
|
# Ignore any responses with non-zero status codes,
|
119
131
|
# such as errors from set operations. That allows
|
120
132
|
# this code to be used at the end of a multi
|
121
133
|
# block to clear any error responses from inside the multi.
|
122
|
-
next unless
|
134
|
+
next unless resp_header.ok?
|
123
135
|
|
124
|
-
key, value = unpack_response_body(extra_len, key_len, body, true)
|
136
|
+
key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
|
125
137
|
hash[key] = value
|
126
138
|
end
|
127
139
|
end
|
@@ -137,11 +149,58 @@ module Dalli
|
|
137
149
|
raise Dalli::NetworkError, "Unexpected message format: #{extra_len} #{count}"
|
138
150
|
end
|
139
151
|
|
140
|
-
def auth_response
|
141
|
-
|
142
|
-
|
152
|
+
def auth_response(buf = read_header)
|
153
|
+
resp_header = ResponseHeader.new(buf)
|
154
|
+
body_len = resp_header.body_len
|
155
|
+
validate_auth_format(resp_header.extra_len, body_len)
|
143
156
|
content = read(body_len) if body_len.positive?
|
144
|
-
[status, content]
|
157
|
+
[resp_header.status, content]
|
158
|
+
end
|
159
|
+
|
160
|
+
def contains_header?(buf)
|
161
|
+
return false unless buf
|
162
|
+
|
163
|
+
buf.bytesize >= ResponseHeader::SIZE
|
164
|
+
end
|
165
|
+
|
166
|
+
def response_header_from_buffer(buf)
|
167
|
+
header = buf.slice(0, ResponseHeader::SIZE)
|
168
|
+
ResponseHeader.new(header)
|
169
|
+
end
|
170
|
+
|
171
|
+
##
|
172
|
+
# This method returns an array of values used in a pipelined
|
173
|
+
# getk process. The first value is the number of bytes by
|
174
|
+
# which to advance the pointer in the buffer. If the
|
175
|
+
# complete response is found in the buffer, this will
|
176
|
+
# be the response size. Otherwise it is zero.
|
177
|
+
#
|
178
|
+
# The remaining three values in the array are the ResponseHeader,
|
179
|
+
# key, and value.
|
180
|
+
##
|
181
|
+
def getk_response_from_buffer(buf)
|
182
|
+
# There's no header in the buffer, so don't advance
|
183
|
+
return [0, nil, nil, nil] unless contains_header?(buf)
|
184
|
+
|
185
|
+
resp_header = response_header_from_buffer(buf)
|
186
|
+
body_len = resp_header.body_len
|
187
|
+
|
188
|
+
# We have a complete response that has no body.
|
189
|
+
# This is either the response to the terminating
|
190
|
+
# noop or, if the status is not zero, an intermediate
|
191
|
+
# error response that needs to be discarded.
|
192
|
+
return [ResponseHeader::SIZE, resp_header, nil, nil] if body_len.zero?
|
193
|
+
|
194
|
+
resp_size = ResponseHeader::SIZE + body_len
|
195
|
+
# The header is in the buffer, but the body is not. As we don't have
|
196
|
+
# a complete response, don't advance the buffer
|
197
|
+
return [0, nil, nil, nil] unless buf.bytesize >= resp_size
|
198
|
+
|
199
|
+
# The full response is in our buffer, so parse it and return
|
200
|
+
# 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]
|
145
204
|
end
|
146
205
|
end
|
147
206
|
end
|
@@ -10,7 +10,7 @@ module Dalli
|
|
10
10
|
def perform_auth_negotiation
|
11
11
|
write(RequestFormatter.standard_request(opkey: :auth_negotiation))
|
12
12
|
|
13
|
-
status, content =
|
13
|
+
status, content = response_processor.auth_response
|
14
14
|
return [status, []] if content.nil?
|
15
15
|
|
16
16
|
# Substitute spaces for the \x00 returned by
|