dalli 2.7.8 → 3.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/{History.md → CHANGELOG.md} +168 -0
  3. data/Gemfile +5 -1
  4. data/README.md +27 -223
  5. data/lib/dalli/cas/client.rb +1 -57
  6. data/lib/dalli/client.rb +227 -254
  7. data/lib/dalli/compressor.rb +12 -2
  8. data/lib/dalli/key_manager.rb +121 -0
  9. data/lib/dalli/options.rb +6 -7
  10. data/lib/dalli/pipelined_getter.rb +177 -0
  11. data/lib/dalli/protocol/base.rb +241 -0
  12. data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
  13. data/lib/dalli/protocol/binary/response_header.rb +36 -0
  14. data/lib/dalli/protocol/binary/response_processor.rb +239 -0
  15. data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
  16. data/lib/dalli/protocol/binary.rb +173 -0
  17. data/lib/dalli/protocol/connection_manager.rb +252 -0
  18. data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
  19. data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
  20. data/lib/dalli/protocol/meta/response_processor.rb +211 -0
  21. data/lib/dalli/protocol/meta.rb +178 -0
  22. data/lib/dalli/protocol/response_buffer.rb +54 -0
  23. data/lib/dalli/protocol/server_config_parser.rb +86 -0
  24. data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
  25. data/lib/dalli/protocol/value_compressor.rb +85 -0
  26. data/lib/dalli/protocol/value_marshaller.rb +59 -0
  27. data/lib/dalli/protocol/value_serializer.rb +91 -0
  28. data/lib/dalli/protocol.rb +8 -0
  29. data/lib/dalli/ring.rb +94 -83
  30. data/lib/dalli/server.rb +3 -746
  31. data/lib/dalli/servers_arg_normalizer.rb +54 -0
  32. data/lib/dalli/socket.rb +117 -137
  33. data/lib/dalli/version.rb +4 -1
  34. data/lib/dalli.rb +43 -15
  35. data/lib/rack/session/dalli.rb +103 -94
  36. metadata +65 -28
  37. data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -82
  38. data/lib/active_support/cache/dalli_store.rb +0 -429
  39. data/lib/dalli/railtie.rb +0 -8
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/md5'
4
+
5
+ module Dalli
6
+ ##
7
+ # This class manages and validates keys sent to Memcached, ensuring
8
+ # that they meet Memcached key length requirements, and supporting
9
+ # the implementation of optional namespaces on a per-Dalli client
10
+ # basis.
11
+ ##
12
+ class KeyManager
13
+ MAX_KEY_LENGTH = 250
14
+
15
+ NAMESPACE_SEPARATOR = ':'
16
+
17
+ # This is a hard coded md5 for historical reasons
18
+ TRUNCATED_KEY_SEPARATOR = ':md5:'
19
+
20
+ # This is 249 for historical reasons
21
+ TRUNCATED_KEY_TARGET_SIZE = 249
22
+
23
+ DEFAULTS = {
24
+ digest_class: ::Digest::MD5
25
+ }.freeze
26
+
27
+ OPTIONS = %i[digest_class namespace].freeze
28
+
29
+ attr_reader :namespace
30
+
31
+ def initialize(client_options)
32
+ @key_options =
33
+ DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
34
+ validate_digest_class_option(@key_options)
35
+
36
+ @namespace = namespace_from_options
37
+ end
38
+
39
+ ##
40
+ # Validates the key, and transforms as needed.
41
+ #
42
+ # If the key is nil or empty, raises ArgumentError. Whitespace
43
+ # characters are allowed for historical reasons, but likely shouldn't
44
+ # be used.
45
+ # If the key (with namespace) is shorter than the memcached maximum
46
+ # allowed key length, just returns the argument key
47
+ # Otherwise computes a "truncated" key that uses a truncated prefix
48
+ # combined with a 32-byte hex digest of the whole key.
49
+ ##
50
+ def validate_key(key)
51
+ raise ArgumentError, 'key cannot be blank' unless key&.length&.positive?
52
+
53
+ key = key_with_namespace(key)
54
+ key.length > MAX_KEY_LENGTH ? truncated_key(key) : key
55
+ end
56
+
57
+ ##
58
+ # Returns the key with the namespace prefixed, if a namespace is
59
+ # defined. Otherwise just returns the key
60
+ ##
61
+ def key_with_namespace(key)
62
+ return key if namespace.nil?
63
+
64
+ "#{evaluate_namespace}#{NAMESPACE_SEPARATOR}#{key}"
65
+ end
66
+
67
+ def key_without_namespace(key)
68
+ return key if namespace.nil?
69
+
70
+ key.sub(namespace_regexp, '')
71
+ end
72
+
73
+ def digest_class
74
+ @digest_class ||= @key_options[:digest_class]
75
+ end
76
+
77
+ def namespace_regexp
78
+ return /\A#{Regexp.escape(evaluate_namespace)}:/ if namespace.is_a?(Proc)
79
+
80
+ @namespace_regexp ||= /\A#{Regexp.escape(namespace)}:/.freeze unless namespace.nil?
81
+ end
82
+
83
+ def validate_digest_class_option(opts)
84
+ return if opts[:digest_class].respond_to?(:hexdigest)
85
+
86
+ raise ArgumentError, 'The digest_class object must respond to the hexdigest method'
87
+ end
88
+
89
+ def namespace_from_options
90
+ raw_namespace = @key_options[:namespace]
91
+ return nil unless raw_namespace
92
+ return raw_namespace.to_s unless raw_namespace.is_a?(Proc)
93
+
94
+ raw_namespace
95
+ end
96
+
97
+ def evaluate_namespace
98
+ return namespace.call.to_s if namespace.is_a?(Proc)
99
+
100
+ namespace
101
+ end
102
+
103
+ ##
104
+ # Produces a truncated key, if the raw key is longer than the maximum allowed
105
+ # length. The truncated key is produced by generating a hex digest
106
+ # of the key, and appending that to a truncated section of the key.
107
+ ##
108
+ def truncated_key(key)
109
+ digest = digest_class.hexdigest(key)
110
+ "#{key[0, prefix_length(digest)]}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
111
+ end
112
+
113
+ def prefix_length(digest)
114
+ return TRUNCATED_KEY_TARGET_SIZE - (TRUNCATED_KEY_SEPARATOR.length + digest.length) if namespace.nil?
115
+
116
+ # For historical reasons, truncated keys with namespaces had a length of 250 rather
117
+ # than 249
118
+ TRUNCATED_KEY_TARGET_SIZE + 1 - (TRUNCATED_KEY_SEPARATOR.length + digest.length)
119
+ end
120
+ end
121
+ end
data/lib/dalli/options.rb CHANGED
@@ -1,20 +1,19 @@
1
1
  # frozen_string_literal: true
