http-threshold 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,7 @@
1
+ *~
2
+ *.sqlite3
3
+ .bundle
4
+ pkg
5
+ log
6
+
7
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ gemspec
@@ -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.
@@ -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,12 @@
1
+ require 'http_threshold/cache'
2
+ require 'http_threshold/throttle'
3
+ require 'http_threshold/client'
4
+ require 'http_threshold/request_holder'
5
+ require 'pp'
6
+
7
+ module HttpThreshold
8
+
9
+ mattr_accessor :redis_url
10
+ @@redis_url = nil
11
+
12
+ end
@@ -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: []