readthis 1.1.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/sorentwo/readthis.svg?branch=master)](https://travis-ci.org/sorentwo/readthis)
|
3
3
|
[![Code Climate](https://codeclimate.com/github/sorentwo/readthis/badges/gpa.svg)](https://codeclimate.com/github/sorentwo/readthis)
|
4
4
|
[![Coverage Status](https://coveralls.io/repos/sorentwo/readthis/badge.svg?branch=master&service=github)](https://coveralls.io/github/sorentwo/readthis?branch=master)
|
5
|
+
[![Inline Docs](http://inch-ci.org/github/sorentwo/readthis.svg?branch=master)](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:
|