matador 0.0.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 +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +59 -0
- data/Rakefile +11 -0
- data/lib/matador.rb +19 -0
- data/lib/matador/fetcher.rb +93 -0
- data/lib/matador/version.rb +3 -0
- data/matador.gemspec +26 -0
- data/spec/lib/matador/fetcher_spec.rb +148 -0
- data/spec/matador_spec.rb +12 -0
- data/spec/spec_helper.rb +7 -0
- metadata +130 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6256fdbcce0efe4709e095ff091d42043ae112c4
|
4
|
+
data.tar.gz: f69fbf4b7b818efff53d920749ee7c59938de7f5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 83e8a3c051a03ff5583f85d87f83a634d4ab77cb9a07bfbf834a7cee2465821d8c52343a8706d88117df129110faf404798f6e8a5d2139ca579bf2b741dcc618
|
7
|
+
data.tar.gz: 6d1ccab8eecfa921b2e456296c9a5a7696168519ca6ea949a5c932c76320a4d1be5455ab6481e21840c96b54ad95090378ed51860da8d4bc90ed4aeb00c84959
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Steven Li
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# Matador
|
2
|
+
|
3
|
+
[](https://travis-ci.org/StevenJL/matador)
|
4
|
+
|
5
|
+
Stop cache expiration triggered [thundering herd](https://en.wikipedia.org/wiki/Thundering_herd_problem) problems with Matador. Currently works with redis.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'matador'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install matador
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Set your cache service object (currently only supports redis)
|
24
|
+
```ruby
|
25
|
+
require "matador"
|
26
|
+
require "redis"
|
27
|
+
|
28
|
+
$redis = Redis.new(host: "127.0.0.1", port: "16379")
|
29
|
+
Matador.cache_store = $redis
|
30
|
+
```
|
31
|
+
|
32
|
+
Caching with an expiration is susceptible to a thundering herd in high traffic situations:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
data = time_consuming_operation(...)
|
36
|
+
$redis.set("some_key", data)
|
37
|
+
$redis.expire("some_key", 120)
|
38
|
+
```
|
39
|
+
|
40
|
+
Wrap it in matador. When the cache expires after 2 minutes, the first request goes through to generate the new cache. While the new cache is being built (next 10 seconds), all other requests are served the stale cache.
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
Matador.fetch("some_key", :ttl => 120, :therd_ttl => 10) do
|
44
|
+
time_consuming_operation
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
## TO DO
|
49
|
+
|
50
|
+
1. Integrate with Memcache
|
51
|
+
|
52
|
+
## Contributing
|
53
|
+
|
54
|
+
1. Fork it ( http://github.com/<my-github-username>/matador/fork )
|
55
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
56
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
57
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
58
|
+
5. Create new Pull Request
|
59
|
+
|
data/Rakefile
ADDED
data/lib/matador.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$:.unshift(File.expand_path("../", __FILE__))
|
2
|
+
|
3
|
+
module Matador
|
4
|
+
autoload :VERSION, "matador/version"
|
5
|
+
autoload :Fetcher, "matador/fetcher"
|
6
|
+
|
7
|
+
def self.cache_store=(cache_store_arg)
|
8
|
+
@@cache_store = cache_store_arg
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.cache_store
|
12
|
+
@@cache_store
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.fetch(key, options={}, &block)
|
16
|
+
Matador::Fetcher.new(key, options, block).perform
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Matador
|
2
|
+
class Fetcher
|
3
|
+
attr_reader :key, :ttl, :therd_ttl, :block
|
4
|
+
|
5
|
+
def initialize(key, options, block)
|
6
|
+
@key = key
|
7
|
+
@ttl = options[:ttl]
|
8
|
+
@therd_ttl = options[:therd_ttl]
|
9
|
+
@block = block
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform
|
13
|
+
if (cache_valid? && (valid_cache = current_cache))
|
14
|
+
valid_cache
|
15
|
+
elsif !(cache_valid? || locked_for_rebuilding_cache?)
|
16
|
+
# cache just expired but no request has gone through yet
|
17
|
+
rebuild_cache_with_first_request
|
18
|
+
elsif (locked_for_rebuilding_cache? && (stale_value = current_cache))
|
19
|
+
# first request already went through
|
20
|
+
# serving up stale value while new cache is built
|
21
|
+
stale_value
|
22
|
+
else
|
23
|
+
# fallback case (this should be a rare occurence)
|
24
|
+
# Happens if therd_ttl isn't long enough
|
25
|
+
# or cache_store evicted key
|
26
|
+
# or cache_store failed to save key in the first place
|
27
|
+
|
28
|
+
if locked_for_rebuilding_cache?
|
29
|
+
# already rebuilding cache so just return the value
|
30
|
+
block.call
|
31
|
+
else
|
32
|
+
# have not started rebuilding cache
|
33
|
+
# so this becomes the "first request"
|
34
|
+
rebuild_cache_with_first_request
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def locked_for_rebuilding_cache?
|
41
|
+
!!cache_store.get(key_rebuilding)
|
42
|
+
end
|
43
|
+
|
44
|
+
def cache_valid?
|
45
|
+
!!cache_store.get(key_nominal_ttl)
|
46
|
+
end
|
47
|
+
|
48
|
+
def current_cache
|
49
|
+
cache_store.get(key)
|
50
|
+
end
|
51
|
+
|
52
|
+
def cache_store
|
53
|
+
Matador.cache_store
|
54
|
+
end
|
55
|
+
|
56
|
+
def rebuild_cache_with_first_request
|
57
|
+
lock_rebuilding_cache
|
58
|
+
|
59
|
+
new_value = block.call
|
60
|
+
# compute new value
|
61
|
+
|
62
|
+
cache_store.set(key, new_value)
|
63
|
+
cache_store.expire(key, ttl + therd_ttl)
|
64
|
+
# cache new value and expire accordingly
|
65
|
+
|
66
|
+
cache_store.set(key_nominal_ttl, "1")
|
67
|
+
cache_store.expire(key_nominal_ttl, ttl)
|
68
|
+
# set the nominal (ie. end user) ttl
|
69
|
+
|
70
|
+
unlock_rebuilding_cache
|
71
|
+
|
72
|
+
new_value
|
73
|
+
end
|
74
|
+
|
75
|
+
def lock_rebuilding_cache
|
76
|
+
cache_store.set(key_rebuilding, "1")
|
77
|
+
cache_store.expire(key_rebuilding, therd_ttl)
|
78
|
+
end
|
79
|
+
|
80
|
+
def unlock_rebuilding_cache
|
81
|
+
cache_store.del(key_rebuilding)
|
82
|
+
end
|
83
|
+
|
84
|
+
def key_nominal_ttl
|
85
|
+
"matador:nominal_ttl:#{key}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def key_rebuilding
|
89
|
+
"matador:rebuilding:#{key}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
data/matador.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'matador/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "matador"
|
8
|
+
spec.version = Matador::VERSION
|
9
|
+
spec.authors = ["Steven Li"]
|
10
|
+
spec.email = ["sli@bleacherreport.com"]
|
11
|
+
spec.summary = %q{Prevent cache-expiration-triggered thundering herds}
|
12
|
+
spec.description = %q{Prevent cache-expiration-triggered thundering herds}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "redis", "3.1.0"
|
24
|
+
spec.add_development_dependency "rspec", "3.0.0"
|
25
|
+
spec.add_development_dependency "pry", "0.10.1"
|
26
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Matador::Fetcher do
|
4
|
+
before(:all) do
|
5
|
+
@redis = Redis.new(host: "127.0.0.1", port: "6379")
|
6
|
+
Matador.cache_store = @redis
|
7
|
+
@redis.del("test_key1")
|
8
|
+
@redis.del("matador:nominal_ttl:test_key1")
|
9
|
+
@redis.del("matador:rebuilding:test_key1")
|
10
|
+
end
|
11
|
+
|
12
|
+
context "#fetch" do
|
13
|
+
after(:each) do
|
14
|
+
@redis.del("test_key1")
|
15
|
+
@redis.del("matador:nominal_ttl:test_key1")
|
16
|
+
@redis.del("matador:rebuilding:test_key1")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "#fetch gets the cache if it is still valid" do
|
20
|
+
Matador.fetch("test_key1", :ttl => 60, :therd_ttl => 10) do
|
21
|
+
"foobar"
|
22
|
+
end
|
23
|
+
# set value to foobar
|
24
|
+
|
25
|
+
value = Matador.fetch("test_key1", :ttl => 60, :therd_ttl => 10) do
|
26
|
+
"foobaz"
|
27
|
+
end
|
28
|
+
# cache should still be active so value sould be foobar, not foobaz
|
29
|
+
|
30
|
+
expect(value == "foobar").to be true
|
31
|
+
end
|
32
|
+
|
33
|
+
it "#fetch kicks off a single request to rebuild cache after it expires" do
|
34
|
+
Matador::Fetcher.any_instance.stub(:rebuild_cache_with_first_request).and_return("first_of_the_herd")
|
35
|
+
|
36
|
+
Matador.fetch("test_key1", :ttl => 1, :therd_ttl => 5) do
|
37
|
+
"foobar"
|
38
|
+
end
|
39
|
+
|
40
|
+
sleep 2
|
41
|
+
|
42
|
+
value = Matador.fetch("test_key1", :ttl => 1, :therd_ttl => 5) do
|
43
|
+
"foobaz"
|
44
|
+
end
|
45
|
+
|
46
|
+
expect(value).to eq("first_of_the_herd")
|
47
|
+
end
|
48
|
+
|
49
|
+
it "#fetch returns stale_value after first post-expiration request went through to rebuild cache" do
|
50
|
+
Matador::Fetcher.any_instance.stub(:locked_for_rebuilding_cache?).and_return(true)
|
51
|
+
Matador::Fetcher.any_instance.stub(:valid_cache?).and_return(false)
|
52
|
+
Matador::Fetcher.any_instance.stub(:current_cache).and_return("stale cache")
|
53
|
+
|
54
|
+
value = Matador.fetch("test_key1", :ttl => 1, :therd_ttl => 5) do
|
55
|
+
"foobar"
|
56
|
+
end
|
57
|
+
|
58
|
+
expect(value).to eq("stale cache")
|
59
|
+
end
|
60
|
+
|
61
|
+
it "no matter how many requests come in after the first request, they do not rebuild the cache" do
|
62
|
+
@redis.set("test_key1", "stale_cache")
|
63
|
+
|
64
|
+
Thread.new do
|
65
|
+
Matador.fetch("test_key1", :ttl => 100, :therd_ttl => 12) do
|
66
|
+
sleep 10
|
67
|
+
"foobar"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
sleep 2
|
72
|
+
|
73
|
+
value1 = Matador.fetch("test_key1", :ttl => 100, :therd_ttl => 10) do
|
74
|
+
"foobar"
|
75
|
+
end
|
76
|
+
|
77
|
+
sleep 2
|
78
|
+
|
79
|
+
value2 = Matador.fetch("test_key1", :ttl => 100, :therd_ttl => 10) do
|
80
|
+
"foobar"
|
81
|
+
end
|
82
|
+
|
83
|
+
sleep 2
|
84
|
+
|
85
|
+
value3 = Matador.fetch("test_key1", :ttl => 100, :therd_ttl => 10) do
|
86
|
+
"foobar"
|
87
|
+
end
|
88
|
+
|
89
|
+
sleep 8
|
90
|
+
|
91
|
+
value4 = Matador.fetch("test_key1", :ttl => 100, :therd_ttl => 10) do
|
92
|
+
"foobar"
|
93
|
+
end
|
94
|
+
|
95
|
+
expect(value1).to eq("stale_cache")
|
96
|
+
expect(value2).to eq("stale_cache")
|
97
|
+
expect(value3).to eq("stale_cache")
|
98
|
+
expect(value4).to eq("foobar")
|
99
|
+
end
|
100
|
+
|
101
|
+
context "when therd_ttl is not long enough" do
|
102
|
+
after(:each) do
|
103
|
+
@redis.del("test_key1")
|
104
|
+
@redis.del("matador:nominal_ttl:test_key1")
|
105
|
+
@redis.del("matador:rebuilding:test_key1")
|
106
|
+
end
|
107
|
+
|
108
|
+
it "returns the computation once while cache is building in the background" do
|
109
|
+
@redis.set("test_key1", "stale_cache")
|
110
|
+
|
111
|
+
Thread.new do
|
112
|
+
Matador.fetch("test_key1", :ttl => 100, :therd_ttl => 5) do
|
113
|
+
sleep 10
|
114
|
+
"foobar"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
value = Matador.fetch("test_key1", :ttl => 100, :therd_ttl => 10) do
|
119
|
+
"foobaz"
|
120
|
+
end
|
121
|
+
|
122
|
+
expect(value).to eq("foobaz")
|
123
|
+
end
|
124
|
+
|
125
|
+
it "rebuilds the cache with first request if that has not happened already" do
|
126
|
+
@redis.set("test_key1", "stale_cache")
|
127
|
+
|
128
|
+
Thread.new do
|
129
|
+
Matador.fetch("test_key1", :ttl => 100, :therd_ttl => 3) do
|
130
|
+
sleep 100
|
131
|
+
"foobar"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
sleep 5
|
136
|
+
|
137
|
+
Matador.fetch("test_key1", :ttl => 100, :therd_ttl => 10) do
|
138
|
+
"foobaz"
|
139
|
+
end
|
140
|
+
|
141
|
+
value = @redis.get("test_key1")
|
142
|
+
|
143
|
+
expect(value).to eq("foobaz")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
$root = File.expand_path('../../', __FILE__)
|
2
|
+
require "#{$root}/spec/spec_helper"
|
3
|
+
|
4
|
+
describe Matador do
|
5
|
+
it "registers a cache_store" do
|
6
|
+
redis = Redis.new(host: "127.0.0.1", port: "6379")
|
7
|
+
Matador.cache_store = redis
|
8
|
+
|
9
|
+
expect(Matador.cache_store).to be_a Redis
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: matador
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Steven Li
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-07-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: redis
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.1.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.1.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.0.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.0.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.10.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.10.1
|
83
|
+
description: Prevent cache-expiration-triggered thundering herds
|
84
|
+
email:
|
85
|
+
- sli@bleacherreport.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- ".travis.yml"
|
92
|
+
- Gemfile
|
93
|
+
- LICENSE.txt
|
94
|
+
- README.md
|
95
|
+
- Rakefile
|
96
|
+
- lib/matador.rb
|
97
|
+
- lib/matador/fetcher.rb
|
98
|
+
- lib/matador/version.rb
|
99
|
+
- matador.gemspec
|
100
|
+
- spec/lib/matador/fetcher_spec.rb
|
101
|
+
- spec/matador_spec.rb
|
102
|
+
- spec/spec_helper.rb
|
103
|
+
homepage: ''
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.4.2
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: Prevent cache-expiration-triggered thundering herds
|
127
|
+
test_files:
|
128
|
+
- spec/lib/matador/fetcher_spec.rb
|
129
|
+
- spec/matador_spec.rb
|
130
|
+
- spec/spec_helper.rb
|