Class: Dalli::KeyManager

Inherits:
Object
  • Object
show all
Defined in:
lib/dalli/key_manager.rb

Overview

This class manages and validates keys sent to Memcached, ensuring that they meet Memcached key length requirements, and supporting the implementation of optional namespaces on a per-Dalli client basis.

Constant Summary collapse

MAX_KEY_LENGTH =
250
DEFAULT_NAMESPACE_SEPARATOR =
':'
TRUNCATED_KEY_SEPARATOR =

This is a hard coded md5 for historical reasons

':md5:'
TRUNCATED_KEY_TARGET_SIZE =

This is 249 for historical reasons

249
DEFAULTS =
{
  digest_class: ::Digest::MD5,
  namespace_separator: DEFAULT_NAMESPACE_SEPARATOR
}.freeze
OPTIONS =
%i[digest_class namespace namespace_separator].freeze
VALID_NAMESPACE_SEPARATORS =

Valid separators: non-alphanumeric, single printable ASCII characters Excludes: alphanumerics, whitespace, control characters

/\A[^a-zA-Z0-9 \x00-\x1F\x7F]\z/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client_options) ⇒ KeyManager

Returns a new instance of KeyManager.



36
37
38
39
40
41
42
43
44
# File 'lib/dalli/key_manager.rb', line 36

def initialize(client_options)
  @key_options =
    DEFAULTS.merge(client_options.slice(*OPTIONS))
  validate_digest_class_option(@key_options)
  validate_namespace_separator_option(@key_options)

  @namespace = namespace_from_options
  @namespace_separator = @key_options[:namespace_separator]
end

Instance Attribute Details

#namespaceObject (readonly)

Returns the value of attribute namespace.



30
31
32
# File 'lib/dalli/key_manager.rb', line 30

def namespace
  @namespace
end

#namespace_separatorObject (readonly)

Returns the value of attribute namespace_separator.



30
31
32
# File 'lib/dalli/key_manager.rb', line 30

def namespace_separator
  @namespace_separator
end

Instance Method Details

#digest_classObject



80
81
82
# File 'lib/dalli/key_manager.rb', line 80

def digest_class
  @digest_class ||= @key_options[:digest_class]
end

#evaluate_namespaceObject



112
113
114
115
116
# File 'lib/dalli/key_manager.rb', line 112

def evaluate_namespace
  return namespace.call.to_s if namespace.is_a?(Proc)

  namespace
end

#key_with_namespace(key) ⇒ Object

Returns the key with the namespace prefixed, if a namespace is defined. Otherwise just returns the key



68
69
70
71
72
# File 'lib/dalli/key_manager.rb', line 68

def key_with_namespace(key)
  return key if namespace.nil?

  "#{evaluate_namespace}#{namespace_separator}#{key}"
end

#key_without_namespace(key) ⇒ Object



74
75
76
77
78
# File 'lib/dalli/key_manager.rb', line 74

def key_without_namespace(key)
  return key if namespace.nil?

  key.sub(namespace_regexp, '')
end

#namespace_from_optionsObject



104
105
106
107
108
109
110
# File 'lib/dalli/key_manager.rb', line 104

def namespace_from_options
  raw_namespace = @key_options[:namespace]
  return nil unless raw_namespace
  return raw_namespace.to_s unless raw_namespace.is_a?(Proc)

  raw_namespace
end

#namespace_regexpObject



84
85
86
87
88
# File 'lib/dalli/key_manager.rb', line 84

def namespace_regexp
  return /\A#{Regexp.escape(evaluate_namespace)}#{Regexp.escape(namespace_separator)}/ if namespace.is_a?(Proc)

  @namespace_regexp ||= /\A#{Regexp.escape(namespace)}#{Regexp.escape(namespace_separator)}/ unless namespace.nil?
end

#prefix_length(digest) ⇒ Object



128
129
130
131
132
133
134
# File 'lib/dalli/key_manager.rb', line 128

def prefix_length(digest)
  return TRUNCATED_KEY_TARGET_SIZE - (TRUNCATED_KEY_SEPARATOR.length + digest.length) if namespace.nil?

  # For historical reasons, truncated keys with namespaces had a length of 250 rather
  # than 249
  TRUNCATED_KEY_TARGET_SIZE + 1 - (TRUNCATED_KEY_SEPARATOR.length + digest.length)
end

#truncated_key(key) ⇒ Object

Produces a truncated key, if the raw key is longer than the maximum allowed length. The truncated key is produced by generating a hex digest of the key, and appending that to a truncated section of the key.



123
124
125
126
# File 'lib/dalli/key_manager.rb', line 123

def truncated_key(key)
  digest = digest_class.hexdigest(key)
  "#{key[0, prefix_length(digest)]}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
end

#validate_digest_class_option(opts) ⇒ Object

Raises:

  • (ArgumentError)


90
91
92
93
94
# File 'lib/dalli/key_manager.rb', line 90

def validate_digest_class_option(opts)
  return if opts[:digest_class].respond_to?(:hexdigest)

  raise ArgumentError, 'The digest_class object must respond to the hexdigest method'
end

#validate_key(key) ⇒ Object

Validates the key, and transforms as needed.

If the key is nil or empty, raises ArgumentError. Whitespace characters are allowed for historical reasons, but likely shouldn’t be used. If the key (with namespace) is shorter than the memcached maximum allowed key length, just returns the argument key Otherwise computes a “truncated” key that uses a truncated prefix combined with a 32-byte hex digest of the whole key.

Raises:

  • (ArgumentError)


57
58
59
60
61
62
# File 'lib/dalli/key_manager.rb', line 57

def validate_key(key)
  raise ArgumentError, 'key cannot be blank' unless key&.length&.positive?

  key = key_with_namespace(key)
  key.length > MAX_KEY_LENGTH ? truncated_key(key) : key
end

#validate_namespace_separator_option(opts) ⇒ Object

Raises:

  • (ArgumentError)


96
97
98
99
100
101
102
# File 'lib/dalli/key_manager.rb', line 96

def validate_namespace_separator_option(opts)
  sep = opts[:namespace_separator]
  return if VALID_NAMESPACE_SEPARATORS.match?(sep)

  raise ArgumentError,
        'namespace_separator must be a single non-alphanumeric character (e.g., ":", "/", "|")'
end