readthis 2.1.0 → 2.2.0
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 +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
|
#
|