faraday_throttler 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +8 -0
- data/README.md +118 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/examples/Gemfile +6 -0
- data/examples/Gemfile.lock +19 -0
- data/examples/README.md +25 -0
- data/examples/client.rb +30 -0
- data/examples/config.ru +7 -0
- data/faraday_throttler.gemspec +25 -0
- data/lib/faraday_throttler/cache.rb +25 -0
- data/lib/faraday_throttler/errors.rb +6 -0
- data/lib/faraday_throttler/fallbacks.rb +16 -0
- data/lib/faraday_throttler/gauge.rb +27 -0
- data/lib/faraday_throttler/key_resolver.rb +14 -0
- data/lib/faraday_throttler/mem_lock.rb +26 -0
- data/lib/faraday_throttler/middleware.rb +177 -0
- data/lib/faraday_throttler/redis_cache.rb +32 -0
- data/lib/faraday_throttler/redis_lock.rb +16 -0
- data/lib/faraday_throttler/retryable.rb +21 -0
- data/lib/faraday_throttler/serializer.rb +31 -0
- data/lib/faraday_throttler/version.rb +3 -0
- data/lib/faraday_throttler.rb +7 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ecd40afc7d5acf7024059f4fcd820accc8da1bd1
|
4
|
+
data.tar.gz: 67e615a50d4067f00175991e00b1161b624bbb7a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4c3cc18b494510df0da3d56210bc1fee14518b1d9f9ff012d601af0dfcdfbccbee8c3d3a5bee0e4bf4c57a189dbd7b5f6dbc9939aa525072df4c28fcd73a6988
|
7
|
+
data.tar.gz: 026e803e53c5097b8c8623555932eb87826d7908ebdc209c3b81f1b96682fd9a9ebc642096a7c692999f489c15e3891a6e431cf4732aae3f7d92a086e668597c
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
[ ![Codeship Status for ismasan/faraday_throttler](https://codeship.com/projects/40d401a0-5c01-0133-561a-22b0ee77d2e6/status?branch=master)](https://codeship.com/projects/110895)
|
2
|
+
|
3
|
+
# FaradayThrottler
|
4
|
+
|
5
|
+
Configurable Faraday middleware for Ruby HTTP clients that:
|
6
|
+
|
7
|
+
* limits request rate to backend services.
|
8
|
+
* does its best to return cached or placeholder responses to clients while backend service is unavailable or slow.
|
9
|
+
* optionally uses Redis to rate-limit outgoing requests across processes and servers.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'faraday_throttler'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install faraday_throttler
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Defaults
|
30
|
+
|
31
|
+
The defaul configuration use an in-memory lock and in-memory cache. Not suitable for multi-server deployments.
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
require 'faraday'
|
35
|
+
require 'faraday_throttler'
|
36
|
+
|
37
|
+
client = Faraday.new(:url => 'https://my.api.com') do |c|
|
38
|
+
c.use(
|
39
|
+
:throttler,
|
40
|
+
# Allow up to 1 request every 3 seconds, per path, to backend
|
41
|
+
rate: 3,
|
42
|
+
# Queued requests will wait for up to 2 seconds for current in-flight request
|
43
|
+
# to the same path.
|
44
|
+
# If in-flight request hasn't finished after that time, return a default placeholder response.
|
45
|
+
wait: 2
|
46
|
+
)
|
47
|
+
c.adapter Faraday.default_adapter
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
Make some requests:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
resp = client.get('/foobar')
|
55
|
+
resp.body
|
56
|
+
```
|
57
|
+
|
58
|
+
The configuration above will only issue 1 request every 3 seconds to `my.api.com/foobar`. Requests to the same path will wait for up to 2 seconds for current _in-flight_ request to finish.
|
59
|
+
|
60
|
+
If an in-flight request finishes within that period, queued requests will respond with the same data.
|
61
|
+
|
62
|
+
If the in-flight request doesn't finish within 2 seconds, queued requests will attempt to serve a previous response from the same resource from cache.
|
63
|
+
|
64
|
+
If no matching response found in cache, a default fallback response will be used (status 204 No Content). Fallback responses can be cofigured.
|
65
|
+
|
66
|
+
Tweaking the `rate` and `wait` arguments allows you to control the rate of cached, fresh and fallback reponses.
|
67
|
+
|
68
|
+
### Distributed Redis lock and cache
|
69
|
+
|
70
|
+
The defaults use in-memory lock and cache store implementations. To make the most efficient use of this gem across processes and servers, you can use [Redis](http://redis.io/) as a distributed lock and cache store.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
require 'redis'
|
74
|
+
require 'faraday_throttler/redis_lock'
|
75
|
+
require 'faraday_throttler/redis_cache'
|
76
|
+
|
77
|
+
redis = Redis.new(uri: 'redis://my-redis-server.com:1234')
|
78
|
+
|
79
|
+
redis_lock = FaradayThrottler::RedisLock.new(redis)
|
80
|
+
|
81
|
+
# Cache entries will be available for 1 hour
|
82
|
+
redis_cache = FaradayThrottler::RedisCache.new(redis: redis, ttl: 3600)
|
83
|
+
|
84
|
+
client = Faraday.new(:url => 'https://my.api.com') do |c|
|
85
|
+
c.use(
|
86
|
+
:throttler,
|
87
|
+
rate: 3,
|
88
|
+
wait: 2,
|
89
|
+
# Use Redis-backed lock
|
90
|
+
lock: redis_lock,
|
91
|
+
# Use Redis-backed cache with set expiration
|
92
|
+
cache: redis_cache
|
93
|
+
)
|
94
|
+
c.adapter Faraday.default_adapter
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
## Advanced usage
|
99
|
+
|
100
|
+
Most internal behaviours are split into delegate objects that you can pass as middleware arguments to override the defaults. See the details [in the code](https://github.com/ismasan/faraday_throttler/blob/master/lib/faraday_throttler/middleware.rb#L16).
|
101
|
+
|
102
|
+
## Development
|
103
|
+
|
104
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
105
|
+
|
106
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
107
|
+
|
108
|
+
## Contributing
|
109
|
+
|
110
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ismasan/faraday_throttler.
|
111
|
+
|
112
|
+
To contribute with code:
|
113
|
+
|
114
|
+
1. Fork it ( http://github.com/ismasan/faraday_throttler/fork )
|
115
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
116
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
117
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
118
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "faraday_throttler"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/examples/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
faraday (0.9.2)
|
5
|
+
multipart-post (>= 1.2, < 3)
|
6
|
+
multipart-post (2.0.0)
|
7
|
+
rack (1.6.4)
|
8
|
+
redis (3.2.1)
|
9
|
+
|
10
|
+
PLATFORMS
|
11
|
+
ruby
|
12
|
+
|
13
|
+
DEPENDENCIES
|
14
|
+
faraday
|
15
|
+
rack
|
16
|
+
redis
|
17
|
+
|
18
|
+
BUNDLED WITH
|
19
|
+
1.10.6
|
data/examples/README.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
## Examples
|
2
|
+
|
3
|
+
```
|
4
|
+
bundle install
|
5
|
+
```
|
6
|
+
|
7
|
+
Run test rack server in one terminal window.
|
8
|
+
|
9
|
+
```
|
10
|
+
rackup config.ru
|
11
|
+
```
|
12
|
+
|
13
|
+
Run Redis in another:
|
14
|
+
|
15
|
+
```
|
16
|
+
redis-server
|
17
|
+
```
|
18
|
+
|
19
|
+
Run test client in another:
|
20
|
+
|
21
|
+
```
|
22
|
+
ruby client.rb
|
23
|
+
```
|
24
|
+
|
25
|
+
Tweak middleware option in `client.rb`
|
data/examples/client.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'redis'
|
3
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
4
|
+
require 'faraday_throttler/middleware'
|
5
|
+
require 'faraday_throttler/redis_lock'
|
6
|
+
require 'faraday_throttler/redis_cache'
|
7
|
+
|
8
|
+
|
9
|
+
redis = Redis.new
|
10
|
+
lock = FaradayThrottler::RedisLock.new(redis)
|
11
|
+
cache = FaradayThrottler::RedisCache.new(redis: redis, ttl: 60)
|
12
|
+
|
13
|
+
conn = Faraday.new(:url => 'http://localhost:9292') do |faraday|
|
14
|
+
# faraday.response :logger # log requests to STDOUT
|
15
|
+
faraday.use :throttler, rate: 3, wait: 1, lock: lock, cache: cache
|
16
|
+
faraday.adapter Faraday.default_adapter
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
tr = (1..100).map do |i|
|
21
|
+
Thread.new do
|
22
|
+
sleep (rand * 10)
|
23
|
+
n = Time.now
|
24
|
+
r = conn.get('/foo/bar')
|
25
|
+
puts %([#{n}] #{r.headers['X-Throttler']} took: #{r.headers['X-ThrottlerTime']} - #{r.body})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
tr.map{|t| t.join }
|
30
|
+
|
data/examples/config.ru
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'faraday_throttler/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "faraday_throttler"
|
8
|
+
spec.version = FaradayThrottler::VERSION
|
9
|
+
spec.authors = ["Ismael Celis"]
|
10
|
+
spec.email = ["ismaelct@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Redis-backed request throttler requests to protect backend APIs against request stampedes}
|
13
|
+
spec.description = %q{Configure how often you want to hit backend APIs, and fallback responses to keep clients happy}
|
14
|
+
spec.homepage = "https://github.com/ismasan/faraday_throttler"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "faraday", ">= 0.9.1"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "rspec"
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'faraday_throttler/retryable'
|
2
|
+
|
3
|
+
module FaradayThrottler
|
4
|
+
class Cache
|
5
|
+
include Retryable
|
6
|
+
|
7
|
+
def initialize(store = {})
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@store = store
|
10
|
+
end
|
11
|
+
|
12
|
+
def set(key, resp)
|
13
|
+
mutex.synchronize { store[key] = resp }
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(key, wait = 0)
|
17
|
+
with_retry(wait) {
|
18
|
+
mutex.synchronize { store[key] }
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
attr_reader :store, :mutex
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module FaradayThrottler
|
2
|
+
class Fallbacks
|
3
|
+
DEFAULT_CONTENT_TYPE = 'application/json'.freeze
|
4
|
+
|
5
|
+
def call(req)
|
6
|
+
{
|
7
|
+
url: req[:url],
|
8
|
+
status: 204,
|
9
|
+
body: '',
|
10
|
+
response_headers: {
|
11
|
+
'Content-Type' => req.fetch(:request_headers, {}).fetch('Content-Type', DEFAULT_CONTENT_TYPE)
|
12
|
+
}
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module FaradayThrottler
|
2
|
+
class Gauge
|
3
|
+
def initialize(rate:, wait:)
|
4
|
+
@rate, @wait = rate, wait
|
5
|
+
end
|
6
|
+
|
7
|
+
def start(req_id, time = Time.now)
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
def update(req_id, state)
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def finish(req_id, state)
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def rate(req_id)
|
20
|
+
@rate
|
21
|
+
end
|
22
|
+
|
23
|
+
def wait(req_id)
|
24
|
+
@wait
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module FaradayThrottler
|
2
|
+
class MemLock
|
3
|
+
def initialize
|
4
|
+
@locks = {}
|
5
|
+
@mutex = Mutex.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def set(key, ttl = 30)
|
9
|
+
mutex.synchronize {
|
10
|
+
now = Time.now
|
11
|
+
exp = locks[key]
|
12
|
+
|
13
|
+
if !exp || exp < now
|
14
|
+
locks[key] = now + ttl
|
15
|
+
return true
|
16
|
+
else
|
17
|
+
return false
|
18
|
+
end
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
attr_reader :locks, :mutex
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'faraday'
|
3
|
+
require 'faraday_throttler/key_resolver'
|
4
|
+
require 'faraday_throttler/mem_lock'
|
5
|
+
require 'faraday_throttler/cache'
|
6
|
+
require 'faraday_throttler/fallbacks'
|
7
|
+
require 'faraday_throttler/gauge'
|
8
|
+
|
9
|
+
module FaradayThrottler
|
10
|
+
|
11
|
+
class Middleware < Faraday::Middleware
|
12
|
+
def initialize(
|
13
|
+
# The base Faraday adapter.
|
14
|
+
app,
|
15
|
+
|
16
|
+
# Request lock. This checks that only one unique request is in-flight at a given time.
|
17
|
+
# Request uniqueness is defined by :lock_key_resolver.
|
18
|
+
# Interface:
|
19
|
+
# #set(key String, ttl Integer)
|
20
|
+
#
|
21
|
+
# Returns _true_ if new lock aquired (no previous in-flight request)
|
22
|
+
# Returns _false_ if no lock aquired (there is a current lock on an in-flight request).
|
23
|
+
# MemLock is an in-memory lock. On a multi-threaded / multi-process environment
|
24
|
+
# prefer the RedisLock implementation, which uses Redis as a distributed lock.
|
25
|
+
lock: MemLock.new,
|
26
|
+
|
27
|
+
# Response cache. Caches fresh responses from backend service,
|
28
|
+
# so they can be used as a first fallback when connection exceeds :wait time.
|
29
|
+
# Interface:
|
30
|
+
# #set(key String, response_env Hash)
|
31
|
+
# #get(key String, wait_seconds Integer)
|
32
|
+
#
|
33
|
+
# #get can implement polling/blocking behaviour
|
34
|
+
# to wait for inflight-request to populate cache
|
35
|
+
cache: Cache.new,
|
36
|
+
|
37
|
+
# Resolves request unique key to use as lock
|
38
|
+
# Interface:
|
39
|
+
# #call(request_env Hash) String
|
40
|
+
lock_key_resolver: KeyResolver.new,
|
41
|
+
|
42
|
+
# Resolves response unique key to use as cache key
|
43
|
+
# Interface:
|
44
|
+
# #call(response_env Hash) String
|
45
|
+
cache_key_resolver: KeyResolver.new,
|
46
|
+
|
47
|
+
# Allow up to 1 request every 10 seconds, per path, to backend
|
48
|
+
rate: 10,
|
49
|
+
|
50
|
+
# Queued requests will wait for up to 5 seconds for current in-flight request
|
51
|
+
# to the same path.
|
52
|
+
# If in-flight request hasn't finished after that time, return a default placeholder response.
|
53
|
+
wait: 5,
|
54
|
+
|
55
|
+
# Wraps requests to backend service in a timeout block, in seconds.
|
56
|
+
# If request takes longer than this:
|
57
|
+
# * `gauge` receives #update(req_id, :timeout)
|
58
|
+
# * Attempt to serve old response from cache. `gauge` receives #finish(req_id, :cached) if successful.
|
59
|
+
# * If no cached response, delegate to fallbacks#call(request_env). `gauge` receives #finish(req_id, :fallback)
|
60
|
+
# timeout: 0 disables this behaviour.
|
61
|
+
timeout: 0,
|
62
|
+
|
63
|
+
# Fallbacks resolver. Returns a fallback response when conection has waited over :wait time
|
64
|
+
# for an in-flight response.
|
65
|
+
# Use this to return sensible empty or error responses to your clients.
|
66
|
+
# Interface:
|
67
|
+
# #call(request_env Hash) response_env Hash
|
68
|
+
fallbacks: Fallbacks.new,
|
69
|
+
|
70
|
+
# Gauge exposes #rate and #wait, to be used as TTL for lock and cache wait time.
|
71
|
+
# The #start and #finish methods are called during a request/response cycle.
|
72
|
+
# This should allow custom gauges to implement their own heuristic to calculate #rate and #wait on the fly.
|
73
|
+
# By default a Null Gauge is used that just returns the values in the :rate and :wait arguments.
|
74
|
+
# Interface:
|
75
|
+
# #rate(request_id String) Integer
|
76
|
+
# #wait(request_id String) Integer
|
77
|
+
# #start(request_id String, start_time Time)
|
78
|
+
# #update(request_id String, state Symbol)
|
79
|
+
# #finish(request_id String, state Symbol)
|
80
|
+
#
|
81
|
+
# `request_id` is the result of cache_key_resolver#call, normally an MD5 hash of the request full URL.
|
82
|
+
# `state` can be one of :fresh, :cached, :timeout, :fallback
|
83
|
+
gauge: nil
|
84
|
+
)
|
85
|
+
|
86
|
+
validate_dep! lock, :lock, :set
|
87
|
+
validate_dep! cache, :cache, :get, :set
|
88
|
+
validate_dep! lock_key_resolver, :lock_key_resolver, :call
|
89
|
+
validate_dep! cache_key_resolver, :cache_key_resolver, :call
|
90
|
+
validate_dep! fallbacks, :fallbacks, :call
|
91
|
+
|
92
|
+
@lock = lock
|
93
|
+
@cache = cache
|
94
|
+
@lock_key_resolver = lock_key_resolver
|
95
|
+
@cache_key_resolver = cache_key_resolver
|
96
|
+
@rate = rate.to_i
|
97
|
+
@wait = wait.to_i
|
98
|
+
@timeout = timeout.to_i
|
99
|
+
@fallbacks = fallbacks
|
100
|
+
@gauge = gauge || Gauge.new(rate: @rate, wait: @wait)
|
101
|
+
|
102
|
+
validate_dep! @gauge, :gauge, :start, :update, :finish
|
103
|
+
|
104
|
+
super app
|
105
|
+
end
|
106
|
+
|
107
|
+
def call(request_env)
|
108
|
+
return app.call(request_env) if request_env[:method] != :get
|
109
|
+
|
110
|
+
start = Time.now
|
111
|
+
|
112
|
+
lock_key = lock_key_resolver.call(request_env)
|
113
|
+
cache_key = cache_key_resolver.call(request_env)
|
114
|
+
|
115
|
+
gauge.start cache_key, start
|
116
|
+
|
117
|
+
if lock.set(lock_key, gauge.rate(cache_key))
|
118
|
+
begin
|
119
|
+
with_timeout(timeout) {
|
120
|
+
app.call(request_env).on_complete do |response_env|
|
121
|
+
cache.set cache_key, response_env
|
122
|
+
gauge.finish cache_key, :fresh
|
123
|
+
debug_headers response_env, :fresh, start
|
124
|
+
end
|
125
|
+
}
|
126
|
+
rescue ::Timeout::Error => e
|
127
|
+
gauge.update cache_key, :timeout
|
128
|
+
serve_from_cache_or_fallback request_env, cache_key, start
|
129
|
+
end
|
130
|
+
else
|
131
|
+
serve_from_cache_or_fallback request_env, cache_key, start
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
attr_reader :app, :lock, :cache, :lock_key_resolver, :cache_key_resolver, :rate, :wait, :timeout, :fallbacks, :gauge
|
137
|
+
|
138
|
+
def serve_from_cache_or_fallback(request_env, cache_key, start)
|
139
|
+
if cached_response = cache.get(cache_key, gauge.wait(cache_key))
|
140
|
+
gauge.finish cache_key, :cached
|
141
|
+
resp cached_response, :cached, start
|
142
|
+
else
|
143
|
+
gauge.finish cache_key, :fallback
|
144
|
+
resp fallbacks.call(request_env), :fallback, start
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def resp(resp_env, status = :fresh, start = Time.now)
|
149
|
+
resp_env = Faraday::Env.from(resp_env)
|
150
|
+
debug_headers resp_env, status, start
|
151
|
+
::Faraday::Response.new(resp_env)
|
152
|
+
end
|
153
|
+
|
154
|
+
def validate_dep!(dep, dep_name, *methods)
|
155
|
+
methods.each do |m|
|
156
|
+
raise ArgumentError, %(#{dep_name} must implement :#{m}) unless dep.respond_to?(m)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def debug_headers(resp_env, status, start)
|
161
|
+
resp_env[:response_headers].merge!(
|
162
|
+
'X-Throttler' => status.to_s,
|
163
|
+
'X-ThrottlerTime' => (Time.now - start)
|
164
|
+
)
|
165
|
+
end
|
166
|
+
|
167
|
+
def with_timeout(seconds, &block)
|
168
|
+
if seconds == 0
|
169
|
+
yield
|
170
|
+
else
|
171
|
+
::Timeout.timeout(seconds, &block)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
Faraday::Middleware.register_middleware throttler: ->{ Middleware }
|
177
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'faraday_throttler/retryable'
|
2
|
+
require 'faraday_throttler/serializer'
|
3
|
+
|
4
|
+
module FaradayThrottler
|
5
|
+
class RedisCache
|
6
|
+
NAMESPACE = 'throttler:cache:'.freeze
|
7
|
+
|
8
|
+
include Retryable
|
9
|
+
|
10
|
+
def initialize(redis: Redis.new, ttl: 0, serializer: Serializer.new)
|
11
|
+
@redis = redis
|
12
|
+
@ttl = ttl
|
13
|
+
@serializer = serializer
|
14
|
+
end
|
15
|
+
|
16
|
+
def set(key, resp)
|
17
|
+
opts = {}
|
18
|
+
opts[:ex] = ttl if ttl > 0
|
19
|
+
redis.set [NAMESPACE, key].join, serializer.serialize(resp), opts
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(key, wait = 10)
|
23
|
+
with_retry(wait) {
|
24
|
+
r = redis.get([NAMESPACE, key].join)
|
25
|
+
r ? serializer.deserialize(r) : nil
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
attr_reader :redis, :ttl, :serializer
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module FaradayThrottler
|
2
|
+
class RedisLock
|
3
|
+
NAMESPACE = 'throttler:lock:'.freeze
|
4
|
+
|
5
|
+
def initialize(redis = Redis.new)
|
6
|
+
@redis = redis
|
7
|
+
end
|
8
|
+
|
9
|
+
def set(key, ttl = 30)
|
10
|
+
redis.set([NAMESPACE, key].join, '1', ex: ttl, nx: true)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
attr_reader :redis
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module FaradayThrottler
|
2
|
+
module Retryable
|
3
|
+
private
|
4
|
+
|
5
|
+
def with_retry(wait, &block)
|
6
|
+
r = block.call
|
7
|
+
return r if r || wait == 0
|
8
|
+
|
9
|
+
value = nil
|
10
|
+
ticks = 0
|
11
|
+
while ticks <= wait do
|
12
|
+
Kernel.sleep 1
|
13
|
+
ticks += 1
|
14
|
+
value = block.call
|
15
|
+
end
|
16
|
+
|
17
|
+
value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'faraday_throttler/errors'
|
3
|
+
|
4
|
+
module FaradayThrottler
|
5
|
+
class Serializer
|
6
|
+
def serialize(resp)
|
7
|
+
validate_response! resp
|
8
|
+
|
9
|
+
hash = {
|
10
|
+
status: resp[:status],
|
11
|
+
body: resp[:body],
|
12
|
+
response_headers: resp[:response_headers]
|
13
|
+
}
|
14
|
+
|
15
|
+
JSON.dump hash
|
16
|
+
end
|
17
|
+
|
18
|
+
def deserialize(json)
|
19
|
+
JSON.parse(json.to_s)
|
20
|
+
rescue JSON::ParserError => e
|
21
|
+
raise Errors::SerializerError, e.message
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def validate_response!(resp)
|
26
|
+
unless resp.has_key?(:status) && resp.has_key?(:body) && resp.has_key?(:response_headers)
|
27
|
+
raise Errors::SerializerError, "response is not valid. Fields: #{resp.keys}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: faraday_throttler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ismael Celis
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.9.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.9.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.9'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.9'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.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: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: Configure how often you want to hit backend APIs, and fallback responses
|
70
|
+
to keep clients happy
|
71
|
+
email:
|
72
|
+
- ismaelct@gmail.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- ".rspec"
|
79
|
+
- ".travis.yml"
|
80
|
+
- Gemfile
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bin/console
|
84
|
+
- bin/setup
|
85
|
+
- examples/Gemfile
|
86
|
+
- examples/Gemfile.lock
|
87
|
+
- examples/README.md
|
88
|
+
- examples/client.rb
|
89
|
+
- examples/config.ru
|
90
|
+
- faraday_throttler.gemspec
|
91
|
+
- lib/faraday_throttler.rb
|
92
|
+
- lib/faraday_throttler/cache.rb
|
93
|
+
- lib/faraday_throttler/errors.rb
|
94
|
+
- lib/faraday_throttler/fallbacks.rb
|
95
|
+
- lib/faraday_throttler/gauge.rb
|
96
|
+
- lib/faraday_throttler/key_resolver.rb
|
97
|
+
- lib/faraday_throttler/mem_lock.rb
|
98
|
+
- lib/faraday_throttler/middleware.rb
|
99
|
+
- lib/faraday_throttler/redis_cache.rb
|
100
|
+
- lib/faraday_throttler/redis_lock.rb
|
101
|
+
- lib/faraday_throttler/retryable.rb
|
102
|
+
- lib/faraday_throttler/serializer.rb
|
103
|
+
- lib/faraday_throttler/version.rb
|
104
|
+
homepage: https://github.com/ismasan/faraday_throttler
|
105
|
+
licenses: []
|
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.8
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: Redis-backed request throttler requests to protect backend APIs against request
|
127
|
+
stampedes
|
128
|
+
test_files: []
|