legion-cache 1.3.21 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -0
- data/legion-cache.gemspec +1 -0
- data/lib/legion/cache/async_writer.rb +126 -0
- data/lib/legion/cache/cacheable.rb +4 -4
- data/lib/legion/cache/helper.rb +11 -11
- data/lib/legion/cache/local.rb +91 -29
- data/lib/legion/cache/memcached.rb +53 -40
- data/lib/legion/cache/memory.rb +107 -19
- data/lib/legion/cache/pool.rb +4 -1
- data/lib/legion/cache/reconnector.rb +108 -0
- data/lib/legion/cache/redis.rb +108 -47
- data/lib/legion/cache/redis_hash.rb +8 -8
- data/lib/legion/cache/settings.rb +52 -39
- data/lib/legion/cache/version.rb +1 -1
- data/lib/legion/cache.rb +310 -70
- metadata +17 -1
data/lib/legion/cache/redis.rb
CHANGED
|
@@ -13,24 +13,37 @@ module Legion
|
|
|
13
13
|
extend self
|
|
14
14
|
extend Legion::Logging::Helper
|
|
15
15
|
|
|
16
|
-
def client(
|
|
17
|
-
logger: nil,
|
|
18
|
-
fixed_hostname: nil, username: nil, password: nil, db: nil, reconnect_attempts: [0, 0.5, 1], **)
|
|
16
|
+
def client(server: nil, servers: [], pool_size: nil, timeout: nil, # rubocop:disable Metrics/ParameterLists
|
|
17
|
+
username: nil, password: nil, logger: nil, **opts)
|
|
19
18
|
return @client unless @client.nil?
|
|
20
19
|
|
|
20
|
+
settings = defined?(Legion::Settings) ? Legion::Settings[:cache] : {}
|
|
21
|
+
pool_size ||= settings[:pool_size] || 10
|
|
22
|
+
timeout ||= settings[:timeout] || 5
|
|
23
|
+
|
|
24
|
+
cluster = opts.delete(:cluster)
|
|
25
|
+
replica = opts.delete(:replica) || false
|
|
26
|
+
fixed_hostname = opts.delete(:fixed_hostname)
|
|
27
|
+
db = opts.delete(:db)
|
|
28
|
+
reconnect_attempts = opts.delete(:reconnect_attempts) || [0, 0.5, 1]
|
|
29
|
+
|
|
21
30
|
@pool_size = pool_size
|
|
22
31
|
@timeout = timeout
|
|
23
32
|
@cluster_mode = Array(cluster).compact.any?
|
|
24
33
|
@component_logger = logger || log
|
|
25
34
|
|
|
26
|
-
@
|
|
35
|
+
@connection_opts = { username: username, password: password, timeout: @timeout }.compact
|
|
36
|
+
@connection_opts.merge!(redis_tls_options(port: resolve_primary_port(server: server, servers: servers, cluster: cluster)))
|
|
37
|
+
|
|
38
|
+
checkout_timeout = opts[:pool_checkout_timeout] || settings[:pool_checkout_timeout] || @timeout
|
|
39
|
+
@client = ConnectionPool.new(size: pool_size, timeout: checkout_timeout) do
|
|
27
40
|
build_redis_client(server: server, servers: servers, cluster: cluster,
|
|
28
41
|
replica: replica, fixed_hostname: fixed_hostname,
|
|
29
42
|
username: username, password: password, db: db,
|
|
30
43
|
reconnect_attempts: reconnect_attempts)
|
|
31
44
|
end
|
|
32
45
|
@connected = true
|
|
33
|
-
|
|
46
|
+
log.info "Redis connected to #{resolved_redis_address(server: server, servers: servers, cluster: cluster)}"
|
|
34
47
|
@client
|
|
35
48
|
rescue StandardError => e
|
|
36
49
|
handle_exception(e, level: :error, handled: false, operation: :redis_client,
|
|
@@ -69,40 +82,51 @@ module Legion
|
|
|
69
82
|
end
|
|
70
83
|
|
|
71
84
|
def get(key)
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
raw = client.with { |conn| conn.get(key) }
|
|
86
|
+
result = deserialize_value(raw)
|
|
87
|
+
log.debug { "[cache] GET #{key} hit=#{!result.nil?}" }
|
|
74
88
|
result
|
|
75
|
-
rescue
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
handle_exception(e, level: :warn, handled: true, operation: :redis_get, key: key)
|
|
91
|
+
nil
|
|
78
92
|
end
|
|
79
93
|
|
|
80
|
-
def fetch(key, ttl
|
|
94
|
+
def fetch(key, ttl: nil)
|
|
81
95
|
result = get(key)
|
|
82
96
|
return result unless result.nil? && block_given?
|
|
83
97
|
|
|
84
98
|
result = yield
|
|
85
|
-
set(key, result, ttl)
|
|
99
|
+
set(key, result, ttl: ttl)
|
|
86
100
|
result
|
|
87
101
|
end
|
|
88
102
|
|
|
89
|
-
def set(key, value, ttl
|
|
103
|
+
def set(key, value, ttl: nil, **)
|
|
104
|
+
set_sync(key, value, ttl: ttl, **)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def set_sync(key, value, ttl: nil, **)
|
|
108
|
+
effective_ttl = ttl || default_ttl
|
|
90
109
|
args = {}
|
|
91
|
-
args[:ex] =
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
args[:ex] = effective_ttl unless effective_ttl.nil?
|
|
111
|
+
serialized = serialize_value(value)
|
|
112
|
+
result = client.with { |conn| conn.set(key, serialized, **args) == 'OK' }
|
|
113
|
+
log.debug { "[cache] SET #{key} ttl=#{effective_ttl.inspect} success=#{result}" }
|
|
94
114
|
result
|
|
95
|
-
rescue
|
|
96
|
-
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
handle_exception(e, level: :error, handled: false, operation: :redis_set_sync, key: key, ttl: effective_ttl)
|
|
97
117
|
raise
|
|
98
118
|
end
|
|
99
119
|
|
|
100
|
-
def delete(key)
|
|
120
|
+
def delete(key, **)
|
|
121
|
+
delete_sync(key)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def delete_sync(key)
|
|
101
125
|
result = client.with { |conn| conn.del(key) == 1 }
|
|
102
|
-
|
|
126
|
+
log.debug { "[cache] DELETE #{key} success=#{result}" }
|
|
103
127
|
result
|
|
104
|
-
rescue
|
|
105
|
-
|
|
128
|
+
rescue StandardError => e
|
|
129
|
+
handle_exception(e, level: :error, handled: false, operation: :redis_delete_sync, key: key)
|
|
106
130
|
raise
|
|
107
131
|
end
|
|
108
132
|
|
|
@@ -114,11 +138,11 @@ module Legion
|
|
|
114
138
|
conn.flushdb == 'OK'
|
|
115
139
|
end
|
|
116
140
|
end
|
|
117
|
-
|
|
141
|
+
log.debug { '[cache] FLUSH completed' }
|
|
118
142
|
result
|
|
119
|
-
rescue
|
|
120
|
-
|
|
121
|
-
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
handle_exception(e, level: :warn, handled: true, operation: :redis_flush)
|
|
145
|
+
nil
|
|
122
146
|
end
|
|
123
147
|
|
|
124
148
|
def mget(*keys)
|
|
@@ -133,34 +157,64 @@ module Legion
|
|
|
133
157
|
keys.zip(values).to_h
|
|
134
158
|
end
|
|
135
159
|
end
|
|
136
|
-
|
|
160
|
+
result = result.transform_values { |v| deserialize_value(v) }
|
|
161
|
+
log.debug { "[cache] MGET keys=#{keys.size}" }
|
|
137
162
|
result
|
|
138
|
-
rescue
|
|
139
|
-
|
|
140
|
-
|
|
163
|
+
rescue StandardError => e
|
|
164
|
+
handle_exception(e, level: :warn, handled: true, operation: :redis_mget, key_count: keys.size)
|
|
165
|
+
{}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def mset(hash, ttl: nil, **)
|
|
169
|
+
mset_sync(hash, ttl: ttl)
|
|
141
170
|
end
|
|
142
171
|
|
|
143
|
-
def
|
|
172
|
+
def mset_sync(hash, ttl: nil, **)
|
|
144
173
|
return true if hash.empty?
|
|
145
174
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
conn.mset(*hash.flatten) == 'OK'
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
cache_logger.debug "[cache] MSET keys=#{hash.size}"
|
|
154
|
-
result
|
|
155
|
-
rescue ::Redis::BaseError => e
|
|
156
|
-
log_cluster_error('redis_mset', e, key_count: hash.size)
|
|
175
|
+
hash.each { |key, value| set_sync(key, value, ttl: ttl) }
|
|
176
|
+
true
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
handle_exception(e, level: :error, handled: false, operation: :redis_mset_sync, key_count: hash.size)
|
|
157
179
|
raise
|
|
158
180
|
end
|
|
159
181
|
|
|
182
|
+
SERIALIZE_STRING = "S\x00".b.freeze
|
|
183
|
+
SERIALIZE_JSON = "J\x00".b.freeze
|
|
184
|
+
|
|
160
185
|
private
|
|
161
186
|
|
|
162
|
-
def
|
|
163
|
-
|
|
187
|
+
def serialize_value(value)
|
|
188
|
+
case value
|
|
189
|
+
when String
|
|
190
|
+
"#{SERIALIZE_STRING}#{value}"
|
|
191
|
+
else
|
|
192
|
+
"#{SERIALIZE_JSON}#{Legion::JSON.dump(value)}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def deserialize_value(raw)
|
|
197
|
+
return nil if raw.nil?
|
|
198
|
+
|
|
199
|
+
raw = raw.b if raw.respond_to?(:b)
|
|
200
|
+
if raw.start_with?(SERIALIZE_JSON)
|
|
201
|
+
Legion::JSON.load(raw.byteslice(2..))
|
|
202
|
+
elsif raw.start_with?(SERIALIZE_STRING)
|
|
203
|
+
raw.byteslice(2..)
|
|
204
|
+
else
|
|
205
|
+
raw # legacy data, no prefix
|
|
206
|
+
end
|
|
207
|
+
rescue StandardError => e
|
|
208
|
+
handle_exception(e, level: :warn, handled: true, operation: :redis_deserialize)
|
|
209
|
+
raw
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def default_ttl
|
|
213
|
+
return 3600 unless defined?(Legion::Settings)
|
|
214
|
+
|
|
215
|
+
Legion::Settings.dig(:cache, :default_ttl) || 3600
|
|
216
|
+
rescue StandardError
|
|
217
|
+
3600
|
|
164
218
|
end
|
|
165
219
|
|
|
166
220
|
def cluster_mget(conn, keys)
|
|
@@ -187,7 +241,7 @@ module Legion
|
|
|
187
241
|
primaries = node_info.lines.select { |l| l.include?('master') }.map { |l| l.split[1].split('@').first }
|
|
188
242
|
primaries.each do |addr|
|
|
189
243
|
host, port = Legion::Cache::Settings.parse_server_address(addr, default_port: 6379)
|
|
190
|
-
node = ::Redis.new(host: host, port: port.to_i)
|
|
244
|
+
node = ::Redis.new(host: host, port: port.to_i, **(@connection_opts || {}))
|
|
191
245
|
node.flushdb
|
|
192
246
|
node.close
|
|
193
247
|
end
|
|
@@ -215,8 +269,15 @@ module Legion
|
|
|
215
269
|
'unknown'
|
|
216
270
|
end
|
|
217
271
|
|
|
218
|
-
def
|
|
219
|
-
|
|
272
|
+
def resolve_primary_port(server: nil, servers: [], cluster: nil)
|
|
273
|
+
nodes = Array(cluster).compact
|
|
274
|
+
return 6379 if nodes.any?
|
|
275
|
+
|
|
276
|
+
resolved = Legion::Cache::Settings.resolve_servers(driver: 'redis', server: server, servers: Array(servers))
|
|
277
|
+
_, port = Legion::Cache::Settings.parse_server_address(resolved.first, default_port: 6379)
|
|
278
|
+
port.to_i
|
|
279
|
+
rescue StandardError
|
|
280
|
+
6379
|
|
220
281
|
end
|
|
221
282
|
|
|
222
283
|
def redis_tls_options(port:)
|
|
@@ -11,7 +11,7 @@ module Legion
|
|
|
11
11
|
|
|
12
12
|
# Returns true when the Redis driver is loaded and the connection pool is live.
|
|
13
13
|
def redis_available?
|
|
14
|
-
pool = Legion::Cache.
|
|
14
|
+
pool = Legion::Cache.pool
|
|
15
15
|
return false if pool.nil?
|
|
16
16
|
return false unless Legion::Cache.respond_to?(:driver_name) && Legion::Cache.driver_name == 'redis'
|
|
17
17
|
|
|
@@ -26,7 +26,7 @@ module Legion
|
|
|
26
26
|
def hset(key, hash)
|
|
27
27
|
return false unless redis_available?
|
|
28
28
|
|
|
29
|
-
Legion::Cache.
|
|
29
|
+
Legion::Cache.pool.with do |conn|
|
|
30
30
|
flat = hash.flat_map { |k, v| [k.to_s, v.to_s] }
|
|
31
31
|
conn.hset(key, *flat)
|
|
32
32
|
end
|
|
@@ -41,7 +41,7 @@ module Legion
|
|
|
41
41
|
def hgetall(key)
|
|
42
42
|
return nil unless redis_available?
|
|
43
43
|
|
|
44
|
-
result = Legion::Cache.
|
|
44
|
+
result = Legion::Cache.pool.with do |conn|
|
|
45
45
|
conn.hgetall(key)
|
|
46
46
|
end
|
|
47
47
|
log.debug "[cache:redis_hash] HGETALL #{key} fields=#{result.size}"
|
|
@@ -55,7 +55,7 @@ module Legion
|
|
|
55
55
|
def hdel(key, *fields)
|
|
56
56
|
return 0 unless redis_available?
|
|
57
57
|
|
|
58
|
-
result = Legion::Cache.
|
|
58
|
+
result = Legion::Cache.pool.with do |conn|
|
|
59
59
|
conn.hdel(key, *fields)
|
|
60
60
|
end
|
|
61
61
|
log.debug "[cache:redis_hash] HDEL #{key} fields=#{fields.size} removed=#{result}"
|
|
@@ -69,7 +69,7 @@ module Legion
|
|
|
69
69
|
def zadd(key, score, member)
|
|
70
70
|
return false unless redis_available?
|
|
71
71
|
|
|
72
|
-
Legion::Cache.
|
|
72
|
+
Legion::Cache.pool.with do |conn|
|
|
73
73
|
conn.zadd(key, score.to_f, member.to_s)
|
|
74
74
|
end
|
|
75
75
|
log.debug "[cache:redis_hash] ZADD #{key} member=#{member}"
|
|
@@ -87,7 +87,7 @@ module Legion
|
|
|
87
87
|
opts = {}
|
|
88
88
|
opts[:limit] = limit if limit
|
|
89
89
|
|
|
90
|
-
result = Legion::Cache.
|
|
90
|
+
result = Legion::Cache.pool.with do |conn|
|
|
91
91
|
conn.zrangebyscore(key, min, max, **opts)
|
|
92
92
|
end
|
|
93
93
|
log.debug "[cache:redis_hash] ZRANGEBYSCORE #{key} results=#{result.size}"
|
|
@@ -101,7 +101,7 @@ module Legion
|
|
|
101
101
|
def zrem(key, member)
|
|
102
102
|
return false unless redis_available?
|
|
103
103
|
|
|
104
|
-
Legion::Cache.
|
|
104
|
+
Legion::Cache.pool.with do |conn|
|
|
105
105
|
conn.zrem(key, member.to_s)
|
|
106
106
|
end
|
|
107
107
|
log.debug "[cache:redis_hash] ZREM #{key} member=#{member}"
|
|
@@ -115,7 +115,7 @@ module Legion
|
|
|
115
115
|
def expire(key, seconds)
|
|
116
116
|
return false unless redis_available?
|
|
117
117
|
|
|
118
|
-
result = Legion::Cache.
|
|
118
|
+
result = Legion::Cache.pool.with do |conn|
|
|
119
119
|
conn.expire(key, seconds.to_i) == 1
|
|
120
120
|
end
|
|
121
121
|
log.debug "[cache:redis_hash] EXPIRE #{key} seconds=#{seconds} success=#{result}"
|
|
@@ -19,50 +19,63 @@ module Legion
|
|
|
19
19
|
|
|
20
20
|
def self.default
|
|
21
21
|
{
|
|
22
|
-
driver:
|
|
23
|
-
servers:
|
|
24
|
-
connected:
|
|
25
|
-
enabled:
|
|
26
|
-
namespace:
|
|
27
|
-
compress:
|
|
28
|
-
failover:
|
|
29
|
-
threadsafe:
|
|
30
|
-
expires_in:
|
|
31
|
-
cache_nils:
|
|
32
|
-
pool_size:
|
|
33
|
-
timeout:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
22
|
+
driver: driver,
|
|
23
|
+
servers: [],
|
|
24
|
+
connected: false,
|
|
25
|
+
enabled: true,
|
|
26
|
+
namespace: 'legion',
|
|
27
|
+
compress: false,
|
|
28
|
+
failover: true,
|
|
29
|
+
threadsafe: true,
|
|
30
|
+
expires_in: 0,
|
|
31
|
+
cache_nils: false,
|
|
32
|
+
pool_size: 10,
|
|
33
|
+
timeout: 5,
|
|
34
|
+
pool_checkout_timeout: 5,
|
|
35
|
+
default_ttl: 3600,
|
|
36
|
+
failback_to_local: true,
|
|
37
|
+
serializer: Legion::JSON,
|
|
38
|
+
cluster: nil,
|
|
39
|
+
replica: false,
|
|
40
|
+
fixed_hostname: nil,
|
|
41
|
+
username: nil,
|
|
42
|
+
password: nil,
|
|
43
|
+
db: nil,
|
|
44
|
+
reconnect_attempts: [0, 0.5, 1].freeze,
|
|
45
|
+
async: {
|
|
46
|
+
pool_size: 4,
|
|
47
|
+
queue_size: 1000,
|
|
48
|
+
shutdown_timeout: 5
|
|
49
|
+
}.freeze,
|
|
50
|
+
reconnect: {
|
|
51
|
+
initial_delay: 1,
|
|
52
|
+
max_delay: 60,
|
|
53
|
+
enabled: true
|
|
54
|
+
}.freeze
|
|
43
55
|
}
|
|
44
56
|
end
|
|
45
57
|
|
|
46
58
|
def self.local
|
|
47
59
|
{
|
|
48
|
-
driver:
|
|
49
|
-
servers:
|
|
50
|
-
connected:
|
|
51
|
-
enabled:
|
|
52
|
-
namespace:
|
|
53
|
-
compress:
|
|
54
|
-
failover:
|
|
55
|
-
threadsafe:
|
|
56
|
-
expires_in:
|
|
57
|
-
cache_nils:
|
|
58
|
-
pool_size:
|
|
59
|
-
timeout:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
driver: driver,
|
|
61
|
+
servers: [],
|
|
62
|
+
connected: false,
|
|
63
|
+
enabled: true,
|
|
64
|
+
namespace: 'legion_local',
|
|
65
|
+
compress: false,
|
|
66
|
+
failover: true,
|
|
67
|
+
threadsafe: true,
|
|
68
|
+
expires_in: 0,
|
|
69
|
+
cache_nils: false,
|
|
70
|
+
pool_size: 5,
|
|
71
|
+
timeout: 3,
|
|
72
|
+
pool_checkout_timeout: 5,
|
|
73
|
+
default_ttl: 21_600,
|
|
74
|
+
serializer: Legion::JSON,
|
|
75
|
+
username: nil,
|
|
76
|
+
password: nil,
|
|
77
|
+
db: nil,
|
|
78
|
+
reconnect_attempts: [0, 0.25, 0.5].freeze
|
|
66
79
|
}
|
|
67
80
|
end
|
|
68
81
|
|