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.
@@ -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
- req = RequestFormatter.meta_get(key: encoded_key, base64: base64)
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
- RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true)
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
- req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64)
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?, base64: base64)
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. Starts
23
- # by advancing the buffer to the specified start position
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
- advance(bytes)
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/.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
@@ -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.select { |k, _| OPTIONS.include?(k) })
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 = (bitflags & FLAG_COMPRESSED) != 0
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.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)
@@ -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.select { |k, _| OPTIONS.include?(k) })
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
- 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
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
- def filter_type_error(err)
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 filter_name_error(err)
69
- raise err unless err.message.include?(NAME_ERR_STR)
70
-
71
- raise UnmarshalError, "Unable to unmarshal value: #{err.message}"
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
@@ -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