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 +4 -4
- data/{History.md → CHANGELOG.md} +55 -2
- data/Gemfile +9 -4
- data/README.md +8 -2
- data/lib/dalli/client.rb +10 -12
- data/lib/dalli/key_manager.rb +11 -3
- data/lib/dalli/pid_cache.rb +40 -0
- data/lib/dalli/pipelined_getter.rb +1 -1
- data/lib/dalli/protocol/base.rb +25 -9
- data/lib/dalli/protocol/binary/request_formatter.rb +3 -3
- data/lib/dalli/protocol/binary/response_processor.rb +2 -2
- data/lib/dalli/protocol/binary/sasl_authentication.rb +1 -1
- data/lib/dalli/protocol/binary.rb +0 -7
- data/lib/dalli/protocol/connection_manager.rb +29 -16
- data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
- data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
- data/lib/dalli/protocol/meta/response_processor.rb +211 -0
- data/lib/dalli/protocol/meta.rb +178 -0
- data/lib/dalli/protocol/response_buffer.rb +1 -0
- data/lib/dalli/protocol/server_config_parser.rb +2 -0
- data/lib/dalli/protocol.rb +11 -0
- data/lib/dalli/ring.rb +4 -2
- data/lib/dalli/servers_arg_normalizer.rb +1 -1
- data/lib/dalli/socket.rb +1 -1
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +1 -0
- data/lib/rack/session/dalli.rb +16 -7
- metadata +11 -96
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9a374b5939ef874ce8067059c88b088c9129e7ab09ff891b1ad1ca3cf13c3ce
|
4
|
+
data.tar.gz: d5937c697bcd86a6d3d4207d328ce7e73c854b93a1d19e402630c3f80caec2ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 36010355440b325c1f8d5b922e882a8a8e199b8bd48dda92b1dcc4866f29e81b536ce41ff4896441c6c2c4f4e8e8faaf891ca6355e47de91733baf10bdfa9eee
|
7
|
+
data.tar.gz: 0c2b42e88960838ceef44c6c0f696b938925ade65da7c8791a07539ca36e18d6e874693dfde840a08e78ac9076f6810735a05d137c98d8c64387414f89d6f016
|
data/{History.md → CHANGELOG.md}
RENAMED
@@ -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
|
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 (
|
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 '
|
9
|
-
gem '
|
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/
|
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
|
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
|
-
# - :
|
47
|
-
#
|
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
|
-
@
|
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
|
-
|
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
|
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
|
##
|
data/lib/dalli/key_manager.rb
CHANGED
@@ -61,7 +61,7 @@ module Dalli
|
|
61
61
|
def key_with_namespace(key)
|
62
62
|
return key if namespace.nil?
|
63
63
|
|
64
|
-
"#{
|
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.
|
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
|
-
|
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
|
data/lib/dalli/protocol/base.rb
CHANGED
@@ -32,7 +32,13 @@ module Dalli
|
|
32
32
|
verify_state(opkey)
|
33
33
|
|
34
34
|
begin
|
35
|
-
|
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
|
-
|
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
|
-
|
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,
|
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
|
136
|
+
@options[:username] || ENV.fetch('MEMCACHE_USERNAME', nil)
|
131
137
|
end
|
132
138
|
|
133
139
|
def password
|
134
|
-
@options[:password] || ENV
|
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].
|
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,
|
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(
|
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 =
|
55
|
+
value = @value_marshaller.retrieve(value, bitflags) if parse_as_stored_value
|
56
56
|
[key, value]
|
57
57
|
end
|
58
58
|
|
@@ -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 =
|
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
|
-
|
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
|
137
|
-
|
138
|
-
|
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,
|
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
|
-
|
147
|
-
|
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.
|
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 !=
|
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
|