dalli 3.2.8 → 4.3.3
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 +169 -1
- data/Gemfile +15 -2
- data/README.md +92 -0
- data/lib/dalli/client.rb +246 -11
- data/lib/dalli/instrumentation.rb +141 -0
- data/lib/dalli/key_manager.rb +23 -8
- data/lib/dalli/pipelined_deleter.rb +82 -0
- data/lib/dalli/pipelined_getter.rb +46 -20
- data/lib/dalli/pipelined_setter.rb +87 -0
- data/lib/dalli/protocol/base.rb +82 -10
- data/lib/dalli/protocol/binary/response_processor.rb +5 -15
- data/lib/dalli/protocol/binary.rb +27 -0
- data/lib/dalli/protocol/connection_manager.rb +16 -11
- data/lib/dalli/protocol/meta/key_regularizer.rb +1 -1
- data/lib/dalli/protocol/meta/request_formatter.rb +42 -10
- data/lib/dalli/protocol/meta/response_processor.rb +72 -26
- data/lib/dalli/protocol/meta.rb +96 -5
- data/lib/dalli/protocol/response_buffer.rb +36 -12
- data/lib/dalli/protocol/server_config_parser.rb +1 -1
- 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/protocol_deprecations.rb +45 -0
- data/lib/dalli/socket.rb +70 -14
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +11 -2
- data/lib/rack/session/dalli.rb +43 -8
- metadata +25 -10
- data/lib/dalli/server.rb +0 -6
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -25,21 +25,28 @@ module Dalli
|
|
|
25
25
|
# Retrieval Commands
|
|
26
26
|
def get(key, options = nil)
|
|
27
27
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
28
|
-
|
|
28
|
+
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
29
|
+
skip_flags = raw_mode? || (options && options[:raw])
|
|
30
|
+
req = RequestFormatter.meta_get(key: encoded_key, base64: base64, skip_flags: skip_flags)
|
|
29
31
|
write(req)
|
|
32
|
+
@connection_manager.flush
|
|
30
33
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
def quiet_get_request(key)
|
|
34
37
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
35
|
-
|
|
38
|
+
# Skip bitflags in raw mode - saves 2 bytes per request and skips parsing
|
|
39
|
+
RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true,
|
|
40
|
+
skip_flags: raw_mode?)
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
def gat(key, ttl, options = nil)
|
|
39
44
|
ttl = TtlSanitizer.sanitize(ttl)
|
|
40
45
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
41
|
-
|
|
46
|
+
skip_flags = raw_mode? || (options && options[:raw])
|
|
47
|
+
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64, skip_flags: skip_flags)
|
|
42
48
|
write(req)
|
|
49
|
+
@connection_manager.flush
|
|
43
50
|
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
|
|
44
51
|
end
|
|
45
52
|
|
|
@@ -48,6 +55,7 @@ module Dalli
|
|
|
48
55
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
49
56
|
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
|
|
50
57
|
write(req)
|
|
58
|
+
@connection_manager.flush
|
|
51
59
|
response_processor.meta_get_without_value
|
|
52
60
|
end
|
|
53
61
|
|
|
@@ -57,15 +65,77 @@ module Dalli
|
|
|
57
65
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
58
66
|
req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
|
|
59
67
|
write(req)
|
|
68
|
+
@connection_manager.flush
|
|
60
69
|
response_processor.meta_get_with_value_and_cas
|
|
61
70
|
end
|
|
62
71
|
|
|
72
|
+
# Comprehensive meta get with support for all metadata flags.
|
|
73
|
+
# @note Requires memcached 1.6+ (meta protocol feature)
|
|
74
|
+
#
|
|
75
|
+
# This is the full-featured get method that supports:
|
|
76
|
+
# - Thundering herd protection (vivify_ttl, recache_ttl)
|
|
77
|
+
# - Item metadata (hit_status, last_access)
|
|
78
|
+
# - LRU control (skip_lru_bump)
|
|
79
|
+
#
|
|
80
|
+
# @param key [String] the key to retrieve
|
|
81
|
+
# @param options [Hash] options controlling what metadata to return
|
|
82
|
+
# - :vivify_ttl [Integer] creates a stub on miss with this TTL (N flag)
|
|
83
|
+
# - :recache_ttl [Integer] wins recache race if remaining TTL is below this (R flag)
|
|
84
|
+
# - :return_hit_status [Boolean] return whether item was previously accessed (h flag)
|
|
85
|
+
# - :return_last_access [Boolean] return seconds since last access (l flag)
|
|
86
|
+
# - :skip_lru_bump [Boolean] don't bump LRU or update access stats (u flag)
|
|
87
|
+
# - :cache_nils [Boolean] whether to cache nil values
|
|
88
|
+
# @return [Hash] containing:
|
|
89
|
+
# - :value - the cached value (or nil on miss)
|
|
90
|
+
# - :cas - the CAS value
|
|
91
|
+
# - :won_recache - true if client won recache race (W flag)
|
|
92
|
+
# - :stale - true if item is stale (X flag)
|
|
93
|
+
# - :lost_recache - true if another client is recaching (Z flag)
|
|
94
|
+
# - :hit_before - true/false if previously accessed (only if return_hit_status: true)
|
|
95
|
+
# - :last_access - seconds since last access (only if return_last_access: true)
|
|
96
|
+
def meta_get(key, options = {})
|
|
97
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
98
|
+
req = RequestFormatter.meta_get(
|
|
99
|
+
key: encoded_key, value: true, return_cas: true, base64: base64,
|
|
100
|
+
vivify_ttl: options[:vivify_ttl], recache_ttl: options[:recache_ttl],
|
|
101
|
+
return_hit_status: options[:return_hit_status],
|
|
102
|
+
return_last_access: options[:return_last_access], skip_lru_bump: options[:skip_lru_bump]
|
|
103
|
+
)
|
|
104
|
+
write(req)
|
|
105
|
+
@connection_manager.flush
|
|
106
|
+
response_processor.meta_get_with_metadata(
|
|
107
|
+
cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
|
|
108
|
+
return_last_access: options[:return_last_access]
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Delete with stale invalidation instead of actual deletion.
|
|
113
|
+
# Used with thundering herd protection to mark items as stale rather than removing them.
|
|
114
|
+
# @note Requires memcached 1.6+ (meta protocol feature)
|
|
115
|
+
#
|
|
116
|
+
# @param key [String] the key to invalidate
|
|
117
|
+
# @param cas [Integer] optional CAS value for compare-and-swap
|
|
118
|
+
# @return [Boolean] true if successful
|
|
119
|
+
def delete_stale(key, cas = nil)
|
|
120
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
121
|
+
req = RequestFormatter.meta_delete(key: encoded_key, cas: cas, base64: base64, stale: true)
|
|
122
|
+
write(req)
|
|
123
|
+
@connection_manager.flush
|
|
124
|
+
response_processor.meta_delete
|
|
125
|
+
end
|
|
126
|
+
|
|
63
127
|
# Storage Commands
|
|
64
128
|
def set(key, value, ttl, cas, options)
|
|
65
129
|
write_storage_req(:set, key, value, ttl, cas, options)
|
|
66
130
|
response_processor.meta_set_with_cas unless quiet?
|
|
67
131
|
end
|
|
68
132
|
|
|
133
|
+
# Pipelined set - writes a quiet set request without reading response.
|
|
134
|
+
# Used by PipelinedSetter for bulk operations.
|
|
135
|
+
def pipelined_set(key, value, ttl, options)
|
|
136
|
+
write_storage_req(:set, key, value, ttl, nil, options, quiet: true)
|
|
137
|
+
end
|
|
138
|
+
|
|
69
139
|
def add(key, value, ttl, options)
|
|
70
140
|
write_storage_req(:add, key, value, ttl, nil, options)
|
|
71
141
|
response_processor.meta_set_with_cas unless quiet?
|
|
@@ -77,14 +147,17 @@ module Dalli
|
|
|
77
147
|
end
|
|
78
148
|
|
|
79
149
|
# rubocop:disable Metrics/ParameterLists
|
|
80
|
-
def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {})
|
|
150
|
+
def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
|
|
81
151
|
(value, bitflags) = @value_marshaller.store(key, raw_value, options)
|
|
82
152
|
ttl = TtlSanitizer.sanitize(ttl) if ttl
|
|
83
153
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
84
154
|
req = RequestFormatter.meta_set(key: encoded_key, value: value,
|
|
85
155
|
bitflags: bitflags, cas: cas,
|
|
86
|
-
ttl: ttl, mode: mode, quiet: quiet
|
|
156
|
+
ttl: ttl, mode: mode, quiet: quiet, base64: base64)
|
|
87
157
|
write(req)
|
|
158
|
+
write(value)
|
|
159
|
+
write(TERMINATOR)
|
|
160
|
+
@connection_manager.flush unless quiet
|
|
88
161
|
end
|
|
89
162
|
# rubocop:enable Metrics/ParameterLists
|
|
90
163
|
|
|
@@ -105,6 +178,9 @@ module Dalli
|
|
|
105
178
|
req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
|
|
106
179
|
cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
|
|
107
180
|
write(req)
|
|
181
|
+
write(value)
|
|
182
|
+
write(TERMINATOR)
|
|
183
|
+
@connection_manager.flush unless quiet?
|
|
108
184
|
end
|
|
109
185
|
# rubocop:enable Metrics/ParameterLists
|
|
110
186
|
|
|
@@ -114,9 +190,18 @@ module Dalli
|
|
|
114
190
|
req = RequestFormatter.meta_delete(key: encoded_key, cas: cas,
|
|
115
191
|
base64: base64, quiet: quiet?)
|
|
116
192
|
write(req)
|
|
193
|
+
@connection_manager.flush unless quiet?
|
|
117
194
|
response_processor.meta_delete unless quiet?
|
|
118
195
|
end
|
|
119
196
|
|
|
197
|
+
# Pipelined delete - writes a quiet delete request without reading response.
|
|
198
|
+
# Used by PipelinedDeleter for bulk operations.
|
|
199
|
+
def pipelined_delete(key)
|
|
200
|
+
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
201
|
+
req = RequestFormatter.meta_delete(key: encoded_key, base64: base64, quiet: true)
|
|
202
|
+
write(req)
|
|
203
|
+
end
|
|
204
|
+
|
|
120
205
|
# Arithmetic Commands
|
|
121
206
|
def decr(key, count, ttl, initial)
|
|
122
207
|
decr_incr false, key, count, ttl, initial
|
|
@@ -131,12 +216,14 @@ module Dalli
|
|
|
131
216
|
encoded_key, base64 = KeyRegularizer.encode(key)
|
|
132
217
|
write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
|
|
133
218
|
quiet: quiet?, base64: base64))
|
|
219
|
+
@connection_manager.flush unless quiet?
|
|
134
220
|
response_processor.decr_incr unless quiet?
|
|
135
221
|
end
|
|
136
222
|
|
|
137
223
|
# Other Commands
|
|
138
224
|
def flush(delay = 0)
|
|
139
225
|
write(RequestFormatter.flush(delay: delay))
|
|
226
|
+
@connection_manager.flush unless quiet?
|
|
140
227
|
response_processor.flush unless quiet?
|
|
141
228
|
end
|
|
142
229
|
|
|
@@ -149,21 +236,25 @@ module Dalli
|
|
|
149
236
|
|
|
150
237
|
def stats(info = nil)
|
|
151
238
|
write(RequestFormatter.stats(info))
|
|
239
|
+
@connection_manager.flush
|
|
152
240
|
response_processor.stats
|
|
153
241
|
end
|
|
154
242
|
|
|
155
243
|
def reset_stats
|
|
156
244
|
write(RequestFormatter.stats('reset'))
|
|
245
|
+
@connection_manager.flush
|
|
157
246
|
response_processor.reset
|
|
158
247
|
end
|
|
159
248
|
|
|
160
249
|
def version
|
|
161
250
|
write(RequestFormatter.version)
|
|
251
|
+
@connection_manager.flush
|
|
162
252
|
response_processor.version
|
|
163
253
|
end
|
|
164
254
|
|
|
165
255
|
def write_noop
|
|
166
256
|
write(RequestFormatter.meta_noop)
|
|
257
|
+
@connection_manager.flush
|
|
167
258
|
end
|
|
168
259
|
|
|
169
260
|
def authenticate_connection
|
|
@@ -7,48 +7,72 @@ module Dalli
|
|
|
7
7
|
module Protocol
|
|
8
8
|
##
|
|
9
9
|
# Manages the buffer for responses from memcached.
|
|
10
|
+
# Uses an offset-based approach to avoid string allocations
|
|
11
|
+
# when advancing through parsed responses.
|
|
10
12
|
##
|
|
11
13
|
class ResponseBuffer
|
|
14
|
+
# Compact the buffer when the consumed portion exceeds this
|
|
15
|
+
# threshold and represents more than half the buffer
|
|
16
|
+
COMPACT_THRESHOLD = 4096
|
|
17
|
+
|
|
12
18
|
def initialize(io_source, response_processor)
|
|
13
19
|
@io_source = io_source
|
|
14
20
|
@response_processor = response_processor
|
|
15
21
|
@buffer = nil
|
|
22
|
+
@offset = 0
|
|
16
23
|
end
|
|
17
24
|
|
|
18
25
|
def read
|
|
19
26
|
@buffer << @io_source.read_nonblock
|
|
20
27
|
end
|
|
21
28
|
|
|
22
|
-
# Attempts to process a single response from the buffer
|
|
23
|
-
#
|
|
29
|
+
# Attempts to process a single response from the buffer,
|
|
30
|
+
# advancing the offset past the consumed bytes.
|
|
24
31
|
def process_single_getk_response
|
|
25
|
-
bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer)
|
|
26
|
-
|
|
32
|
+
bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer, @offset)
|
|
33
|
+
@offset += bytes
|
|
34
|
+
compact_if_needed
|
|
27
35
|
[status, cas, key, value]
|
|
28
36
|
end
|
|
29
37
|
|
|
30
|
-
# Advances the internal response buffer by bytes_to_advance
|
|
31
|
-
# bytes. The
|
|
32
|
-
def advance(bytes_to_advance)
|
|
33
|
-
return unless bytes_to_advance.positive?
|
|
34
|
-
|
|
35
|
-
@buffer = @buffer.byteslice(bytes_to_advance..-1)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
38
|
# Resets the internal buffer to an empty state,
|
|
39
39
|
# so that we're ready to read pipelined responses
|
|
40
40
|
def reset
|
|
41
41
|
@buffer = ''.b
|
|
42
|
+
@offset = 0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Ensures the buffer is initialized for reading without discarding
|
|
46
|
+
# existing data. Used by interleaved pipelined get which may have
|
|
47
|
+
# already buffered partial responses during the send phase.
|
|
48
|
+
def ensure_ready
|
|
49
|
+
return if in_progress?
|
|
50
|
+
|
|
51
|
+
@buffer = ''.b
|
|
52
|
+
@offset = 0
|
|
42
53
|
end
|
|
43
54
|
|
|
44
55
|
# Clear the internal response buffer
|
|
45
56
|
def clear
|
|
46
57
|
@buffer = nil
|
|
58
|
+
@offset = 0
|
|
47
59
|
end
|
|
48
60
|
|
|
49
61
|
def in_progress?
|
|
50
62
|
!@buffer.nil?
|
|
51
63
|
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Only compact when we've consumed a significant portion of the buffer.
|
|
68
|
+
# This avoids per-response string allocation while preventing unbounded
|
|
69
|
+
# memory growth for large pipelines.
|
|
70
|
+
def compact_if_needed
|
|
71
|
+
return unless @offset > COMPACT_THRESHOLD && @offset > @buffer.bytesize / 2
|
|
72
|
+
|
|
73
|
+
@buffer = @buffer.byteslice(@offset..)
|
|
74
|
+
@offset = 0
|
|
75
|
+
end
|
|
52
76
|
end
|
|
53
77
|
end
|
|
54
78
|
end
|
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dalli
|
|
4
|
+
##
|
|
5
|
+
# Handles deprecation warnings for protocol and authentication features
|
|
6
|
+
# that will be removed in Dalli 5.0.
|
|
7
|
+
##
|
|
8
|
+
module ProtocolDeprecations
|
|
9
|
+
BINARY_PROTOCOL_DEPRECATION_MESSAGE = <<~MSG.chomp
|
|
10
|
+
[DEPRECATION] The binary protocol is deprecated and will be removed in Dalli 5.0. \
|
|
11
|
+
Please use `protocol: :meta` instead. The meta protocol requires memcached 1.6+. \
|
|
12
|
+
See https://github.com/petergoldstein/dalli for migration details.
|
|
13
|
+
MSG
|
|
14
|
+
|
|
15
|
+
SASL_AUTH_DEPRECATION_MESSAGE = <<~MSG.chomp
|
|
16
|
+
[DEPRECATION] SASL authentication is deprecated and will be removed in Dalli 5.0. \
|
|
17
|
+
SASL is only supported by the binary protocol, which is being removed. \
|
|
18
|
+
Consider using network-level security (firewall rules, VPN) or memcached's TLS support instead.
|
|
19
|
+
MSG
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def emit_deprecation_warnings
|
|
24
|
+
emit_binary_protocol_deprecation_warning
|
|
25
|
+
emit_sasl_auth_deprecation_warning
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def emit_binary_protocol_deprecation_warning
|
|
29
|
+
protocol = @options[:protocol]
|
|
30
|
+
# Binary is used when protocol is nil, :binary, or 'binary'
|
|
31
|
+
return if protocol.to_s == 'meta'
|
|
32
|
+
|
|
33
|
+
warn BINARY_PROTOCOL_DEPRECATION_MESSAGE
|
|
34
|
+
Dalli.logger.warn(BINARY_PROTOCOL_DEPRECATION_MESSAGE)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def emit_sasl_auth_deprecation_warning
|
|
38
|
+
username = @options[:username] || ENV.fetch('MEMCACHE_USERNAME', nil)
|
|
39
|
+
return unless username
|
|
40
|
+
|
|
41
|
+
warn SASL_AUTH_DEPRECATION_MESSAGE
|
|
42
|
+
Dalli.logger.warn(SASL_AUTH_DEPRECATION_MESSAGE)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|