dalli 2.7.3 → 3.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +5 -5
  2. data/{History.md → CHANGELOG.md} +211 -0
  3. data/Gemfile +3 -6
  4. data/LICENSE +1 -1
  5. data/README.md +30 -208
  6. data/lib/dalli/cas/client.rb +2 -57
  7. data/lib/dalli/client.rb +254 -253
  8. data/lib/dalli/compressor.rb +13 -2
  9. data/lib/dalli/key_manager.rb +121 -0
  10. data/lib/dalli/options.rb +7 -7
  11. data/lib/dalli/pipelined_getter.rb +177 -0
  12. data/lib/dalli/protocol/base.rb +241 -0
  13. data/lib/dalli/protocol/binary/request_formatter.rb +117 -0
  14. data/lib/dalli/protocol/binary/response_header.rb +36 -0
  15. data/lib/dalli/protocol/binary/response_processor.rb +239 -0
  16. data/lib/dalli/protocol/binary/sasl_authentication.rb +60 -0
  17. data/lib/dalli/protocol/binary.rb +173 -0
  18. data/lib/dalli/protocol/connection_manager.rb +252 -0
  19. data/lib/dalli/protocol/meta/key_regularizer.rb +31 -0
  20. data/lib/dalli/protocol/meta/request_formatter.rb +121 -0
  21. data/lib/dalli/protocol/meta/response_processor.rb +211 -0
  22. data/lib/dalli/protocol/meta.rb +178 -0
  23. data/lib/dalli/protocol/response_buffer.rb +54 -0
  24. data/lib/dalli/protocol/server_config_parser.rb +86 -0
  25. data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
  26. data/lib/dalli/protocol/value_compressor.rb +85 -0
  27. data/lib/dalli/protocol/value_marshaller.rb +59 -0
  28. data/lib/dalli/protocol/value_serializer.rb +91 -0
  29. data/lib/dalli/protocol.rb +8 -0
  30. data/lib/dalli/ring.rb +97 -86
  31. data/lib/dalli/server.rb +4 -719
  32. data/lib/dalli/servers_arg_normalizer.rb +54 -0
  33. data/lib/dalli/socket.rb +123 -115
  34. data/lib/dalli/version.rb +5 -1
  35. data/lib/dalli.rb +45 -14
  36. data/lib/rack/session/dalli.rb +162 -42
  37. metadata +136 -63
  38. data/Performance.md +0 -42
  39. data/Rakefile +0 -43
  40. data/dalli.gemspec +0 -29
  41. data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -81
  42. data/lib/active_support/cache/dalli_store.rb +0 -372
  43. data/lib/dalli/railtie.rb +0 -7
  44. data/test/benchmark_test.rb +0 -243
  45. data/test/helper.rb +0 -56
  46. data/test/memcached_mock.rb +0 -201
  47. data/test/sasl/memcached.conf +0 -1
  48. data/test/sasl/sasldb +0 -1
  49. data/test/test_active_support.rb +0 -541
  50. data/test/test_cas_client.rb +0 -107
  51. data/test/test_compressor.rb +0 -52
  52. data/test/test_dalli.rb +0 -682
  53. data/test/test_encoding.rb +0 -32
  54. data/test/test_failover.rb +0 -137
  55. data/test/test_network.rb +0 -64
  56. data/test/test_rack_session.rb +0 -341
  57. data/test/test_ring.rb +0 -85
  58. data/test/test_sasl.rb +0 -105
  59. data/test/test_serializer.rb +0 -29
  60. data/test/test_server.rb +0 -110
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require 'socket'
5
+ require 'timeout'
6
+
7
+ module Dalli
8
+ module Protocol
9
+ ##
10
+ # Access point for a single Memcached server, accessed via Memcached's meta
11
+ # protocol. Contains logic for managing connection state to the server (retries, etc),
12
+ # formatting requests to the server, and unpacking responses.
13
+ ##
14
+ class Meta < Base
15
+ TERMINATOR = "\r\n"
16
+
17
+ def response_processor
18
+ @response_processor ||= ResponseProcessor.new(@connection_manager, @value_marshaller)
19
+ end
20
+
21
+ # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
22
+
23
+ private
24
+
25
+ # Retrieval Commands
26
+ def get(key, options = nil)
27
+ encoded_key, base64 = KeyRegularizer.encode(key)
28
+ req = RequestFormatter.meta_get(key: encoded_key, base64: base64)
29
+ write(req)
30
+ response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
31
+ end
32
+
33
+ def quiet_get_request(key)
34
+ encoded_key, base64 = KeyRegularizer.encode(key)
35
+ RequestFormatter.meta_get(key: encoded_key, return_cas: true, base64: base64, quiet: true)
36
+ end
37
+
38
+ def gat(key, ttl, options = nil)
39
+ ttl = TtlSanitizer.sanitize(ttl)
40
+ encoded_key, base64 = KeyRegularizer.encode(key)
41
+ req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64)
42
+ write(req)
43
+ response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
44
+ end
45
+
46
+ def touch(key, ttl)
47
+ ttl = TtlSanitizer.sanitize(ttl)
48
+ encoded_key, base64 = KeyRegularizer.encode(key)
49
+ req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
50
+ write(req)
51
+ response_processor.meta_get_without_value
52
+ end
53
+
54
+ # TODO: This is confusing, as there's a cas command in memcached
55
+ # and this isn't it. Maybe rename? Maybe eliminate?
56
+ def cas(key)
57
+ encoded_key, base64 = KeyRegularizer.encode(key)
58
+ req = RequestFormatter.meta_get(key: encoded_key, value: true, return_cas: true, base64: base64)
59
+ write(req)
60
+ response_processor.meta_get_with_value_and_cas
61
+ end
62
+
63
+ # Storage Commands
64
+ def set(key, value, ttl, cas, options)
65
+ write_storage_req(:set, key, value, ttl, cas, options)
66
+ response_processor.meta_set_with_cas unless quiet?
67
+ end
68
+
69
+ def add(key, value, ttl, options)
70
+ write_storage_req(:add, key, value, ttl, nil, options)
71
+ response_processor.meta_set_with_cas unless quiet?
72
+ end
73
+
74
+ def replace(key, value, ttl, cas, options)
75
+ write_storage_req(:replace, key, value, ttl, cas, options)
76
+ response_processor.meta_set_with_cas unless quiet?
77
+ end
78
+
79
+ # rubocop:disable Metrics/ParameterLists
80
+ def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {})
81
+ (value, bitflags) = @value_marshaller.store(key, raw_value, options)
82
+ ttl = TtlSanitizer.sanitize(ttl) if ttl
83
+ encoded_key, base64 = KeyRegularizer.encode(key)
84
+ req = RequestFormatter.meta_set(key: encoded_key, value: value,
85
+ bitflags: bitflags, cas: cas,
86
+ ttl: ttl, mode: mode, quiet: quiet?, base64: base64)
87
+ write(req)
88
+ end
89
+ # rubocop:enable Metrics/ParameterLists
90
+
91
+ def append(key, value)
92
+ write_append_prepend_req(:append, key, value)
93
+ response_processor.meta_set_append_prepend unless quiet?
94
+ end
95
+
96
+ def prepend(key, value)
97
+ write_append_prepend_req(:prepend, key, value)
98
+ response_processor.meta_set_append_prepend unless quiet?
99
+ end
100
+
101
+ # rubocop:disable Metrics/ParameterLists
102
+ def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, _options = {})
103
+ ttl = TtlSanitizer.sanitize(ttl) if ttl
104
+ encoded_key, base64 = KeyRegularizer.encode(key)
105
+ req = RequestFormatter.meta_set(key: encoded_key, value: value, base64: base64,
106
+ cas: cas, ttl: ttl, mode: mode, quiet: quiet?)
107
+ write(req)
108
+ end
109
+ # rubocop:enable Metrics/ParameterLists
110
+
111
+ # Delete Commands
112
+ def delete(key, cas)
113
+ encoded_key, base64 = KeyRegularizer.encode(key)
114
+ req = RequestFormatter.meta_delete(key: encoded_key, cas: cas,
115
+ base64: base64, quiet: quiet?)
116
+ write(req)
117
+ response_processor.meta_delete unless quiet?
118
+ end
119
+
120
+ # Arithmetic Commands
121
+ def decr(key, count, ttl, initial)
122
+ decr_incr false, key, count, ttl, initial
123
+ end
124
+
125
+ def incr(key, count, ttl, initial)
126
+ decr_incr true, key, count, ttl, initial
127
+ end
128
+
129
+ def decr_incr(incr, key, delta, ttl, initial)
130
+ ttl = initial ? TtlSanitizer.sanitize(ttl) : nil # Only set a TTL if we want to set a value on miss
131
+ encoded_key, base64 = KeyRegularizer.encode(key)
132
+ write(RequestFormatter.meta_arithmetic(key: encoded_key, delta: delta, initial: initial, incr: incr, ttl: ttl,
133
+ quiet: quiet?, base64: base64))
134
+ response_processor.decr_incr unless quiet?
135
+ end
136
+
137
+ # Other Commands
138
+ def flush(delay = 0)
139
+ write(RequestFormatter.flush(delay: delay))
140
+ response_processor.flush unless quiet?
141
+ end
142
+
143
+ # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
144
+ # We need to read all the responses at once.
145
+ def noop
146
+ write_noop
147
+ response_processor.consume_all_responses_until_mn
148
+ end
149
+
150
+ def stats(info = nil)
151
+ write(RequestFormatter.stats(info))
152
+ response_processor.stats
153
+ end
154
+
155
+ def reset_stats
156
+ write(RequestFormatter.stats('reset'))
157
+ response_processor.reset
158
+ end
159
+
160
+ def version
161
+ write(RequestFormatter.version)
162
+ response_processor.version
163
+ end
164
+
165
+ def write_noop
166
+ write(RequestFormatter.meta_noop)
167
+ end
168
+
169
+ def authenticate_connection
170
+ raise Dalli::DalliError, 'Authentication not supported for the meta protocol.'
171
+ end
172
+
173
+ require_relative 'meta/key_regularizer'
174
+ require_relative 'meta/request_formatter'
175
+ require_relative 'meta/response_processor'
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'timeout'
5
+
6
+ module Dalli
7
+ module Protocol
8
+ ##
9
+ # Manages the buffer for responses from memcached.
10
+ ##
11
+ class ResponseBuffer
12
+ def initialize(io_source, response_processor)
13
+ @io_source = io_source
14
+ @response_processor = response_processor
15
+ @buffer = nil
16
+ end
17
+
18
+ def read
19
+ @buffer << @io_source.read_nonblock
20
+ end
21
+
22
+ # Attempts to process a single response from the buffer. Starts
23
+ # by advancing the buffer to the specified start position
24
+ def process_single_getk_response
25
+ bytes, status, cas, key, value = @response_processor.getk_response_from_buffer(@buffer)
26
+ advance(bytes)
27
+ [status, cas, key, value]
28
+ end
29
+
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
+ # Resets the internal buffer to an empty state,
39
+ # so that we're ready to read pipelined responses
40
+ def reset
41
+ @buffer = ''.b
42
+ end
43
+
44
+ # Clear the internal response buffer
45
+ def clear
46
+ @buffer = nil
47
+ end
48
+
49
+ def in_progress?
50
+ !@buffer.nil?
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Dalli
6
+ module Protocol
7
+ ##
8
+ # Dalli::Protocol::ServerConfigParser parses a server string passed to
9
+ # a Dalli::Protocol::Binary instance into the hostname, port, weight, and
10
+ # socket_type.
11
+ ##
12
+ class ServerConfigParser
13
+ MEMCACHED_URI_PROTOCOL = 'memcached://'
14
+
15
+ # TODO: Revisit this, especially the IP/domain part. Likely
16
+ # can limit character set to LDH + '.'. Hex digit section
17
+ # is there to support IPv6 addresses, which need to be specified with
18
+ # a bounding []
19
+ SERVER_CONFIG_REGEXP = /\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z/.freeze
20
+
21
+ DEFAULT_PORT = 11_211
22
+ DEFAULT_WEIGHT = 1
23
+
24
+ def self.parse(str)
25
+ return parse_non_uri(str) unless str.start_with?(MEMCACHED_URI_PROTOCOL)
26
+
27
+ parse_uri(str)
28
+ end
29
+
30
+ def self.parse_uri(str)
31
+ uri = URI.parse(str)
32
+ auth_details = {
33
+ username: uri.user,
34
+ password: uri.password
35
+ }
36
+ [uri.host, normalize_port(uri.port), :tcp, DEFAULT_WEIGHT, auth_details]
37
+ end
38
+
39
+ def self.parse_non_uri(str)
40
+ res = deconstruct_string(str)
41
+
42
+ hostname = normalize_host_from_match(str, res)
43
+ if hostname.start_with?('/')
44
+ socket_type = :unix
45
+ port, weight = attributes_for_unix_socket(res)
46
+ else
47
+ socket_type = :tcp
48
+ port, weight = attributes_for_tcp_socket(res)
49
+ end
50
+ [hostname, port, socket_type, weight, {}]
51
+ end
52
+
53
+ def self.deconstruct_string(str)
54
+ mtch = str.match(SERVER_CONFIG_REGEXP)
55
+ raise Dalli::DalliError, "Could not parse hostname #{str}" if mtch.nil? || mtch[1] == '[]'
56
+
57
+ mtch
58
+ end
59
+
60
+ def self.attributes_for_unix_socket(res)
61
+ # in case of unix socket, allow only setting of weight, not port
62
+ raise Dalli::DalliError, "Could not parse hostname #{res[0]}" if res[4]
63
+
64
+ [nil, normalize_weight(res[3])]
65
+ end
66
+
67
+ def self.attributes_for_tcp_socket(res)
68
+ [normalize_port(res[3]), normalize_weight(res[4])]
69
+ end
70
+
71
+ def self.normalize_host_from_match(str, res)
72
+ raise Dalli::DalliError, "Could not parse hostname #{str}" if res.nil? || res[1] == '[]'
73
+
74
+ res[2] || res[1]
75
+ end
76
+
77
+ def self.normalize_port(port)
78
+ Integer(port || DEFAULT_PORT)
79
+ end
80
+
81
+ def self.normalize_weight(weight)
82
+ Integer(weight || DEFAULT_WEIGHT)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ module Protocol
5
+ ##
6
+ # Utility class for sanitizing TTL arguments based on Memcached rules.
7
+ # TTLs are either expirations times in seconds (with a maximum value of
8
+ # 30 days) or expiration timestamps. This class sanitizes TTLs to ensure
9
+ # they meet those restrictions.
10
+ ##
11
+ class TtlSanitizer
12
+ # https://github.com/memcached/memcached/blob/master/doc/protocol.txt#L79
13
+ # > An expiration time, in seconds. Can be up to 30 days. After 30 days, is
14
+ # treated as a unix timestamp of an exact date.
15
+ MAX_ACCEPTABLE_EXPIRATION_INTERVAL = 30 * 24 * 60 * 60 # 30 days
16
+
17
+ # Ensures the TTL passed to Memcached is a valid TTL in the expected format.
18
+ def self.sanitize(ttl)
19
+ ttl_as_i = ttl.to_i
20
+ return ttl_as_i if less_than_max_expiration_interval?(ttl_as_i)
21
+
22
+ as_timestamp(ttl_as_i)
23
+ end
24
+
25
+ def self.less_than_max_expiration_interval?(ttl_as_i)
26
+ ttl_as_i <= MAX_ACCEPTABLE_EXPIRATION_INTERVAL
27
+ end
28
+
29
+ def self.as_timestamp(ttl_as_i)
30
+ now = current_timestamp
31
+ return ttl_as_i if ttl_as_i > now # Already a timestamp
32
+
33
+ Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached " \
34
+ 'and too short to be a future timestamp,' \
35
+ 'converting to an expiration timestamp'
36
+ now + ttl_as_i
37
+ end
38
+
39
+ # Pulled out into a method so it's easy to stub time
40
+ def self.current_timestamp
41
+ Time.now.to_i
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
5
+ module Dalli
6
+ module Protocol
7
+ ##
8
+ # Dalli::Protocol::ValueCompressor compartmentalizes the logic for managing
9
+ # compression and decompression of stored values. It manages interpreting
10
+ # relevant options from both client and request, determining whether to
11
+ # compress/decompress on store/retrieve, and processes bitflags as necessary.
12
+ ##
13
+ class ValueCompressor
14
+ DEFAULTS = {
15
+ compress: true,
16
+ compressor: ::Dalli::Compressor,
17
+ # min byte size to attempt compression
18
+ compression_min_size: 4 * 1024 # 4K
19
+ }.freeze
20
+
21
+ OPTIONS = DEFAULTS.keys.freeze
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
+ 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
+ @compression_options =
38
+ DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
39
+ end
40
+
41
+ def store(value, req_options, bitflags)
42
+ do_compress = compress_value?(value, req_options)
43
+ store_value = do_compress ? compressor.compress(value) : value
44
+ bitflags |= FLAG_COMPRESSED if do_compress
45
+
46
+ [store_value, bitflags]
47
+ end
48
+
49
+ def retrieve(value, bitflags)
50
+ compressed = (bitflags & FLAG_COMPRESSED) != 0
51
+ compressed ? compressor.decompress(value) : value
52
+
53
+ # TODO: We likely want to move this rescue into the Dalli::Compressor / Dalli::GzipCompressor
54
+ # itself, since not all compressors necessarily use Zlib. For now keep it here, so the behavior
55
+ # of custom compressors doesn't change.
56
+ rescue Zlib::Error
57
+ raise UnmarshalError, "Unable to uncompress value: #{$ERROR_INFO.message}"
58
+ end
59
+
60
+ def compress_by_default?
61
+ @compression_options[:compress]
62
+ end
63
+
64
+ def compressor
65
+ @compression_options[:compressor]
66
+ end
67
+
68
+ def compression_min_size
69
+ @compression_options[:compression_min_size]
70
+ end
71
+
72
+ # Checks whether we should apply compression when serializing a value
73
+ # based on the specified options. Returns false unless the value
74
+ # is greater than the minimum compression size. Otherwise returns
75
+ # based on a method-level option if specified, falling back to the
76
+ # server default.
77
+ def compress_value?(value, req_options)
78
+ return false unless value.bytesize >= compression_min_size
79
+ return compress_by_default? unless req_options && !req_options[:compress].nil?
80
+
81
+ req_options[:compress]
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Dalli
6
+ module Protocol
7
+ ##
8
+ # Dalli::Protocol::ValueMarshaller compartmentalizes the logic for marshalling
9
+ # and unmarshalling unstructured data (values) to Memcached. It also enforces
10
+ # limits on the maximum size of marshalled data.
11
+ ##
12
+ class ValueMarshaller
13
+ extend Forwardable
14
+
15
+ DEFAULTS = {
16
+ # max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
17
+ value_max_bytes: 1024 * 1024
18
+ }.freeze
19
+
20
+ OPTIONS = DEFAULTS.keys.freeze
21
+
22
+ def_delegators :@value_serializer, :serializer
23
+ def_delegators :@value_compressor, :compressor, :compression_min_size, :compress_by_default?
24
+
25
+ def initialize(client_options)
26
+ @value_serializer = ValueSerializer.new(client_options)
27
+ @value_compressor = ValueCompressor.new(client_options)
28
+
29
+ @marshal_options =
30
+ DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
31
+ end
32
+
33
+ def store(key, value, options = nil)
34
+ bitflags = 0
35
+ value, bitflags = @value_serializer.store(value, options, bitflags)
36
+ value, bitflags = @value_compressor.store(value, options, bitflags)
37
+
38
+ error_if_over_max_value_bytes(key, value)
39
+ [value, bitflags]
40
+ end
41
+
42
+ def retrieve(value, flags)
43
+ value = @value_compressor.retrieve(value, flags)
44
+ @value_serializer.retrieve(value, flags)
45
+ end
46
+
47
+ def value_max_bytes
48
+ @marshal_options[:value_max_bytes]
49
+ end
50
+
51
+ def error_if_over_max_value_bytes(key, value)
52
+ return if value.bytesize <= value_max_bytes
53
+
54
+ message = "Value for #{key} over max size: #{value_max_bytes} <= #{value.bytesize}"
55
+ raise Dalli::ValueOverMaxSize, message
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ module Protocol
5
+ ##
6
+ # Dalli::Protocol::ValueSerializer compartmentalizes the logic for managing
7
+ # serialization and deserialization of stored values. It manages interpreting
8
+ # relevant options from both client and request, determining whether to
9
+ # serialize/deserialize on store/retrieve, and processes bitflags as necessary.
10
+ ##
11
+ class ValueSerializer
12
+ DEFAULTS = {
13
+ serializer: Marshal
14
+ }.freeze
15
+
16
+ OPTIONS = DEFAULTS.keys.freeze
17
+
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
21
+
22
+ attr_accessor :serialization_options
23
+
24
+ def initialize(protocol_options)
25
+ @serialization_options =
26
+ DEFAULTS.merge(protocol_options.select { |k, _| OPTIONS.include?(k) })
27
+ end
28
+
29
+ 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)
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}"
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}"
72
+ end
73
+
74
+ def serializer
75
+ @serialization_options[:serializer]
76
+ end
77
+
78
+ def serialize_value(value)
79
+ serializer.dump(value)
80
+ rescue Timeout::Error => e
81
+ raise e
82
+ rescue StandardError => e
83
+ # Serializing can throw several different types of generic Ruby exceptions.
84
+ # Convert to a specific exception so we can special case it higher up the stack.
85
+ exc = Dalli::MarshalError.new(e.message)
86
+ exc.set_backtrace e.backtrace
87
+ raise exc
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ module Protocol
5
+ # Preserved for backwards compatibility. Should be removed in 4.0
6
+ NOT_FOUND = ::Dalli::NOT_FOUND
7
+ end
8
+ end