dalli 3.2.15 → 4.0.0
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 +24 -102
- data/Gemfile +2 -0
- data/README.md +0 -35
- data/lib/dalli/client.rb +5 -30
- data/lib/dalli/compressor.rb +5 -53
- data/lib/dalli/key_manager.rb +4 -30
- data/lib/dalli/protocol/base.rb +9 -79
- data/lib/dalli/protocol/binary/response_processor.rb +4 -20
- data/lib/dalli/protocol/binary.rb +6 -8
- data/lib/dalli/protocol/connection_manager.rb +12 -69
- data/lib/dalli/protocol/meta/key_regularizer.rb +1 -1
- data/lib/dalli/protocol/meta/request_formatter.rb +11 -25
- data/lib/dalli/protocol/meta/response_processor.rb +16 -54
- data/lib/dalli/protocol/meta.rb +9 -40
- data/lib/dalli/protocol/server_config_parser.rb +1 -1
- data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
- data/lib/dalli/protocol/value_compressor.rb +8 -46
- data/lib/dalli/protocol/value_marshaller.rb +1 -1
- data/lib/dalli/protocol/value_serializer.rb +49 -40
- data/lib/dalli/protocol.rb +0 -7
- data/lib/dalli/socket.rb +29 -36
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +3 -3
- data/lib/rack/session/dalli.rb +42 -7
- metadata +3 -5
- data/lib/dalli/json_serializer.rb +0 -28
- data/lib/dalli/server.rb +0 -6
|
@@ -15,33 +15,18 @@ module Dalli
|
|
|
15
15
|
compress: true,
|
|
16
16
|
compressor: ::Dalli::Compressor,
|
|
17
17
|
# min byte size to attempt compression
|
|
18
|
-
compression_min_size: 4 * 1024
|
|
19
|
-
# max size a stored value may decompress to (nil: no limit). Guards
|
|
20
|
-
# against a small compressed value expanding into gigabytes on read.
|
|
21
|
-
decompressed_max_bytes: 128 * 1024 * 1024 # 128 MiB
|
|
18
|
+
compression_min_size: 4 * 1024 # 4K
|
|
22
19
|
}.freeze
|
|
23
20
|
|
|
24
21
|
OPTIONS = DEFAULTS.keys.freeze
|
|
25
22
|
|
|
26
|
-
KEYWORD_PARAMETER_TYPES = %i[key keyreq].freeze
|
|
27
|
-
private_constant :KEYWORD_PARAMETER_TYPES
|
|
28
|
-
|
|
29
23
|
# https://www.hjp.at/zettel/m/memcached_flags.rxml
|
|
30
24
|
# Looks like most clients use bit 1 to indicate gzip compression.
|
|
31
25
|
FLAG_COMPRESSED = 0x2
|
|
32
26
|
|
|
33
27
|
def initialize(client_options)
|
|
34
|
-
# Support the deprecated compression option, but don't allow it to override
|
|
35
|
-
# an explicit compress
|
|
36
|
-
# Remove this with 4.0
|
|
37
|
-
if client_options.key?(:compression) && !client_options.key?(:compress)
|
|
38
|
-
Dalli.logger.warn "DEPRECATED: Dalli's :compression option is now just 'compress: true'. " \
|
|
39
|
-
'Please update your configuration.'
|
|
40
|
-
client_options[:compress] = client_options.delete(:compression)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
28
|
@compression_options =
|
|
44
|
-
DEFAULTS.merge(client_options.
|
|
29
|
+
DEFAULTS.merge(client_options.slice(*OPTIONS))
|
|
45
30
|
end
|
|
46
31
|
|
|
47
32
|
def store(value, req_options, bitflags)
|
|
@@ -53,16 +38,8 @@ module Dalli
|
|
|
53
38
|
end
|
|
54
39
|
|
|
55
40
|
def retrieve(value, bitflags)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
max_bytes = @compression_options[:decompressed_max_bytes]
|
|
59
|
-
# Custom compressors that only define decompress(data) keep working,
|
|
60
|
-
# without the limit
|
|
61
|
-
if max_bytes && compressor_accepts_max_bytes?
|
|
62
|
-
compressor.decompress(value, max_bytes: max_bytes)
|
|
63
|
-
else
|
|
64
|
-
compressor.decompress(value)
|
|
65
|
-
end
|
|
41
|
+
compressed = bitflags.anybits?(FLAG_COMPRESSED)
|
|
42
|
+
compressed ? compressor.decompress(value) : value
|
|
66
43
|
|
|
67
44
|
# TODO: We likely want to move this rescue into the Dalli::Compressor / Dalli::GzipCompressor
|
|
68
45
|
# itself, since not all compressors necessarily use Zlib. For now keep it here, so the behavior
|
|
@@ -71,19 +48,6 @@ module Dalli
|
|
|
71
48
|
raise UnmarshalError, "Unable to uncompress value: #{$ERROR_INFO.message}"
|
|
72
49
|
end
|
|
73
50
|
|
|
74
|
-
def compressor_accepts_max_bytes?
|
|
75
|
-
return @compressor_accepts_max_bytes if defined?(@compressor_accepts_max_bytes)
|
|
76
|
-
|
|
77
|
-
@compressor_accepts_max_bytes =
|
|
78
|
-
begin
|
|
79
|
-
compressor.method(:decompress).parameters.any? do |type, name|
|
|
80
|
-
name == :max_bytes && KEYWORD_PARAMETER_TYPES.include?(type)
|
|
81
|
-
end
|
|
82
|
-
rescue NameError # an object without a reflectable decompress method
|
|
83
|
-
false
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
51
|
def compress_by_default?
|
|
88
52
|
@compression_options[:compress]
|
|
89
53
|
end
|
|
@@ -97,13 +61,11 @@ module Dalli
|
|
|
97
61
|
end
|
|
98
62
|
|
|
99
63
|
# Checks whether we should apply compression when serializing a value
|
|
100
|
-
# based on the specified options. Returns false
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
# falling back to the server default.
|
|
64
|
+
# based on the specified options. Returns false unless the value
|
|
65
|
+
# is greater than the minimum compression size. Otherwise returns
|
|
66
|
+
# based on a method-level option if specified, falling back to the
|
|
67
|
+
# server default.
|
|
105
68
|
def compress_value?(value, req_options)
|
|
106
|
-
return false if req_options&.dig(:raw)
|
|
107
69
|
return false unless value.bytesize >= compression_min_size
|
|
108
70
|
return compress_by_default? unless req_options && !req_options[:compress].nil?
|
|
109
71
|
|
|
@@ -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,53 @@ 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
|
-
|
|
33
|
-
|
|
35
|
+
if req_options
|
|
36
|
+
return [value.to_s, bitflags] if req_options[:raw]
|
|
37
|
+
|
|
38
|
+
# If the value is a simple string, going through serialization is costly
|
|
39
|
+
# for no benefit other than preserving encoding.
|
|
40
|
+
# Assuming most strings are either UTF-8 or BINARY we can just store
|
|
41
|
+
# that information in the bitflags.
|
|
42
|
+
if req_options[:string_fastpath] && value.instance_of?(String)
|
|
43
|
+
case value.encoding
|
|
44
|
+
when Encoding::BINARY
|
|
45
|
+
return [value, bitflags]
|
|
46
|
+
when Encoding::UTF_8
|
|
47
|
+
return [value, bitflags | FLAG_UTF8]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[serialize_value(value), bitflags | FLAG_SERIALIZED]
|
|
34
53
|
end
|
|
35
54
|
|
|
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
55
|
def retrieve(value, bitflags)
|
|
46
|
-
serialized = (
|
|
47
|
-
serialized
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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}"
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def filter_name_error(err)
|
|
69
|
-
raise err unless err.message.include?(NAME_ERR_STR)
|
|
70
|
-
|
|
71
|
-
raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
|
|
56
|
+
serialized = bitflags.anybits?(FLAG_SERIALIZED)
|
|
57
|
+
if serialized
|
|
58
|
+
begin
|
|
59
|
+
serializer.load(value)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
raise UnmarshalError, 'Unable to unmarshal value'
|
|
62
|
+
end
|
|
63
|
+
elsif bitflags.anybits?(FLAG_UTF8)
|
|
64
|
+
value.force_encoding(Encoding::UTF_8)
|
|
65
|
+
else
|
|
66
|
+
value
|
|
67
|
+
end
|
|
72
68
|
end
|
|
73
69
|
|
|
74
70
|
def serializer
|
|
@@ -86,6 +82,19 @@ module Dalli
|
|
|
86
82
|
exc.set_backtrace e.backtrace
|
|
87
83
|
raise exc
|
|
88
84
|
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def warn_if_marshal_default(protocol_options)
|
|
89
|
+
return if protocol_options.key?(:serializer)
|
|
90
|
+
return if @@marshal_warning_logged
|
|
91
|
+
|
|
92
|
+
Dalli.logger.warn 'SECURITY WARNING: Dalli is using Marshal for serialization. ' \
|
|
93
|
+
'Marshal can execute arbitrary code during deserialization. ' \
|
|
94
|
+
'If your memcached server could be compromised, consider using ' \
|
|
95
|
+
'a safer serializer like JSON: Dalli::Client.new(servers, serializer: JSON)'
|
|
96
|
+
@@marshal_warning_logged = true # rubocop:disable Style/ClassVars
|
|
97
|
+
end
|
|
89
98
|
end
|
|
90
99
|
end
|
|
91
100
|
end
|
data/lib/dalli/protocol.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'openssl'
|
|
4
3
|
require 'timeout'
|
|
5
4
|
|
|
6
5
|
module Dalli
|
|
@@ -16,11 +15,5 @@ module Dalli
|
|
|
16
15
|
else
|
|
17
16
|
[Timeout::Error]
|
|
18
17
|
end
|
|
19
|
-
|
|
20
|
-
# SSL errors during the TLS handshake count as a failed request: they
|
|
21
|
-
# trigger reconnection and, after socket_max_failures, mark the server
|
|
22
|
-
# down. openssl is required above because this file loads before
|
|
23
|
-
# dalli/socket, so OpenSSL may not be defined yet.
|
|
24
|
-
SSL_ERRORS = [OpenSSL::SSL::SSLError].freeze
|
|
25
18
|
end
|
|
26
19
|
end
|
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,6 +86,7 @@ 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
|
|
|
@@ -117,57 +119,41 @@ module Dalli
|
|
|
117
119
|
end
|
|
118
120
|
|
|
119
121
|
def self.init_socket_options(sock, options)
|
|
122
|
+
configure_tcp_options(sock, options)
|
|
123
|
+
configure_socket_buffers(sock, options)
|
|
124
|
+
configure_timeout(sock, options)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def self.configure_tcp_options(sock, options)
|
|
120
128
|
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true)
|
|
121
129
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true) if options[:keepalive]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.configure_socket_buffers(sock, options)
|
|
122
133
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVBUF, options[:rcvbuf]) if options[:rcvbuf]
|
|
123
134
|
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
|
|
135
|
+
end
|
|
124
136
|
|
|
137
|
+
def self.configure_timeout(sock, options)
|
|
125
138
|
return unless options[:socket_timeout]
|
|
126
139
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
if sock.respond_to?(:timeout=)
|
|
141
|
+
sock.timeout = options[:socket_timeout]
|
|
142
|
+
else
|
|
143
|
+
seconds, fractional = options[:socket_timeout].divmod(1)
|
|
144
|
+
timeval = [seconds, fractional * 1_000_000].pack('l_2')
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
|
|
146
|
+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO, timeval)
|
|
147
|
+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDTIMEO, timeval)
|
|
148
|
+
end
|
|
133
149
|
end
|
|
134
150
|
|
|
135
|
-
# Closes the TCP socket if the handshake fails or times out, so a failed
|
|
136
|
-
# attempt doesn't hold a file descriptor until GC.
|
|
137
151
|
def self.wrapping_ssl_socket(tcp_socket, host, ssl_context)
|
|
138
|
-
warn_if_unverified(ssl_context)
|
|
139
|
-
connected = false
|
|
140
152
|
ssl_socket = Dalli::Socket::SSLSocket.new(tcp_socket, ssl_context)
|
|
141
153
|
ssl_socket.hostname = host
|
|
142
154
|
ssl_socket.sync_close = true
|
|
143
155
|
ssl_socket.connect
|
|
144
|
-
connected = true
|
|
145
156
|
ssl_socket
|
|
146
|
-
ensure
|
|
147
|
-
tcp_socket.close unless connected
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
# Verification comes entirely from the caller's ssl_context, and a bare
|
|
151
|
-
# SSLContext.new verifies nothing, so warn (once per process) when the
|
|
152
|
-
# certificate or the hostname won't be checked.
|
|
153
|
-
def self.warn_if_unverified(ssl_context)
|
|
154
|
-
return if @ssl_verification_warning_logged
|
|
155
|
-
|
|
156
|
-
problem = unverified_problem(ssl_context)
|
|
157
|
-
return unless problem
|
|
158
|
-
|
|
159
|
-
Dalli.logger.warn "SECURITY WARNING: Dalli's ssl_context #{problem}. " \
|
|
160
|
-
'Use ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER, ' \
|
|
161
|
-
'verify_hostname: true) to verify the server.'
|
|
162
|
-
@ssl_verification_warning_logged = true
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def self.unverified_problem(ssl_context)
|
|
166
|
-
if !ssl_context.verify_mode.to_i.anybits?(::OpenSSL::SSL::VERIFY_PEER)
|
|
167
|
-
'verify_mode is VERIFY_NONE, so the server certificate is not checked'
|
|
168
|
-
elsif !ssl_context.verify_hostname
|
|
169
|
-
'verify_hostname is false, so the certificate is not checked against the hostname'
|
|
170
|
-
end
|
|
171
157
|
end
|
|
172
158
|
end
|
|
173
159
|
|
|
@@ -198,9 +184,16 @@ module Dalli
|
|
|
198
184
|
Timeout.timeout(options[:socket_timeout]) do
|
|
199
185
|
sock = new(path)
|
|
200
186
|
sock.options = { path: path }.merge(options)
|
|
187
|
+
init_socket_options(sock, options)
|
|
201
188
|
sock
|
|
202
189
|
end
|
|
203
190
|
end
|
|
191
|
+
|
|
192
|
+
def self.init_socket_options(sock, options)
|
|
193
|
+
# https://man7.org/linux/man-pages/man7/unix.7.html
|
|
194
|
+
sock.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_SNDBUF, options[:sndbuf]) if options[:sndbuf]
|
|
195
|
+
sock.timeout = options[:socket_timeout] if options[:socket_timeout] && sock.respond_to?(:timeout=)
|
|
196
|
+
end
|
|
204
197
|
end
|
|
205
198
|
end
|
|
206
199
|
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,9 @@ 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
|
+
|
|
30
31
|
# Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
|
|
31
32
|
class NilObject; end # rubocop:disable Lint/EmptyClass
|
|
32
33
|
NOT_FOUND = NilObject.new
|
|
@@ -57,7 +58,6 @@ end
|
|
|
57
58
|
require_relative 'dalli/version'
|
|
58
59
|
|
|
59
60
|
require_relative 'dalli/compressor'
|
|
60
|
-
require_relative 'dalli/json_serializer'
|
|
61
61
|
require_relative 'dalli/client'
|
|
62
62
|
require_relative 'dalli/key_manager'
|
|
63
63
|
require_relative 'dalli/pipelined_getter'
|
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
|
|
@@ -145,6 +163,15 @@ module Rack
|
|
|
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,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dalli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter M. Goldstein
|
|
@@ -40,7 +40,6 @@ files:
|
|
|
40
40
|
- lib/dalli/cas/client.rb
|
|
41
41
|
- lib/dalli/client.rb
|
|
42
42
|
- lib/dalli/compressor.rb
|
|
43
|
-
- lib/dalli/json_serializer.rb
|
|
44
43
|
- lib/dalli/key_manager.rb
|
|
45
44
|
- lib/dalli/options.rb
|
|
46
45
|
- lib/dalli/pid_cache.rb
|
|
@@ -64,7 +63,6 @@ files:
|
|
|
64
63
|
- lib/dalli/protocol/value_marshaller.rb
|
|
65
64
|
- lib/dalli/protocol/value_serializer.rb
|
|
66
65
|
- lib/dalli/ring.rb
|
|
67
|
-
- lib/dalli/server.rb
|
|
68
66
|
- lib/dalli/servers_arg_normalizer.rb
|
|
69
67
|
- lib/dalli/socket.rb
|
|
70
68
|
- lib/dalli/version.rb
|
|
@@ -83,14 +81,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
83
81
|
requirements:
|
|
84
82
|
- - ">="
|
|
85
83
|
- !ruby/object:Gem::Version
|
|
86
|
-
version: '
|
|
84
|
+
version: '3.1'
|
|
87
85
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
86
|
requirements:
|
|
89
87
|
- - ">="
|
|
90
88
|
- !ruby/object:Gem::Version
|
|
91
89
|
version: '0'
|
|
92
90
|
requirements: []
|
|
93
|
-
rubygems_version: 4.0.
|
|
91
|
+
rubygems_version: 4.0.3
|
|
94
92
|
specification_version: 4
|
|
95
93
|
summary: High performance memcached client for Ruby
|
|
96
94
|
test_files: []
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
|
-
module Dalli
|
|
6
|
-
##
|
|
7
|
-
# A serializer that stores values as JSON and never creates Ruby objects
|
|
8
|
-
# from what it reads.
|
|
9
|
-
#
|
|
10
|
-
# Passing the JSON module itself as the serializer (serializer: JSON) reads
|
|
11
|
-
# values back with JSON.load, which on json gem versions before 3.0 honors
|
|
12
|
-
# "json_class" and instantiates other classes when the json gem's additions
|
|
13
|
-
# are loaded. This serializer reads with JSON.parse, so a value written to
|
|
14
|
-
# memcached by someone else can only come back as plain hashes, arrays,
|
|
15
|
-
# strings, numbers, booleans and nil.
|
|
16
|
-
#
|
|
17
|
-
# Dalli::Client.new(servers, serializer: Dalli::JSONSerializer)
|
|
18
|
-
##
|
|
19
|
-
module JSONSerializer
|
|
20
|
-
def self.dump(value)
|
|
21
|
-
JSON.generate(value)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def self.load(data)
|
|
25
|
-
JSON.parse(data)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|