dalli 2.7.11 → 3.0.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ module Protocol
5
+ ##
6
+ # Dalli::Protocol::ServerConfigParser parses a server string passed to
7
+ # a Dalli::Protocol::Binary instance into the hostname, port, weight, and
8
+ # socket_type.
9
+ ##
10
+ class ServerConfigParser
11
+ # TODO: Revisit this, especially the IP/domain part. Likely
12
+ # can limit character set to LDH + '.'. Hex digit section
13
+ # appears to have been added to support IPv6, but as far as
14
+ # I can tell it doesn't work
15
+ SERVER_CONFIG_REGEXP = /\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z/.freeze
16
+
17
+ DEFAULT_PORT = 11_211
18
+ DEFAULT_WEIGHT = 1
19
+
20
+ def self.parse(str)
21
+ res = deconstruct_string(str)
22
+
23
+ hostname = normalize_hostname(str, res)
24
+ if hostname.start_with?('/')
25
+ socket_type = :unix
26
+ port, weight = attributes_for_unix_socket(res)
27
+ else
28
+ socket_type = :tcp
29
+ port, weight = attributes_for_tcp_socket(res)
30
+ end
31
+ [hostname, port, weight, socket_type]
32
+ end
33
+
34
+ def self.deconstruct_string(str)
35
+ mtch = str.match(SERVER_CONFIG_REGEXP)
36
+ raise Dalli::DalliError, "Could not parse hostname #{str}" if mtch.nil? || mtch[1] == '[]'
37
+
38
+ mtch
39
+ end
40
+
41
+ def self.attributes_for_unix_socket(res)
42
+ # in case of unix socket, allow only setting of weight, not port
43
+ raise Dalli::DalliError, "Could not parse hostname #{res[0]}" if res[4]
44
+
45
+ [nil, normalize_weight(res[3])]
46
+ end
47
+
48
+ def self.attributes_for_tcp_socket(res)
49
+ [normalize_port(res[3]), normalize_weight(res[4])]
50
+ end
51
+
52
+ def self.normalize_hostname(str, res)
53
+ raise Dalli::DalliError, "Could not parse hostname #{str}" if res.nil? || res[1] == '[]'
54
+
55
+ res[2] || res[1]
56
+ end
57
+
58
+ def self.normalize_port(port)
59
+ Integer(port || DEFAULT_PORT)
60
+ end
61
+
62
+ def self.normalize_weight(weight)
63
+ Integer(weight || DEFAULT_WEIGHT)
64
+ end
65
+ end
66
+ end
67
+ 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dalli
4
+ module Protocol
5
+ # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
6
+ class NilObject; end
7
+ NOT_FOUND = NilObject.new
8
+ end
9
+ end
data/lib/dalli/ring.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require 'digest/sha1'
3
- require 'zlib'
2
+
3
+ require "digest/sha1"
4
+ require "zlib"
4
5
 
5
6
  module Dalli
6
7
  class Ring
@@ -32,7 +33,13 @@ module Dalli
32
33
  if @continuum
33
34
  hkey = hash_for(key)
34
35
  20.times do |try|
35
- entryidx = binary_search(@continuum, hkey)
36
+ # Find the closest index in the Ring with value <= the given value
37
+ entryidx = @continuum.bsearch_index { |entry| entry.value > hkey }
38
+ if entryidx.nil?
39
+ entryidx = @continuum.size - 1
40
+ else
41
+ entryidx -= 1
42
+ end
36
43
  server = @continuum[entryidx].server
37
44
  return server if server.alive?
38
45
  break unless @failover
@@ -40,7 +47,7 @@ module Dalli
40
47
  end
41
48
  else
42
49
  server = @servers.first
43
- return server if server && server.alive?
50
+ return server if server&.alive?
44
51
  end
45
52
 
46
53
  raise Dalli::RingError, "No server available"
@@ -49,7 +56,7 @@ module Dalli
49
56
  def lock
50
57
  @servers.each(&:lock!)
51
58
  begin
52
- return yield
59
+ yield
53
60
  ensure
54
61
  @servers.each(&:unlock!)
55
62
  end
@@ -71,63 +78,6 @@ module Dalli
71
78
  ((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
72
79
  end
73
80
 
74
- # Native extension to perform the binary search within the continuum
75
- # space. Fallback to a pure Ruby version if the compilation doesn't work.
76
- # optional for performance and only necessary if you are using multiple
77
- # memcached servers.
78
- begin
79
- require 'inline'
80
- inline do |builder|
81
- builder.c <<-EOM
82
- int binary_search(VALUE ary, unsigned int r) {
83
- long upper = RARRAY_LEN(ary) - 1;
84
- long lower = 0;
85
- long idx = 0;
86
- ID value = rb_intern("value");
87
- VALUE continuumValue;
88
- unsigned int l;
89
-
90
- while (lower <= upper) {
91
- idx = (lower + upper) / 2;
92
-
93
- continuumValue = rb_funcall(RARRAY_PTR(ary)[idx], value, 0);
94
- l = NUM2UINT(continuumValue);
95
- if (l == r) {
96
- return idx;
97
- }
98
- else if (l > r) {
99
- upper = idx - 1;
100
- }
101
- else {
102
- lower = idx + 1;
103
- }
104
- }
105
- return upper;
106
- }
107
- EOM
108
- end
109
- rescue LoadError
110
- # Find the closest index in the Ring with value <= the given value
111
- def binary_search(ary, value)
112
- upper = ary.size - 1
113
- lower = 0
114
-
115
- while (lower <= upper) do
116
- idx = (lower + upper) / 2
117
- comp = ary[idx].value <=> value
118
-
119
- if comp == 0
120
- return idx
121
- elsif comp > 0
122
- upper = idx - 1
123
- else
124
- lower = idx + 1
125
- end
126
- end
127
- upper
128
- end
129
- end
130
-
131
81
  class Entry
132
82
  attr_reader :value
133
83
  attr_reader :server
@@ -137,6 +87,5 @@ module Dalli
137
87
  @server = srv
138
88
  end
139
89
  end
140
-
141
90
  end
142
91
  end