http-threshold 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 +7 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +24 -0
- data/LICENSE +21 -0
- data/README.md +46 -0
- data/http-threshold.gemspec +17 -0
- data/lib/http-threshold.rb +1 -0
- data/lib/http_threshold.rb +12 -0
- data/lib/http_threshold/cache.rb +17 -0
- data/lib/http_threshold/client.rb +46 -0
- data/lib/http_threshold/request_holder.rb +15 -0
- data/lib/http_threshold/throttle.rb +55 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dd6259ff9cac77fc952d5099d7374634aff25038
|
4
|
+
data.tar.gz: 5055cd6cac2e8c06a26c91c34dc35d2a397de40b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2accd9766182e7f04bc000851fc70107668a63c58f55f639e4ec0322c57da577cdbf20b1d0ea166e1da7b4c84bb4da77fcd0945ee1daecf66f93f80b6acfe414
|
7
|
+
data.tar.gz: 1aa7bd8035be0e7894eef358716a66c89c25c4d436b6692116b717e785c80dad165d0556c817ffe231244d38baf22a3a839ebbcf393f1ea20990dbdc2879f47c
|
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
gemspec
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
threshold (0.0.2)
|
5
|
+
rack-attack (~> 5.0)
|
6
|
+
redlock (~> 0.2)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
specs:
|
10
|
+
rack (2.0.3)
|
11
|
+
rack-attack (5.0.1)
|
12
|
+
rack
|
13
|
+
redis (3.3.3)
|
14
|
+
redlock (0.2.1)
|
15
|
+
redis (>= 3.0.0, < 5.0)
|
16
|
+
|
17
|
+
PLATFORMS
|
18
|
+
ruby
|
19
|
+
|
20
|
+
DEPENDENCIES
|
21
|
+
threshold!
|
22
|
+
|
23
|
+
BUNDLED WITH
|
24
|
+
1.15.4
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017 Ankun Yu
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# Threshold
|
2
|
+
|
3
|
+
A gem to control external API calls. Many API server has rate limit.e.g. Recurly: 1k requests/1minute, Shopify: 4 requests/second.
|
4
|
+
To resolve external API call issue, I create this gem.
|
5
|
+
|
6
|
+
Ideas are come from `rack-attach`, `redlock`.
|
7
|
+
This gem add extension to `Net::HTTPRequest`.
|
8
|
+
Before you made request, check redis if out request reach the limit.
|
9
|
+
If reach the limit, sleep and wait rack-attack expire the out rate limit count.
|
10
|
+
|
11
|
+
|
12
|
+
## How to use
|
13
|
+
|
14
|
+
`gem install threshold`
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
|
18
|
+
# currently, throttle is based on host + port
|
19
|
+
HttpThreshold.redis_url = "redis://localhost:6379/0"
|
20
|
+
HttpThreshold::Client.set_throttle("your-store.shopify.com", limit: 4, period: 1.second)
|
21
|
+
HttpThreshold::Client.set_throttle("localhost:3000", limit: 50, period: 1.minute)
|
22
|
+
HttpThreshold::Client.set_throttle("google.com", limit: 5, period: 10.second)
|
23
|
+
```
|
24
|
+
Then when you run any http call like below
|
25
|
+
```
|
26
|
+
require 'httparty'
|
27
|
+
HTTParty.get("https://www.google.com")#ok
|
28
|
+
HTTParty.get("https://www.google.com")#ok
|
29
|
+
HTTParty.get("https://www.google.com")#ok
|
30
|
+
HTTParty.get("https://www.google.com")#ok
|
31
|
+
HTTParty.get("https://www.google.com")#ok
|
32
|
+
HTTParty.get("https://www.google.com")#will sleep until this 10 seconds past, and then get response
|
33
|
+
|
34
|
+
```
|
35
|
+
|
36
|
+
It support distributed system, because we use redis lock.
|
37
|
+
|
38
|
+
## Note
|
39
|
+
The best way to use this, is add threshold at background job, set a more strict limit in a period.
|
40
|
+
So that it could leave enough rate resources for website.
|
41
|
+
|
42
|
+
Background job sleep is OK. A user's request sleep long would cause problem.
|
43
|
+
|
44
|
+
|
45
|
+
## Todo
|
46
|
+
Add test
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.platform = Gem::Platform::RUBY
|
4
|
+
s.name = "http-threshold"
|
5
|
+
s.version = "0.0.1"
|
6
|
+
s.date = "2017-11-15"
|
7
|
+
s.homepage = 'https://github.com/yakjuly/http-threshold'
|
8
|
+
s.summary = "A gem for to control external API rate,Could useful for Shopify, Recurly API etc"
|
9
|
+
s.author = 'Ankun Yu'
|
10
|
+
s.email = 'yakjuly@gmail.com'
|
11
|
+
s.files = `git ls-files`.split("\n").reject { |f| f.match(/^spec/) && !f.match(/^spec\/fixtures/) }
|
12
|
+
s.require_paths = "lib"
|
13
|
+
s.license = 'MIT'
|
14
|
+
|
15
|
+
s.add_dependency 'rack-attack', '~> 5.0'
|
16
|
+
s.add_dependency 'redlock', '~> 0.2'
|
17
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'http_threshold'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rack/attack'
|
2
|
+
module HttpThreshold
|
3
|
+
class Cache < Rack::Attack::Cache
|
4
|
+
attr_accessor :prefix
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
self.store = ::Rails.cache if defined?(::Rails.cache)
|
8
|
+
@prefix = 'threshold'
|
9
|
+
end
|
10
|
+
|
11
|
+
def read_count(unprefixed_key, period)
|
12
|
+
key, _ = key_and_expiry(unprefixed_key, period)
|
13
|
+
store.read(key).to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'redlock'
|
2
|
+
|
3
|
+
module HttpThreshold
|
4
|
+
class Client
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def throttles
|
8
|
+
@throttles ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def cache
|
12
|
+
@cache ||= Cache.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def lock_manager
|
16
|
+
@lock_manager ||= Redlock::Client.new([Redis.new(:url => HttpThreshold.redis_url)])
|
17
|
+
end
|
18
|
+
|
19
|
+
def incr_count(domain)
|
20
|
+
self.throttles[domain].try(:incr_count)
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_throttle(domain, options = {})
|
24
|
+
self.throttles[domain] = HttpThreshold::Throttle.new(domain, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def sleep_until_allowed(host)
|
28
|
+
throttle = self.throttles[host]
|
29
|
+
if throttle
|
30
|
+
if !throttle.incr_count
|
31
|
+
puts "#{host} reach threshold limit"
|
32
|
+
sleep 0.25
|
33
|
+
|
34
|
+
while !throttle.incr_count
|
35
|
+
sleep 0.25
|
36
|
+
end
|
37
|
+
end
|
38
|
+
yield
|
39
|
+
else
|
40
|
+
yield
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module HttpThreshold
|
2
|
+
module RequestHold
|
3
|
+
|
4
|
+
def exec(sock, ver, path)
|
5
|
+
host = @header["host"].first
|
6
|
+
# here can stop the request
|
7
|
+
HttpThreshold::Client.sleep_until_allowed(host) do
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Net::HTTPRequest.include HttpThreshold::RequestHold
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module HttpThreshold
|
2
|
+
class Throttle
|
3
|
+
MANDATORY_OPTIONS = [:limit, :period]
|
4
|
+
|
5
|
+
attr_reader :name, :limit, :period, :block, :type
|
6
|
+
def initialize(name, options)
|
7
|
+
@name = name
|
8
|
+
MANDATORY_OPTIONS.each do |opt|
|
9
|
+
raise ArgumentError.new("Must pass #{opt.inspect} option") unless options[opt]
|
10
|
+
end
|
11
|
+
@limit = options[:limit]
|
12
|
+
@period = options[:period].to_i
|
13
|
+
end
|
14
|
+
|
15
|
+
def cache
|
16
|
+
HttpThreshold::Client.cache
|
17
|
+
end
|
18
|
+
|
19
|
+
def lock_manager
|
20
|
+
HttpThreshold::Client.lock_manager
|
21
|
+
end
|
22
|
+
|
23
|
+
# similar as method #[]
|
24
|
+
def incr_count
|
25
|
+
begin
|
26
|
+
result = lock_manager.lock!("lock:#{name}", 2000) do
|
27
|
+
if reach_limit?
|
28
|
+
false
|
29
|
+
else
|
30
|
+
cache.count(threshold_key, period)
|
31
|
+
true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
result
|
35
|
+
rescue Redlock::LockError
|
36
|
+
sleep 0.25
|
37
|
+
retry
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def reach_limit?
|
42
|
+
count = cache.read_count(threshold_key, period)
|
43
|
+
count >= limit
|
44
|
+
end
|
45
|
+
|
46
|
+
def status
|
47
|
+
count = cache.read_count(threshold_key, period)
|
48
|
+
"#{count}/#{limit}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def threshold_key
|
52
|
+
"threshold:#{name}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: http-threshold
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ankun Yu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack-attack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: redlock
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.2'
|
41
|
+
description:
|
42
|
+
email: yakjuly@gmail.com
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files: []
|
46
|
+
files:
|
47
|
+
- ".gitignore"
|
48
|
+
- Gemfile
|
49
|
+
- Gemfile.lock
|
50
|
+
- LICENSE
|
51
|
+
- README.md
|
52
|
+
- http-threshold.gemspec
|
53
|
+
- lib/http-threshold.rb
|
54
|
+
- lib/http_threshold.rb
|
55
|
+
- lib/http_threshold/cache.rb
|
56
|
+
- lib/http_threshold/client.rb
|
57
|
+
- lib/http_threshold/request_holder.rb
|
58
|
+
- lib/http_threshold/throttle.rb
|
59
|
+
homepage: https://github.com/yakjuly/http-threshold
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
metadata: {}
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 2.6.8
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: A gem for to control external API rate,Could useful for Shopify, Recurly
|
83
|
+
API etc
|
84
|
+
test_files: []
|