readthis 0.8.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +90 -16
- data/lib/active_support/cache/readthis_store.rb +1 -1
- data/lib/readthis/cache.rb +28 -30
- data/lib/readthis/entity.rb +120 -28
- data/lib/readthis/errors.rb +7 -0
- data/lib/readthis/expanders.rb +1 -1
- data/lib/readthis/serializers.rb +108 -0
- data/lib/readthis/version.rb +1 -1
- data/lib/readthis.rb +37 -0
- data/spec/readthis/cache_spec.rb +90 -22
- data/spec/readthis/entity_spec.rb +69 -12
- data/spec/readthis/expanders_spec.rb +1 -1
- data/spec/readthis/serializers_spec.rb +87 -0
- data/spec/readthis_spec.rb +21 -0
- data/spec/spec_helper.rb +5 -3
- metadata +24 -24
- data/.gitignore +0 -15
- data/.rspec +0 -2
- data/.travis.yml +0 -15
- data/CHANGELOG.md +0 -114
- data/CONTRIBUTING.md +0 -14
- data/Gemfile +0 -14
- data/LICENSE.txt +0 -22
- data/PERFORMANCE.md +0 -73
- data/Rakefile +0 -2
- data/benchmarks/compressed.rb +0 -74
- data/benchmarks/driver.rb +0 -18
- data/benchmarks/marshalling.rb +0 -40
- data/benchmarks/memory.rb +0 -11
- data/benchmarks/multi.rb +0 -64
- data/benchmarks/profile.rb +0 -20
- data/bin/rspec +0 -16
- data/lib/readthis/notifications.rb +0 -7
- data/readthis.gemspec +0 -27
- data/spec/readthis/notifications_spec.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2c3b9c325211a544d3cdffbbb4356c7c392a5c04
|
4
|
+
data.tar.gz: ecd6d0982a96bcff0f9bfafff60b6d758ab5ee55
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 92323a5bfb41f8a29ea0b3082ff500078af9944d85fd756a1147b2cc1a7899e7fa4900c086463bfaa775608cf3d7c86137b4e54084f72ce1a2110b3f14d55c2b
|
7
|
+
data.tar.gz: f2d19489b82236baee8be73033b07f3a375585195fc251b17c5ce9f15972911e696e038b282a949a513c8afda4465706f2a581b90c99529da7255e364fe9517b
|
data/README.md
CHANGED
@@ -5,9 +5,10 @@
|
|
5
5
|
|
6
6
|
# Readthis
|
7
7
|
|
8
|
-
Readthis is a
|
9
|
-
|
10
|
-
|
8
|
+
Readthis is a Redis backed cache client for Ruby. It is a drop in replacement
|
9
|
+
for any `ActiveSupport` compliant cache and can also be used for [session
|
10
|
+
storage](#session-storage). Above all Readthis emphasizes performance,
|
11
|
+
simplicity, and explicitness.
|
11
12
|
|
12
13
|
For new projects there isn't any reason to stick with Memcached. Redis is as
|
13
14
|
fast, if not faster in many scenarios, and is far more likely to be used
|
@@ -52,7 +53,11 @@ cache = Readthis::Cache.new(
|
|
52
53
|
)
|
53
54
|
```
|
54
55
|
|
56
|
+
You can also specify `host`, `port`, `db` or any other valid Redis options. For
|
57
|
+
more details about connection options see in [redis gem documentation][redisrb]
|
58
|
+
|
55
59
|
[store]: http://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html
|
60
|
+
[redisrb]: https://github.com/redis/redis-rb#getting-started
|
56
61
|
|
57
62
|
### Instances & Databases
|
58
63
|
|
@@ -61,24 +66,29 @@ instances have numerous benefits like: more predictable performance, avoiding
|
|
61
66
|
expires in favor of LRU, and tuning the persistence mechanism. See [Optimizing
|
62
67
|
Redis Usage for Caching][optimizing-usage] for more details.
|
63
68
|
|
64
|
-
|
65
|
-
|
66
|
-
At the very least you'll want to use a specific database for caching. In the
|
69
|
+
At the very least, you'll want to use a specific database for caching. In the
|
67
70
|
event the database needs to be purged you can do so with a single `clear`
|
68
71
|
command, rather than finding all keys in a namespace and deleting them.
|
69
72
|
Appending a number between 0 and 15 will specify the redis database, which
|
70
|
-
defaults to 0
|
73
|
+
defaults to `0`. For example, using database `2`:
|
71
74
|
|
72
75
|
```bash
|
73
76
|
REDIS_URL=redis://localhost:6379/2
|
74
77
|
```
|
75
78
|
|
79
|
+
[optimizing-usage]: http://sorentwo.com/2015/07/27/optimizing-redis-usage-for-caching.html
|
80
|
+
|
76
81
|
### Expiration
|
77
82
|
|
78
83
|
Be sure to use an integer value when setting expiration time. The default
|
79
84
|
representation of `ActiveSupport::Duration` values won't work when setting
|
80
85
|
expiration time, which will cause all keys to have `-1` as the TTL. Expiration
|
81
|
-
values are always cast as an integer on write.
|
86
|
+
values are always cast as an integer on write. For example:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
Readthis::Cache.new(expires_in: 1.week) # don't do this
|
90
|
+
Readthis::Cache.new(expires_in: 1.week.to_i) # do this
|
91
|
+
```
|
82
92
|
|
83
93
|
### Compression
|
84
94
|
|
@@ -96,23 +106,64 @@ config.cache_store = :readthis_store, {
|
|
96
106
|
}
|
97
107
|
```
|
98
108
|
|
99
|
-
###
|
109
|
+
### Serializing
|
110
|
+
|
111
|
+
Readthis uses Ruby's `Marshal` module for serializing all values by default.
|
112
|
+
This isn't always the fastest option, and depending on your use case it may be
|
113
|
+
desirable to use a faster but less flexible serializer.
|
114
|
+
|
115
|
+
By default Readthis knows about 3 different serializers:
|
100
116
|
|
101
|
-
|
102
|
-
|
103
|
-
|
117
|
+
* Marshal
|
118
|
+
* JSON
|
119
|
+
* Passthrough
|
104
120
|
|
105
|
-
|
121
|
+
If all cached data can safely be represented as a string then use the
|
122
|
+
pass-through serializer:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
Readthis::Cache.new(marshal: Readthis::Passthrough)
|
126
|
+
```
|
127
|
+
|
128
|
+
You can introduce up to four additional serializers by configuring `serializers`
|
129
|
+
on the Readthis module. For example, if you wanted to use the extremely fast Oj
|
130
|
+
library for JSON serialization:
|
106
131
|
|
107
132
|
```ruby
|
133
|
+
Readthis.serializers << Oj
|
134
|
+
|
135
|
+
# Freeze the serializers to ensure they aren't changed at runtime.
|
136
|
+
Readthis.serializers.freeze!
|
137
|
+
|
108
138
|
Readthis::Cache.new(marshal: Oj)
|
109
139
|
```
|
110
140
|
|
111
|
-
|
112
|
-
|
141
|
+
Be aware that the *order in which you add serializers matters*. Serializers are
|
142
|
+
sticky and a flag is stored with each cached value. If you subsequently go to
|
143
|
+
deserialize values and haven't configured the same serializers in the same order
|
144
|
+
your application will raise errors.
|
145
|
+
|
146
|
+
## Fault Tolerance
|
147
|
+
|
148
|
+
In some situations it is desirable to keep serving requests from disk or the
|
149
|
+
database if Redis crashes. This can be achieved with connection fault tolerance
|
150
|
+
by enabling it at the top level:
|
113
151
|
|
114
152
|
```ruby
|
115
|
-
Readthis
|
153
|
+
Readthis.fault_tolerant = true
|
154
|
+
```
|
155
|
+
|
156
|
+
The default value is `false`, because while it may work for `fetch` operations,
|
157
|
+
it isn't compatible with other state-based commands like `increment`.
|
158
|
+
|
159
|
+
## Running Arbitrary Redis Commands
|
160
|
+
|
161
|
+
Readthis provides access to the underlying Redis connection pool, allowing you
|
162
|
+
to run arbitrary commands directly through the cache instance. For example, if
|
163
|
+
you wanted to expire a key manually using an instance of `Rails.cache`:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
Rails.cache.pool.with { |client| client.expire('foo-key', 60) }
|
116
167
|
```
|
117
168
|
|
118
169
|
## Differences From ActiveSupport::Cache
|
@@ -122,11 +173,34 @@ Readthis supports all of standard cache methods except for the following:
|
|
122
173
|
* `cleanup` - Redis does this with TTL or LRU already.
|
123
174
|
* `delete_matched` - You really don't want to perform key matching operations in
|
124
175
|
Redis. They are linear time and only support basic globbing.
|
176
|
+
* `mute` and `silence!` - You can subscribe to the events `/cache*+active_support/` with `ActiveSupport::Notifications` to [log cache calls manually][notifications].
|
177
|
+
|
178
|
+
[notifications]: https://github.com/sorentwo/readthis/issues/22#issuecomment-142595938
|
125
179
|
|
126
180
|
Like other `ActiveSupport::Cache` implementations it is possible to cache `nil`
|
127
181
|
as a value. However, the fetch methods treat `nil` values as a cache miss and
|
128
182
|
re-generate/re-cache the value. Caching `nil` isn't recommended.
|
129
183
|
|
184
|
+
## Session Storage
|
185
|
+
|
186
|
+
By using [ActionDispatch::Session::CacheStore][cache-store] it's possible to
|
187
|
+
reuse `:readthis_store` or specify a new Readthis cache store for storing
|
188
|
+
sessions.
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
Rails.application.config.session_store :cache_store
|
192
|
+
```
|
193
|
+
|
194
|
+
To specify a separate Readthis instance you can use the `:cache` option:
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
Rails.application.config.session_store :cache_store,
|
198
|
+
cache: Readthis::Cache.new,
|
199
|
+
expire_after: 2.weeks.to_i
|
200
|
+
```
|
201
|
+
|
202
|
+
[cache-store]: http://api.rubyonrails.org/classes/ActionDispatch/Session/CacheStore.html
|
203
|
+
|
130
204
|
## Contributing
|
131
205
|
|
132
206
|
1. Fork it
|
data/lib/readthis/cache.rb
CHANGED
@@ -1,24 +1,19 @@
|
|
1
1
|
require 'readthis/entity'
|
2
2
|
require 'readthis/expanders'
|
3
|
-
require 'readthis/notifications'
|
4
3
|
require 'readthis/passthrough'
|
5
4
|
require 'redis'
|
6
5
|
require 'connection_pool'
|
7
6
|
|
8
7
|
module Readthis
|
9
8
|
class Cache
|
10
|
-
attr_reader :entity, :
|
9
|
+
attr_reader :entity, :notifications, :options, :pool
|
11
10
|
|
12
11
|
# Provide a class level lookup of the proper notifications module.
|
13
12
|
# Instrumention is expected to occur within applications that have
|
14
13
|
# ActiveSupport::Notifications available, but needs to work even when it
|
15
14
|
# isn't.
|
16
15
|
def self.notifications
|
17
|
-
if defined?(ActiveSupport::Notifications)
|
18
|
-
ActiveSupport::Notifications
|
19
|
-
else
|
20
|
-
Readthis::Notifications
|
21
|
-
end
|
16
|
+
ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
|
22
17
|
end
|
23
18
|
|
24
19
|
# Creates a new Readthis::Cache object with the given options.
|
@@ -39,9 +34,7 @@ module Readthis
|
|
39
34
|
# Readthis::Cache.new(compress: true, compression_threshold: 2048)
|
40
35
|
#
|
41
36
|
def initialize(options = {})
|
42
|
-
@options
|
43
|
-
@expires_in = options.fetch(:expires_in, nil)
|
44
|
-
@namespace = options.fetch(:namespace, nil)
|
37
|
+
@options = options
|
45
38
|
|
46
39
|
@entity = Readthis::Entity.new(
|
47
40
|
marshal: options.fetch(:marshal, Marshal),
|
@@ -145,6 +138,7 @@ module Readthis
|
|
145
138
|
# cache.fetch('today', force: true) # => nil
|
146
139
|
#
|
147
140
|
def fetch(key, options = {})
|
141
|
+
options ||= {}
|
148
142
|
value = read(key, options) unless options[:force]
|
149
143
|
|
150
144
|
if value.nil? && block_given?
|
@@ -171,7 +165,7 @@ module Readthis
|
|
171
165
|
# cache.increment('counter', 2) # => 3
|
172
166
|
#
|
173
167
|
def increment(key, amount = 1, options = {})
|
174
|
-
invoke(:
|
168
|
+
invoke(:increment, key) do |_store|
|
175
169
|
alter(key, amount, options)
|
176
170
|
end
|
177
171
|
end
|
@@ -192,7 +186,7 @@ module Readthis
|
|
192
186
|
# cache.decrement('counter', 2) # => 17
|
193
187
|
#
|
194
188
|
def decrement(key, amount = 1, options = {})
|
195
|
-
invoke(:decrement, key) do |
|
189
|
+
invoke(:decrement, key) do |_store|
|
196
190
|
alter(key, amount * -1, options)
|
197
191
|
end
|
198
192
|
end
|
@@ -218,7 +212,7 @@ module Readthis
|
|
218
212
|
return {} if keys.empty?
|
219
213
|
|
220
214
|
invoke(:read_multi, keys) do |store|
|
221
|
-
values = store.mget(mapping).map { |value| entity.load(value) }
|
215
|
+
values = store.mget(*mapping).map { |value| entity.load(value) }
|
222
216
|
|
223
217
|
keys.zip(values).to_h
|
224
218
|
end
|
@@ -271,13 +265,13 @@ module Readthis
|
|
271
265
|
extracted = extract_options!(keys)
|
272
266
|
missing = {}
|
273
267
|
|
274
|
-
invoke(:fetch_multi, keys) do |
|
268
|
+
invoke(:fetch_multi, keys) do |_store|
|
275
269
|
results.each do |key, value|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
270
|
+
next unless value.nil?
|
271
|
+
|
272
|
+
value = yield(key)
|
273
|
+
missing[key] = value
|
274
|
+
results[key] = value
|
281
275
|
end
|
282
276
|
end
|
283
277
|
|
@@ -310,7 +304,7 @@ module Readthis
|
|
310
304
|
# @example
|
311
305
|
#
|
312
306
|
# cache.clear #=> 'OK'
|
313
|
-
def clear(
|
307
|
+
def clear(_options = nil)
|
314
308
|
invoke(:clear, '*', &:flushdb)
|
315
309
|
end
|
316
310
|
|
@@ -318,11 +312,12 @@ module Readthis
|
|
318
312
|
|
319
313
|
def write_entity(key, value, store, options)
|
320
314
|
namespaced = namespaced_key(key, options)
|
315
|
+
dumped = entity.dump(value, options)
|
321
316
|
|
322
317
|
if expiration = options[:expires_in]
|
323
|
-
store.setex(namespaced, expiration.to_i,
|
318
|
+
store.setex(namespaced, expiration.to_i, dumped)
|
324
319
|
else
|
325
|
-
store.set(namespaced,
|
320
|
+
store.set(namespaced, dumped)
|
326
321
|
end
|
327
322
|
end
|
328
323
|
|
@@ -335,17 +330,23 @@ module Readthis
|
|
335
330
|
delta
|
336
331
|
end
|
337
332
|
|
338
|
-
def instrument(
|
339
|
-
|
340
|
-
|
333
|
+
def instrument(name, key)
|
334
|
+
if self.class.notifications
|
335
|
+
name = "cache_#{name}.active_support"
|
336
|
+
payload = { key: key, name: name }
|
341
337
|
|
342
|
-
|
338
|
+
self.class.notifications.instrument(name, payload) { yield(payload) }
|
339
|
+
else
|
340
|
+
yield
|
341
|
+
end
|
343
342
|
end
|
344
343
|
|
345
344
|
def invoke(operation, key, &block)
|
346
345
|
instrument(operation, key) do
|
347
346
|
pool.with(&block)
|
348
347
|
end
|
348
|
+
rescue Redis::BaseError => error
|
349
|
+
raise error unless Readthis.fault_tolerant?
|
349
350
|
end
|
350
351
|
|
351
352
|
def extract_options!(array)
|
@@ -353,10 +354,7 @@ module Readthis
|
|
353
354
|
end
|
354
355
|
|
355
356
|
def merged_options(options)
|
356
|
-
options
|
357
|
-
options[:namespace] ||= namespace
|
358
|
-
options[:expires_in] ||= expires_in
|
359
|
-
options
|
357
|
+
@options.merge(options || {})
|
360
358
|
end
|
361
359
|
|
362
360
|
def pool_options(options)
|
data/lib/readthis/entity.rb
CHANGED
@@ -2,51 +2,143 @@ require 'zlib'
|
|
2
2
|
|
3
3
|
module Readthis
|
4
4
|
class Entity
|
5
|
-
|
6
|
-
|
5
|
+
DEFAULT_OPTIONS = {
|
6
|
+
compress: false,
|
7
|
+
marshal: Marshal,
|
8
|
+
threshold: 8 * 1024
|
9
|
+
}.freeze
|
7
10
|
|
8
|
-
|
11
|
+
COMPRESSED_FLAG = 0x8
|
9
12
|
|
13
|
+
# Creates a Readthis::Entity with default options. Each option can be
|
14
|
+
# overridden later when entities are being dumped.
|
15
|
+
#
|
16
|
+
# Options are sticky, meaning that whatever is used when dumping will
|
17
|
+
# automatically be used again when loading, regardless of how current
|
18
|
+
# options are set.
|
19
|
+
#
|
20
|
+
# @option [Boolean] :compress (false) Enable or disable automatic compression
|
21
|
+
# @option [Module] :marshal (Marshal) Any module that responds to `dump` and `load`
|
22
|
+
# @option [Number] :threshold (8k) The size a string must be for compression
|
23
|
+
#
|
10
24
|
def initialize(options = {})
|
11
|
-
@
|
12
|
-
@compression = options.fetch(:compress, false)
|
13
|
-
@threshold = options.fetch(:threshold, DEFAULT_THRESHOLD)
|
25
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
14
26
|
end
|
15
27
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
28
|
+
# Output a value prepared for cache storage. Passed options will override
|
29
|
+
# whatever has been specified for the instance.
|
30
|
+
#
|
31
|
+
# @param [String] String to dump
|
32
|
+
# @option [Boolean] :compress Enable or disable automatic compression
|
33
|
+
# @option [Module] :marshal Any module that responds to `dump` and `load`
|
34
|
+
# @option [Number] :threshold The size a string must be for compression
|
35
|
+
# @return [String] The prepared, possibly compressed, string
|
36
|
+
#
|
37
|
+
# @example Dumping a value using defaults
|
38
|
+
#
|
39
|
+
# entity.dump(string)
|
40
|
+
#
|
41
|
+
# @example Dumping a value with overrides
|
42
|
+
#
|
43
|
+
# entity.dump(string, compress: false, marshal: JSON)
|
44
|
+
#
|
45
|
+
def dump(value, options = {})
|
46
|
+
compress = with_fallback(options, :compress)
|
47
|
+
marshal = with_fallback(options, :marshal)
|
48
|
+
threshold = with_fallback(options, :threshold)
|
49
|
+
|
50
|
+
dumped = deflate(marshal.dump(value), compress, threshold)
|
51
|
+
|
52
|
+
compose(dumped, marshal, compress)
|
22
53
|
end
|
23
54
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
55
|
+
# Parse a dumped value using the embedded options.
|
56
|
+
#
|
57
|
+
# @param [String] Option embedded string to load
|
58
|
+
# @return [String] The original dumped string, restored
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
#
|
62
|
+
# entity.load(dumped)
|
63
|
+
#
|
64
|
+
def load(string)
|
65
|
+
marshal, compress, value = decompose(string)
|
66
|
+
|
67
|
+
marshal.load(inflate(value, compress))
|
68
|
+
rescue TypeError, NoMethodError
|
69
|
+
string
|
32
70
|
end
|
33
71
|
|
34
|
-
|
35
|
-
|
72
|
+
# Composes a single byte comprised of the chosen serializer and compression
|
73
|
+
# options. The byte is formatted as:
|
74
|
+
#
|
75
|
+
# | 0000 | 0 | 000 |
|
76
|
+
#
|
77
|
+
# Where there are four unused bits, 1 compression bit, and 3 bits for the
|
78
|
+
# serializer. This allows up to 8 different serializers for marshaling.
|
79
|
+
#
|
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
|
83
|
+
# @return [String] The original string with a single byte prefixed
|
84
|
+
#
|
85
|
+
# @example Compose an option embedded string
|
86
|
+
#
|
87
|
+
# entity.compose(string, Marshal, false) => 0x1 + string
|
88
|
+
# entity.compose(string, JSON, true) => 0x10 + string
|
89
|
+
#
|
90
|
+
def compose(value, marshal, compress)
|
91
|
+
flags = serializers.assoc(marshal)
|
92
|
+
flags |= COMPRESSED_FLAG if compress
|
93
|
+
|
94
|
+
value.prepend([flags].pack('C'))
|
36
95
|
end
|
37
96
|
|
38
|
-
|
39
|
-
|
97
|
+
# Decompose an option embedded string into marshal, compression and value.
|
98
|
+
#
|
99
|
+
# @param [String] Option embedded string to
|
100
|
+
# @return [Array<Module, Boolean, String>] An array comprised of the
|
101
|
+
# marshal, compression flag, and the base string.
|
102
|
+
#
|
103
|
+
def decompose(string)
|
104
|
+
flags = string[0].unpack('C').first
|
105
|
+
|
106
|
+
if flags < 16
|
107
|
+
marshal = serializers.rassoc(flags)
|
108
|
+
compress = (flags & COMPRESSED_FLAG) != 0
|
109
|
+
|
110
|
+
[marshal, compress, string[1..-1]]
|
111
|
+
else
|
112
|
+
[@options[:marshal], @options[:compress], string]
|
113
|
+
end
|
40
114
|
end
|
41
115
|
|
42
116
|
private
|
43
117
|
|
44
|
-
def
|
45
|
-
|
118
|
+
def deflate(value, compress, threshold)
|
119
|
+
if compress && value.bytesize >= threshold
|
120
|
+
Zlib::Deflate.deflate(value)
|
121
|
+
else
|
122
|
+
value
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def inflate(value, decompress)
|
127
|
+
if decompress
|
128
|
+
Zlib::Inflate.inflate(value)
|
129
|
+
else
|
130
|
+
value
|
131
|
+
end
|
132
|
+
rescue Zlib::Error
|
133
|
+
value
|
134
|
+
end
|
135
|
+
|
136
|
+
def serializers
|
137
|
+
Readthis.serializers
|
46
138
|
end
|
47
139
|
|
48
|
-
def
|
49
|
-
|
140
|
+
def with_fallback(options, key)
|
141
|
+
options.key?(key) ? options[key] : @options[key]
|
50
142
|
end
|
51
143
|
end
|
52
144
|
end
|
data/lib/readthis/expanders.rb
CHANGED
@@ -7,7 +7,7 @@ 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.sort_by { |
|
10
|
+
key.sort_by { |hkey, _| hkey.to_s }.map { |hkey, val| "#{hkey}=#{val}" }.join('/')
|
11
11
|
when key.respond_to?(:to_param)
|
12
12
|
key.to_param
|
13
13
|
else
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'readthis/errors'
|
3
|
+
require 'readthis/passthrough'
|
4
|
+
|
5
|
+
module Readthis
|
6
|
+
class Serializers
|
7
|
+
BASE_SERIALIZERS = {
|
8
|
+
Marshal => 0x1,
|
9
|
+
Passthrough => 0x2,
|
10
|
+
JSON => 0x3
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
SERIALIZER_LIMIT = 7
|
14
|
+
|
15
|
+
attr_reader :serializers, :inverted
|
16
|
+
|
17
|
+
# Creates a new Readthis::Serializers entity. No configuration is expected
|
18
|
+
# during initialization.
|
19
|
+
#
|
20
|
+
def initialize
|
21
|
+
reset!
|
22
|
+
end
|
23
|
+
|
24
|
+
# Append a new serializer. Up to 7 total serializers may be configured for
|
25
|
+
# any single application be configured for any single application. This
|
26
|
+
# limit is based on the number of bytes available in the option flag.
|
27
|
+
#
|
28
|
+
# @param [Module] Any object that responds to `dump` and `load`
|
29
|
+
# @return [self] Returns itself for possible chaining
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
#
|
33
|
+
# serializers = Readthis::Serializers.new
|
34
|
+
# serializers << Oj
|
35
|
+
#
|
36
|
+
def <<(serializer)
|
37
|
+
case
|
38
|
+
when serializers.frozen?
|
39
|
+
fail SerializersFrozenError
|
40
|
+
when serializers.length > SERIALIZER_LIMIT
|
41
|
+
fail SerializersLimitError
|
42
|
+
else
|
43
|
+
@serializers[serializer] = flags.max.succ
|
44
|
+
@inverted = @serializers.invert
|
45
|
+
end
|
46
|
+
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
# Freeze the serializers hash, preventing modification.
|
51
|
+
#
|
52
|
+
def freeze!
|
53
|
+
serializers.freeze
|
54
|
+
end
|
55
|
+
|
56
|
+
# Reset the instance back to the default state. Useful for cleanup during
|
57
|
+
# testing.
|
58
|
+
#
|
59
|
+
def reset!
|
60
|
+
@serializers = BASE_SERIALIZERS.dup
|
61
|
+
@inverted = @serializers.invert
|
62
|
+
end
|
63
|
+
|
64
|
+
# Find a flag for a serializer object.
|
65
|
+
#
|
66
|
+
# @param [Object] Look up a flag by object
|
67
|
+
# @return [Number] Corresponding flag for the serializer object
|
68
|
+
# @raise [UnknownSerializerError] Indicates that a serializer was
|
69
|
+
# specified, but hasn't been configured for usage.
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
#
|
73
|
+
# serializers.assoc(JSON) #=> 1
|
74
|
+
#
|
75
|
+
def assoc(serializer)
|
76
|
+
flag = serializers[serializer]
|
77
|
+
|
78
|
+
unless flag
|
79
|
+
fail UnknownSerializerError, "'#{serializer}' hasn't been configured"
|
80
|
+
end
|
81
|
+
|
82
|
+
flag
|
83
|
+
end
|
84
|
+
|
85
|
+
# Find a serializer object by flag value.
|
86
|
+
#
|
87
|
+
# @param [Number] Flag to look up the serializer object by
|
88
|
+
# @return [Module] The serializer object
|
89
|
+
#
|
90
|
+
# @example
|
91
|
+
#
|
92
|
+
# serializers.rassoc(1) #=> Marshal
|
93
|
+
#
|
94
|
+
def rassoc(flag)
|
95
|
+
inverted[flag & inverted.length]
|
96
|
+
end
|
97
|
+
|
98
|
+
# @private
|
99
|
+
def marshals
|
100
|
+
serializers.keys
|
101
|
+
end
|
102
|
+
|
103
|
+
# @private
|
104
|
+
def flags
|
105
|
+
serializers.values
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/readthis/version.rb
CHANGED
data/lib/readthis.rb
CHANGED
@@ -1,5 +1,42 @@
|
|
1
1
|
require 'readthis/cache'
|
2
|
+
require 'readthis/errors'
|
3
|
+
require 'readthis/serializers'
|
2
4
|
require 'readthis/version'
|
3
5
|
|
4
6
|
module Readthis
|
7
|
+
extend self
|
8
|
+
|
9
|
+
# The current, global, instance of serializers that is used by all cache
|
10
|
+
# instances.
|
11
|
+
#
|
12
|
+
# @returns [Readthis::Serializers] An cached Serializers instance
|
13
|
+
#
|
14
|
+
# @see readthis/serializers
|
15
|
+
#
|
16
|
+
def serializers
|
17
|
+
@serializers ||= Readthis::Serializers.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Indicates whether connection error tolerance is enabled. With tolerance
|
21
|
+
# enabled every operation will return a `nil` value.
|
22
|
+
#
|
23
|
+
# @returns [Boolean] True for enabled, false for disabled
|
24
|
+
#
|
25
|
+
def fault_tolerant?
|
26
|
+
@fault_tolerant
|
27
|
+
end
|
28
|
+
|
29
|
+
# Toggle fault tolerance for connection errors.
|
30
|
+
#
|
31
|
+
# @param [Boolean] The new value for fault tolerance
|
32
|
+
#
|
33
|
+
def fault_tolerant=(value)
|
34
|
+
@fault_tolerant = value
|
35
|
+
end
|
36
|
+
|
37
|
+
# @private
|
38
|
+
def reset!
|
39
|
+
@fault_tolerant = nil
|
40
|
+
@serializers = nil
|
41
|
+
end
|
5
42
|
end
|