dalli 3.2.8 → 5.0.5
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 +305 -0
- data/Gemfile +16 -2
- data/README.md +96 -2
- data/lib/dalli/client.rb +289 -28
- data/lib/dalli/flags.rb +15 -0
- data/lib/dalli/instrumentation.rb +153 -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 +95 -25
- data/lib/dalli/protocol/connection_manager.rb +37 -14
- data/lib/dalli/protocol/{meta/key_regularizer.rb → key_regularizer.rb} +1 -1
- data/lib/dalli/protocol/meta.rb +185 -20
- data/lib/dalli/protocol/{meta/request_formatter.rb → request_formatter.rb} +49 -16
- data/lib/dalli/protocol/response_buffer.rb +36 -12
- data/lib/dalli/protocol/{meta/response_processor.rb → response_processor.rb} +93 -37
- 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 +3 -16
- data/lib/dalli/protocol/value_marshaller.rb +1 -1
- data/lib/dalli/protocol/value_serializer.rb +61 -44
- 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 +79 -14
- data/lib/dalli/version.rb +2 -2
- data/lib/dalli.rb +13 -5
- data/lib/rack/session/dalli.rb +43 -8
- metadata +27 -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
|
@@ -21,6 +21,17 @@ module Dalli
|
|
|
21
21
|
STAT = 'STAT'
|
|
22
22
|
VA = 'VA'
|
|
23
23
|
VERSION = 'VERSION'
|
|
24
|
+
SERVER_ERROR = 'SERVER_ERROR'
|
|
25
|
+
|
|
26
|
+
T_OK = [OK].freeze
|
|
27
|
+
T_RESET = [RESET].freeze
|
|
28
|
+
T_EN_HD = [EN, HD].freeze
|
|
29
|
+
T_VERSION = [VERSION].freeze
|
|
30
|
+
T_VA_EN_HD = [VA, EN, HD].freeze
|
|
31
|
+
T_HD_NF_EX = [HD, NF, EX].freeze
|
|
32
|
+
T_HD_NS_NF_EX = [HD, NS, NF, EX].freeze
|
|
33
|
+
T_VA_NF_NS_EX = [VA, NF, NS, EX].freeze
|
|
34
|
+
T_END_TOKEN_STAT = [END_TOKEN, STAT].freeze
|
|
24
35
|
|
|
25
36
|
def initialize(io_source, value_marshaller)
|
|
26
37
|
@io_source = io_source
|
|
@@ -28,49 +39,84 @@ module Dalli
|
|
|
28
39
|
end
|
|
29
40
|
|
|
30
41
|
def meta_get_with_value(cache_nils: false)
|
|
31
|
-
tokens = error_on_unexpected!(
|
|
42
|
+
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
32
43
|
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
33
44
|
return true unless tokens.first == VA
|
|
34
45
|
|
|
35
|
-
@value_marshaller.retrieve(
|
|
46
|
+
@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
|
|
36
47
|
end
|
|
37
48
|
|
|
38
49
|
def meta_get_with_value_and_cas
|
|
39
|
-
tokens = error_on_unexpected!(
|
|
50
|
+
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
40
51
|
return [nil, 0] if tokens.first == EN
|
|
41
52
|
|
|
42
53
|
cas = cas_from_tokens(tokens)
|
|
43
54
|
return [nil, cas] unless tokens.first == VA
|
|
44
55
|
|
|
45
|
-
[@value_marshaller.retrieve(
|
|
56
|
+
[@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
|
|
46
57
|
end
|
|
47
58
|
|
|
48
59
|
def meta_get_without_value
|
|
49
|
-
tokens = error_on_unexpected!(
|
|
60
|
+
tokens = error_on_unexpected!(T_EN_HD)
|
|
50
61
|
tokens.first == EN ? nil : true
|
|
51
62
|
end
|
|
52
63
|
|
|
64
|
+
# Returns a hash with all requested metadata:
|
|
65
|
+
# - :value - the cached value (or nil if miss)
|
|
66
|
+
# - :cas - the CAS value (if return_cas was requested)
|
|
67
|
+
# - :won_recache - true if client won the right to recache (W flag)
|
|
68
|
+
# - :stale - true if the item is stale (X flag)
|
|
69
|
+
# - :lost_recache - true if another client is already recaching (Z flag)
|
|
70
|
+
# - :hit_before - true/false if item was previously accessed (h flag, if requested)
|
|
71
|
+
# - :last_access - seconds since last access (l flag, if requested)
|
|
72
|
+
#
|
|
73
|
+
# Used by meta_get for comprehensive metadata retrieval.
|
|
74
|
+
# Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
|
|
75
|
+
def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false)
|
|
76
|
+
tokens = error_on_unexpected!(T_VA_EN_HD)
|
|
77
|
+
result = build_metadata_result(tokens)
|
|
78
|
+
result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
|
|
79
|
+
result[:last_access] = last_access_from_tokens(tokens) if return_last_access
|
|
80
|
+
result[:value] = parse_value_from_tokens(tokens, cache_nils)
|
|
81
|
+
result
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_metadata_result(tokens)
|
|
85
|
+
{
|
|
86
|
+
value: nil, cas: cas_from_tokens(tokens),
|
|
87
|
+
won_recache: tokens.include?('W'), stale: tokens.include?('X'),
|
|
88
|
+
lost_recache: tokens.include?('Z')
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def parse_value_from_tokens(tokens, cache_nils)
|
|
93
|
+
return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
|
|
94
|
+
return unless tokens.first == VA
|
|
95
|
+
|
|
96
|
+
@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
|
|
97
|
+
end
|
|
98
|
+
|
|
53
99
|
def meta_set_with_cas
|
|
54
|
-
tokens = error_on_unexpected!(
|
|
100
|
+
tokens = error_on_unexpected!(T_HD_NS_NF_EX)
|
|
55
101
|
return false unless tokens.first == HD
|
|
56
102
|
|
|
57
103
|
cas_from_tokens(tokens)
|
|
58
104
|
end
|
|
59
105
|
|
|
60
106
|
def meta_set_append_prepend
|
|
61
|
-
tokens = error_on_unexpected!(
|
|
107
|
+
tokens = error_on_unexpected!(T_HD_NS_NF_EX)
|
|
62
108
|
return false unless tokens.first == HD
|
|
63
109
|
|
|
64
110
|
true
|
|
65
111
|
end
|
|
66
112
|
|
|
67
113
|
def meta_delete
|
|
68
|
-
tokens = error_on_unexpected!(
|
|
114
|
+
tokens = error_on_unexpected!(T_HD_NF_EX)
|
|
69
115
|
tokens.first == HD
|
|
70
116
|
end
|
|
71
117
|
|
|
72
118
|
def decr_incr
|
|
73
|
-
tokens = error_on_unexpected!(
|
|
119
|
+
tokens = error_on_unexpected!(T_VA_NF_NS_EX)
|
|
74
120
|
return false if [NS, EX].include?(tokens.first)
|
|
75
121
|
return nil if tokens.first == NF
|
|
76
122
|
|
|
@@ -78,7 +124,7 @@ module Dalli
|
|
|
78
124
|
end
|
|
79
125
|
|
|
80
126
|
def stats
|
|
81
|
-
tokens = error_on_unexpected!(
|
|
127
|
+
tokens = error_on_unexpected!(T_END_TOKEN_STAT)
|
|
82
128
|
values = {}
|
|
83
129
|
while tokens.first != END_TOKEN
|
|
84
130
|
values[tokens[1]] = tokens[2]
|
|
@@ -88,19 +134,19 @@ module Dalli
|
|
|
88
134
|
end
|
|
89
135
|
|
|
90
136
|
def flush
|
|
91
|
-
error_on_unexpected!(
|
|
137
|
+
error_on_unexpected!(T_OK)
|
|
92
138
|
|
|
93
139
|
true
|
|
94
140
|
end
|
|
95
141
|
|
|
96
142
|
def reset
|
|
97
|
-
error_on_unexpected!(
|
|
143
|
+
error_on_unexpected!(T_RESET)
|
|
98
144
|
|
|
99
145
|
true
|
|
100
146
|
end
|
|
101
147
|
|
|
102
148
|
def version
|
|
103
|
-
tokens = error_on_unexpected!(
|
|
149
|
+
tokens = error_on_unexpected!(T_VERSION)
|
|
104
150
|
tokens.last
|
|
105
151
|
end
|
|
106
152
|
|
|
@@ -111,14 +157,6 @@ module Dalli
|
|
|
111
157
|
true
|
|
112
158
|
end
|
|
113
159
|
|
|
114
|
-
def tokens_from_header_buffer(buf)
|
|
115
|
-
header = header_from_buffer(buf)
|
|
116
|
-
tokens = header.split
|
|
117
|
-
header_len = header.bytesize + TERMINATOR.length
|
|
118
|
-
body_len = body_len_from_tokens(tokens)
|
|
119
|
-
[tokens, header_len, body_len]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
160
|
def full_response_from_buffer(tokens, body, resp_size)
|
|
123
161
|
value = @value_marshaller.retrieve(body, bitflags_from_tokens(tokens))
|
|
124
162
|
[resp_size, tokens.first == VA, cas_from_tokens(tokens), key_from_tokens(tokens), value]
|
|
@@ -134,11 +172,15 @@ module Dalli
|
|
|
134
172
|
# The remaining three values in the array are the ResponseHeader,
|
|
135
173
|
# key, and value.
|
|
136
174
|
##
|
|
137
|
-
def getk_response_from_buffer(buf)
|
|
138
|
-
#
|
|
139
|
-
|
|
175
|
+
def getk_response_from_buffer(buf, offset = 0)
|
|
176
|
+
# Find the header terminator starting from offset
|
|
177
|
+
term_idx = buf.index(TERMINATOR, offset)
|
|
178
|
+
return [0, nil, nil, nil, nil] unless term_idx
|
|
140
179
|
|
|
141
|
-
|
|
180
|
+
header = buf.byteslice(offset, term_idx - offset)
|
|
181
|
+
tokens = header.split
|
|
182
|
+
header_len = header.bytesize + TERMINATOR.length
|
|
183
|
+
body_len = body_len_from_tokens(tokens)
|
|
142
184
|
|
|
143
185
|
# We have a complete response that has no body.
|
|
144
186
|
# This is either the response to the terminating
|
|
@@ -149,27 +191,22 @@ module Dalli
|
|
|
149
191
|
resp_size = header_len + body_len + TERMINATOR.length
|
|
150
192
|
# The header is in the buffer, but the body is not. As we don't have
|
|
151
193
|
# a complete response, don't advance the buffer
|
|
152
|
-
return [0, nil, nil, nil, nil] unless buf.bytesize >= resp_size
|
|
194
|
+
return [0, nil, nil, nil, nil] unless buf.bytesize >= offset + resp_size
|
|
153
195
|
|
|
154
196
|
# The full response is in our buffer, so parse it and return
|
|
155
197
|
# the values
|
|
156
|
-
body = buf.
|
|
198
|
+
body = buf.byteslice(offset + header_len, body_len)
|
|
157
199
|
full_response_from_buffer(tokens, body, resp_size)
|
|
158
200
|
end
|
|
159
201
|
|
|
160
|
-
def contains_header?(buf)
|
|
161
|
-
buf.include?(TERMINATOR)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def header_from_buffer(buf)
|
|
165
|
-
buf.split(TERMINATOR, 2).first
|
|
166
|
-
end
|
|
167
|
-
|
|
168
202
|
def error_on_unexpected!(expected_codes)
|
|
169
203
|
tokens = next_line_to_tokens
|
|
170
|
-
raise Dalli::DalliError, "Response error: #{tokens.first}" unless expected_codes.include?(tokens.first)
|
|
171
204
|
|
|
172
|
-
tokens
|
|
205
|
+
return tokens if expected_codes.include?(tokens.first)
|
|
206
|
+
|
|
207
|
+
raise Dalli::ServerError, tokens.join(' ').to_s if tokens.first == SERVER_ERROR
|
|
208
|
+
|
|
209
|
+
raise Dalli::DalliError, "Response error: #{tokens.first}"
|
|
173
210
|
end
|
|
174
211
|
|
|
175
212
|
def bitflags_from_tokens(tokens)
|
|
@@ -186,6 +223,21 @@ module Dalli
|
|
|
186
223
|
KeyRegularizer.decode(encoded_key, base64_encoded)
|
|
187
224
|
end
|
|
188
225
|
|
|
226
|
+
# Returns true if item was previously hit, false if first access, nil if not requested
|
|
227
|
+
# The h flag returns h0 (first access) or h1 (previously accessed)
|
|
228
|
+
def hit_status_from_tokens(tokens)
|
|
229
|
+
hit_token = tokens.find { |t| t.start_with?('h') && t.length == 2 }
|
|
230
|
+
return nil unless hit_token
|
|
231
|
+
|
|
232
|
+
hit_token[1] == '1'
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Returns seconds since last access, or nil if not requested
|
|
236
|
+
# The l flag returns l<seconds>
|
|
237
|
+
def last_access_from_tokens(tokens)
|
|
238
|
+
value_from_tokens(tokens, 'l')&.to_i
|
|
239
|
+
end
|
|
240
|
+
|
|
189
241
|
def body_len_from_tokens(tokens)
|
|
190
242
|
value_from_tokens(tokens, 's')&.to_i
|
|
191
243
|
end
|
|
@@ -205,6 +257,10 @@ module Dalli
|
|
|
205
257
|
line = read_line
|
|
206
258
|
line&.split || []
|
|
207
259
|
end
|
|
260
|
+
|
|
261
|
+
def read_data(data_size)
|
|
262
|
+
@io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
|
|
263
|
+
end
|
|
208
264
|
end
|
|
209
265
|
end
|
|
210
266
|
end
|
|
@@ -6,7 +6,7 @@ module Dalli
|
|
|
6
6
|
module Protocol
|
|
7
7
|
##
|
|
8
8
|
# Dalli::Protocol::ServerConfigParser parses a server string passed to
|
|
9
|
-
# a Dalli::Protocol::
|
|
9
|
+
# a Dalli::Protocol::Meta instance into the hostname, port, weight, and
|
|
10
10
|
# socket_type.
|
|
11
11
|
##
|
|
12
12
|
class ServerConfigParser
|
|
@@ -16,7 +16,7 @@ module Dalli
|
|
|
16
16
|
# can limit character set to LDH + '.'. Hex digit section
|
|
17
17
|
# is there to support IPv6 addresses, which need to be specified with
|
|
18
18
|
# a bounding []
|
|
19
|
-
SERVER_CONFIG_REGEXP = /\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z
|
|
19
|
+
SERVER_CONFIG_REGEXP = /\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z/
|
|
20
20
|
|
|
21
21
|
DEFAULT_PORT = 11_211
|
|
22
22
|
DEFAULT_WEIGHT = 1
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
module Protocol
|
|
5
|
+
##
|
|
6
|
+
# Dalli::Protocol::StringMarshaller is a pass-through marshaller for use with
|
|
7
|
+
# the :raw client option. It bypasses serialization and compression entirely,
|
|
8
|
+
# expecting values to already be strings (e.g., pre-serialized by Rails'
|
|
9
|
+
# ActiveSupport::Cache). It still enforces the maximum value size limit.
|
|
10
|
+
##
|
|
11
|
+
class StringMarshaller
|
|
12
|
+
DEFAULTS = {
|
|
13
|
+
# max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
|
|
14
|
+
value_max_bytes: 1024 * 1024
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :value_max_bytes
|
|
18
|
+
|
|
19
|
+
def initialize(client_options)
|
|
20
|
+
@value_max_bytes = client_options.fetch(:value_max_bytes) do
|
|
21
|
+
DEFAULTS.fetch(:value_max_bytes)
|
|
22
|
+
end.to_i
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def store(key, value, _options = nil)
|
|
26
|
+
raise MarshalError, "Dalli in :raw mode only supports strings, got: #{value.class}" unless value.is_a?(String)
|
|
27
|
+
|
|
28
|
+
error_if_over_max_value_bytes(key, value)
|
|
29
|
+
[value, 0]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def retrieve(value, _flags)
|
|
33
|
+
value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Interface compatibility methods - these return nil since
|
|
37
|
+
# StringMarshaller bypasses serialization and compression entirely.
|
|
38
|
+
|
|
39
|
+
def serializer
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def compressor
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def compression_min_size
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def compress_by_default?
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def error_if_over_max_value_bytes(key, value)
|
|
58
|
+
return if value.bytesize <= value_max_bytes
|
|
59
|
+
|
|
60
|
+
message = "Value for #{key} over max size: #{value_max_bytes} <= #{value.bytesize}"
|
|
61
|
+
raise Dalli::ValueOverMaxSize, message
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -31,7 +31,7 @@ module Dalli
|
|
|
31
31
|
return ttl_as_i if ttl_as_i > now # Already a timestamp
|
|
32
32
|
|
|
33
33
|
Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached " \
|
|
34
|
-
'and too short to be a future timestamp,' \
|
|
34
|
+
'and too short to be a future timestamp, ' \
|
|
35
35
|
'converting to an expiration timestamp'
|
|
36
36
|
now + ttl_as_i
|
|
37
37
|
end
|
|
@@ -20,34 +20,21 @@ module Dalli
|
|
|
20
20
|
|
|
21
21
|
OPTIONS = DEFAULTS.keys.freeze
|
|
22
22
|
|
|
23
|
-
# https://www.hjp.at/zettel/m/memcached_flags.rxml
|
|
24
|
-
# Looks like most clients use bit 1 to indicate gzip compression.
|
|
25
|
-
FLAG_COMPRESSED = 0x2
|
|
26
|
-
|
|
27
23
|
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
24
|
@compression_options =
|
|
38
|
-
DEFAULTS.merge(client_options.
|
|
25
|
+
DEFAULTS.merge(client_options.slice(*OPTIONS))
|
|
39
26
|
end
|
|
40
27
|
|
|
41
28
|
def store(value, req_options, bitflags)
|
|
42
29
|
do_compress = compress_value?(value, req_options)
|
|
43
30
|
store_value = do_compress ? compressor.compress(value) : value
|
|
44
|
-
bitflags |=
|
|
31
|
+
bitflags |= Flags::COMPRESSED if do_compress
|
|
45
32
|
|
|
46
33
|
[store_value, bitflags]
|
|
47
34
|
end
|
|
48
35
|
|
|
49
36
|
def retrieve(value, bitflags)
|
|
50
|
-
compressed = (
|
|
37
|
+
compressed = bitflags.anybits?(Flags::COMPRESSED)
|
|
51
38
|
compressed ? compressor.decompress(value) : value
|
|
52
39
|
|
|
53
40
|
# 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)
|
|
@@ -10,65 +10,43 @@ module Dalli
|
|
|
10
10
|
##
|
|
11
11
|
class ValueSerializer
|
|
12
12
|
DEFAULTS = {
|
|
13
|
-
serializer: Marshal
|
|
13
|
+
serializer: Marshal,
|
|
14
|
+
string_fastpath: false
|
|
14
15
|
}.freeze
|
|
15
16
|
|
|
16
17
|
OPTIONS = DEFAULTS.keys.freeze
|
|
17
18
|
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
FLAG_SERIALIZED = 0x1
|
|
19
|
+
# Class variable to track whether the Marshal warning has been logged
|
|
20
|
+
@@marshal_warning_logged = false # rubocop:disable Style/ClassVars
|
|
21
21
|
|
|
22
22
|
attr_accessor :serialization_options
|
|
23
23
|
|
|
24
24
|
def initialize(protocol_options)
|
|
25
25
|
@serialization_options =
|
|
26
|
-
DEFAULTS.merge(protocol_options.
|
|
26
|
+
DEFAULTS.merge(protocol_options.slice(*OPTIONS))
|
|
27
|
+
warn_if_marshal_default(protocol_options) unless protocol_options[:silence_marshal_warning]
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
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
|
|
55
|
-
|
|
56
|
-
def filter_type_error(err)
|
|
57
|
-
raise err unless TYPE_ERR_REGEXP.match?(err.message)
|
|
31
|
+
return store_raw(value, bitflags) if req_options&.dig(:raw)
|
|
32
|
+
return store_string_fastpath(value, bitflags) if use_string_fastpath?(value, req_options)
|
|
58
33
|
|
|
59
|
-
|
|
34
|
+
[serialize_value(value), bitflags | Flags::SERIALIZED]
|
|
60
35
|
end
|
|
61
36
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
37
|
+
def retrieve(value, bitflags)
|
|
38
|
+
serialized = bitflags.anybits?(Flags::SERIALIZED)
|
|
39
|
+
if serialized
|
|
40
|
+
begin
|
|
41
|
+
serializer.load(value)
|
|
42
|
+
rescue StandardError
|
|
43
|
+
raise UnmarshalError, 'Unable to unmarshal value'
|
|
44
|
+
end
|
|
45
|
+
elsif bitflags.anybits?(Flags::UTF8)
|
|
46
|
+
value.force_encoding(Encoding::UTF_8)
|
|
47
|
+
else
|
|
48
|
+
value
|
|
49
|
+
end
|
|
72
50
|
end
|
|
73
51
|
|
|
74
52
|
def serializer
|
|
@@ -86,6 +64,45 @@ module Dalli
|
|
|
86
64
|
exc.set_backtrace e.backtrace
|
|
87
65
|
raise exc
|
|
88
66
|
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def store_raw(value, bitflags)
|
|
71
|
+
unless value.is_a?(String)
|
|
72
|
+
raise Dalli::MarshalError, "Dalli raw mode requires string values, got: #{value.class}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
[value, bitflags]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# If the value is a simple string, going through serialization is costly
|
|
79
|
+
# for no benefit other than preserving encoding.
|
|
80
|
+
# Assuming most strings are either UTF-8 or BINARY we can just store
|
|
81
|
+
# that information in the bitflags.
|
|
82
|
+
def store_string_fastpath(value, bitflags)
|
|
83
|
+
case value.encoding
|
|
84
|
+
when Encoding::BINARY then [value, bitflags]
|
|
85
|
+
when Encoding::UTF_8 then [value, bitflags | Flags::UTF8]
|
|
86
|
+
else [serialize_value(value), bitflags | Flags::SERIALIZED]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def use_string_fastpath?(value, req_options)
|
|
91
|
+
fastpath = req_options&.dig(:string_fastpath)
|
|
92
|
+
fastpath = @serialization_options[:string_fastpath] if fastpath.nil?
|
|
93
|
+
fastpath && value.instance_of?(String)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def warn_if_marshal_default(protocol_options)
|
|
97
|
+
return if protocol_options.key?(:serializer)
|
|
98
|
+
return if @@marshal_warning_logged
|
|
99
|
+
|
|
100
|
+
Dalli.logger.warn 'SECURITY WARNING: Dalli is using Marshal for serialization. ' \
|
|
101
|
+
'Marshal can execute arbitrary code during deserialization. ' \
|
|
102
|
+
'If your memcached server could be compromised, consider using ' \
|
|
103
|
+
'a safer serializer like JSON: Dalli::Client.new(servers, serializer: JSON)'
|
|
104
|
+
@@marshal_warning_logged = true # rubocop:disable Style/ClassVars
|
|
105
|
+
end
|
|
89
106
|
end
|
|
90
107
|
end
|
|
91
108
|
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
|