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.
@@ -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: []