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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1141e77be7834868ab37636024f4eeea8c719581
4
- data.tar.gz: aed591612989be4abf58211a409542c9f51c189e
3
+ metadata.gz: 99eccddb9064b17d7043869d9f62d2a39438cd12
4
+ data.tar.gz: 9cd79f5b34f472c9c855b981cab782e4af3f6086
5
5
  SHA512:
6
- metadata.gz: 4f0bcdf511a0c61b65ee32a4b7a82c1f14864c08bd9a7c5c609970f6bc8f13cbe3c4c2e183966fb75d16cbeb2ce5a7f5f490c1ab9f02f268df8c1f324248370a
7
- data.tar.gz: 7bffc2e75ce62b71a0a3db99c917653304291bb7ad854582a04caf7103bbfc18deb2a9c2e9026959ca51bccf143b9d4d532aa886d5afa312531cdd8ae65a771d
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
- ## Installation
8
+ ## Quickstart
9
9
 
10
- Add this line to your application's Gemfile:
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
- gem 'cachext'
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
- And then execute:
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
- $ bundle
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
- Replace `Rails.cache.fetch` with `Cachext.fetch`.
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
 
@@ -40,7 +40,7 @@ module Cachext
40
40
  end
41
41
 
42
42
  def handle_error key, options, error
43
- @config.error_logger.error error
43
+ @config.error_logger.call error if @config.log_errors?
44
44
  raise if @config.raise_errors && reraise_errors
45
45
  end
46
46
 
@@ -36,5 +36,9 @@ module Cachext
36
36
  def lock_redis
37
37
  @lock_redis ||= Redis::Namespace.new :cachext, redis: redis
38
38
  end
39
+
40
+ def log_errors?
41
+ error_logger.present?
42
+ end
39
43
  end
40
44
  end
@@ -4,7 +4,7 @@ require "active_support/core_ext/hash/reverse_merge"
4
4
 
5
5
  module Cachext
6
6
  class Multi
7
- delegate :cache, :default_errors, :error_logger, to: :@config
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 :cache, to: :multi
46
+ delegate :config, :heartbeat_expires, to: :multi
47
+ delegate :cache, :lock_manager, :max_lock_wait, to: :config
43
48
 
44
- def initialize(multi, ids, lookup)
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
- records = uncached_where uncached_or_stale_ids
68
- write_cache records
69
- records
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
- records = @lookup.call ids
135
+ with_heartbeat_extender lock_key_from_ids(ids) do
136
+ records = @lookup.call ids
99
137
 
100
- if records.is_a?(Array)
101
- records = records.each_with_object({}) do |record, acc|
102
- acc[record.id] = record
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
- delete_backups ids - records.keys
107
- records
108
- rescue *multi.default_errors => e
109
- multi.error_logger.error e
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
@@ -8,12 +8,14 @@ module Cachext
8
8
  :not_found_error,
9
9
  :heartbeat_expires
10
10
 
11
- def initialize config, expires_in: config.default_expires_in,
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
@@ -1,3 +1,3 @@
1
1
  module Cachext
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cachext
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Donald Plummer