cachext 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +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