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.
- checksums.yaml +5 -5
- data/Gemfile +5 -7
- data/History.md +151 -0
- data/LICENSE +1 -1
- data/README.md +56 -102
- data/lib/dalli/cas/client.rb +1 -58
- data/lib/dalli/client.rb +263 -139
- data/lib/dalli/compressor.rb +5 -3
- data/lib/dalli/options.rb +4 -4
- data/lib/dalli/protocol/binary.rb +703 -0
- data/lib/dalli/protocol/server_config_parser.rb +67 -0
- data/lib/dalli/protocol/ttl_sanitizer.rb +45 -0
- data/lib/dalli/protocol/value_compressor.rb +85 -0
- data/lib/dalli/protocol.rb +9 -0
- data/lib/dalli/ring.rb +17 -68
- data/lib/dalli/server.rb +3 -689
- data/lib/dalli/socket.rb +79 -83
- data/lib/dalli/version.rb +3 -1
- data/lib/dalli.rb +20 -16
- data/lib/rack/session/dalli.rb +124 -30
- metadata +33 -77
- data/Performance.md +0 -42
- data/Rakefile +0 -42
- data/dalli.gemspec +0 -29
- data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -81
- data/lib/active_support/cache/dalli_store.rb +0 -361
- data/lib/dalli/railtie.rb +0 -7
- data/test/benchmark_test.rb +0 -242
- data/test/helper.rb +0 -55
- data/test/memcached_mock.rb +0 -121
- data/test/sasldb +0 -1
- data/test/test_active_support.rb +0 -427
- data/test/test_cas_client.rb +0 -107
- data/test/test_compressor.rb +0 -53
- data/test/test_dalli.rb +0 -601
- data/test/test_encoding.rb +0 -32
- data/test/test_failover.rb +0 -128
- data/test/test_network.rb +0 -54
- data/test/test_rack_session.rb +0 -321
- data/test/test_ring.rb +0 -85
- data/test/test_sasl.rb +0 -110
- data/test/test_serializer.rb +0 -30
@@ -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
|
data/lib/dalli/ring.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
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.
|
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.
|
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
|
-
|
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
|
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
|
57
|
+
@servers.each(&:lock!)
|
50
58
|
begin
|
51
|
-
|
59
|
+
yield
|
52
60
|
ensure
|
53
|
-
@servers.each
|
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
|