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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +305 -0
  3. data/Gemfile +16 -2
  4. data/README.md +96 -2
  5. data/lib/dalli/client.rb +289 -28
  6. data/lib/dalli/flags.rb +15 -0
  7. data/lib/dalli/instrumentation.rb +153 -0
  8. data/lib/dalli/key_manager.rb +23 -8
  9. data/lib/dalli/options.rb +1 -1
  10. data/lib/dalli/pid_cache.rb +1 -1
  11. data/lib/dalli/pipelined_deleter.rb +82 -0
  12. data/lib/dalli/pipelined_getter.rb +44 -20
  13. data/lib/dalli/pipelined_setter.rb +87 -0
  14. data/lib/dalli/protocol/base.rb +95 -25
  15. data/lib/dalli/protocol/connection_manager.rb +37 -14
  16. data/lib/dalli/protocol/{meta/key_regularizer.rb → key_regularizer.rb} +1 -1
  17. data/lib/dalli/protocol/meta.rb +185 -20
  18. data/lib/dalli/protocol/{meta/request_formatter.rb → request_formatter.rb} +49 -16
  19. data/lib/dalli/protocol/response_buffer.rb +36 -12
  20. data/lib/dalli/protocol/{meta/response_processor.rb → response_processor.rb} +93 -37
  21. data/lib/dalli/protocol/server_config_parser.rb +2 -2
  22. data/lib/dalli/protocol/string_marshaller.rb +65 -0
  23. data/lib/dalli/protocol/ttl_sanitizer.rb +1 -1
  24. data/lib/dalli/protocol/value_compressor.rb +3 -16
  25. data/lib/dalli/protocol/value_marshaller.rb +1 -1
  26. data/lib/dalli/protocol/value_serializer.rb +61 -44
  27. data/lib/dalli/protocol.rb +10 -0
  28. data/lib/dalli/ring.rb +2 -2
  29. data/lib/dalli/servers_arg_normalizer.rb +1 -1
  30. data/lib/dalli/socket.rb +79 -14
  31. data/lib/dalli/version.rb +2 -2
  32. data/lib/dalli.rb +13 -5
  33. data/lib/rack/session/dalli.rb +43 -8
  34. metadata +27 -17
  35. data/lib/dalli/protocol/binary/request_formatter.rb +0 -117
  36. data/lib/dalli/protocol/binary/response_header.rb +0 -36
  37. data/lib/dalli/protocol/binary/response_processor.rb +0 -239
  38. data/lib/dalli/protocol/binary/sasl_authentication.rb +0 -60
  39. data/lib/dalli/protocol/binary.rb +0 -173
  40. 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!([VA, EN, HD])
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(read_line, bitflags_from_tokens(tokens))
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!([VA, EN, HD])
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(read_line, bitflags_from_tokens(tokens)), cas]
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!([EN, HD])
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!([HD, NS, NF, EX])
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!([HD, NS, NF, EX])
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!([HD, NF, EX])
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!([VA, NF, NS, EX])
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!([END_TOKEN, STAT])
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!([OK])
137
+ error_on_unexpected!(T_OK)
92
138
 
93
139
  true
94
140
  end
95
141
 
96
142
  def reset
97
- error_on_unexpected!([RESET])
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!([VERSION])
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
- # There's no header in the buffer, so don't advance
139
- return [0, nil, nil, nil, nil] unless contains_header?(buf)
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
- tokens, header_len, body_len = tokens_from_header_buffer(buf)
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.slice(header_len, body_len)
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::Binary instance into the hostname, port, weight, and
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/.freeze
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.select { |k, _| OPTIONS.include?(k) })
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 |= FLAG_COMPRESSED if do_compress
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 = (bitflags & FLAG_COMPRESSED) != 0
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.select { |k, _| OPTIONS.include?(k) })
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
- # https://www.hjp.at/zettel/m/memcached_flags.rxml
19
- # Looks like most clients use bit 0 to indicate native language serialization
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.select { |k, _| OPTIONS.include?(k) })
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
- do_serialize = !(req_options && req_options[:raw])
31
- store_value = do_serialize ? serialize_value(value) : value.to_s
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
- raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
34
+ [serialize_value(value), bitflags | Flags::SERIALIZED]
60
35
  end
61
36
 
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}"
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
@@ -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, protocol_implementation, options)
26
+ def initialize(servers_arg, options)
27
27
  @servers = servers_arg.map do |s|
28
- protocol_implementation.new(s, options)
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, which will typically include a username/password
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