dalli 3.2.8 → 5.0.1
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 +4 -4
- data/CHANGELOG.md +235 -1
- data/Gemfile +16 -2
- data/README.md +82 -2
- data/lib/dalli/client.rb +239 -25
- data/lib/dalli/instrumentation.rb +143 -0
- data/lib/dalli/key_manager.rb +23 -8
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pid_cache.rb +1 -1
- data/lib/dalli/pipelined_deleter.rb +82 -0
- data/lib/dalli/pipelined_getter.rb +44 -20
- data/lib/dalli/pipelined_setter.rb +87 -0
- data/lib/dalli/protocol/base.rb +94 -24
- data/lib/dalli/protocol/connection_manager.rb +23 -12
- data/lib/dalli/protocol/{meta/key_regularizer.rb → key_regularizer.rb} +1 -1
- data/lib/dalli/protocol/meta.rb +99 -12
- data/lib/dalli/protocol/{meta/request_formatter.rb → request_formatter.rb} +42 -10
- data/lib/dalli/protocol/response_buffer.rb +36 -12
- data/lib/dalli/protocol/{meta/response_processor.rb → response_processor.rb} +72 -26
- data/lib/dalli/protocol/server_config_parser.rb +2 -2
- data/lib/dalli/protocol/string_marshaller.rb +65 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
- data/lib/dalli/protocol/value_compressor.rb +2 -11
- data/lib/dalli/protocol/value_marshaller.rb +1 -1
- data/lib/dalli/protocol/value_serializer.rb +59 -40
- data/lib/dalli/protocol.rb +10 -0
- data/lib/dalli/ring.rb +2 -2
- data/lib/dalli/servers_arg_normalizer.rb +1 -1
- data/lib/dalli/socket.rb +74 -14
- data/lib/dalli/version.rb +2 -2
- data/lib/dalli.rb +12 -5
- data/lib/rack/session/dalli.rb +43 -8
- metadata +26 -17
- data/lib/dalli/protocol/binary/request_formatter.rb +0 -117
- data/lib/dalli/protocol/binary/response_header.rb +0 -36
- data/lib/dalli/protocol/binary/response_processor.rb +0 -239
- data/lib/dalli/protocol/binary/sasl_authentication.rb +0 -60
- data/lib/dalli/protocol/binary.rb +0 -173
- data/lib/dalli/server.rb +0 -6
|
@@ -25,17 +25,8 @@ module Dalli
|
|
|
25
25
|
FLAG_COMPRESSED = 0x2
|
|
26
26
|
|
|
27
27
|
def initialize(client_options)
|
|
28
|
-
# Support the deprecated compression option, but don't allow it to override
|
|
29
|
-
# an explicit compress
|
|
30
|
-
# Remove this with 4.0
|
|
31
|
-
if client_options.key?(:compression) && !client_options.key?(:compress)
|
|
32
|
-
Dalli.logger.warn "DEPRECATED: Dalli's :compression option is now just 'compress: true'. " \
|
|
33
|
-
'Please update your configuration.'
|
|
34
|
-
client_options[:compress] = client_options.delete(:compression)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
28
|
@compression_options =
|
|
38
|
-
DEFAULTS.merge(client_options.
|
|
29
|
+
DEFAULTS.merge(client_options.slice(*OPTIONS))
|
|
39
30
|
end
|
|
40
31
|
|
|
41
32
|
def store(value, req_options, bitflags)
|
|
@@ -47,7 +38,7 @@ module Dalli
|
|
|
47
38
|
end
|
|
48
39
|
|
|
49
40
|
def retrieve(value, bitflags)
|
|
50
|
-
compressed = (
|
|
41
|
+
compressed = bitflags.anybits?(FLAG_COMPRESSED)
|
|
51
42
|
compressed ? compressor.decompress(value) : value
|
|
52
43
|
|
|
53
44
|
# TODO: We likely want to move this rescue into the Dalli::Compressor / Dalli::GzipCompressor
|
|
@@ -27,7 +27,7 @@ module Dalli
|
|
|
27
27
|
@value_compressor = ValueCompressor.new(client_options)
|
|
28
28
|
|
|
29
29
|
@marshal_options =
|
|
30
|
-
DEFAULTS.merge(client_options.
|
|
30
|
+
DEFAULTS.merge(client_options.slice(*OPTIONS))
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def store(key, value, options = nil)
|
|
@@ -18,57 +18,39 @@ module Dalli
|
|
|
18
18
|
# https://www.hjp.at/zettel/m/memcached_flags.rxml
|
|
19
19
|
# Looks like most clients use bit 0 to indicate native language serialization
|
|
20
20
|
FLAG_SERIALIZED = 0x1
|
|
21
|
+
FLAG_UTF8 = 0x2
|
|
22
|
+
|
|
23
|
+
# Class variable to track whether the Marshal warning has been logged
|
|
24
|
+
@@marshal_warning_logged = false # rubocop:disable Style/ClassVars
|
|
21
25
|
|
|
22
26
|
attr_accessor :serialization_options
|
|
23
27
|
|
|
24
28
|
def initialize(protocol_options)
|
|
25
29
|
@serialization_options =
|
|
26
|
-
DEFAULTS.merge(protocol_options.
|
|
30
|
+
DEFAULTS.merge(protocol_options.slice(*OPTIONS))
|
|
31
|
+
warn_if_marshal_default(protocol_options) unless protocol_options[:silence_marshal_warning]
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
def store(value, req_options, bitflags)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
bitflags |= FLAG_SERIALIZED if do_serialize
|
|
33
|
-
[store_value, bitflags]
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# TODO: Some of these error messages need to be validated. It's not obvious
|
|
37
|
-
# that all of them are actually generated by the invoked code
|
|
38
|
-
# in current systems
|
|
39
|
-
# rubocop:disable Layout/LineLength
|
|
40
|
-
TYPE_ERR_REGEXP = %r{needs to have method `_load'|exception class/object expected|instance of IO needed|incompatible marshal file format}.freeze
|
|
41
|
-
ARGUMENT_ERR_REGEXP = /undefined class|marshal data too short/.freeze
|
|
42
|
-
NAME_ERR_STR = 'uninitialized constant'
|
|
43
|
-
# rubocop:enable Layout/LineLength
|
|
44
|
-
|
|
45
|
-
def retrieve(value, bitflags)
|
|
46
|
-
serialized = (bitflags & FLAG_SERIALIZED) != 0
|
|
47
|
-
serialized ? serializer.load(value) : value
|
|
48
|
-
rescue TypeError => e
|
|
49
|
-
filter_type_error(e)
|
|
50
|
-
rescue ArgumentError => e
|
|
51
|
-
filter_argument_error(e)
|
|
52
|
-
rescue NameError => e
|
|
53
|
-
filter_name_error(e)
|
|
54
|
-
end
|
|
35
|
+
return store_raw(value, bitflags) if req_options&.dig(:raw)
|
|
36
|
+
return store_string_fastpath(value, bitflags) if use_string_fastpath?(value, req_options)
|
|
55
37
|
|
|
56
|
-
|
|
57
|
-
raise err unless TYPE_ERR_REGEXP.match?(err.message)
|
|
58
|
-
|
|
59
|
-
raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def filter_argument_error(err)
|
|
63
|
-
raise err unless ARGUMENT_ERR_REGEXP.match?(err.message)
|
|
64
|
-
|
|
65
|
-
raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
|
|
38
|
+
[serialize_value(value), bitflags | FLAG_SERIALIZED]
|
|
66
39
|
end
|
|
67
40
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
41
|
+
def retrieve(value, bitflags)
|
|
42
|
+
serialized = bitflags.anybits?(FLAG_SERIALIZED)
|
|
43
|
+
if serialized
|
|
44
|
+
begin
|
|
45
|
+
serializer.load(value)
|
|
46
|
+
rescue StandardError
|
|
47
|
+
raise UnmarshalError, 'Unable to unmarshal value'
|
|
48
|
+
end
|
|
49
|
+
elsif bitflags.anybits?(FLAG_UTF8)
|
|
50
|
+
value.force_encoding(Encoding::UTF_8)
|
|
51
|
+
else
|
|
52
|
+
value
|
|
53
|
+
end
|
|
72
54
|
end
|
|
73
55
|
|
|
74
56
|
def serializer
|
|
@@ -86,6 +68,43 @@ module Dalli
|
|
|
86
68
|
exc.set_backtrace e.backtrace
|
|
87
69
|
raise exc
|
|
88
70
|
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def store_raw(value, bitflags)
|
|
75
|
+
unless value.is_a?(String)
|
|
76
|
+
raise Dalli::MarshalError, "Dalli raw mode requires string values, got: #{value.class}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
[value, bitflags]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# If the value is a simple string, going through serialization is costly
|
|
83
|
+
# for no benefit other than preserving encoding.
|
|
84
|
+
# Assuming most strings are either UTF-8 or BINARY we can just store
|
|
85
|
+
# that information in the bitflags.
|
|
86
|
+
def store_string_fastpath(value, bitflags)
|
|
87
|
+
case value.encoding
|
|
88
|
+
when Encoding::BINARY then [value, bitflags]
|
|
89
|
+
when Encoding::UTF_8 then [value, bitflags | FLAG_UTF8]
|
|
90
|
+
else [serialize_value(value), bitflags | FLAG_SERIALIZED]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def use_string_fastpath?(value, req_options)
|
|
95
|
+
req_options&.dig(:string_fastpath) && value.instance_of?(String)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def warn_if_marshal_default(protocol_options)
|
|
99
|
+
return if protocol_options.key?(:serializer)
|
|
100
|
+
return if @@marshal_warning_logged
|
|
101
|
+
|
|
102
|
+
Dalli.logger.warn 'SECURITY WARNING: Dalli is using Marshal for serialization. ' \
|
|
103
|
+
'Marshal can execute arbitrary code during deserialization. ' \
|
|
104
|
+
'If your memcached server could be compromised, consider using ' \
|
|
105
|
+
'a safer serializer like JSON: Dalli::Client.new(servers, serializer: JSON)'
|
|
106
|
+
@@marshal_warning_logged = true # rubocop:disable Style/ClassVars
|
|
107
|
+
end
|
|
89
108
|
end
|
|
90
109
|
end
|
|
91
110
|
end
|
data/lib/dalli/protocol.rb
CHANGED
|
@@ -15,5 +15,15 @@ module Dalli
|
|
|
15
15
|
else
|
|
16
16
|
[Timeout::Error]
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
# SSL errors that occur during read/write operations (not during initial
|
|
20
|
+
# handshake) should trigger reconnection. These indicate transient network
|
|
21
|
+
# issues, not configuration problems.
|
|
22
|
+
SSL_ERRORS =
|
|
23
|
+
if defined?(OpenSSL::SSL::SSLError)
|
|
24
|
+
[OpenSSL::SSL::SSLError]
|
|
25
|
+
else
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
18
28
|
end
|
|
19
29
|
end
|
data/lib/dalli/ring.rb
CHANGED
|
@@ -23,9 +23,9 @@ module Dalli
|
|
|
23
23
|
|
|
24
24
|
attr_accessor :servers, :continuum
|
|
25
25
|
|
|
26
|
-
def initialize(servers_arg,
|
|
26
|
+
def initialize(servers_arg, options)
|
|
27
27
|
@servers = servers_arg.map do |s|
|
|
28
|
-
|
|
28
|
+
Dalli::Protocol::Meta.new(s, options)
|
|
29
29
|
end
|
|
30
30
|
@continuum = nil
|
|
31
31
|
@continuum = build_continuum(servers) if servers.size > 1
|
|
@@ -16,7 +16,7 @@ module Dalli
|
|
|
16
16
|
# weight are optional (e.g. 'localhost', 'abc.com:12345', 'example.org:22222:3')
|
|
17
17
|
# * A colon separated string of (UNIX socket, weight) where the weight is optional
|
|
18
18
|
# (e.g. '/var/run/memcached/socket', '/tmp/xyz:3') (not supported on Windows)
|
|
19
|
-
# * A URI with a 'memcached' protocol
|
|
19
|
+
# * A URI with a 'memcached' protocol (e.g. 'memcached://localhost:11211')
|
|
20
20
|
#
|
|
21
21
|
# The methods in this module do not validate the format of individual server strings, but
|
|
22
22
|
# rather normalize the argument into a compact array, wherein each array entry corresponds
|
data/lib/dalli/socket.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Dalli
|
|
|
52
52
|
|
|
53
53
|
FILTERED_OUT_OPTIONS = %i[username password].freeze
|
|
54
54
|
def logged_options
|
|
55
|
-
options.
|
|
55
|
+
options.except(*FILTERED_OUT_OPTIONS)
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
@@ -63,6 +63,7 @@ module Dalli
|
|
|
63
63
|
##
|
|
64
64
|
class SSLSocket < ::OpenSSL::SSL::SSLSocket
|
|
65
65
|
include Dalli::Socket::InstanceMethods
|
|
66
|
+
|
|
66
67
|
def options
|
|
67
68
|
io.options
|
|
68
69
|
end
|
|
@@ -85,9 +86,16 @@ module Dalli
|
|
|
85
86
|
##
|
|
86
87
|
class TCP < TCPSocket
|
|
87
88
|
include Dalli::Socket::InstanceMethods
|
|
89
|
+
|
|
88
90
|
# options - supports enhanced logging in the case of a timeout
|
|
89
91
|
attr_accessor :options
|
|
90
92
|
|
|
93
|
+
# Expected parameter signature for unmodified TCPSocket#initialize.
|
|
94
|
+
# Used to detect when gems like socksify or resolv-replace have monkey-patched
|
|
95
|
+
# TCPSocket, which breaks the connect_timeout: keyword argument.
|
|
96
|
+
TCPSOCKET_NATIVE_PARAMETERS = [[:rest]].freeze
|
|
97
|
+
private_constant :TCPSOCKET_NATIVE_PARAMETERS
|
|
98
|
+
|
|
91
99
|
def self.open(host, port, options = {})
|
|
92
100
|
create_socket_with_timeout(host, port, options) do |sock|
|
|
93
101
|
sock.options = { host: host, port: port }.merge(options)
|
|
@@ -97,15 +105,20 @@ module Dalli
|
|
|
97
105
|
end
|
|
98
106
|
end
|
|
99
107
|
|
|
108
|
+
# Detect and cache whether TCPSocket supports the connect_timeout: keyword argument.
|
|
109
|
+
# Returns false if TCPSocket#initialize has been monkey-patched by gems like
|
|
110
|
+
# socksify or resolv-replace, which don't support keyword arguments.
|
|
111
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
112
|
+
def self.supports_connect_timeout?
|
|
113
|
+
return @supports_connect_timeout if defined?(@supports_connect_timeout)
|
|
114
|
+
|
|
115
|
+
@supports_connect_timeout = RUBY_VERSION >= '3.0' &&
|
|
116
|
+
::TCPSocket.instance_method(:initialize).parameters == TCPSOCKET_NATIVE_PARAMETERS
|
|
117
|
+
end
|
|
118
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
119
|
+
|
|
100
120
|
def self.create_socket_with_timeout(host, port, options)
|
|
101
|
-
|
|
102
|
-
# (part of ruby standard library since 3.0.0, should be removed in 3.4.0),
|
|
103
|
-
# as it does not handle keyword arguments correctly.
|
|
104
|
-
# To check this we are using the fact that resolv-replace
|
|
105
|
-
# aliases TCPSocket#initialize method to #original_resolv_initialize.
|
|
106
|
-
# https://github.com/ruby/resolv-replace/blob/v0.1.1/lib/resolv-replace.rb#L21
|
|
107
|
-
if RUBY_VERSION >= '3.0' &&
|
|
108
|
-
!::TCPSocket.private_instance_methods.include?(:original_resolv_initialize)
|
|
121
|
+
if supports_connect_timeout?
|
|
109
122
|
sock = new(host, port, connect_timeout: options[:socket_timeout])
|
|
110
123
|
yield(sock)
|
|
111
124
|
else
|
|
@@ -117,19 +130,59 @@ module Dalli
|
|
|
117
130
|
end
|
|
118
131
|
|
|
119
132
|
def self.init_socket_options(sock, options)
|
|
133
|
+
configure_tcp_options(sock, options)
|
|
134
|
+
configure_socket_buffers(sock, options)
|
|
135
|
+
configure_timeout(sock, options)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.configure_tcp_options(sock, options)
|
|
120
139
|
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
|
|
121
140
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options[:keepalive]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.configure_socket_buffers(sock, options)
|
|
122
144
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
|
|
123
145
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
|
|
146
|
+
end
|
|
124
147
|
|
|
148
|
+
def self.configure_timeout(sock, options)
|
|
125
149
|
return unless options[:socket_timeout]
|
|
126
150
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
151
|
+
if sock.respond_to?(:timeout=)
|
|
152
|
+
# Ruby 3.2+ has IO#timeout for reliable cross-platform timeout handling
|
|
153
|
+
sock.timeout = options[:socket_timeout]
|
|
154
|
+
else
|
|
155
|
+
# Ruby 3.1 fallback using socket options
|
|
156
|
+
# struct timeval has architecture-dependent sizes (time_t, suseconds_t)
|
|
157
|
+
seconds, fractional = options[:socket_timeout].divmod(1)
|
|
158
|
+
microseconds = (fractional * 1_000_000).to_i
|
|
159
|
+
timeval = pack_timeval(sock, seconds, microseconds)
|
|
160
|
+
|
|
161
|
+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
|
|
162
|
+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
130
165
|
|
|
131
|
-
|
|
132
|
-
|
|
166
|
+
# Pack formats for struct timeval across architectures.
|
|
167
|
+
# Uses fixed-size formats for JRuby compatibility (JRuby doesn't support _ modifier on q).
|
|
168
|
+
# - ll: 8 bytes (32-bit time_t, 32-bit suseconds_t)
|
|
169
|
+
# - qq: 16 bytes (64-bit time_t, 64-bit suseconds_t or padded 32-bit)
|
|
170
|
+
TIMEVAL_PACK_FORMATS = %w[ll qq].freeze
|
|
171
|
+
TIMEVAL_TEST_VALUES = [0, 0].freeze
|
|
172
|
+
|
|
173
|
+
# Detect and cache the correct pack format for struct timeval on this platform.
|
|
174
|
+
# Different architectures have different sizes for time_t and suseconds_t.
|
|
175
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
176
|
+
def self.timeval_pack_format(sock)
|
|
177
|
+
@timeval_pack_format ||= begin
|
|
178
|
+
expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
|
|
179
|
+
TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
183
|
+
|
|
184
|
+
def self.pack_timeval(sock, seconds, microseconds)
|
|
185
|
+
[seconds, microseconds].pack(timeval_pack_format(sock))
|
|
133
186
|
end
|
|
134
187
|
|
|
135
188
|
def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
|
|
@@ -168,9 +221,16 @@ module Dalli
|
|
|
168
221
|
Timeout.timeout(options[:socket_timeout]) do
|
|
169
222
|
sock = new(path)
|
|
170
223
|
sock.options = { path: path }.merge(options)
|
|
224
|
+
init_socket_options(sock, options)
|
|
171
225
|
sock
|
|
172
226
|
end
|
|
173
227
|
end
|
|
228
|
+
|
|
229
|
+
def self.init_socket_options(sock, options)
|
|
230
|
+
# https://man7.org/linux/man-pages/man7/unix.7.html
|
|
231
|
+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
|
|
232
|
+
sock.timeout = options[:socket_timeout] if options[:socket_timeout] && sock.respond_to?(:timeout=)
|
|
233
|
+
end
|
|
174
234
|
end
|
|
175
235
|
end
|
|
176
236
|
end
|
data/lib/dalli/version.rb
CHANGED
data/lib/dalli.rb
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
# Namespace for all Dalli code.
|
|
5
5
|
##
|
|
6
6
|
module Dalli
|
|
7
|
-
autoload :Server, 'dalli/server'
|
|
8
|
-
|
|
9
7
|
# generic error
|
|
10
8
|
class DalliError < RuntimeError; end
|
|
11
9
|
|
|
@@ -27,6 +25,12 @@ module Dalli
|
|
|
27
25
|
# operation is not permitted in a multi block
|
|
28
26
|
class NotPermittedMultiOpError < DalliError; end
|
|
29
27
|
|
|
28
|
+
# raised when Memcached response with a SERVER_ERROR
|
|
29
|
+
class ServerError < DalliError; end
|
|
30
|
+
|
|
31
|
+
# socket/server communication error that can be retried
|
|
32
|
+
class RetryableNetworkError < NetworkError; end
|
|
33
|
+
|
|
30
34
|
# Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
|
|
31
35
|
class NilObject; end # rubocop:disable Lint/EmptyClass
|
|
32
36
|
NOT_FOUND = NilObject.new
|
|
@@ -34,7 +38,7 @@ module Dalli
|
|
|
34
38
|
QUIET = :dalli_multi
|
|
35
39
|
|
|
36
40
|
def self.logger
|
|
37
|
-
@logger ||= rails_logger || default_logger
|
|
41
|
+
@logger ||= rails_logger || default_logger # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
def self.rails_logger
|
|
@@ -50,20 +54,22 @@ module Dalli
|
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
def self.logger=(logger)
|
|
53
|
-
@logger = logger
|
|
57
|
+
@logger = logger # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
54
58
|
end
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
require_relative 'dalli/version'
|
|
62
|
+
require_relative 'dalli/instrumentation'
|
|
58
63
|
|
|
59
64
|
require_relative 'dalli/compressor'
|
|
60
65
|
require_relative 'dalli/client'
|
|
61
66
|
require_relative 'dalli/key_manager'
|
|
62
67
|
require_relative 'dalli/pipelined_getter'
|
|
68
|
+
require_relative 'dalli/pipelined_setter'
|
|
69
|
+
require_relative 'dalli/pipelined_deleter'
|
|
63
70
|
require_relative 'dalli/ring'
|
|
64
71
|
require_relative 'dalli/protocol'
|
|
65
72
|
require_relative 'dalli/protocol/base'
|
|
66
|
-
require_relative 'dalli/protocol/binary'
|
|
67
73
|
require_relative 'dalli/protocol/connection_manager'
|
|
68
74
|
require_relative 'dalli/protocol/meta'
|
|
69
75
|
require_relative 'dalli/protocol/response_buffer'
|
|
@@ -71,6 +77,7 @@ require_relative 'dalli/protocol/server_config_parser'
|
|
|
71
77
|
require_relative 'dalli/protocol/ttl_sanitizer'
|
|
72
78
|
require_relative 'dalli/protocol/value_compressor'
|
|
73
79
|
require_relative 'dalli/protocol/value_marshaller'
|
|
80
|
+
require_relative 'dalli/protocol/string_marshaller'
|
|
74
81
|
require_relative 'dalli/protocol/value_serializer'
|
|
75
82
|
require_relative 'dalli/servers_arg_normalizer'
|
|
76
83
|
require_relative 'dalli/socket'
|
data/lib/rack/session/dalli.rb
CHANGED
|
@@ -9,6 +9,10 @@ module Rack
|
|
|
9
9
|
module Session
|
|
10
10
|
# Rack::Session::Dalli provides memcached based session management.
|
|
11
11
|
class Dalli < Abstract::PersistedSecure
|
|
12
|
+
class MissingSessionError < StandardError; end
|
|
13
|
+
|
|
14
|
+
RACK_SESSION_PERSISTED = 'rack.session.persisted'
|
|
15
|
+
|
|
12
16
|
attr_reader :data
|
|
13
17
|
|
|
14
18
|
# Don't freeze this until we fix the specs/implementation
|
|
@@ -70,23 +74,37 @@ module Rack
|
|
|
70
74
|
@data = build_data_source(options)
|
|
71
75
|
end
|
|
72
76
|
|
|
73
|
-
def
|
|
77
|
+
def call(*_args)
|
|
78
|
+
super
|
|
79
|
+
rescue MissingSessionError
|
|
80
|
+
[401, {}, ['Wrong session ID']]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def find_session(req, sid)
|
|
74
84
|
with_dalli_client([nil, {}]) do |dc|
|
|
75
85
|
existing_session = existing_session_for_sid(dc, sid)
|
|
76
|
-
|
|
86
|
+
if existing_session.nil?
|
|
87
|
+
sid = create_sid_with_empty_session(dc)
|
|
88
|
+
existing_session = {}
|
|
89
|
+
end
|
|
77
90
|
|
|
78
|
-
|
|
91
|
+
update_session_persisted_data(req, { id: sid })
|
|
92
|
+
return [sid, existing_session]
|
|
79
93
|
end
|
|
80
94
|
end
|
|
81
95
|
|
|
82
|
-
def write_session(
|
|
96
|
+
def write_session(req, sid, session, options)
|
|
83
97
|
return false unless sid
|
|
84
98
|
|
|
85
99
|
key = memcached_key_from_sid(sid)
|
|
86
100
|
return false unless key
|
|
87
101
|
|
|
88
102
|
with_dalli_client(false) do |dc|
|
|
89
|
-
|
|
103
|
+
write_session_safely!(
|
|
104
|
+
dc, sid, session_persisted_data(req),
|
|
105
|
+
write_args: [memcached_key_from_sid(sid), session, ttl(options[:expire_after])]
|
|
106
|
+
)
|
|
107
|
+
|
|
90
108
|
sid
|
|
91
109
|
end
|
|
92
110
|
end
|
|
@@ -139,12 +157,21 @@ module Rack
|
|
|
139
157
|
::Dalli::Client.new(server_configurations, client_options)
|
|
140
158
|
else
|
|
141
159
|
ensure_connection_pool_added!
|
|
142
|
-
ConnectionPool.new(pool_options) do
|
|
160
|
+
ConnectionPool.new(**pool_options) do
|
|
143
161
|
::Dalli::Client.new(server_configurations, client_options.merge(threadsafe: false))
|
|
144
162
|
end
|
|
145
163
|
end
|
|
146
164
|
end
|
|
147
165
|
|
|
166
|
+
def write_session_safely!(dalli_client, sid, persisted_data, write_args:)
|
|
167
|
+
if persisted_data && persisted_data[:id] == sid # That means that we update the existing session
|
|
168
|
+
# Override the session only if it still exists in the store!
|
|
169
|
+
raise MissingSessionError unless dalli_client.replace(*write_args)
|
|
170
|
+
else
|
|
171
|
+
dalli_client.set(*write_args)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
148
175
|
def extract_dalli_options(options)
|
|
149
176
|
raise 'Rack::Session::Dalli no longer supports the :cache option.' if options[:cache]
|
|
150
177
|
|
|
@@ -175,8 +202,8 @@ module Rack
|
|
|
175
202
|
raise e
|
|
176
203
|
end
|
|
177
204
|
|
|
178
|
-
def with_dalli_client(result_on_error = nil, &
|
|
179
|
-
@data.with(&
|
|
205
|
+
def with_dalli_client(result_on_error = nil, &)
|
|
206
|
+
@data.with(&)
|
|
180
207
|
rescue ::Dalli::DalliError, Errno::ECONNREFUSED
|
|
181
208
|
raise if $ERROR_INFO.message.include?('undefined class')
|
|
182
209
|
|
|
@@ -190,6 +217,14 @@ module Rack
|
|
|
190
217
|
def ttl(expire_after)
|
|
191
218
|
expire_after.nil? ? 0 : expire_after + 1
|
|
192
219
|
end
|
|
220
|
+
|
|
221
|
+
def session_persisted_data(req)
|
|
222
|
+
req.get_header RACK_SESSION_PERSISTED
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def update_session_persisted_data(req, data)
|
|
226
|
+
req.set_header RACK_SESSION_PERSISTED, data
|
|
227
|
+
end
|
|
193
228
|
end
|
|
194
229
|
end
|
|
195
230
|
end
|
metadata
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dalli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 5.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
8
8
|
- Mike Perham
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
13
|
-
dependencies:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: logger
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
14
27
|
description: High performance memcached client for Ruby
|
|
15
28
|
email:
|
|
16
29
|
- peter.m.goldstein@gmail.com
|
|
@@ -27,30 +40,28 @@ files:
|
|
|
27
40
|
- lib/dalli/cas/client.rb
|
|
28
41
|
- lib/dalli/client.rb
|
|
29
42
|
- lib/dalli/compressor.rb
|
|
43
|
+
- lib/dalli/instrumentation.rb
|
|
30
44
|
- lib/dalli/key_manager.rb
|
|
31
45
|
- lib/dalli/options.rb
|
|
32
46
|
- lib/dalli/pid_cache.rb
|
|
47
|
+
- lib/dalli/pipelined_deleter.rb
|
|
33
48
|
- lib/dalli/pipelined_getter.rb
|
|
49
|
+
- lib/dalli/pipelined_setter.rb
|
|
34
50
|
- lib/dalli/protocol.rb
|
|
35
51
|
- lib/dalli/protocol/base.rb
|
|
36
|
-
- lib/dalli/protocol/binary.rb
|
|
37
|
-
- lib/dalli/protocol/binary/request_formatter.rb
|
|
38
|
-
- lib/dalli/protocol/binary/response_header.rb
|
|
39
|
-
- lib/dalli/protocol/binary/response_processor.rb
|
|
40
|
-
- lib/dalli/protocol/binary/sasl_authentication.rb
|
|
41
52
|
- lib/dalli/protocol/connection_manager.rb
|
|
53
|
+
- lib/dalli/protocol/key_regularizer.rb
|
|
42
54
|
- lib/dalli/protocol/meta.rb
|
|
43
|
-
- lib/dalli/protocol/
|
|
44
|
-
- lib/dalli/protocol/meta/request_formatter.rb
|
|
45
|
-
- lib/dalli/protocol/meta/response_processor.rb
|
|
55
|
+
- lib/dalli/protocol/request_formatter.rb
|
|
46
56
|
- lib/dalli/protocol/response_buffer.rb
|
|
57
|
+
- lib/dalli/protocol/response_processor.rb
|
|
47
58
|
- lib/dalli/protocol/server_config_parser.rb
|
|
59
|
+
- lib/dalli/protocol/string_marshaller.rb
|
|
48
60
|
- lib/dalli/protocol/ttl_sanitizer.rb
|
|
49
61
|
- lib/dalli/protocol/value_compressor.rb
|
|
50
62
|
- lib/dalli/protocol/value_marshaller.rb
|
|
51
63
|
- lib/dalli/protocol/value_serializer.rb
|
|
52
64
|
- lib/dalli/ring.rb
|
|
53
|
-
- lib/dalli/server.rb
|
|
54
65
|
- lib/dalli/servers_arg_normalizer.rb
|
|
55
66
|
- lib/dalli/socket.rb
|
|
56
67
|
- lib/dalli/version.rb
|
|
@@ -62,7 +73,6 @@ metadata:
|
|
|
62
73
|
bug_tracker_uri: https://github.com/petergoldstein/dalli/issues
|
|
63
74
|
changelog_uri: https://github.com/petergoldstein/dalli/blob/main/CHANGELOG.md
|
|
64
75
|
rubygems_mfa_required: 'true'
|
|
65
|
-
post_install_message:
|
|
66
76
|
rdoc_options: []
|
|
67
77
|
require_paths:
|
|
68
78
|
- lib
|
|
@@ -70,15 +80,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
70
80
|
requirements:
|
|
71
81
|
- - ">="
|
|
72
82
|
- !ruby/object:Gem::Version
|
|
73
|
-
version: '
|
|
83
|
+
version: '3.3'
|
|
74
84
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
85
|
requirements:
|
|
76
86
|
- - ">="
|
|
77
87
|
- !ruby/object:Gem::Version
|
|
78
88
|
version: '0'
|
|
79
89
|
requirements: []
|
|
80
|
-
rubygems_version:
|
|
81
|
-
signing_key:
|
|
90
|
+
rubygems_version: 4.0.6
|
|
82
91
|
specification_version: 4
|
|
83
92
|
summary: High performance memcached client for Ruby
|
|
84
93
|
test_files: []
|