readthis 2.1.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/LICENSE.txt +22 -0
- data/README.md +1 -1
- data/lib/active_support/cache/readthis_store.rb +3 -1
- data/lib/readthis.rb +2 -0
- data/lib/readthis/cache.rb +66 -26
- data/lib/readthis/entity.rb +2 -0
- data/lib/readthis/errors.rb +2 -0
- data/lib/readthis/expanders.rb +53 -4
- data/lib/readthis/passthrough.rb +19 -1
- data/lib/readthis/scripts.rb +3 -1
- data/lib/readthis/serializers.rb +44 -3
- data/lib/readthis/version.rb +3 -1
- data/readthis.gemspec +27 -0
- metadata +32 -48
- data/spec/matchers/redis_matchers.rb +0 -13
- data/spec/readthis/cache_spec.rb +0 -418
- data/spec/readthis/entity_spec.rb +0 -143
- data/spec/readthis/expanders_spec.rb +0 -40
- data/spec/readthis/passthrough_spec.rb +0 -16
- data/spec/readthis/scripts_spec.rb +0 -31
- data/spec/readthis/serializers_spec.rb +0 -96
- data/spec/readthis_spec.rb +0 -19
- data/spec/spec_helper.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9ce73086f82fd00b89f037d6aba17fee9ba94b76766f020c65f1305c1faa122d
|
4
|
+
data.tar.gz: f63760bc92f7a0f4023b2e4817192282b5e77e34b61c73e6a5f7d4b60bc3ce02
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bcd82892c461b1b34f9fa15873cb4948db25974e39acdb34615de49155346f6e858543c06f07207f0c00bfc3b8064d2856c4c05f49edab9c36b5884dc78b4c63
|
7
|
+
data.tar.gz: 51d85402830b660eba320626bd4a4db3928f33c3a3f77fbc2934fe553793b2809bd1074a8abaea9d63183001c75f1e5ee5f5b0d612991da2badc05e703c31be5
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Parker Selbert
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -8,7 +8,7 @@
|
|
8
8
|
|
9
9
|
Readthis is a Redis backed cache client for Ruby. It is a drop in replacement
|
10
10
|
for any `ActiveSupport` compliant cache and can also be used for [session
|
11
|
-
storage](#session-storage).
|
11
|
+
storage](#session-storage). Above all Readthis emphasizes performance,
|
12
12
|
simplicity, and explicitness.
|
13
13
|
|
14
14
|
For new projects there isn't any reason to stick with Memcached. Redis is as
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'readthis'
|
2
4
|
|
3
5
|
module ActiveSupport
|
@@ -5,6 +7,6 @@ module ActiveSupport
|
|
5
7
|
# the ActiveSupport `cache_store` is set to `:readthis_store` it will resolve
|
6
8
|
# to `Readthis::Cache`.
|
7
9
|
module Cache
|
8
|
-
ReadthisStore ||= Readthis::Cache # rubocop:disable
|
10
|
+
ReadthisStore ||= Readthis::Cache # rubocop:disable Naming/ConstantName
|
9
11
|
end
|
10
12
|
end
|
data/lib/readthis.rb
CHANGED
data/lib/readthis/cache.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'readthis/entity'
|
2
4
|
require 'readthis/expanders'
|
3
5
|
require 'readthis/passthrough'
|
@@ -6,6 +8,9 @@ require 'redis'
|
|
6
8
|
require 'connection_pool'
|
7
9
|
|
8
10
|
module Readthis
|
11
|
+
# Readthis is a Redis backed cache client. It is a drop in replacement for
|
12
|
+
# any `ActiveSupport` compliant cache Above all Readthis emphasizes
|
13
|
+
# performance, simplicity, and explicitness.
|
9
14
|
class Cache
|
10
15
|
attr_reader :entity, :notifications, :options, :pool, :scripts
|
11
16
|
|
@@ -156,7 +161,7 @@ module Readthis
|
|
156
161
|
count = options.fetch(:count, 1000)
|
157
162
|
deleted = 0
|
158
163
|
|
159
|
-
until cursor == '0'
|
164
|
+
until cursor == '0'
|
160
165
|
cursor, matched = store.scan(cursor || 0, match: namespaced, count: count)
|
161
166
|
|
162
167
|
if matched.any?
|
@@ -218,7 +223,18 @@ module Readthis
|
|
218
223
|
# Increment a key in the store.
|
219
224
|
#
|
220
225
|
# If the key doesn't exist it will be initialized at 0. If the key exists
|
221
|
-
# but it isn't a Fixnum it will be
|
226
|
+
# but it isn't a Fixnum it will be coerced to 0.
|
227
|
+
#
|
228
|
+
# Note that this method does *not* use Redis' native `incr` or `incrby`
|
229
|
+
# commands. Those commands only work with number-like strings, and are
|
230
|
+
# incompatible with the encoded values Readthis writes to the store. The
|
231
|
+
# behavior of `incrby` is preserved as much as possible, but incrementing
|
232
|
+
# is not an atomic action. If multiple clients are incrementing the same
|
233
|
+
# key there will be a "last write wins" race condition, causing incorrect
|
234
|
+
# counts.
|
235
|
+
#
|
236
|
+
# If you absolutely require correct counts it is better to use the Redis
|
237
|
+
# client directly.
|
222
238
|
#
|
223
239
|
# @param [String] key Key for lookup
|
224
240
|
# @param [Fixnum] amount Value to increment by
|
@@ -226,20 +242,20 @@ module Readthis
|
|
226
242
|
#
|
227
243
|
# @example
|
228
244
|
#
|
229
|
-
# cache.increment('counter') # => 0
|
230
245
|
# cache.increment('counter') # => 1
|
231
246
|
# cache.increment('counter', 2) # => 3
|
232
247
|
#
|
233
248
|
def increment(key, amount = 1, options = {})
|
234
|
-
invoke(:increment, key) do |
|
235
|
-
alter(key, amount, options)
|
249
|
+
invoke(:increment, key) do |store|
|
250
|
+
alter(store, key, amount, options)
|
236
251
|
end
|
237
252
|
end
|
238
253
|
|
239
254
|
# Decrement a key in the store.
|
240
255
|
#
|
241
256
|
# If the key doesn't exist it will be initialized at 0. If the key exists
|
242
|
-
# but it isn't a Fixnum it will be
|
257
|
+
# but it isn't a Fixnum it will be coerced to 0. Like `increment`, this
|
258
|
+
# does not make use of the native `decr` or `decrby` commands.
|
243
259
|
#
|
244
260
|
# @param [String] key Key for lookup
|
245
261
|
# @param [Fixnum] amount Value to decrement by
|
@@ -252,8 +268,8 @@ module Readthis
|
|
252
268
|
# cache.decrement('counter', 2) # => 17
|
253
269
|
#
|
254
270
|
def decrement(key, amount = 1, options = {})
|
255
|
-
invoke(:decrement, key) do |
|
256
|
-
alter(key, amount
|
271
|
+
invoke(:decrement, key) do |store|
|
272
|
+
alter(store, key, -amount, options)
|
257
273
|
end
|
258
274
|
end
|
259
275
|
|
@@ -365,17 +381,27 @@ module Readthis
|
|
365
381
|
end
|
366
382
|
end
|
367
383
|
|
368
|
-
# Clear the entire cache
|
369
|
-
# globbing is applied.
|
384
|
+
# Clear the entire cache by flushing the current database.
|
370
385
|
#
|
371
|
-
#
|
386
|
+
# This flushes everything in the current database, with no globbing
|
387
|
+
# applied. Data in other numbered databases will be preserved.
|
388
|
+
#
|
389
|
+
# @option options [Hash] :async Flush the database asynchronously, only
|
390
|
+
# supported in Redis 4.0+
|
372
391
|
#
|
373
392
|
# @example
|
374
393
|
#
|
375
394
|
# cache.clear #=> 'OK'
|
376
|
-
#
|
377
|
-
|
378
|
-
|
395
|
+
# cache.clear(async: true) #=> 'OK'
|
396
|
+
#
|
397
|
+
def clear(options = {})
|
398
|
+
invoke(:clear, '*') do |store|
|
399
|
+
if options[:async]
|
400
|
+
store.flushdb(async: true)
|
401
|
+
else
|
402
|
+
store.flushdb
|
403
|
+
end
|
404
|
+
end
|
379
405
|
end
|
380
406
|
|
381
407
|
protected
|
@@ -389,8 +415,8 @@ module Readthis
|
|
389
415
|
end
|
390
416
|
|
391
417
|
def write_entity(key, value, store, options)
|
392
|
-
namespaced =
|
393
|
-
dumped =
|
418
|
+
namespaced = namespaced_key(key, options)
|
419
|
+
dumped = entity.dump(value, options)
|
394
420
|
|
395
421
|
if (expiration = options[:expires_in])
|
396
422
|
store.setex(namespaced, coerce_expiration(expiration), dumped)
|
@@ -401,20 +427,34 @@ module Readthis
|
|
401
427
|
|
402
428
|
private
|
403
429
|
|
404
|
-
def alter(key, amount, options)
|
405
|
-
|
406
|
-
|
407
|
-
delta
|
408
|
-
end
|
430
|
+
def alter(store, key, amount, options)
|
431
|
+
options = merged_options(options)
|
432
|
+
namespaced = namespaced_key(key, options)
|
409
433
|
|
410
|
-
|
411
|
-
|
434
|
+
loaded = entity.load(store.get(namespaced))
|
435
|
+
change = loaded.to_i + amount
|
436
|
+
dumped = entity.dump(change, options)
|
437
|
+
expiration = fallback_expiration(store, namespaced, options)
|
438
|
+
|
439
|
+
if expiration
|
440
|
+
store.setex(namespaced, coerce_expiration(expiration), dumped)
|
441
|
+
else
|
442
|
+
store.set(namespaced, dumped)
|
443
|
+
end
|
444
|
+
|
445
|
+
change
|
412
446
|
end
|
413
447
|
|
414
|
-
def
|
415
|
-
|
448
|
+
def fallback_expiration(store, key, options)
|
449
|
+
options.fetch(:expires_in) do
|
450
|
+
ttl = store.ttl(key)
|
416
451
|
|
417
|
-
|
452
|
+
ttl > 0 ? ttl : nil
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def coerce_expiration(expires_in)
|
457
|
+
Float(expires_in).ceil
|
418
458
|
end
|
419
459
|
|
420
460
|
def instrument(name, key)
|
data/lib/readthis/entity.rb
CHANGED
data/lib/readthis/errors.rb
CHANGED
data/lib/readthis/expanders.rb
CHANGED
@@ -1,9 +1,38 @@
|
|
1
1
|
module Readthis
|
2
|
+
# Expander methods are used to transform an object into a string suitable for
|
3
|
+
# use as a cache key.
|
2
4
|
module Expanders
|
5
|
+
# Expand an object into a suitable cache key.
|
6
|
+
#
|
7
|
+
# The behavior of `expand_key` is largely modeled on the `expand_cache_key`
|
8
|
+
# method from `ActiveSupport::Cache`, with some subtle additions.
|
9
|
+
#
|
10
|
+
# @param [Object] key An object to stringify. Arrays, hashes, and objects
|
11
|
+
# that respond to either `cache_key` or `to_param` have direct support.
|
12
|
+
# All other objects will be coerced to a string, and frozen strings will
|
13
|
+
# be duplicated.
|
14
|
+
#
|
15
|
+
# @return [String] A cache key string.
|
16
|
+
#
|
17
|
+
# @example String expansion
|
18
|
+
#
|
19
|
+
# Readthis::Expanders.expand_key('typical-key')
|
20
|
+
# 'typical-key'
|
21
|
+
#
|
22
|
+
# @example Array string expansion
|
23
|
+
#
|
24
|
+
# Readthis::Expanders.expand_key(['a', 'b', [:c, 'd'], 1])
|
25
|
+
# 'a/b/c/d/1'
|
26
|
+
#
|
27
|
+
# @example Hash expansion
|
28
|
+
#
|
29
|
+
# Readthis::Expanders.expand_key(c: 1, 'a' => 3, b: 2)
|
30
|
+
# 'a=3/b=2/c=1'
|
31
|
+
#
|
3
32
|
def self.expand_key(key)
|
4
33
|
case
|
5
|
-
when key.
|
6
|
-
key.
|
34
|
+
when key.is_a?(String)
|
35
|
+
key.frozen? ? key.dup : key
|
7
36
|
when key.is_a?(Array)
|
8
37
|
key.flat_map { |elem| expand_key(elem) }.join('/')
|
9
38
|
when key.is_a?(Hash)
|
@@ -11,6 +40,8 @@ module Readthis
|
|
11
40
|
.sort_by { |hkey, _| hkey.to_s }
|
12
41
|
.map { |hkey, val| "#{hkey}=#{val}" }
|
13
42
|
.join('/')
|
43
|
+
when key.respond_to?(:cache_key)
|
44
|
+
key.cache_key
|
14
45
|
when key.respond_to?(:to_param)
|
15
46
|
key.to_param
|
16
47
|
else
|
@@ -18,14 +49,32 @@ module Readthis
|
|
18
49
|
end
|
19
50
|
end
|
20
51
|
|
21
|
-
|
52
|
+
# Prepend a namespace to a key after expanding it.
|
53
|
+
#
|
54
|
+
# @param [Object] key An object to stringify.
|
55
|
+
# @param [String] namespace An optional namespace to prepend, if `nil` it
|
56
|
+
# is ignored.
|
57
|
+
#
|
58
|
+
# @return [String] A binary encoded string combining the namespace and key.
|
59
|
+
#
|
60
|
+
# @example Applying a namespace
|
61
|
+
#
|
62
|
+
# Knuckles::Expanders.namespace_key('alpha', 'greek')
|
63
|
+
# 'greek:alpha'
|
64
|
+
#
|
65
|
+
# @example Omitting a namespace
|
66
|
+
#
|
67
|
+
# Knuckles::Expanders.namespace_key('alpha', nil)
|
68
|
+
# 'alpha'
|
69
|
+
#
|
70
|
+
def self.namespace_key(key, namespace = nil)
|
22
71
|
expanded = expand_key(key)
|
23
72
|
|
24
73
|
if namespace
|
25
74
|
"#{namespace}:#{expanded}"
|
26
75
|
else
|
27
76
|
expanded
|
28
|
-
end
|
77
|
+
end.force_encoding(Encoding::BINARY)
|
29
78
|
end
|
30
79
|
end
|
31
80
|
end
|
data/lib/readthis/passthrough.rb
CHANGED
@@ -1,11 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Readthis
|
4
|
+
# The `Passthrough` serializer performs no encoding on objects. It should be
|
5
|
+
# used when caching simple string objects when the overhead of marshalling or
|
6
|
+
# other serialization isn't desired.
|
4
7
|
module Passthrough
|
8
|
+
# Dump an object to string, without performing any encoding on it.
|
9
|
+
#
|
10
|
+
# @param [Object] value Any object to be dumped as a string. Frozen strings
|
11
|
+
# will be duplicated.
|
12
|
+
#
|
13
|
+
# @return [String] The converted object.
|
14
|
+
#
|
5
15
|
def self.dump(value)
|
6
|
-
value
|
16
|
+
case value
|
17
|
+
when String then value.dup
|
18
|
+
else value.to_s
|
19
|
+
end
|
7
20
|
end
|
8
21
|
|
22
|
+
# Load an object without modifying it at all.
|
23
|
+
#
|
24
|
+
# @param [String] value The object to return, expected to be a string.
|
25
|
+
#
|
26
|
+
# @return [String] The original value.
|
9
27
|
def self.load(value)
|
10
28
|
value
|
11
29
|
end
|
data/lib/readthis/scripts.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Readthis
|
2
4
|
# The `Scripts` class is used to conveniently execute lua scripts. The first
|
3
5
|
# time a command is run it is stored on the server and subsequently referred
|
@@ -48,7 +50,7 @@ module Readthis
|
|
48
50
|
end
|
49
51
|
|
50
52
|
def abs_path(filename)
|
51
|
-
dir = File.expand_path(
|
53
|
+
dir = File.expand_path(__dir__)
|
52
54
|
|
53
55
|
File.join(dir, '../../script', filename)
|
54
56
|
end
|
data/lib/readthis/serializers.rb
CHANGED
@@ -1,8 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'readthis/errors'
|
3
5
|
require 'readthis/passthrough'
|
4
6
|
|
5
7
|
module Readthis
|
8
|
+
# Instances of the `Serializers` class are used to configure the use of
|
9
|
+
# multiple "serializers" for a single cache.
|
10
|
+
#
|
11
|
+
# Readthis uses Ruby's `Marshal` module for serializing all values by
|
12
|
+
# default. This isn't always the fastest option, and depending on your use
|
13
|
+
# case it may be desirable to use a faster but less flexible serializer.
|
14
|
+
#
|
15
|
+
# By default Readthis knows about 3 different serializers:
|
16
|
+
#
|
17
|
+
# * Marshal
|
18
|
+
# * JSON
|
19
|
+
# * Passthrough
|
20
|
+
#
|
21
|
+
# If all cached data can safely be represented as a string then use the
|
22
|
+
# pass-through serializer:
|
23
|
+
#
|
24
|
+
# Readthis::Cache.new(marshal: Readthis::Passthrough)
|
25
|
+
#
|
26
|
+
# You can introduce up to four additional serializers by configuring
|
27
|
+
# `serializers` on the Readthis module. For example, if you wanted to use the
|
28
|
+
# extremely fast Oj library for JSON serialization:
|
29
|
+
#
|
30
|
+
# Readthis.serializers << Oj
|
31
|
+
# # Freeze the serializers to ensure they aren't changed at runtime.
|
32
|
+
# Readthis.serializers.freeze!
|
33
|
+
# Readthis::Cache.new(marshal: Oj)
|
34
|
+
#
|
35
|
+
# Be aware that the *order in which you add serializers matters*. Serializers
|
36
|
+
# are sticky and a flag is stored with each cached value. If you subsequently
|
37
|
+
# go to deserialize values and haven't configured the same serializers in the
|
38
|
+
# same order your application will raise errors.
|
6
39
|
class Serializers
|
7
40
|
# Defines the default set of three serializers: Marshal, Passthrough, and
|
8
41
|
# JSON. With a hard limit of 7 that leaves 4 additional slots.
|
@@ -32,7 +65,7 @@ module Readthis
|
|
32
65
|
# @param [Module] serializer Any object that responds to `dump` and `load`
|
33
66
|
# @return [self] Returns itself for possible chaining
|
34
67
|
#
|
35
|
-
# @example
|
68
|
+
# @example Adding Oj as an accepted serializer
|
36
69
|
#
|
37
70
|
# serializers = Readthis::Serializers.new
|
38
71
|
# serializers << Oj
|
@@ -53,16 +86,24 @@ module Readthis
|
|
53
86
|
|
54
87
|
# Freeze the serializers hash, preventing modification.
|
55
88
|
#
|
89
|
+
# @return [self] The serializer instance.
|
90
|
+
#
|
56
91
|
def freeze!
|
57
92
|
serializers.freeze
|
93
|
+
|
94
|
+
self
|
58
95
|
end
|
59
96
|
|
60
97
|
# Reset the instance back to the default state. Useful for cleanup during
|
61
98
|
# testing.
|
62
99
|
#
|
100
|
+
# @return [self] The serializer instance.
|
101
|
+
#
|
63
102
|
def reset!
|
64
103
|
@serializers = BASE_SERIALIZERS.dup
|
65
104
|
@inverted = @serializers.invert
|
105
|
+
|
106
|
+
self
|
66
107
|
end
|
67
108
|
|
68
109
|
# Find a flag for a serializer object.
|
@@ -72,7 +113,7 @@ module Readthis
|
|
72
113
|
# @raise [UnknownSerializerError] Indicates that a serializer was
|
73
114
|
# specified, but hasn't been configured for usage.
|
74
115
|
#
|
75
|
-
# @example
|
116
|
+
# @example Find the JSON serializer's flag
|
76
117
|
#
|
77
118
|
# serializers.assoc(JSON) #=> 1
|
78
119
|
#
|
@@ -91,7 +132,7 @@ module Readthis
|
|
91
132
|
# @param [Number] flag Integer to look up the serializer object by
|
92
133
|
# @return [Module] The serializer object
|
93
134
|
#
|
94
|
-
# @example
|
135
|
+
# @example Find the serializer associated with flag 1
|
95
136
|
#
|
96
137
|
# serializers.rassoc(1) #=> Marshal
|
97
138
|
#
|