dalli 3.1.5 → 3.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a9612ab509bc2a6b7bd2560a7a10564183b3b9fbb2aa0655f339b30067407fa
4
- data.tar.gz: 510c3ed03a4e2b4ae4e7f8540eac19e7d28508341b768edc938f9886d5504fbe
3
+ metadata.gz: d9a374b5939ef874ce8067059c88b088c9129e7ab09ff891b1ad1ca3cf13c3ce
4
+ data.tar.gz: d5937c697bcd86a6d3d4207d328ce7e73c854b93a1d19e402630c3f80caec2ec
5
5
  SHA512:
6
- metadata.gz: 81e31363e781e948f66c3a143cd506162e4b51b4c67bb76a8cbdbfb2efdcdbd279216b24cdc077c3c2e4ad88b356ffec1a74f4428c556b05d17591b171b874d2
7
- data.tar.gz: ef45c25f8552e203751a51cb32172b5670b6b509ac45cbcc41b19a4f70688f0f50eb01b09af32c3faa05d643dd93acce2e748d8cedbba5dded585070b6ed7ec6
6
+ metadata.gz: 36010355440b325c1f8d5b922e882a8a8e199b8bd48dda92b1dcc4866f29e81b536ce41ff4896441c6c2c4f4e8e8faaf891ca6355e47de91733baf10bdfa9eee
7
+ data.tar.gz: 0c2b42e88960838ceef44c6c0f696b938925ade65da7c8791a07539ca36e18d6e874693dfde840a08e78ac9076f6810735a05d137c98d8c64387414f89d6f016
@@ -4,16 +4,66 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 3.2.6
8
+ ==========
9
+
10
+ - Rescue IO::TimeoutError raised by Ruby since 3.2.0 on blocking reads/writes (skaes)
11
+ - Fix rubydoc link (JuanitoFatas)
12
+
13
+ 3.2.5
14
+ ==========
15
+
16
+ - Better handle memcached requests being interrupted by Thread#raise or Thread#kill (byroot)
17
+ - Unexpected errors are no longer treated as `Dalli::NetworkError`, including errors raised by `Timeout.timeout` (byroot)
18
+
19
+ 3.2.4
20
+ ==========
21
+
22
+ - Cache PID calls for performance since glibc no longer caches in recent versions (byroot)
23
+ - Preallocate the read buffer in Socket#readfull (byroot)
24
+
25
+ 3.2.3
26
+ ==========
27
+
28
+ - Sanitize CAS inputs to ensure additional commands are not passed to memcached (xhzeem / petergoldstein)
29
+ - Sanitize input to flush command to ensure additional commands are not passed to memcached (xhzeem / petergoldstein)
30
+ - Namespaces passed as procs are now evaluated every time, as opposed to just on initialization (nrw505)
31
+ - Fix missing require of uri in ServerConfigParser (adam12)
32
+ - Fix link to the CHANGELOG.md file in README.md (rud)
33
+
34
+ 3.2.2
35
+ ==========
36
+
37
+ - Ensure apps are resilient against old session ids (kbrock)
38
+
39
+ 3.2.1
40
+ ==========
41
+
42
+ - Fix null replacement bug on some SASL-authenticated services (veritas1)
43
+
44
+ 3.2.0
45
+ ==========
46
+
47
+ - BREAKING CHANGE: Remove protocol_implementation client option (petergoldstein)
48
+ - Add protocol option with meta implementation (petergoldstein)
49
+
50
+ 3.1.6
51
+ ==========
52
+
53
+ - Fix bug with cas/cas! with "Not found" value (petergoldstein)
54
+ - Add Ruby 3.1 to CI (petergoldstein)
55
+ - Replace reject(&:nil?) with compact (petergoldstein)
56
+
7
57
  3.1.5
8
58
  ==========
9
59
 
10
- - Fix bug with get_cas key with "Not found" value [petergoldstein]
60
+ - Fix bug with get_cas key with "Not found" value (petergoldstein)
11
61
  - Replace should return nil, not raise error, on miss (petergoldstein)
12
62
 
13
63
  3.1.4
14
64
  ==========
15
65
 
16
- - Improve response parsing performance (casperisfine)
66
+ - Improve response parsing performance (byroot)
17
67
  - Reorganize binary protocol parsing a bit (petergoldstein)
18
68
  - Fix handling of non-ASCII keys in get_multi (petergoldstein)
19
69
 
