readthis 1.1.0 → 1.5.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 +4 -4
- data/README.md +74 -11
- data/lib/active_support/cache/readthis_store.rb +3 -0
- data/lib/readthis.rb +26 -3
- data/lib/readthis/cache.rb +108 -37
- data/lib/readthis/entity.rb +25 -13
- data/lib/readthis/errors.rb +15 -0
- data/lib/readthis/expanders.rb +4 -1
- data/lib/readthis/passthrough.rb +3 -1
- data/lib/readthis/scripts.rb +56 -0
- data/lib/readthis/serializers.rb +12 -8
- data/lib/readthis/version.rb +1 -1
- data/script/mexpire.lua +7 -0
- data/spec/matchers/redis_matchers.rb +13 -0
- data/spec/readthis/cache_spec.rb +83 -9
- data/spec/readthis/entity_spec.rb +0 -1
- data/spec/readthis/expanders_spec.rb +0 -2
- data/spec/readthis/passthrough_spec.rb +3 -4
- data/spec/readthis/scripts_spec.rb +31 -0
- data/spec/readthis/serializers_spec.rb +17 -8
- data/spec/readthis_spec.rb +12 -2
- data/spec/spec_helper.rb +5 -0
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d99b745b536b4761198212736eba995173cf7dd0
|
4
|
+
data.tar.gz: fb66dd2f67cc90114ad5037ea10b684722d6d468
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 992a39d5b3c6fde1796dbd8ac8dc8c03aa44cd79152a15e6b1ca553327d4dec95f754ddb68a2275aa636d75d13658e43b712ebd95ae4b49ae21f6a164ad66b87
|
7
|
+
data.tar.gz: e5521f55739341901c7bbe6d9743127686643ca44cb82bf338f046a81fd16a522f364ee757e5f548f3ed9c647b495904c0faa1144470e47f4610f510cebc23e1
|
data/README.md
CHANGED
@@ -2,12 +2,14 @@
|
|
2
2
|
[](https://travis-ci.org/sorentwo/readthis)
|
3
3
|
[](https://codeclimate.com/github/sorentwo/readthis)
|
4
4
|
[](https://coveralls.io/github/sorentwo/readthis?branch=master)
|
5
|
+
[](http://inch-ci.org/github/sorentwo/readthis)
|
5
6
|
|
6
7
|
# Readthis
|
7
8
|
|
8
|
-
Readthis is a
|
9
|
-
|
10
|
-
|
9
|
+
Readthis is a Redis backed cache client for Ruby. It is a drop in replacement
|
10
|
+
for any `ActiveSupport` compliant cache and can also be used for [session
|
11
|
+
storage](#session-storage). Above all Readthis emphasizes performance,
|
12
|
+
simplicity, and explicitness.
|
11
13
|
|
12
14
|
For new projects there isn't any reason to stick with Memcached. Redis is as
|
13
15
|
fast, if not faster in many scenarios, and is far more likely to be used
|
@@ -65,24 +67,40 @@ instances have numerous benefits like: more predictable performance, avoiding
|
|
65
67
|
expires in favor of LRU, and tuning the persistence mechanism. See [Optimizing
|
66
68
|
Redis Usage for Caching][optimizing-usage] for more details.
|
67
69
|
|
68
|
-
|
69
|
-
|
70
|
-
At the very least you'll want to use a specific database for caching. In the
|
70
|
+
At the very least, you'll want to use a specific database for caching. In the
|
71
71
|
event the database needs to be purged you can do so with a single `clear`
|
72
72
|
command, rather than finding all keys in a namespace and deleting them.
|
73
73
|
Appending a number between 0 and 15 will specify the redis database, which
|
74
|
-
defaults to 0
|
74
|
+
defaults to `0`. For example, using database `2`:
|
75
75
|
|
76
76
|
```bash
|
77
77
|
REDIS_URL=redis://localhost:6379/2
|
78
78
|
```
|
79
79
|
|
80
|
+
[optimizing-usage]: http://sorentwo.com/2015/07/27/optimizing-redis-usage-for-caching.html
|
81
|
+
|
80
82
|
### Expiration
|
81
83
|
|
82
84
|
Be sure to use an integer value when setting expiration time. The default
|
83
85
|
representation of `ActiveSupport::Duration` values won't work when setting
|
84
86
|
expiration time, which will cause all keys to have `-1` as the TTL. Expiration
|
85
|
-
values are always cast as an integer on write.
|
87
|
+
values are always cast as an integer on write. For example:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
Readthis::Cache.new(expires_in: 1.week) # don't do this
|
91
|
+
Readthis::Cache.new(expires_in: 1.week.to_i) # do this
|
92
|
+
```
|
93
|
+
|
94
|
+
By using the `refresh` option the TTL for keys can be refreshed automatically
|
95
|
+
every time the key is read. This is helpful for ensuring commonly hit keys are
|
96
|
+
kept cached, effectively making the cache a hybrid LRU.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
Readthis::Cache.new(refresh: true)
|
100
|
+
```
|
101
|
+
|
102
|
+
Be aware that `refresh` adds a slight overhead to all read operations, as they
|
103
|
+
are now all write operations as well.
|
86
104
|
|
87
105
|
### Compression
|
88
106
|
|
@@ -132,23 +150,68 @@ Readthis.serializers.freeze!
|
|
132
150
|
Readthis::Cache.new(marshal: Oj)
|
133
151
|
```
|
134
152
|
|
135
|
-
Be aware that the order in which you add serializers matters
|
153
|
+
Be aware that the *order in which you add serializers matters*. Serializers are
|
136
154
|
sticky and a flag is stored with each cached value. If you subsequently go to
|
137
155
|
deserialize values and haven't configured the same serializers in the same order
|
138
156
|
your application will raise errors.
|
139
157
|
|
158
|
+
## Fault Tolerance
|
159
|
+
|
160
|
+
In some situations it is desirable to keep serving requests from disk or the
|
161
|
+
database if Redis crashes. This can be achieved with connection fault tolerance
|
162
|
+
by enabling it at the top level:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
Readthis.fault_tolerant = true
|
166
|
+
```
|
167
|
+
|
168
|
+
The default value is `false`, because while it may work for `fetch` operations,
|
169
|
+
it isn't compatible with other state-based commands like `increment`.
|
170
|
+
|
171
|
+
## Running Arbitrary Redis Commands
|
172
|
+
|
173
|
+
Readthis provides access to the underlying Redis connection pool, allowing you
|
174
|
+
to run arbitrary commands directly through the cache instance. For example, if
|
175
|
+
you wanted to expire a key manually using an instance of `Rails.cache`:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
Rails.cache.pool.with { |client| client.expire('foo-key', 60) }
|
179
|
+
```
|
180
|
+
|
140
181
|
## Differences From ActiveSupport::Cache
|
141
182
|
|
142
183
|
Readthis supports all of standard cache methods except for the following:
|
143
184
|
|
144
185
|
* `cleanup` - Redis does this with TTL or LRU already.
|
145
|
-
* `
|
146
|
-
|
186
|
+
* `mute` and `silence!` - You must subscribe to the events `/cache*.active_support/`
|
187
|
+
with `ActiveSupport::Notifications` to [log cache calls manually][notifications].
|
188
|
+
|
189
|
+
[notifications]: https://github.com/sorentwo/readthis/issues/22#issuecomment-142595938
|
147
190
|
|
148
191
|
Like other `ActiveSupport::Cache` implementations it is possible to cache `nil`
|
149
192
|
as a value. However, the fetch methods treat `nil` values as a cache miss and
|
150
193
|
re-generate/re-cache the value. Caching `nil` isn't recommended.
|
151
194
|
|
195
|
+
## Session Storage
|
196
|
+
|
197
|
+
By using [ActionDispatch::Session::CacheStore][cache-store] it's possible to
|
198
|
+
reuse `:readthis_store` or specify a new Readthis cache store for storing
|
199
|
+
sessions.
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
Rails.application.config.session_store :cache_store
|
203
|
+
```
|
204
|
+
|
205
|
+
To specify a separate Readthis instance you can use the `:cache` option:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
Rails.application.config.session_store :cache_store,
|
209
|
+
cache: Readthis::Cache.new,
|
210
|
+
expire_after: 2.weeks.to_i
|
211
|
+
```
|
212
|
+
|
213
|
+
[cache-store]: http://api.rubyonrails.org/classes/ActionDispatch/Session/CacheStore.html
|
214
|
+
|
152
215
|
## Contributing
|
153
216
|
|
154
217
|
1. Fork it
|
@@ -1,6 +1,9 @@
|
|
1
1
|
require 'readthis'
|
2
2
|
|
3
3
|
module ActiveSupport
|
4
|
+
# Provided for compatibility with ActiveSupport's cache lookup behavior. When
|
5
|
+
# the ActiveSupport `cache_store` is set to `:readthis_store` it will resolve
|
6
|
+
# to `Readthis::Cache`.
|
4
7
|
module Cache
|
5
8
|
ReadthisStore ||= Readthis::Cache # rubocop:disable Style/ConstantName
|
6
9
|
end
|
data/lib/readthis.rb
CHANGED
@@ -4,16 +4,39 @@ require 'readthis/serializers'
|
|
4
4
|
require 'readthis/version'
|
5
5
|
|
6
6
|
module Readthis
|
7
|
-
extend self
|
8
|
-
|
9
7
|
# The current, global, instance of serializers that is used by all cache
|
10
8
|
# instances.
|
11
9
|
#
|
12
|
-
# @
|
10
|
+
# @return [Readthis::Serializers] An cached Serializers instance
|
13
11
|
#
|
14
12
|
# @see readthis/serializers
|
15
13
|
#
|
16
14
|
def serializers
|
17
15
|
@serializers ||= Readthis::Serializers.new
|
18
16
|
end
|
17
|
+
|
18
|
+
# Indicates whether connection error tolerance is enabled. With tolerance
|
19
|
+
# enabled every operation will return a `nil` value.
|
20
|
+
#
|
21
|
+
# @return [Boolean] True for enabled, false for disabled
|
22
|
+
#
|
23
|
+
def fault_tolerant?
|
24
|
+
@fault_tolerant
|
25
|
+
end
|
26
|
+
|
27
|
+
# Toggle fault tolerance for connection errors.
|
28
|
+
#
|
29
|
+
# @param [Boolean] value The new value for fault tolerance
|
30
|
+
#
|
31
|
+
def fault_tolerant=(value)
|
32
|
+
@fault_tolerant = value
|
33
|
+
end
|
34
|
+
|
35
|
+
# @private
|
36
|
+
def reset!
|
37
|
+
@fault_tolerant = nil
|
38
|
+
@serializers = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
module_function :serializers, :fault_tolerant?, :fault_tolerant=, :reset!
|
19
42
|
end
|
data/lib/readthis/cache.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
require 'readthis/entity'
|
2
2
|
require 'readthis/expanders'
|
3
3
|
require 'readthis/passthrough'
|
4
|
+
require 'readthis/scripts'
|
4
5
|
require 'redis'
|
5
6
|
require 'connection_pool'
|
6
7
|
|
7
8
|
module Readthis
|
8
9
|
class Cache
|
9
|
-
attr_reader :entity, :notifications, :options, :pool
|
10
|
+
attr_reader :entity, :notifications, :options, :pool, :scripts
|
10
11
|
|
11
12
|
# Provide a class level lookup of the proper notifications module.
|
12
13
|
# Instrumention is expected to occur within applications that have
|
@@ -18,17 +19,19 @@ module Readthis
|
|
18
19
|
|
19
20
|
# Creates a new Readthis::Cache object with the given options.
|
20
21
|
#
|
21
|
-
# @option [Hash] :redis Options that will be passed to the
|
22
|
-
# @option [Boolean] :compress (false) Enable or disable automatic compression
|
23
|
-
# @option [Number] :compression_threshold (8k)
|
24
|
-
# @option [Number] :expires_in The number of seconds until an entry expires
|
25
|
-
# @option [
|
26
|
-
# @option [
|
27
|
-
# @option [
|
28
|
-
# @option [Number] :
|
22
|
+
# @option options [Hash] :redis Options that will be passed to the redis connection
|
23
|
+
# @option options [Boolean] :compress (false) Enable or disable automatic compression
|
24
|
+
# @option options [Number] :compression_threshold (8k) Minimum string size for compression
|
25
|
+
# @option options [Number] :expires_in The number of seconds until an entry expires
|
26
|
+
# @option options [Boolean] :refresh (false) Automatically refresh key expiration
|
27
|
+
# @option options [Module] :marshal (Marshal) Module that responds to `dump` and `load`
|
28
|
+
# @option options [String] :namespace Prefix used to namespace entries
|
29
|
+
# @option options [Number] :pool_size (5) The number of threads in the pool
|
30
|
+
# @option options [Number] :pool_timeout (5) How long before a thread times out
|
29
31
|
#
|
30
32
|
# @example Create a new cache instance
|
31
|
-
# Readthis::Cache.new(namespace: 'cache',
|
33
|
+
# Readthis::Cache.new(namespace: 'cache',
|
34
|
+
# redis: { url: 'redis://localhost:6379/0' })
|
32
35
|
#
|
33
36
|
# @example Create a compressed cache instance
|
34
37
|
# Readthis::Cache.new(compress: true, compression_threshold: 2048)
|
@@ -45,14 +48,16 @@ module Readthis
|
|
45
48
|
@pool = ConnectionPool.new(pool_options(options)) do
|
46
49
|
Redis.new(options.fetch(:redis, {}))
|
47
50
|
end
|
51
|
+
|
52
|
+
@scripts = Readthis::Scripts.new
|
48
53
|
end
|
49
54
|
|
50
55
|
# Fetches data from the cache, using the given key. If there is data in
|
51
56
|
# the cache with the given key, then that data is returned. Otherwise, nil
|
52
57
|
# is returned.
|
53
58
|
#
|
54
|
-
# @param [String] Key for lookup
|
55
|
-
# @param [Hash] Optional overrides
|
59
|
+
# @param [String] key Key for lookup
|
60
|
+
# @param [Hash] options Optional overrides
|
56
61
|
#
|
57
62
|
# @example
|
58
63
|
#
|
@@ -60,18 +65,22 @@ module Readthis
|
|
60
65
|
# cache.read('matched') # => 'some value'
|
61
66
|
#
|
62
67
|
def read(key, options = {})
|
68
|
+
options = merged_options(options)
|
69
|
+
|
63
70
|
invoke(:read, key) do |store|
|
64
|
-
|
71
|
+
key = namespaced_key(key, options)
|
72
|
+
|
73
|
+
refresh_entity(key, store, options)
|
65
74
|
|
66
|
-
entity.load(
|
75
|
+
entity.load(store.get(key))
|
67
76
|
end
|
68
77
|
end
|
69
78
|
|
70
79
|
# Writes data to the cache using the given key. Will overwrite whatever
|
71
80
|
# value is already stored at that key.
|
72
81
|
#
|
73
|
-
# @param [String] Key for lookup
|
74
|
-
# @param [Hash] Optional overrides
|
82
|
+
# @param [String] key Key for lookup
|
83
|
+
# @param [Hash] options Optional overrides
|
75
84
|
#
|
76
85
|
# @example
|
77
86
|
#
|
@@ -90,13 +99,14 @@ module Readthis
|
|
90
99
|
# Delete the value stored at the specified key. Returns `true` if
|
91
100
|
# anything was deleted, `false` otherwise.
|
92
101
|
#
|
93
|
-
# @
|
94
|
-
# @
|
102
|
+
# @param [String] key The key for lookup
|
103
|
+
# @param [Hash] options Optional overrides
|
95
104
|
#
|
96
105
|
# @example
|
97
106
|
#
|
98
107
|
# cache.delete('existing-key') # => true
|
99
108
|
# cache.delete('random-key') # => false
|
109
|
+
#
|
100
110
|
def delete(key, options = {})
|
101
111
|
namespaced = namespaced_key(key, merged_options(options))
|
102
112
|
|
@@ -105,6 +115,49 @@ module Readthis
|
|
105
115
|
end
|
106
116
|
end
|
107
117
|
|
118
|
+
# Delete all values that match a given pattern. The pattern must be defined
|
119
|
+
# using Redis compliant globs. The following examples are borrowed from the
|
120
|
+
# `KEYS` documentation:
|
121
|
+
#
|
122
|
+
# * `h?llo` matches hello, hallo and hxllo
|
123
|
+
# * `h*llo` matches hllo and heeeello
|
124
|
+
# * `h[ae]llo` matches hello and hallo, but not hillo
|
125
|
+
# * `h[^e]llo` matches hallo, hbllo, ... but not hello
|
126
|
+
# * `h[a-b]llo` matches hallo and hbllo
|
127
|
+
#
|
128
|
+
# Note that `delete_matched` does *not* use the `KEYS` command, making it
|
129
|
+
# safe for use in production.
|
130
|
+
#
|
131
|
+
# @param [String] pattern The glob pattern for matching keys
|
132
|
+
# @option [String] :namespace Prepend a namespace to the pattern
|
133
|
+
# @option [Number] :count Configure the number of keys deleted at once
|
134
|
+
#
|
135
|
+
# @example Delete all 'cat' keys
|
136
|
+
#
|
137
|
+
# cache.delete_matched('*cats') #=> 47
|
138
|
+
# cache.delete_matched('*dogs') #=> 0
|
139
|
+
#
|
140
|
+
def delete_matched(pattern, options = {})
|
141
|
+
namespaced = namespaced_key(pattern, merged_options(options))
|
142
|
+
|
143
|
+
invoke(:delete, pattern) do |store|
|
144
|
+
cursor = nil
|
145
|
+
count = options.fetch(:count, 1000)
|
146
|
+
deleted = 0
|
147
|
+
|
148
|
+
until cursor == '0'.freeze
|
149
|
+
cursor, matched = store.scan(cursor || 0, match: namespaced, count: count)
|
150
|
+
|
151
|
+
if matched.any?
|
152
|
+
store.del(*matched)
|
153
|
+
deleted += matched.length
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
deleted
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
108
161
|
# Fetches data from the cache, using the given key. If there is data in the
|
109
162
|
# cache with the given key, then that data is returned.
|
110
163
|
#
|
@@ -114,10 +167,11 @@ module Readthis
|
|
114
167
|
# the block will be written to the cache under the given cache key, and
|
115
168
|
# that return value will be returned.
|
116
169
|
#
|
117
|
-
# @param [String] Key for lookup
|
118
|
-
# @param [Block] Optional block for generating the value when missing
|
170
|
+
# @param [String] key Key for lookup
|
119
171
|
# @param options [Hash] Optional overrides
|
120
172
|
# @option options [Boolean] :force Force a cache miss
|
173
|
+
# @yield [String] Gives a missing key to the block, which is used to
|
174
|
+
# generate the missing value
|
121
175
|
#
|
122
176
|
# @example Typical
|
123
177
|
#
|
@@ -130,7 +184,8 @@ module Readthis
|
|
130
184
|
# cache.fetch('city') do
|
131
185
|
# 'Duckburgh'
|
132
186
|
# end
|
133
|
-
#
|
187
|
+
#
|
188
|
+
# cache.fetch('city') # => "Duckburgh"
|
134
189
|
#
|
135
190
|
# @example Cache Miss
|
136
191
|
#
|
@@ -154,9 +209,9 @@ module Readthis
|
|
154
209
|
# If the key doesn't exist it will be initialized at 0. If the key exists
|
155
210
|
# but it isn't a Fixnum it will be initialized at 0.
|
156
211
|
#
|
157
|
-
# @param [String] Key for lookup
|
158
|
-
# @param [Fixnum] Value to increment by
|
159
|
-
# @param [Hash] Optional overrides
|
212
|
+
# @param [String] key Key for lookup
|
213
|
+
# @param [Fixnum] amount Value to increment by
|
214
|
+
# @param [Hash] options Optional overrides
|
160
215
|
#
|
161
216
|
# @example
|
162
217
|
#
|
@@ -175,9 +230,9 @@ module Readthis
|
|
175
230
|
# If the key doesn't exist it will be initialized at 0. If the key exists
|
176
231
|
# but it isn't a Fixnum it will be initialized at 0.
|
177
232
|
#
|
178
|
-
# @param [String] Key for lookup
|
179
|
-
# @param [Fixnum] Value to decrement by
|
180
|
-
# @param [Hash] Optional overrides
|
233
|
+
# @param [String] key Key for lookup
|
234
|
+
# @param [Fixnum] amount Value to decrement by
|
235
|
+
# @param [Hash] options Optional overrides
|
181
236
|
#
|
182
237
|
# @example
|
183
238
|
#
|
@@ -212,7 +267,9 @@ module Readthis
|
|
212
267
|
return {} if keys.empty?
|
213
268
|
|
214
269
|
invoke(:read_multi, keys) do |store|
|
215
|
-
values = store.mget(mapping).map { |value| entity.load(value) }
|
270
|
+
values = store.mget(*mapping).map { |value| entity.load(value) }
|
271
|
+
|
272
|
+
refresh_entity(mapping, store, options)
|
216
273
|
|
217
274
|
keys.zip(values).to_h
|
218
275
|
end
|
@@ -224,8 +281,8 @@ module Readthis
|
|
224
281
|
#
|
225
282
|
# This is a non-standard, but useful, cache method.
|
226
283
|
#
|
227
|
-
# @param [Hash] Key value hash to write
|
228
|
-
# @param [Hash] Optional overrides
|
284
|
+
# @param [Hash] hash Key value hash to write
|
285
|
+
# @param [Hash] options Optional overrides
|
229
286
|
#
|
230
287
|
# @example
|
231
288
|
#
|
@@ -282,8 +339,8 @@ module Readthis
|
|
282
339
|
|
283
340
|
# Returns `true` if the cache contains an entry for the given key.
|
284
341
|
#
|
285
|
-
# @param [String] Key for lookup
|
286
|
-
# @param [Hash] Optional overrides
|
342
|
+
# @param [String] key Key for lookup
|
343
|
+
# @param [Hash] options Optional overrides
|
287
344
|
#
|
288
345
|
# @example
|
289
346
|
#
|
@@ -299,23 +356,32 @@ module Readthis
|
|
299
356
|
# Clear the entire cache. This flushes the current database, no
|
300
357
|
# globbing is applied.
|
301
358
|
#
|
302
|
-
# @param [Hash] Options, only present for compatibility
|
359
|
+
# @param [Hash] _options Options, only present for compatibility
|
303
360
|
#
|
304
361
|
# @example
|
305
362
|
#
|
306
363
|
# cache.clear #=> 'OK'
|
364
|
+
#
|
307
365
|
def clear(_options = nil)
|
308
366
|
invoke(:clear, '*', &:flushdb)
|
309
367
|
end
|
310
368
|
|
311
369
|
protected
|
312
370
|
|
371
|
+
def refresh_entity(keys, store, options)
|
372
|
+
return unless options[:refresh] && options[:expires_in]
|
373
|
+
|
374
|
+
expiration = coerce_expiration(options[:expires_in])
|
375
|
+
|
376
|
+
scripts.run('mexpire', store, keys, expiration)
|
377
|
+
end
|
378
|
+
|
313
379
|
def write_entity(key, value, store, options)
|
314
380
|
namespaced = namespaced_key(key, options)
|
315
381
|
dumped = entity.dump(value, options)
|
316
382
|
|
317
|
-
if expiration = options[:expires_in]
|
318
|
-
store.setex(namespaced, expiration
|
383
|
+
if (expiration = options[:expires_in])
|
384
|
+
store.setex(namespaced, coerce_expiration(expiration), dumped)
|
319
385
|
else
|
320
386
|
store.set(namespaced, dumped)
|
321
387
|
end
|
@@ -324,12 +390,15 @@ module Readthis
|
|
324
390
|
private
|
325
391
|
|
326
392
|
def alter(key, amount, options)
|
327
|
-
|
328
|
-
delta = number.to_i + amount
|
393
|
+
delta = read(key, options).to_i + amount
|
329
394
|
write(key, delta, options)
|
330
395
|
delta
|
331
396
|
end
|
332
397
|
|
398
|
+
def coerce_expiration(expires_in)
|
399
|
+
Float(expires_in).ceil
|
400
|
+
end
|
401
|
+
|
333
402
|
def instrument(name, key)
|
334
403
|
if self.class.notifications
|
335
404
|
name = "cache_#{name}.active_support"
|
@@ -345,6 +414,8 @@ module Readthis
|
|
345
414
|
instrument(operation, key) do
|
346
415
|
pool.with(&block)
|
347
416
|
end
|
417
|
+
rescue Redis::BaseError => error
|
418
|
+
raise error unless Readthis.fault_tolerant?
|
348
419
|
end
|
349
420
|
|
350
421
|
def extract_options!(array)
|
data/lib/readthis/entity.rb
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
require 'zlib'
|
2
2
|
|
3
3
|
module Readthis
|
4
|
+
# An instance of the Entity class is used to handle `load` and `dump`
|
5
|
+
# operations on cached values.
|
4
6
|
class Entity
|
7
|
+
# Unless they are overridden, these are the options used to load and unload
|
8
|
+
# every value.
|
5
9
|
DEFAULT_OPTIONS = {
|
6
10
|
compress: false,
|
7
11
|
marshal: Marshal,
|
8
12
|
threshold: 8 * 1024
|
9
13
|
}.freeze
|
10
14
|
|
15
|
+
# A hexidecimal compression flag. When it is present within the magic bit
|
16
|
+
# of an entity that entity is considered compressed.
|
11
17
|
COMPRESSED_FLAG = 0x8
|
12
18
|
|
13
19
|
# Creates a Readthis::Entity with default options. Each option can be
|
@@ -17,9 +23,12 @@ module Readthis
|
|
17
23
|
# automatically be used again when loading, regardless of how current
|
18
24
|
# options are set.
|
19
25
|
#
|
20
|
-
# @option [Boolean] :compress (false) Enable or disable automatic
|
21
|
-
#
|
22
|
-
# @option [
|
26
|
+
# @option [Boolean] :compress (false) Enable or disable automatic
|
27
|
+
# compression
|
28
|
+
# @option [Module] :marshal (Marshal) Any module that responds to `dump`
|
29
|
+
# and `load`
|
30
|
+
# @option [Number] :threshold (8k) The size a string must be for
|
31
|
+
# compression
|
23
32
|
#
|
24
33
|
def initialize(options = {})
|
25
34
|
@options = DEFAULT_OPTIONS.merge(options)
|
@@ -28,10 +37,13 @@ module Readthis
|
|
28
37
|
# Output a value prepared for cache storage. Passed options will override
|
29
38
|
# whatever has been specified for the instance.
|
30
39
|
#
|
31
|
-
# @param
|
32
|
-
# @option [Boolean] :compress Enable or disable automatic
|
33
|
-
#
|
34
|
-
# @option [
|
40
|
+
# @param [String] value String to dump
|
41
|
+
# @option options [Boolean] :compress Enable or disable automatic
|
42
|
+
# compression
|
43
|
+
# @option options [Module] :marshal Any module that responds to `dump` and
|
44
|
+
# `load`
|
45
|
+
# @option options [Number] :threshold The size a string must be for
|
46
|
+
# compression
|
35
47
|
# @return [String] The prepared, possibly compressed, string
|
36
48
|
#
|
37
49
|
# @example Dumping a value using defaults
|
@@ -40,7 +52,7 @@ module Readthis
|
|
40
52
|
#
|
41
53
|
# @example Dumping a value with overrides
|
42
54
|
#
|
43
|
-
# entity.dump(string, compress: false)
|
55
|
+
# entity.dump(string, compress: false, marshal: JSON)
|
44
56
|
#
|
45
57
|
def dump(value, options = {})
|
46
58
|
compress = with_fallback(options, :compress)
|
@@ -54,7 +66,7 @@ module Readthis
|
|
54
66
|
|
55
67
|
# Parse a dumped value using the embedded options.
|
56
68
|
#
|
57
|
-
# @param
|
69
|
+
# @param [String] string Option embedded string to load
|
58
70
|
# @return [String] The original dumped string, restored
|
59
71
|
#
|
60
72
|
# @example
|
@@ -77,9 +89,9 @@ module Readthis
|
|
77
89
|
# Where there are four unused bits, 1 compression bit, and 3 bits for the
|
78
90
|
# serializer. This allows up to 8 different serializers for marshaling.
|
79
91
|
#
|
80
|
-
# @param [String] String to prefix with flags
|
81
|
-
# @param [Module] The marshal module to be used
|
82
|
-
# @param [Boolean] Flag determining whether the value is compressed
|
92
|
+
# @param [String] value String to prefix with flags
|
93
|
+
# @param [Module] marshal The marshal module to be used
|
94
|
+
# @param [Boolean] compress Flag determining whether the value is compressed
|
83
95
|
# @return [String] The original string with a single byte prefixed
|
84
96
|
#
|
85
97
|
# @example Compose an option embedded string
|
@@ -96,7 +108,7 @@ module Readthis
|
|
96
108
|
|
97
109
|
# Decompose an option embedded string into marshal, compression and value.
|
98
110
|
#
|
99
|
-
# @param [String] Option embedded string to
|
111
|
+
# @param [String] string Option embedded string to
|
100
112
|
# @return [Array<Module, Boolean, String>] An array comprised of the
|
101
113
|
# marshal, compression flag, and the base string.
|
102
114
|
#
|
data/lib/readthis/errors.rb
CHANGED
@@ -1,7 +1,22 @@
|
|
1
1
|
module Readthis
|
2
|
+
# This is the base error that all other specific errors inherit from,
|
3
|
+
# making it possible to rescue the `ReadthisError` superclass.
|
4
|
+
#
|
5
|
+
# This isn't raised by itself.
|
2
6
|
ReadthisError = Class.new(StandardError)
|
3
7
|
|
8
|
+
# Raised when attempting to modify the serializers after they have been
|
9
|
+
# frozen.
|
4
10
|
SerializersFrozenError = Class.new(ReadthisError)
|
11
|
+
|
12
|
+
# Raised when attempting to add a new serializer after the limit of 7 is
|
13
|
+
# reached.
|
5
14
|
SerializersLimitError = Class.new(ReadthisError)
|
15
|
+
|
16
|
+
# Raised when an unknown script is called.
|
17
|
+
UnknownCommandError = Class.new(ReadthisError)
|
18
|
+
|
19
|
+
# Raised when a serializer was specified, but hasn't been configured for
|
20
|
+
# usage.
|
6
21
|
UnknownSerializerError = Class.new(ReadthisError)
|
7
22
|
end
|
data/lib/readthis/expanders.rb
CHANGED
@@ -7,7 +7,10 @@ module Readthis
|
|
7
7
|
when key.is_a?(Array)
|
8
8
|
key.flat_map { |elem| expand_key(elem) }.join('/')
|
9
9
|
when key.is_a?(Hash)
|
10
|
-
key
|
10
|
+
key
|
11
|
+
.sort_by { |hkey, _| hkey.to_s }
|
12
|
+
.map { |hkey, val| "#{hkey}=#{val}" }
|
13
|
+
.join('/')
|
11
14
|
when key.respond_to?(:to_param)
|
12
15
|
key.to_param
|
13
16
|
else
|
data/lib/readthis/passthrough.rb
CHANGED
@@ -0,0 +1,56 @@
|
|
1
|
+
module Readthis
|
2
|
+
# The `Scripts` class is used to conveniently execute lua scripts. The first
|
3
|
+
# time a command is run it is stored on the server and subsequently referred
|
4
|
+
# to by its SHA. Each instance tracks SHAs separately, they are not global.
|
5
|
+
class Scripts
|
6
|
+
attr_reader :loaded
|
7
|
+
|
8
|
+
# Creates a new Readthis::Scripts instance.
|
9
|
+
def initialize
|
10
|
+
@loaded = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Run a named lua script with the provided keys and arguments.
|
14
|
+
#
|
15
|
+
# @param [String] command The script to run, without a `.lua` extension
|
16
|
+
# @param [#Store] store A Redis client for storing and evaluating the script
|
17
|
+
# @param [Array] keys One or more keys to pass to the command
|
18
|
+
# @param [Array] args One or more args to pass to the command
|
19
|
+
#
|
20
|
+
# @return [Any] The Redis converted value returned on the script
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
#
|
24
|
+
# scripts.run('mexpire', store, %w[a b c], 1) # => 'OK'
|
25
|
+
#
|
26
|
+
def run(command, store, keys, args = [])
|
27
|
+
store.evalsha(
|
28
|
+
sha(command, store),
|
29
|
+
Array(keys),
|
30
|
+
Array(args)
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def sha(command, store)
|
37
|
+
loaded[command] ||= load_script!(command, store)
|
38
|
+
end
|
39
|
+
|
40
|
+
def load_script!(command, store)
|
41
|
+
path = abs_path("#{command}.lua")
|
42
|
+
|
43
|
+
File.open(path) do |file|
|
44
|
+
loaded[command] = store.script(:load, file.read)
|
45
|
+
end
|
46
|
+
rescue Errno::ENOENT
|
47
|
+
raise Readthis::UnknownCommandError, "unknown command '#{command}'"
|
48
|
+
end
|
49
|
+
|
50
|
+
def abs_path(filename)
|
51
|
+
dir = File.expand_path(File.dirname(__FILE__))
|
52
|
+
|
53
|
+
File.join(dir, '../../script', filename)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/readthis/serializers.rb
CHANGED
@@ -4,12 +4,16 @@ require 'readthis/passthrough'
|
|
4
4
|
|
5
5
|
module Readthis
|
6
6
|
class Serializers
|
7
|
+
# Defines the default set of three serializers: Marshal, Passthrough, and
|
8
|
+
# JSON. With a hard limit of 7 that leaves 4 additional slots.
|
7
9
|
BASE_SERIALIZERS = {
|
8
10
|
Marshal => 0x1,
|
9
11
|
Passthrough => 0x2,
|
10
12
|
JSON => 0x3
|
11
13
|
}.freeze
|
12
14
|
|
15
|
+
# The hard serializer limit, based on the number of possible values within
|
16
|
+
# a single 3bit integer.
|
13
17
|
SERIALIZER_LIMIT = 7
|
14
18
|
|
15
19
|
attr_reader :serializers, :inverted
|
@@ -25,7 +29,7 @@ module Readthis
|
|
25
29
|
# any single application be configured for any single application. This
|
26
30
|
# limit is based on the number of bytes available in the option flag.
|
27
31
|
#
|
28
|
-
# @param [Module] Any object that responds to `dump` and `load`
|
32
|
+
# @param [Module] serializer Any object that responds to `dump` and `load`
|
29
33
|
# @return [self] Returns itself for possible chaining
|
30
34
|
#
|
31
35
|
# @example
|
@@ -36,9 +40,9 @@ module Readthis
|
|
36
40
|
def <<(serializer)
|
37
41
|
case
|
38
42
|
when serializers.frozen?
|
39
|
-
|
40
|
-
when serializers.length
|
41
|
-
|
43
|
+
raise SerializersFrozenError
|
44
|
+
when serializers.length >= SERIALIZER_LIMIT
|
45
|
+
raise SerializersLimitError
|
42
46
|
else
|
43
47
|
@serializers[serializer] = flags.max.succ
|
44
48
|
@inverted = @serializers.invert
|
@@ -63,7 +67,7 @@ module Readthis
|
|
63
67
|
|
64
68
|
# Find a flag for a serializer object.
|
65
69
|
#
|
66
|
-
# @param [Object] Look up a flag by object
|
70
|
+
# @param [Object] serializer Look up a flag by object
|
67
71
|
# @return [Number] Corresponding flag for the serializer object
|
68
72
|
# @raise [UnknownSerializerError] Indicates that a serializer was
|
69
73
|
# specified, but hasn't been configured for usage.
|
@@ -76,7 +80,7 @@ module Readthis
|
|
76
80
|
flag = serializers[serializer]
|
77
81
|
|
78
82
|
unless flag
|
79
|
-
|
83
|
+
raise UnknownSerializerError, "'#{serializer}' hasn't been configured"
|
80
84
|
end
|
81
85
|
|
82
86
|
flag
|
@@ -84,7 +88,7 @@ module Readthis
|
|
84
88
|
|
85
89
|
# Find a serializer object by flag value.
|
86
90
|
#
|
87
|
-
# @param [Number]
|
91
|
+
# @param [Number] flag Integer to look up the serializer object by
|
88
92
|
# @return [Module] The serializer object
|
89
93
|
#
|
90
94
|
# @example
|
@@ -92,7 +96,7 @@ module Readthis
|
|
92
96
|
# serializers.rassoc(1) #=> Marshal
|
93
97
|
#
|
94
98
|
def rassoc(flag)
|
95
|
-
inverted[flag &
|
99
|
+
inverted[flag & SERIALIZER_LIMIT]
|
96
100
|
end
|
97
101
|
|
98
102
|
# @private
|
data/lib/readthis/version.rb
CHANGED
data/script/mexpire.lua
ADDED
data/spec/readthis/cache_spec.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
-
require '
|
1
|
+
require 'matchers/redis_matchers'
|
2
2
|
|
3
3
|
RSpec.describe Readthis::Cache do
|
4
|
+
include RedisMatchers
|
5
|
+
|
4
6
|
let(:cache) { Readthis::Cache.new }
|
5
7
|
|
6
8
|
after do
|
@@ -48,16 +50,23 @@ RSpec.describe Readthis::Cache do
|
|
48
50
|
end
|
49
51
|
|
50
52
|
it 'uses a custom expiration' do
|
51
|
-
cache = Readthis::Cache.new(
|
53
|
+
cache = Readthis::Cache.new(expires_in: 10)
|
52
54
|
|
53
55
|
cache.write('some-key', 'some-value')
|
54
56
|
cache.write('other-key', 'other-value', expires_in: 1)
|
55
57
|
|
56
58
|
expect(cache.read('some-key')).not_to be_nil
|
57
59
|
expect(cache.read('other-key')).not_to be_nil
|
58
|
-
|
59
|
-
expect(cache.
|
60
|
-
|
60
|
+
|
61
|
+
expect(cache).to have_ttl('some-key' => 10, 'other-key' => 1)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'rounds floats to a valid expiration value' do
|
65
|
+
cache = Readthis::Cache.new
|
66
|
+
|
67
|
+
cache.write('some-key', 'some-value', expires_in: 0.1)
|
68
|
+
|
69
|
+
expect(cache).to have_ttl('some-key' => 1)
|
61
70
|
end
|
62
71
|
|
63
72
|
it 'expands non-string keys' do
|
@@ -73,6 +82,18 @@ RSpec.describe Readthis::Cache do
|
|
73
82
|
it 'gracefully handles nil options' do
|
74
83
|
expect { cache.read('whatever', nil) }.not_to raise_error
|
75
84
|
end
|
85
|
+
|
86
|
+
it 'can refresh the expiration of an entity' do
|
87
|
+
cache = Readthis::Cache.new(refresh: true)
|
88
|
+
|
89
|
+
cache.write('some-key', 'some-value', expires_in: 1)
|
90
|
+
|
91
|
+
cache.read('some-key', expires_in: 2)
|
92
|
+
expect(cache).to have_ttl('some-key' => 2)
|
93
|
+
|
94
|
+
cache.read('some-key', expires_in: 0.1)
|
95
|
+
expect(cache).to have_ttl('some-key' => 1)
|
96
|
+
end
|
76
97
|
end
|
77
98
|
|
78
99
|
describe 'serializers' do
|
@@ -151,6 +172,12 @@ RSpec.describe Readthis::Cache do
|
|
151
172
|
expect(cache.read('missing-key')).to eq(value)
|
152
173
|
end
|
153
174
|
|
175
|
+
it 'returns computed value when using passthrough marshalling' do
|
176
|
+
cache = Readthis::Cache.new(marshal: Readthis::Passthrough)
|
177
|
+
result = cache.fetch('missing-key') { 'value for you' }
|
178
|
+
expect(result).to eq('value for you')
|
179
|
+
end
|
180
|
+
|
154
181
|
it 'does not set for a missing key without a block' do
|
155
182
|
expect(cache.fetch('missing-key')).to be_nil
|
156
183
|
end
|
@@ -166,6 +193,16 @@ RSpec.describe Readthis::Cache do
|
|
166
193
|
cache.write('great-key', 'great')
|
167
194
|
expect(cache.fetch('great-key', nil)).to eq('great')
|
168
195
|
end
|
196
|
+
|
197
|
+
it 'serves computed content when the cache is down and tolerance is enabled' do
|
198
|
+
Readthis.fault_tolerant = true
|
199
|
+
|
200
|
+
allow(cache.pool).to receive(:with).and_raise(Redis::CannotConnectError)
|
201
|
+
|
202
|
+
computed = cache.fetch('error-key') { 'computed' }
|
203
|
+
|
204
|
+
expect(computed).to eq('computed')
|
205
|
+
end
|
169
206
|
end
|
170
207
|
|
171
208
|
describe '#read_multi' do
|
@@ -194,6 +231,17 @@ RSpec.describe Readthis::Cache do
|
|
194
231
|
it 'returns {} with no keys' do
|
195
232
|
expect(cache.read_multi(namespace: 'cache')).to eq({})
|
196
233
|
end
|
234
|
+
|
235
|
+
it 'refreshes each key that is read' do
|
236
|
+
cache = Readthis::Cache.new(refresh: true)
|
237
|
+
|
238
|
+
cache.write('a', 1, expires_in: 1)
|
239
|
+
cache.write('b', 2, expires_in: 1)
|
240
|
+
|
241
|
+
cache.read_multi('a', 'b', expires_in: 2)
|
242
|
+
|
243
|
+
expect(cache).to have_ttl('a' => 2, 'b' => 2)
|
244
|
+
end
|
197
245
|
end
|
198
246
|
|
199
247
|
describe '#write_multi' do
|
@@ -214,8 +262,8 @@ RSpec.describe Readthis::Cache do
|
|
214
262
|
|
215
263
|
expect(cache.read('a')).to be_nil
|
216
264
|
expect(cache.read('a', namespace: 'multi')).to eq(1)
|
217
|
-
|
218
|
-
expect(cache.
|
265
|
+
|
266
|
+
expect(cache).to have_ttl('multi:a' => 1)
|
219
267
|
end
|
220
268
|
end
|
221
269
|
|
@@ -279,6 +327,32 @@ RSpec.describe Readthis::Cache do
|
|
279
327
|
end
|
280
328
|
end
|
281
329
|
|
330
|
+
describe '#delete_matched' do
|
331
|
+
it 'deletes all matching keys' do
|
332
|
+
cache.write('tomcat', 'cat')
|
333
|
+
cache.write('wildcat', 'cat')
|
334
|
+
cache.write('bobcat', 'cat')
|
335
|
+
cache.write('cougar', 'cat')
|
336
|
+
|
337
|
+
expect(cache.delete_matched('tomcat')).to eq(1)
|
338
|
+
expect(cache.read('tomcat')).to be_nil
|
339
|
+
expect(cache.read('bobcat')).not_to be_nil
|
340
|
+
expect(cache.read('wildcat')).not_to be_nil
|
341
|
+
|
342
|
+
expect(cache.delete_matched('*cat', count: 1)).to eq(2)
|
343
|
+
expect(cache.read('wildcat')).to be_nil
|
344
|
+
expect(cache.read('bobcat')).to be_nil
|
345
|
+
|
346
|
+
expect(cache.delete_matched('*cat')).to eq(0)
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'respects namespacing when matching keys' do
|
350
|
+
cache.write('tomcat', 'cat', namespace: 'feral')
|
351
|
+
|
352
|
+
expect(cache.delete_matched('tom*', namespace: 'feral')).to eq(1)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
282
356
|
describe '#increment' do
|
283
357
|
it 'atomically increases the stored integer' do
|
284
358
|
cache.write('counter', 10)
|
@@ -318,10 +392,10 @@ RSpec.describe Readthis::Cache do
|
|
318
392
|
cache.read('a')
|
319
393
|
|
320
394
|
expect(events.length).to eq(2)
|
321
|
-
expect(events.map(&:name)).to eq
|
395
|
+
expect(events.map(&:name)).to eq %w[
|
322
396
|
cache_write.active_support
|
323
397
|
cache_read.active_support
|
324
|
-
]
|
398
|
+
]
|
325
399
|
end
|
326
400
|
end
|
327
401
|
end
|
@@ -1,17 +1,16 @@
|
|
1
|
-
require 'readthis/passthrough'
|
2
|
-
|
3
1
|
RSpec.describe Readthis::Passthrough do
|
2
|
+
let(:value) { 'skywalker' }
|
3
|
+
|
4
4
|
describe '.load' do
|
5
5
|
it 'passes through the provided value' do
|
6
|
-
value = Object.new
|
7
6
|
expect(Readthis::Passthrough.load(value)).to eq(value)
|
8
7
|
end
|
9
8
|
end
|
10
9
|
|
11
10
|
describe '.dump' do
|
12
11
|
it 'passes through the provided value' do
|
13
|
-
value = Object.new
|
14
12
|
expect(Readthis::Passthrough.dump(value)).to eq(value)
|
13
|
+
expect(Readthis::Passthrough.dump(value)).not_to be(value)
|
15
14
|
end
|
16
15
|
end
|
17
16
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
RSpec.describe Readthis::Scripts do
|
2
|
+
let(:scripts) { Readthis::Scripts.new }
|
3
|
+
|
4
|
+
describe '#run' do
|
5
|
+
it 'raises an error with an unknown command' do
|
6
|
+
expect do
|
7
|
+
scripts.run('unknown', nil, [])
|
8
|
+
end.to raise_error(Readthis::UnknownCommandError)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'runs the script command with a single key' do
|
12
|
+
store = Redis.new
|
13
|
+
|
14
|
+
store.set('alpha', 'content')
|
15
|
+
scripts.run('mexpire', store, 'alpha', 1)
|
16
|
+
|
17
|
+
expect(store.ttl('alpha')).to eq(1)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'runs the script command with multiple keys' do
|
21
|
+
store = Redis.new
|
22
|
+
|
23
|
+
store.set('beta', 'content')
|
24
|
+
store.set('gamma', 'content')
|
25
|
+
scripts.run('mexpire', store, %w[beta gamma], 1)
|
26
|
+
|
27
|
+
expect(store.ttl('beta')).to eq(1)
|
28
|
+
expect(store.ttl('gamma')).to eq(1)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'readthis/serializers'
|
2
|
-
|
3
1
|
RSpec.describe Readthis::Serializers do
|
4
2
|
CustomSerializer = Class.new
|
5
3
|
AnotherSerializer = Class.new
|
@@ -24,9 +22,9 @@ RSpec.describe Readthis::Serializers do
|
|
24
22
|
|
25
23
|
it 'prevents more than seven serializers' do
|
26
24
|
serializers = Readthis::Serializers.new
|
27
|
-
|
25
|
+
serializers << Class.new until serializers.flags.length >= 7
|
28
26
|
expect do
|
29
|
-
|
27
|
+
serializers << Class.new
|
30
28
|
end.to raise_error(Readthis::SerializersLimitError)
|
31
29
|
end
|
32
30
|
end
|
@@ -48,18 +46,29 @@ RSpec.describe Readthis::Serializers do
|
|
48
46
|
end
|
49
47
|
|
50
48
|
describe '#rassoc' do
|
51
|
-
|
52
|
-
serializers = Readthis::Serializers.new
|
49
|
+
let(:serializers) { Readthis::Serializers.new }
|
53
50
|
|
51
|
+
it 'inverts the current set of serializers' do
|
54
52
|
expect(serializers.rassoc(1)).to eq(Marshal)
|
55
53
|
end
|
56
54
|
|
57
55
|
it 'returns custom serializers' do
|
58
|
-
serializers = Readthis::Serializers.new
|
59
56
|
serializers << CustomSerializer
|
60
|
-
|
61
57
|
expect(serializers.rassoc(4)).to eq(CustomSerializer)
|
62
58
|
end
|
59
|
+
|
60
|
+
it 'inverts default serializers after adding custom one' do
|
61
|
+
serializers << CustomSerializer
|
62
|
+
expect(serializers.rassoc(1)).to eq(Marshal)
|
63
|
+
expect(serializers.rassoc(3)).to eq(JSON)
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'takes into account only first 3 bytes of passed integer' do
|
67
|
+
expect(serializers.rassoc(1)).to eq(Marshal)
|
68
|
+
expect(serializers.rassoc(11)).to eq(JSON)
|
69
|
+
serializers << CustomSerializer
|
70
|
+
expect(serializers.rassoc(12)).to eq(CustomSerializer)
|
71
|
+
end
|
63
72
|
end
|
64
73
|
|
65
74
|
describe '#freeze!' do
|
data/spec/readthis_spec.rb
CHANGED
@@ -1,9 +1,19 @@
|
|
1
|
-
require 'readthis'
|
2
|
-
|
3
1
|
RSpec.describe Readthis do
|
4
2
|
describe '#serializers' do
|
5
3
|
it 'lists currently configured serializers' do
|
6
4
|
expect(Readthis.serializers.marshals).to include(Marshal, JSON)
|
7
5
|
end
|
8
6
|
end
|
7
|
+
|
8
|
+
describe '#fault_tolerant?' do
|
9
|
+
it 'defaults to being false' do
|
10
|
+
expect(Readthis).not_to be_fault_tolerant
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'can be enabled' do
|
14
|
+
Readthis.fault_tolerant = true
|
15
|
+
|
16
|
+
expect(Readthis).to be_fault_tolerant
|
17
|
+
end
|
18
|
+
end
|
9
19
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: readthis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Parker Selbert
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-07-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -123,12 +123,16 @@ files:
|
|
123
123
|
- lib/readthis/errors.rb
|
124
124
|
- lib/readthis/expanders.rb
|
125
125
|
- lib/readthis/passthrough.rb
|
126
|
+
- lib/readthis/scripts.rb
|
126
127
|
- lib/readthis/serializers.rb
|
127
128
|
- lib/readthis/version.rb
|
129
|
+
- script/mexpire.lua
|
130
|
+
- spec/matchers/redis_matchers.rb
|
128
131
|
- spec/readthis/cache_spec.rb
|
129
132
|
- spec/readthis/entity_spec.rb
|
130
133
|
- spec/readthis/expanders_spec.rb
|
131
134
|
- spec/readthis/passthrough_spec.rb
|
135
|
+
- spec/readthis/scripts_spec.rb
|
132
136
|
- spec/readthis/serializers_spec.rb
|
133
137
|
- spec/readthis_spec.rb
|
134
138
|
- spec/spec_helper.rb
|
@@ -152,15 +156,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
156
|
version: '0'
|
153
157
|
requirements: []
|
154
158
|
rubyforge_project:
|
155
|
-
rubygems_version: 2.
|
159
|
+
rubygems_version: 2.5.1
|
156
160
|
signing_key:
|
157
161
|
specification_version: 4
|
158
162
|
summary: Pooled active support compliant caching with redis
|
159
163
|
test_files:
|
164
|
+
- spec/matchers/redis_matchers.rb
|
160
165
|
- spec/readthis/cache_spec.rb
|
161
166
|
- spec/readthis/entity_spec.rb
|
162
167
|
- spec/readthis/expanders_spec.rb
|
163
168
|
- spec/readthis/passthrough_spec.rb
|
169
|
+
- spec/readthis/scripts_spec.rb
|
164
170
|
- spec/readthis/serializers_spec.rb
|
165
171
|
- spec/readthis_spec.rb
|
166
172
|
- spec/spec_helper.rb
|
173
|
+
has_rdoc:
|