dalli 3.1.5 → 3.2.6

Sign up to get free protection for your applications and to get access to all the features.
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