dalli 2.7.0 → 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,5 +1,7 @@
1
- require 'digest/sha1'
2
- require 'zlib'
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+ require "zlib"
3
5
 
4
6
  module Dalli
5
7
  class Ring
@@ -15,12 +17,12 @@ module Dalli
15
17
  continuum = []
16
18
  servers.each do |server|
17
19
  entry_count_for(server, servers.size, total_weight).times do |idx|
18
- hash = Digest::SHA1.hexdigest("#{server.hostname}:#{server.port}:#{idx}")
20
+ hash = Digest::SHA1.hexdigest("#{server.name}:#{idx}")
19
21
  value = Integer("0x#{hash[0..7]}")
20
22
  continuum << Dalli::Ring::Entry.new(value, server)
21
23
  end
22
24
  end
23
- @continuum = continuum.sort { |a, b| a.value <=> b.value }
25
+ @continuum = continuum.sort_by(&:value)
24
26
  end
25
27
 
26
28
  threadsafe! unless options[:threadsafe] == false
@@ -31,7 +33,13 @@ module Dalli
31
33
  if @continuum
32
34
  hkey = hash_for(key)
33
35
  20.times do |try|
34
- 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
35
43
  server = @continuum[entryidx].server
36
44
  return server if server.alive?
37
45
  break unless @failover
@@ -39,18 +47,18 @@ module Dalli
39
47
  end
40
48
  else
41
49
  server = @servers.first
42
- return server if server && server.alive?
50
+ return server if server&.alive?
43
51
  end
44
52
 
45
53
  raise Dalli::RingError, "No server available"
46
54
  end
47
55
 
48
56
  def lock
49
- @servers.each { |s| s.lock! }
57
+ @servers.each(&:lock!)
50
58
  begin
51
- return yield
59
+ yield
52
60
  ensure
53
- @servers.each { |s| s.unlock! }
61
+ @servers.each(&:unlock!)
54
62
  end
55
63
  end
56
64
 
@@ -70,64 +78,6 @@ module Dalli
70
78
  ((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
71
79
  end
72
80
 
73
- # Native extension to perform the binary search within the continuum
74
- # space. Fallback to a pure Ruby version if the compilation doesn't work.
75
- # optional for performance and only necessary if you are using multiple
76
- # memcached servers.
77
- begin
78
- require 'inline'
79
- inline do |builder|
80
- builder.c <<-EOM
81
- int binary_search(VALUE ary, unsigned int r) {
82
- long upper = RARRAY_LEN(ary) - 1;
83
- long lower = 0;
84
- long idx = 0;
85
- ID value = rb_intern("value");
86
- VALUE continuumValue;
87
- unsigned int l;
88
-
89
- while (lower <= upper) {
90
- idx = (lower + upper) / 2;
91
-
92
- continuumValue = rb_funcall(RARRAY_PTR(ary)[idx], value, 0);
93
- l = NUM2UINT(continuumValue);
94
- if (l == r) {
95
- return idx;
96
- }
97
- else if (l > r) {
98
- upper = idx - 1;
99
- }
100
- else {
101
- lower = idx + 1;
102
- }
103
- }
104
- return upper;
105
- }
106
- EOM
107
- end
108
- rescue LoadError
109
- # Find the closest index in the Ring with value <= the given value
110
- def binary_search(ary, value)
111
- upper = ary.size - 1
112
- lower = 0
113
- idx = 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
- return 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