@@ -100,6 +150,9 @@ Unreleased
100
150
  * The Rack session adapter has been refactored to remove support for thread-unsafe
101
151
  configurations. You will need to include the `connection_pool` gem in
102
152
  your Gemfile to ensure session operations are thread-safe.
153
+ * When using namespaces, the algorithm for calculating truncated keys was
154
+ changed. Non-truncated keys and truncated keys for the non-namespace
155
+ case were left unchanged.
103
156
 
104
157
  - Raise NetworkError when multi response gets into corrupt state (mervync, #783)
105
158
  - Validate servers argument (semaperepelitsa, petergoldstein, #776)
data/Gemfile CHANGED
@@ -4,13 +4,18 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
- group :test do
8
- gem 'minitest'
9
- gem 'rake'
7
+ group :development, :test do
8
+ gem 'connection_pool'
9
+ gem 'minitest', '~> 5'
10
+ gem 'rack', '~> 2.0', '>= 2.2.0'
11
+ gem 'rake', '~> 13.0'
10
12
  gem 'rubocop'
11
13
  gem 'rubocop-minitest'
12
14
  gem 'rubocop-performance'
13
15
  gem 'rubocop-rake'
14
- gem 'ruby-prof', platform: :mri
15
16
  gem 'simplecov'
16
17
  end
18
+
19
+ group :test do
20
+ gem 'ruby-prof', platform: :mri
21
+ end
data/README.md CHANGED
@@ -23,11 +23,17 @@ The name is a variant of Salvador Dali for his famous painting [The Persistence
23
23
  * [Announcements](https://github.com/petergoldstein/dalli/discussions/categories/announcements) - Announcements of interest to the Dalli community will be posted here.
24
24
  * [Bug Reports](https://github.com/petergoldstein/dalli/issues) - If you discover a problem with Dalli, please submit a bug report in the tracker.
25
25
  * [Forum](https://github.com/petergoldstein/dalli/discussions/categories/q-a) - If you have questions about Dalli, please post them here.
26
- * [Client API](https://www.rubydoc.info/github/petergoldstein/dalli/master/Dalli/Client) - Ruby documentation for the `Dalli::Client` API
26
+ * [Client API](https://www.rubydoc.info/gems/dalli) - Ruby documentation for the `Dalli::Client` API
27
+
28
+ ## Development
29
+
30
+ After checking out the repo, run `bin/setup` to install dependencies. You can run `bin/console` for an interactive prompt that will allow you to experiment.
31
+
32
+ To install this gem onto your local machine, run `bundle exec rake install`.
27
33
 
28
34
  ## Contributing
29
35
 
30
- If you have a fix you wish to provide, please fork the code, fix in your local project and then send a pull request on github. Please ensure that you include a test which verifies your fix and update `History.md` with a one sentence description of your fix so you get credit as a contributor.
36
+ If you have a fix you wish to provide, please fork the code, fix in your local project and then send a pull request on github. Please ensure that you include a test which verifies your fix and update the [changelog](CHANGELOG.md) with a one sentence description of your fix so you get credit as a contributor.
31
37
 
32
38
  ## Appreciation
33
39
 
data/lib/dalli/client.rb CHANGED
@@ -43,11 +43,11 @@ module Dalli
43
43
  # #fetch operations.
44
44
  # - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
45
45
  # useful for injecting a FIPS compliant hash object.
46
- # - :protocol_implementation - defaults to Dalli::Protocol::Binary which uses the binary protocol. Allows you to
47
- # pass an alternative implementation using another protocol.
46
+ # - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
47
+ # to communicate with memcached.
48
48
  #
49
49
  def initialize(servers = nil, options = {})
50
- @servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
50
+ @normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
51
51
  @options = normalize_options(options)
52
52
  @key_manager = ::Dalli::KeyManager.new(@options)
53
53
  @ring = nil
@@ -375,7 +375,6 @@ module Dalli
375
375
 
376
376
  def cas_core(key, always_set, ttl = nil, req_options = nil)
377
377
  (value, cas) = perform(:cas, key)
378
- value = nil if !value || value == 'Not found'
379
378
  return if value.nil? && !always_set
380
379
 
381
380
  newvalue = yield(value)
@@ -393,17 +392,16 @@ module Dalli
393
392
  end
394
393
 
395
394
  def ring
396
- # TODO: This server initialization should probably be pushed down
397
- # to the Ring
398
- @ring ||= Dalli::Ring.new(
399
- @servers.map do |s|
400
- protocol_implementation.new(s, @options)
401
- end, @options
402
- )
395
+ @ring ||= Dalli::Ring.new(@normalized_servers, protocol_implementation, @options)
403
396
  end
404
397
 
405
398
  def protocol_implementation
406
- @protocol_implementation ||= @options.fetch(:protocol_implementation, Dalli::Protocol::Binary)
399
+ @protocol_implementation ||= case @options[:protocol]&.to_s
400
+ when 'meta'
401
+ Dalli::Protocol::Meta
402
+ else
403
+ Dalli::Protocol::Binary
404
+ end
407
405
  end
408
406
 
409
407
  ##
@@ -61,7 +61,7 @@ module Dalli
61
61
  def key_with_namespace(key)
62
62
  return key if namespace.nil?
63
63
 
64
- "#{namespace}#{NAMESPACE_SEPARATOR}#{key}"
64
+ "#{evaluate_namespace}#{NAMESPACE_SEPARATOR}#{key}"
65
65
  end
66
66
 
67
67
  def key_without_namespace(key)
@@ -75,6 +75,8 @@ module Dalli
75
75
  end
76
76
 
77
77
  def namespace_regexp
78
+ return /\A#{Regexp.escape(evaluate_namespace)}:/ if namespace.is_a?(Proc)
79
+
78
80
  @namespace_regexp ||= /\A#{Regexp.escape(namespace)}:/.freeze unless namespace.nil?
79
81
  end
80
82
 
@@ -87,9 +89,15 @@ module Dalli
87
89
  def namespace_from_options
88
90
  raw_namespace = @key_options[:namespace]
89
91
  return nil unless raw_namespace
90
- return raw_namespace.call.to_s if raw_namespace.is_a?(Proc)
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)
91
99
 
92
- raw_namespace.to_s
100
+ namespace
93
101
  end
94
102
 
95
103
  ##
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ ##
5
+ # Dalli::PIDCache is a wrapper class for PID checking to avoid system calls when checking the PID.
6
+ ##
7
+ module PIDCache
8
+ if !Process.respond_to?(:fork) # JRuby or TruffleRuby
9
+ @pid = Process.pid
10
+ singleton_class.attr_reader(:pid)
11
+ elsif Process.respond_to?(:_fork) # Ruby 3.1+
12
+ class << self
13
+ attr_reader :pid
14
+
15
+ def update!
16
+ @pid = Process.pid
17
+ end
18
+ end
19
+ update!
20
+
21
+ ##
22
+ # Dalli::PIDCache::CoreExt hooks into Process to be able to reset the PID cache after fork
23
+ ##
24
+ module CoreExt
25
+ def _fork
26
+ child_pid = super
27
+ PIDCache.update! if child_pid.zero?
28
+ child_pid
29
+ end
30
+ end
31
+ Process.singleton_class.prepend(CoreExt)
32
+ else # Ruby 3.0 or older
33
+ class << self
34
+ def pid
35
+ Process.pid
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -167,7 +167,7 @@ module Dalli
167
167
  groups = @ring.keys_grouped_by_server(keys)
168
168
  if (unfound_keys = groups.delete(nil))
169
169
  Dalli.logger.debug do
170
- "unable to get keys for #{unfound_keys.length} keys "\
170
+ "unable to get keys for #{unfound_keys.length} keys " \
171
171
  'because no matching server was found'
172
172
  end
173
173
  end
@@ -32,7 +32,13 @@ module Dalli
32
32
  verify_state(opkey)
33
33
 
34
34
  begin
35
- send(opkey, *args)
35
+ @connection_manager.start_request!
36
+ response = send(opkey, *args)
37
+
38
+ # pipelined_get emit query but doesn't read the response(s)
39
+ @connection_manager.finish_request! unless opkey == :pipelined_get
40
+
41
+ response
36
42
  rescue Dalli::MarshalError => e
37
43
  log_marshal_err(args.first, e)
38
44
  raise
@@ -40,7 +46,8 @@ module Dalli
40
46
  raise
41
47
  rescue StandardError => e
42
48
  log_unexpected_err(e)
43
- down!
49
+ close
50
+ raise
44
51
  end
45
52
  end
46
53
 
@@ -65,10 +72,9 @@ module Dalli
65
72
  #
66
73
  # Returns nothing.
67
74
  def pipeline_response_setup
68
- verify_state(:getkq)
75
+ verify_pipelined_state(:getkq)
69
76
  write_noop
70
77
  response_buffer.reset
71
- @connection_manager.start_request!
72
78
  end
73
79
 
74
80
  # Attempt to receive and parse as many key/value pairs as possible
@@ -100,7 +106,7 @@ module Dalli
100
106
  end
101
107
 
102
108
  values
103
- rescue SystemCallError, Timeout::Error, EOFError => e
109
+ rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
104
110
  @connection_manager.error_on_request!(e)
105
111
  end
106
112
 
@@ -127,11 +133,11 @@ module Dalli
127
133
  end
128
134
 
129
135
  def username
130
- @options[:username] || ENV['MEMCACHE_USERNAME']
136
+ @options[:username] || ENV.fetch('MEMCACHE_USERNAME', nil)
131
137
  end
132
138
 
133
139
  def password
134
- @options[:password] || ENV['MEMCACHE_PASSWORD']
140
+ @options[:password] || ENV.fetch('MEMCACHE_PASSWORD', nil)
135
141
  end
136
142
 
137
143
  def require_auth?
@@ -147,6 +153,13 @@ module Dalli
147
153
 
148
154
  private
149
155
 
156
+ ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
157
+ def verify_allowed_quiet!(opkey)
158
+ return if ALLOWED_QUIET_OPS.include?(opkey)
159
+
160
+ raise Dalli::NotPermittedMultiOpError, "The operation #{opkey} is not allowed in a quiet block."
161
+ end
162
+
150
163
  ##
151
164
  # Checks to see if we can execute the specified operation. Checks
152
165
  # whether the connection is in use, and whether the command is allowed
@@ -162,6 +175,11 @@ module Dalli
162
175
  raise_down_error unless ensure_connected!
163
176
  end
164
177
 
178
+ def verify_pipelined_state(_opkey)
179
+ @connection_manager.confirm_in_progress!
180
+ raise_down_error unless connected?
181
+ end
182
+
165
183
  # The socket connection to the underlying server is initialized as a side
166
184
  # effect of this call. In fact, this is the ONLY place where that
167
185
  # socket connection is initialized.
@@ -190,8 +208,6 @@ module Dalli
190
208
  authenticate_connection if require_auth?
191
209
  @version = version # Connect socket if not authed
192
210
  up!
193
- rescue Dalli::DalliError
194
- raise
195
211
  end
196
212
 
197
213
  def pipelined_get(keys)
@@ -86,7 +86,7 @@ module Dalli
86
86
  touch: TTL_AND_KEY,
87
87
  gat: TTL_AND_KEY
88
88
  }.freeze
89
- FORMAT = BODY_FORMATS.transform_values { |v| REQ_HEADER_FORMAT + v; }
89
+ FORMAT = BODY_FORMATS.transform_values { |v| REQ_HEADER_FORMAT + v }
90
90
 
91
91
  # rubocop:disable Metrics/ParameterLists
92
92
  def self.standard_request(opkey:, key: nil, value: nil, opaque: 0, cas: 0, bitflags: nil, ttl: nil)
@@ -94,7 +94,7 @@ module Dalli
94
94
  key_len = key.nil? ? 0 : key.bytesize
95
95
  value_len = value.nil? ? 0 : value.bytesize
96
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].reject(&:nil?)
97
+ body = [bitflags, ttl, key, value].compact
98
98
  (header + body).pack(FORMAT[opkey])
99
99
  end
100
100
  # rubocop:enable Metrics/ParameterLists
@@ -109,7 +109,7 @@ module Dalli
109
109
  end
110
110
 
111
111
  def self.as_8byte_uint(val)
112
- [val >> 32, 0xFFFFFFFF & val]
112
+ [val >> 32, val & 0xFFFFFFFF]
113
113
  end
114
114
  end
115
115
  end
@@ -50,9 +50,9 @@ module Dalli
50
50
  extra_len = resp_header.extra_len
51
51
  key_len = resp_header.key_len
52
52
  bitflags = extra_len.positive? ? body.unpack1('N') : 0x0
53
- key = body.byteslice(extra_len, key_len).force_encoding('UTF-8') if key_len.positive?
53
+ key = body.byteslice(extra_len, key_len).force_encoding(Encoding::UTF_8) if key_len.positive?
54
54
  value = body.byteslice((extra_len + key_len)..-1)
55
- value = parse_as_stored_value ? @value_marshaller.retrieve(value, bitflags) : value
55
+ value = @value_marshaller.retrieve(value, bitflags) if parse_as_stored_value
56
56
  [key, value]
57
57
  end
58
58
 
@@ -15,7 +15,7 @@ module Dalli
15
15
 
16
16
  # Substitute spaces for the \x00 returned by
17
17
  # memcached as a separator for easier
18
- content&.tr("\u0000", ' ')
18
+ content&.tr!("\u0000", ' ')
19
19
  mechanisms = content&.split
20
20
  [status, mechanisms]
21
21
  end
@@ -18,13 +18,6 @@ module Dalli
18
18
 
19
19
  private
20
20
 
21
- ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
22
- def verify_allowed_quiet!(opkey)
23
- return if ALLOWED_QUIET_OPS.include?(opkey)
24
-
25
- raise Dalli::NotPermittedMultiOpError, "The operation #{opkey} is not allowed in a quiet block."
26
- end
27
-
28
21
  # Retrieval Commands
29
22
  def get(key, options = nil)
30
23
  req = RequestFormatter.standard_request(opkey: :get, key: key)
@@ -4,6 +4,8 @@ require 'English'
4
4
  require 'socket'
5
5
  require 'timeout'
6
6
 
7
+ require 'dalli/pid_cache'
8
+
7
9
  module Dalli
8
10
  module Protocol
9
11
  ##
@@ -51,7 +53,8 @@ module Dalli
51
53
  Dalli.logger.debug { "Dalli::Server#connect #{name}" }
52
54
 
53
55
  @sock = memcached_socket
54
- @pid = Process.pid
56
+ @pid = PIDCache.pid
57
+ @request_in_progress = false
55
58
  rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
56
59
  # SocketError = DNS resolution failure
57
60
  error_on_request!(e)
@@ -96,7 +99,13 @@ module Dalli
96
99
  end
97
100
 
98
101
  def confirm_ready!
99
- error_on_request!(RuntimeError.new('Already writing to socket')) if request_in_progress?
102
+ close if request_in_progress?
103
+ close_on_fork if fork_detected?
104
+ end
105
+
106
+ def confirm_in_progress!
107
+ raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless request_in_progress?
108
+
100
109
  close_on_fork if fork_detected?
101
110
  end
102
111
 
@@ -122,10 +131,14 @@ module Dalli
122
131
  end
123
132
 
124
133
  def start_request!
134
+ raise '[Dalli] Request already in progress. This may be a bug in Dalli.' if @request_in_progress
135
+
125
136
  @request_in_progress = true
126
137
  end
127
138
 
128
139
  def finish_request!
140
+ raise '[Dalli] No request in progress. This may be a bug in Dalli.' unless @request_in_progress
141
+
129
142
  @request_in_progress = false
130
143
  end
131
144
 
@@ -133,27 +146,27 @@ module Dalli
133
146
  @request_in_progress = false
134
147
  end
135
148
 
136
- def read(count)
137
- start_request!
138
- data = @sock.readfull(count)
139
- finish_request!
149
+ def read_line
150
+ data = @sock.gets("\r\n")
151
+ error_on_request!('EOF in read_line') if data.nil?
140
152
  data
141
- rescue SystemCallError, Timeout::Error, EOFError => e
153
+ rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
154
+ error_on_request!(e)
155
+ end
156
+
157
+ def read(count)
158
+ @sock.readfull(count)
159
+ rescue SystemCallError, *TIMEOUT_ERRORS, EOFError => e
142
160
  error_on_request!(e)
143
161
  end
144
162
 
145
163
  def write(bytes)
146
- start_request!
147
- result = @sock.write(bytes)
148
- finish_request!
149
- result
150
- rescue SystemCallError, Timeout::Error => e
164
+ @sock.write(bytes)
165
+ rescue SystemCallError, *TIMEOUT_ERRORS => e
151
166
  error_on_request!(e)
152
167
  end
153
168
 
154
- # Non-blocking read. Should only be used in the context
155
- # of a caller who has called start_request!, but not yet
156
- # called finish_request!. Here to support the operation
169
+ # Non-blocking read. Here to support the operation
157
170
  # of the get_multi operation
158
171
  def read_nonblock
159
172
  @sock.read_available
@@ -216,7 +229,7 @@ module Dalli
216
229
  end
217
230
 
218
231
  def fork_detected?
219
- @pid && @pid != Process.pid
232
+ @pid && @pid != PIDCache.pid
220
233
  end
221
234
 
222
235
  def log_down_detected
@@ -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(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