2
- require 'thread'
2
+
3
3
  require 'monitor'
4
4
 
5
5
  module Dalli
6
-
7
6
  # Make Dalli threadsafe by using a lock around all
8
7
  # public server methods.
9
8
  #
10
- # Dalli::Server.extend(Dalli::Threadsafe)
9
+ # Dalli::Protocol::Binary.extend(Dalli::Threadsafe)
11
10
  #
12
11
  module Threadsafe
13
12
  def self.extended(obj)
14
13
  obj.init_threadsafe
15
14
  end
16
15
 
17
- def request(op, *args)
16
+ def request(opcode, *args)
18
17
  @lock.synchronize do
19
18
  super
20
19
  end
@@ -32,19 +31,19 @@ module Dalli
32
31
  end
33
32
  end
34
33
 
35
- def multi_response_start
34
+ def pipeline_response_setup
36
35
  @lock.synchronize do
37
36
  super
38
37
  end
39
38
  end
40
39
 
41
- def multi_response_nonblock
40
+ def pipeline_next_responses
42
41
  @lock.synchronize do
43
42
  super
44
43
  end
45
44
  end
46
45
 
47
- def multi_response_abort
46
+ def pipeline_abort
48
47
  @lock.synchronize do
49
48
  super
50
49
  end
@@ -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,241 @@
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
+ status, cas, key, value = response_buffer.process_single_getk_response
87
+ # status is not nil only if we have a full response to parse
88
+ # in the buffer
89
+ until status.nil?
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 status && 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, cas] unless key.nil?
97
+
98
+ # Get the next response from the buffer
99
+ status, cas, key, value = response_buffer.process_single_getk_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.fetch('MEMCACHE_USERNAME', nil)
131
+ end
132
+
133
+ def password
134
+ @options[:password] || ENV.fetch('MEMCACHE_PASSWORD', nil)
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
+ ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
151
+ def verify_allowed_quiet!(opkey)
152
+ return if ALLOWED_QUIET_OPS.include?(opkey)
153
+
154
+ raise Dalli::NotPermittedMultiOpError, "The operation #{opkey} is not allowed in a quiet block."
155
+ end
156
+
157
+ ##
158
+ # Checks to see if we can execute the specified operation. Checks
159
+ # whether the connection is in use, and whether the command is allowed
160
+ ##
161
+ def verify_state(opkey)
162
+ @connection_manager.confirm_ready!
163
+ verify_allowed_quiet!(opkey) if quiet?
164
+
165
+ # The ensure_connected call has the side effect of connecting the
166
+ # underlying socket if it is not connected, or there's been a disconnect
167
+ # because of timeout or other error. Method raises an error
168
+ # if it can't connect
169
+ raise_down_error unless ensure_connected!
170
+ end
171
+
172
+ # The socket connection to the underlying server is initialized as a side
173
+ # effect of this call. In fact, this is the ONLY place where that
174
+ # socket connection is initialized.
175
+ #
176
+ # Both this method and connect need to be in this class so we can do auth
177
+ # as required
178
+ #
179
+ # Since this is invoked exclusively in verify_state!, we don't need to worry about
180
+ # thread safety. Using it elsewhere may require revisiting that assumption.
181
+ def ensure_connected!
182
+ return true if connected?
183
+ return false unless reconnect_down_server?
184
+
185
+ connect # This call needs to be in this class so we can do auth
186
+ connected?
187
+ end
188
+
189
+ def cache_nils?(opts)
190
+ return false unless opts.is_a?(Hash)
191
+
192
+ opts[:cache_nils] ? true : false
193
+ end
194
+
195
+ def connect
196
+ @connection_manager.establish_connection
197
+ authenticate_connection if require_auth?
198
+ @version = version # Connect socket if not authed
199
+ up!
200
+ rescue Dalli::DalliError
201
+ raise
202
+ end
203
+
204
+ def pipelined_get(keys)
205
+ req = +''
206
+ keys.each do |key|
207
+ req << quiet_get_request(key)
208
+ end
209
+ # Could send noop here instead of in pipeline_response_setup
210
+ write(req)
211
+ end
212
+
213
+ def response_buffer
214
+ @response_buffer ||= ResponseBuffer.new(@connection_manager, response_processor)
215
+ end
216
+
217
+ # Called after the noop response is received at the end of a set
218
+ # of pipelined gets
219
+ def finish_pipeline
220
+ response_buffer.clear
221
+ @connection_manager.finish_request!
222
+
223
+ true # to simplify response
224
+ end
225
+
226
+ def reconnect_on_pipeline_complete!
227
+ @connection_manager.reconnect! 'pipelined get has completed' if pipeline_complete?
228
+ end
229
+
230
+ def log_marshal_err(key, err)
231
+ Dalli.logger.error "Marshalling error for key '#{key}': #{err.message}"
232
+ Dalli.logger.error 'You are trying to cache a Ruby object which cannot be serialized to memcached.'
233
+ end
234
+
235
+ def log_unexpected_err(err)
236
+ Dalli.logger.error "Unexpected exception during Dalli request: #{err.class.name}: #{err.message}"
237
+ Dalli.logger.error err.backtrace.join("\n\t")
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ module Protocol
5
+ class Binary
6
+ ##
7
+ # Class that encapsulates logic for formatting binary protocol requests
8
+ # to memcached.
9
+ ##
10
+ class RequestFormatter
11
+ REQUEST = 0x80
12
+
13
+ OPCODES = {
14
+ get: 0x00,
15
+ set: 0x01,
16
+ add: 0x02,
17
+ replace: 0x03,
18
+ delete: 0x04,
19
+ incr: 0x05,
20
+ decr: 0x06,
21
+ flush: 0x08,
22
+ noop: 0x0A,
23
+ version: 0x0B,
24
+ getkq: 0x0D,
25
+ append: 0x0E,
26
+ prepend: 0x0F,
27
+ stat: 0x10,
28
+ setq: 0x11,
29
+ addq: 0x12,
30
+ replaceq: 0x13,
31
+ deleteq: 0x14,
32
+ incrq: 0x15,
33
+ decrq: 0x16,
34
+ flushq: 0x18,
35
+ appendq: 0x19,
36
+ prependq: 0x1A,
37
+ touch: 0x1C,
38
+ gat: 0x1D,
39
+ auth_negotiation: 0x20,
40
+ auth_request: 0x21,
41
+ auth_continue: 0x22
42
+ }.freeze
43
+
44
+ REQ_HEADER_FORMAT = 'CCnCCnNNQ'
45
+
46
+ KEY_ONLY = 'a*'
47
+ TTL_AND_KEY = 'Na*'
48
+ KEY_AND_VALUE = 'a*a*'
49
+ INCR_DECR = 'NNNNNa*'
50
+ TTL_ONLY = 'N'
51
+ NO_BODY = ''
52
+
53
+ BODY_FORMATS = {
54
+ get: KEY_ONLY,
55
+ getkq: KEY_ONLY,
56
+ delete: KEY_ONLY,
57
+ deleteq: KEY_ONLY,
58
+ stat: KEY_ONLY,
59
+
60
+ append: KEY_AND_VALUE,
61
+ prepend: KEY_AND_VALUE,
62
+ appendq: KEY_AND_VALUE,
63
+ prependq: KEY_AND_VALUE,
64
+ auth_request: KEY_AND_VALUE,
65
+ auth_continue: KEY_AND_VALUE,
66
+
67
+ set: 'NNa*a*',
68
+ setq: 'NNa*a*',
69
+ add: 'NNa*a*',
70
+ addq: 'NNa*a*',
71
+ replace: 'NNa*a*',
72
+ replaceq: 'NNa*a*',
73
+
74
+ incr: INCR_DECR,
75
+ decr: INCR_DECR,
76
+ incrq: INCR_DECR,
77
+ decrq: INCR_DECR,
78
+
79
+ flush: TTL_ONLY,
80
+ flushq: TTL_ONLY,
81
+
82
+ noop: NO_BODY,
83
+ auth_negotiation: NO_BODY,
84
+ version: NO_BODY,
85
+
86
+ touch: TTL_AND_KEY,
87
+ gat: TTL_AND_KEY
88
+ }.freeze
89
+ FORMAT = BODY_FORMATS.transform_values { |v| REQ_HEADER_FORMAT + v; }
90
+
91
+ # rubocop:disable Metrics/ParameterLists
92
+ def self.standard_request(opkey:, key: nil, value: nil, opaque: 0, cas: 0, bitflags: nil, ttl: nil)
93
+ extra_len = (bitflags.nil? ? 0 : 4) + (ttl.nil? ? 0 : 4)
94
+ key_len = key.nil? ? 0 : key.bytesize
95
+ value_len = value.nil? ? 0 : value.bytesize
96
+ header = [REQUEST, OPCODES[opkey], key_len, extra_len, 0, 0, extra_len + key_len + value_len, opaque, cas]
97
+ body = [bitflags, ttl, key, value].compact
98
+ (header + body).pack(FORMAT[opkey])
99
+ end
100
+ # rubocop:enable Metrics/ParameterLists
101
+
102
+ def self.decr_incr_request(opkey:, key: nil, count: nil, initial: nil, expiry: nil)
103
+ extra_len = 20
104
+ (h, l) = as_8byte_uint(count)
105
+ (dh, dl) = as_8byte_uint(initial)
106
+ header = [REQUEST, OPCODES[opkey], key.bytesize, extra_len, 0, 0, key.bytesize + extra_len, 0, 0]
107
+ body = [h, l, dh, dl, expiry, key]
108
+ (header + body).pack(FORMAT[opkey])
109
+ end
110
+
111
+ def self.as_8byte_uint(val)
112
+ [val >> 32, 0xFFFFFFFF & val]
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end