cachext 0.2.0 → 0.3.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 +125 -7
- data/lib/cachext/client.rb +1 -1
- data/lib/cachext/configuration.rb +4 -0
- data/lib/cachext/multi.rb +73 -15
- data/lib/cachext/options.rb +3 -1
- data/lib/cachext/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99eccddb9064b17d7043869d9f62d2a39438cd12
|
4
|
+
data.tar.gz: 9cd79f5b34f472c9c855b981cab782e4af3f6086
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db58512dcee683c1b3a3ea996eb0505f57020e1b81ae8c73e933f10fc9956670fae3cd7d09293063eeeca043305c6a31c800ab4861078265afcb8477866985b7
|
7
|
+
data.tar.gz: 31204397cfda1149f6d77e08a75392359c244a2399a1a7bb8ea0468998b27ebac4e6a13eae8a988621534d95a534596abf29c3a99d4af89377aa20b91b2f7408
|
data/README.md
CHANGED
@@ -2,24 +2,142 @@
|
|
2
2
|
|
3
3
|
Extensions to normal Rails caching:
|
4
4
|
|
5
|
-
* Lock
|
5
|
+
* Lock (inspired by https://github.com/seamusabshere/lock_and_cache)
|
6
6
|
* Backup
|
7
7
|
|
8
|
-
##
|
8
|
+
## Quickstart
|
9
9
|
|
10
|
-
|
10
|
+
```ruby
|
11
|
+
Cachext.config.cache = Rails.cache
|
12
|
+
Cachext.config.redis = Redis.current
|
13
|
+
|
14
|
+
key = [:foo, :bar, 1]
|
15
|
+
Cachext.fetch key, expires_in: 2.hours, default: "cow" do
|
16
|
+
Faraday.get "http://example.com/foo/bar/1"
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
20
|
+
* Other services making the same call at the same time will wait for the
|
21
|
+
first to complete, so only 1 call is made in a 2 hour window
|
22
|
+
* A backup of the value is stored too, so if the service raises a
|
23
|
+
Faraday::Error::ConnectionFailed we'll return the backup
|
24
|
+
* If no backup exists but we got a ConnectionFailed, we'll return the default
|
25
|
+
of "cow"
|
11
26
|
|
12
27
|
```ruby
|
13
|
-
|
28
|
+
Record = Struct.new :id
|
29
|
+
Cachext.multi [:foo, :bar], [1,2,3], expires_in: 5.minutes do |ids|
|
30
|
+
data = JSON.parse Faraday.get("http://example.com/foo/bar?ids=#{ids.join(',')}")
|
31
|
+
data.each_with_object({}) do |record, acc|
|
32
|
+
acc[record["id"]] = Record.new record["id"]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
# => { 1 => Record.new(1), 2 => Record.new(2), 3 => Record.new(3) }
|
14
36
|
```
|
15
37
|
|
16
|
-
|
38
|
+
* The passed block will be called with the ids that were not available in the
|
39
|
+
cache. The return value of the block should either be a hash with keys of
|
40
|
+
ids, or an array of objects that have `id` methods.
|
41
|
+
* In the event of a server error (ie `ConnectionFailed`), backup values are
|
42
|
+
used.
|
17
43
|
|
18
|
-
|
44
|
+
## Configuration options
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
Cachext.config.cache = Rails.cache
|
48
|
+
```
|
49
|
+
|
50
|
+
`Cachext` expects a cache store that has the `ActiveSupport::Cache` interface,
|
51
|
+
so that can be Memcache, Redis, FileStore, etc.
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
Cachext.config.redis = Redis.current
|
55
|
+
```
|
56
|
+
|
57
|
+
`Cachext` uses redis for locking (the
|
58
|
+
[Redlock](https://github.com/leandromoreira/redlock-rb) gem under the hood), so
|
59
|
+
we need at least Redis 2.8.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
Cachext.config.raise_errors = false
|
63
|
+
Cachext.config.default_errors = [
|
64
|
+
Faraday::Error::ConnectionFailed,
|
65
|
+
Faraday::Error::TimeoutError,
|
66
|
+
]
|
67
|
+
```
|
68
|
+
|
69
|
+
By default `Cachext` will not re-raise the standard default errors. Setting
|
70
|
+
this to `true` is helpful in a test environment. The `default_errors` are those
|
71
|
+
caught as transient issues that a backup will be used for.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
Cachext.config.not_found_errors = [Faraday::Error::ResourceNotFound]
|
75
|
+
```
|
76
|
+
|
77
|
+
If a NotFound exception is raised, the backup is *not* used, and any backup
|
78
|
+
that exists will be deleted. Then the exception will be re-raised.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
Cachext.config.default_expires_in = 60 # in seconds
|
82
|
+
```
|
83
|
+
|
84
|
+
The default TTL for values fetched. Only used for the "fresh" cache, not the
|
85
|
+
backup (which has no TTL).
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
Cachext.config.max_lock_wait = 5 # in seconds
|
89
|
+
```
|
90
|
+
|
91
|
+
The most we'll wait for a lock to unlock. If it takes more than this value to
|
92
|
+
get a lock (due to another service holding the lock while making the call),
|
93
|
+
we'll fallback to the backup value.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
Cachext.config.debug = ENV['CACHEXT_DEBUG'] == "true"
|
97
|
+
```
|
98
|
+
|
99
|
+
If `debug` is set to `true` (or you run your program/test with
|
100
|
+
`CACHEXT_DEBUG=true`), you'll get lots of debug messages around the locking and
|
101
|
+
whats going on. Very helpful for debugging :)
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
Cachext.config.heartbeat_expires = 2 # in seconds
|
105
|
+
```
|
106
|
+
|
107
|
+
If a process that holds a lock crashes, other processes will have to wait this
|
108
|
+
many seconds for the lock to expire.
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
Cachext.config.error_logger = nil
|
112
|
+
```
|
113
|
+
|
114
|
+
If set to an object that responds to call, will `call` with any errors caught.
|
19
115
|
|
20
116
|
## Usage
|
21
117
|
|
22
|
-
|
118
|
+
```ruby
|
119
|
+
Cachext.fetch key, options, &block
|
120
|
+
```
|
121
|
+
|
122
|
+
Available options:
|
123
|
+
|
124
|
+
* `expires_in`: override for the `default_expires_in`, in seconds
|
125
|
+
* `default`: object or proc that will be used as the default if no backup is found
|
126
|
+
* `errors`: override for the `default_errors` to be caught
|
127
|
+
* `reraise_errors`: default `true`, if set to `false` NotFound errors will not
|
128
|
+
be raised
|
129
|
+
* `not_found_error`: override for `not_found_errors`
|
130
|
+
* `heartbeat_expires`: override for `heartbeat_expires`
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
Cachext.multi key_base, ids, options, &block
|
134
|
+
```
|
135
|
+
|
136
|
+
Available options:
|
137
|
+
|
138
|
+
* `expires_in`: override for `default_expires_in`, in seconds
|
139
|
+
* `return_array`: return an array instead of a hash. Will include missing
|
140
|
+
records as `Cachext::MissingRecord` objects so you can deal with them.
|
23
141
|
|
24
142
|
## Development
|
25
143
|
|
data/lib/cachext/client.rb
CHANGED
data/lib/cachext/multi.rb
CHANGED
@@ -4,7 +4,7 @@ require "active_support/core_ext/hash/reverse_merge"
|
|
4
4
|
|
5
5
|
module Cachext
|
6
6
|
class Multi
|
7
|
-
|
7
|
+
attr_reader :config, :key_base
|
8
8
|
|
9
9
|
def initialize config, key_base, options = {}
|
10
10
|
@config = config
|
@@ -30,6 +30,10 @@ module Cachext
|
|
30
30
|
@options.fetch :expires_in, @config.default_expires_in
|
31
31
|
end
|
32
32
|
|
33
|
+
def heartbeat_expires
|
34
|
+
@options.fetch :heartbeat_expires, config.heartbeat_expires
|
35
|
+
end
|
36
|
+
|
33
37
|
private
|
34
38
|
|
35
39
|
def missing_records ids
|
@@ -39,9 +43,10 @@ module Cachext
|
|
39
43
|
class FindByIds
|
40
44
|
attr_reader :multi, :ids
|
41
45
|
|
42
|
-
delegate :
|
46
|
+
delegate :config, :heartbeat_expires, to: :multi
|
47
|
+
delegate :cache, :lock_manager, :max_lock_wait, to: :config
|
43
48
|
|
44
|
-
def initialize
|
49
|
+
def initialize multi, ids, lookup
|
45
50
|
@multi = multi
|
46
51
|
@ids = ids
|
47
52
|
@lookup = lookup
|
@@ -64,12 +69,44 @@ module Cachext
|
|
64
69
|
|
65
70
|
def direct
|
66
71
|
@direct ||= if uncached_or_stale_ids.length > 0
|
67
|
-
|
68
|
-
|
69
|
-
|
72
|
+
with_lock uncached_or_stale_ids do
|
73
|
+
records = uncached_where uncached_or_stale_ids
|
74
|
+
write_cache records
|
75
|
+
records
|
76
|
+
end
|
70
77
|
else
|
71
78
|
{}
|
72
79
|
end
|
80
|
+
rescue Features::Lock::TimeoutWaitingForLock => e
|
81
|
+
config.error_logger.call e if config.log_errors?
|
82
|
+
{}
|
83
|
+
end
|
84
|
+
|
85
|
+
def with_lock ids, &block
|
86
|
+
@lock_info = obtain_lock ids
|
87
|
+
block.call
|
88
|
+
ensure
|
89
|
+
lock_manager.unlock @lock_info if @lock_info
|
90
|
+
end
|
91
|
+
|
92
|
+
def obtain_lock ids
|
93
|
+
lock_key = lock_key_from_ids ids
|
94
|
+
|
95
|
+
start_time = Time.now
|
96
|
+
|
97
|
+
until lock_info = lock_manager.lock(lock_key, (heartbeat_expires * 1000).ceil)
|
98
|
+
sleep rand
|
99
|
+
if Time.now - start_time > max_lock_wait
|
100
|
+
raise Features::Lock::TimeoutWaitingForLock
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
lock_info
|
105
|
+
end
|
106
|
+
|
107
|
+
def lock_key_from_ids(ids)
|
108
|
+
key = Key.new multi.key_base + ids
|
109
|
+
key.digest
|
73
110
|
end
|
74
111
|
|
75
112
|
def write_cache records
|
@@ -95,20 +132,41 @@ module Cachext
|
|
95
132
|
end
|
96
133
|
|
97
134
|
def uncached_where ids
|
98
|
-
|
135
|
+
with_heartbeat_extender lock_key_from_ids(ids) do
|
136
|
+
records = @lookup.call ids
|
99
137
|
|
100
|
-
|
101
|
-
|
102
|
-
|
138
|
+
if records.is_a?(Array)
|
139
|
+
records = records.each_with_object({}) do |record, acc|
|
140
|
+
acc[record.id] = record
|
141
|
+
end
|
103
142
|
end
|
104
|
-
end
|
105
143
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
144
|
+
delete_backups ids - records.keys
|
145
|
+
records
|
146
|
+
end
|
147
|
+
rescue *config.default_errors => e
|
148
|
+
config.error_logger.call e if config.log_errors?
|
110
149
|
{}
|
111
150
|
end
|
151
|
+
|
152
|
+
def with_heartbeat_extender lock_key, &block
|
153
|
+
done = false
|
154
|
+
heartbeat_frequency = heartbeat_expires / 2
|
155
|
+
|
156
|
+
Thread.new do
|
157
|
+
loop do
|
158
|
+
break if done
|
159
|
+
sleep heartbeat_frequency
|
160
|
+
break if done
|
161
|
+
lock_manager.lock lock_key, (heartbeat_expires * 1000).ceil, extend: @lock_info
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
block.call
|
166
|
+
ensure
|
167
|
+
lock_manager.unlock @lock_info
|
168
|
+
done = true
|
169
|
+
end
|
112
170
|
end
|
113
171
|
end
|
114
172
|
end
|
data/lib/cachext/options.rb
CHANGED
@@ -8,12 +8,14 @@ module Cachext
|
|
8
8
|
:not_found_error,
|
9
9
|
:heartbeat_expires
|
10
10
|
|
11
|
-
def initialize config,
|
11
|
+
def initialize config,
|
12
|
+
expires_in: config.default_expires_in,
|
12
13
|
default: nil,
|
13
14
|
errors: config.default_errors,
|
14
15
|
reraise_errors: true,
|
15
16
|
not_found_error: config.not_found_errors,
|
16
17
|
heartbeat_expires: config.heartbeat_expires
|
18
|
+
|
17
19
|
@expires_in = expires_in
|
18
20
|
@default = default
|
19
21
|
@errors = errors
|
data/lib/cachext/version.rb
CHANGED