readthis 0.8.1 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|