rate_throttle_client 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +22 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +5 -0
- data/README.md +20 -15
- data/Rakefile +24 -1
- data/lib/rate_throttle_client/chart.rb +68 -0
- data/lib/rate_throttle_client/demo.rb +7 -0
- data/lib/rate_throttle_client/version.rb +1 -1
- data/rate_throttle_client.gemspec +2 -1
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b93186de79a4c1b41605182c30ba093f37b569e1d44a23e6d9dbd7b634827f4e
|
4
|
+
data.tar.gz: 034a009ee78065955c40fe0b5da17c9384deefc961bff812b7b2c61fe2086219
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d10866abfcd48e216bbb11cb7bb40c15711bb9b190ae914073877f552ea5845d969b2b59452ec06fdf1575a6b78065e7a0139e15928b31a7555715b4e26c161
|
7
|
+
data.tar.gz: 96e51b05c20faab8d552d9465f0b914dfac5ae3451cfec48ebec3039e2719ab74386db34af43245a3234ba84a04940e150a06d4b10b8752437868bfa814ac54f
|
data/.circleci/config.yml
CHANGED
@@ -4,6 +4,12 @@ references:
|
|
4
4
|
run:
|
5
5
|
name: Run test suite
|
6
6
|
command: bundle exec rake
|
7
|
+
imagick: &imagick
|
8
|
+
run:
|
9
|
+
name: Image Magick
|
10
|
+
command: |
|
11
|
+
sudo apt-get update
|
12
|
+
sudo apt-get install -y imagemagick ghostscript
|
7
13
|
restore: &restore
|
8
14
|
restore_cache:
|
9
15
|
keys:
|
@@ -21,11 +27,21 @@ references:
|
|
21
27
|
- ./vendor/bundle
|
22
28
|
key: v1-dependencies-{{ checksum "Gemfile.lock" }}
|
23
29
|
jobs:
|
30
|
+
"ruby-2.2":
|
31
|
+
docker:
|
32
|
+
- image: circleci/ruby:2.5
|
33
|
+
steps:
|
34
|
+
- checkout
|
35
|
+
- <<: *imagick
|
36
|
+
- <<: *bundle
|
37
|
+
- <<: *save
|
38
|
+
- <<: *unit
|
24
39
|
"ruby-2.3":
|
25
40
|
docker:
|
26
41
|
- image: circleci/ruby:2.5
|
27
42
|
steps:
|
28
43
|
- checkout
|
44
|
+
- <<: *imagick
|
29
45
|
- <<: *bundle
|
30
46
|
- <<: *save
|
31
47
|
- <<: *unit
|
@@ -34,6 +50,7 @@ jobs:
|
|
34
50
|
- image: circleci/ruby:2.5
|
35
51
|
steps:
|
36
52
|
- checkout
|
53
|
+
- <<: *imagick
|
37
54
|
- <<: *bundle
|
38
55
|
- <<: *save
|
39
56
|
- <<: *unit
|
@@ -42,6 +59,7 @@ jobs:
|
|
42
59
|
- image: circleci/ruby:2.5
|
43
60
|
steps:
|
44
61
|
- checkout
|
62
|
+
- <<: *imagick
|
45
63
|
- <<: *bundle
|
46
64
|
- <<: *save
|
47
65
|
- <<: *unit
|
@@ -50,6 +68,7 @@ jobs:
|
|
50
68
|
- image: circleci/ruby:2.6
|
51
69
|
steps:
|
52
70
|
- checkout
|
71
|
+
- <<: *imagick
|
53
72
|
- <<: *bundle
|
54
73
|
- <<: *save
|
55
74
|
- <<: *unit
|
@@ -58,6 +77,7 @@ jobs:
|
|
58
77
|
- image: circleci/ruby:2.7
|
59
78
|
steps:
|
60
79
|
- checkout
|
80
|
+
- <<: *imagick
|
61
81
|
- <<: *bundle
|
62
82
|
- <<: *save
|
63
83
|
- <<: *unit
|
@@ -66,6 +86,7 @@ jobs:
|
|
66
86
|
- image: circleci/jruby:latest
|
67
87
|
steps:
|
68
88
|
- checkout
|
89
|
+
- <<: *imagick
|
69
90
|
- <<: *bundle
|
70
91
|
- <<: *save
|
71
92
|
- <<: *unit
|
@@ -74,6 +95,7 @@ workflows:
|
|
74
95
|
version: 2
|
75
96
|
build:
|
76
97
|
jobs:
|
98
|
+
- "ruby-2.2"
|
77
99
|
- "ruby-2.3"
|
78
100
|
- "ruby-2.4"
|
79
101
|
- "ruby-2.5"
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# RateThrottleClient
|
2
2
|
|
3
|
-
Rate limiting is for servers, rate throttling is for clients. This library implements a number of strategies for handling rate throttling on the client and a methodology for comparing performance of those clients
|
3
|
+
Rate limiting is for servers, rate throttling is for clients. This library implements a number of strategies for handling rate throttling on the client and a methodology for comparing performance of those clients. We don't just give you the code to rate throttle, we also give you the information to help you figure out the best strategy to rate throttle as well.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -32,14 +32,16 @@ end
|
|
32
32
|
|
33
33
|
If the server returns a `429` status (the HTTP code indicating that a server side rate limit has been reached) then the request will be retried according to the classes' strategy.
|
34
34
|
|
35
|
-
|
35
|
+
## Expected return value from call
|
36
|
+
|
37
|
+
If you're not using Excon to build your API client, then you'll need to make sure the object returned to the block responds to `status` (returning the status code). To use `ExponentialIncreaseProportionalRemainingDecrease` it's expected that `headers["RateLimit-Remaining"].to_i` returns the number of available requests capacity.
|
36
38
|
|
37
39
|
### Config
|
38
40
|
|
39
41
|
```ruby
|
40
42
|
RateThrottleClient.config do |config|
|
41
|
-
config.log_block = ->(info){ puts "I get called when rate
|
42
|
-
config.max_limit = 4500.to_f # Maximum number of requests
|
43
|
+
config.log_block = ->(info){ puts "I get called when rate throttling is triggered #{info.sleep_for} #{info.request}" }
|
44
|
+
config.max_limit = 4500.to_f # Maximum number of requests available
|
43
45
|
config.multiplier = 1.2 # When rate limiting happens, this is amount to the sleep value is increased by
|
44
46
|
end
|
45
47
|
```
|
@@ -53,19 +55,22 @@ This library has a few strategies you can choose between:
|
|
53
55
|
- RateThrottleClient::ExponentialIncreaseProportionalDecrease
|
54
56
|
- RateThrottleClient::ExponentialIncreaseProportionalRemainingDecrease
|
55
57
|
|
56
|
-
To choose, you need to understand what makes a "good" throttling
|
58
|
+
To choose, you need to understand what makes a "good" throttling strategy, and then you need some benchmarks.
|
57
59
|
|
58
60
|
## What Makes a Good Rate Throttle strategy?
|
59
61
|
|
60
|
-
- Minimize retry ratio: For example if every 50 successful requests, the client hits a rate limited request the ratio of retries is 1/50 or 2%. Why minimize this value? It takes CPU and Network resources to make requests that fail, if the client is making requests that are being limited, it's using resources that could be better spent somewhere else. The server also benefits as it spends less time dealing with rate limiting.
|
61
|
-
- Minimize standard deviation of request count across the system: If there are two clients and one client is throttling by sleeping for 100 seconds and the other is throttling for 1 second, the distribution of requests are not equitable. Ideally over time each client might go up or down, but both would see a median of 50 seconds of sleep time. Why? If processes in a system have a high variance, one process is starved for API resources. It then becomes difficult to balance or optimize otherworkloads. When a client is stuck waiting on the API, ideally it can perform other operations (for example in other threads). If one process is using 100% of CPU and slamming the API and other is using 1% of CPU and barely touching the API, it is difficult to balance the workloads.
|
62
|
-
- Minimize sleep/wait time: Retry ratio can be improved artificially by choosing high sleep times. In the real world consumers don't want to wait longer than absolutely necessarry. While a client might be able to "work steal" while it is sleeping/waiting, there's not guarantee that's the case. Essentially assume that any amount of time spent sleeping over the minimum amount of time required is wasted. This value is calculateable, but that calculation requires complete information of the distributed system.
|
63
|
-
- At
|
64
|
-
-
|
62
|
+
- Minimize retry ratio: For example if every 50 successful requests, the client hits a rate limited request the ratio of retries is 1/50 or 2%. Why minimize this value? It takes CPU and Network resources to make requests that fail, if the client is making requests that are being limited, it's using resources that could be better spent somewhere else. The server also benefits as it spends less time dealing with rate limiting. (Tracked via Avg retry rate)
|
63
|
+
- Minimize standard deviation of request count across the system: If there are two clients and one client is throttling by sleeping for 100 seconds and the other is throttling for 1 second, the distribution of requests are not equitable. Ideally over time each client might go up or down, but both would see a median of 50 seconds of sleep time. Why? If processes in a system have a high variance, one process is starved for API resources. It then becomes difficult to balance or optimize otherworkloads. When a client is stuck waiting on the API, ideally it can perform other operations (for example in other threads). If one process is using 100% of CPU and slamming the API and other is using 1% of CPU and barely touching the API, it is difficult to balance the workloads. (Tracked via Stdev Request Count)
|
64
|
+
- Minimize sleep/wait time: Retry ratio can be improved artificially by choosing high sleep times. In the real world consumers don't want to wait longer than absolutely necessarry. While a client might be able to "work steal" while it is sleeping/waiting, there's not guarantee that's the case. Essentially assume that any amount of time spent sleeping over the minimum amount of time required is wasted. This value is calculateable, but that calculation requires complete information of the distributed system. (Tracked via Max sleep time)
|
65
|
+
- At many available requests: It should be able to consume all available requests: If a server allows 100,000 requests in a day then a client should be capable of making 100,000 requests. If the rate limiting algorithm only allows it to make 100 requests it would have low retry ratio but high wait time.
|
66
|
+
- At few available requests: If clients do not sleep enough their retry rate will be very high, if they sleep too much then they they are not are using available resources.
|
67
|
+
- Minimize time to respond to a change in available requests to either slow down or speed up rate throttling: A change can happen when clients are added or removed (for example if the number of servers/dynos are scaled up or down). It can also happen naturally if processing in a background worker or a web endpoint where the workload is cyclical. If there are few requests available and many become available, the rate throttle algorithm should adjust to match the new availability quickly. (Tracked via Time to clear workload)
|
68
|
+
|
69
|
+
The only strategy that handles all these scenarios well is currently: `RateThrottleClient::ExponentialIncreaseProportionalRemainingDecrease` against a [GCRA rate limit strategy, such as the one implemented by the Heroku API]().
|
65
70
|
|
66
71
|
## Benchmarks
|
67
72
|
|
68
|
-
These benchmarks are generated by running `rake bench` against the simulated "GCRA" rate limiting server.
|
73
|
+
These benchmarks are generated by running `rake bench` against the simulated "[GCRA](https://brandur.org/rate-limiting)" rate limiting server.
|
69
74
|
|
70
75
|
**Lower values are better**
|
71
76
|
|
@@ -83,7 +88,7 @@ Raw request_counts: [1317.00, 1314.00, 1015.00, 963.00, 1254.00, 1133.00, 1334.0
|
|
83
88
|
|
84
89
|
```
|
85
90
|
Time to clear workload (4500 requests, starting_sleep: 1s):
|
86
|
-
|
91
|
+
74.33 seconds
|
87
92
|
```
|
88
93
|
|
89
94
|
### RateThrottleClient::ExponentialIncreaseGradualDecrease results (duration: 30.0 minutes, multiplier: 1.2)
|
@@ -100,7 +105,7 @@ Raw request_counts: [48.00, 57.00, 56.00, 49.00, 282.00, 85.00, 83.00, 79.00, 28
|
|
100
105
|
|
101
106
|
```
|
102
107
|
Time to clear workload (4500 requests, starting_sleep: 1s):
|
103
|
-
|
108
|
+
115.54 seconds
|
104
109
|
```
|
105
110
|
|
106
111
|
### RateThrottleClient::ExponentialIncreaseProportionalDecrease results (duration: 30.0 minutes, multiplier: 1.2)
|
@@ -117,7 +122,7 @@ Raw request_counts: [343.00, 123.00, 223.00, 144.00, 128.00, 348.00, 116.00, 383
|
|
117
122
|
|
118
123
|
```
|
119
124
|
Time to clear workload (4500 requests, starting_sleep: 1s):
|
120
|
-
|
125
|
+
551.10 seconds
|
121
126
|
```
|
122
127
|
|
123
128
|
### RateThrottleClient::ExponentialIncreaseProportionalRemainingDecrease results (duration: 30.0 minutes, multiplier: 1.2)
|
@@ -134,7 +139,7 @@ Raw request_counts: [196.00, 269.00, 386.00, 302.00, 239.00, 197.00, 265.00, 150
|
|
134
139
|
|
135
140
|
```
|
136
141
|
Time to clear workload (4500 requests, starting_sleep: 1s):
|
137
|
-
|
142
|
+
84.23 seconds
|
138
143
|
```
|
139
144
|
|
140
145
|
## Development
|
data/Rakefile
CHANGED
@@ -34,16 +34,18 @@ task :bench do
|
|
34
34
|
demo.call
|
35
35
|
ensure
|
36
36
|
demo.print_results
|
37
|
+
demo.chart(true)
|
37
38
|
end
|
38
39
|
|
39
40
|
begin
|
40
41
|
workload = 4500
|
41
42
|
starting_sleep = 1
|
42
|
-
before_time = Time.now
|
43
43
|
rackup_file = Pathname.new(__dir__).join("lib/rate_throttle_client/servers/decrease_only/config.ru")
|
44
44
|
|
45
45
|
client = klass.new(starting_sleep_for: starting_sleep)
|
46
46
|
demo = RateThrottleClient::Demo.new(client: client, time_scale: 10, starting_limit: 4500, duration: duration, remaining_stop_under: 10, rackup_file: rackup_file)
|
47
|
+
|
48
|
+
before_time = Time.now
|
47
49
|
demo.call
|
48
50
|
diff = Time.now - before_time
|
49
51
|
ensure
|
@@ -56,3 +58,24 @@ task :bench do
|
|
56
58
|
end
|
57
59
|
end
|
58
60
|
end
|
61
|
+
|
62
|
+
task :charts do
|
63
|
+
duration = 30 * MINUTE
|
64
|
+
clients = [
|
65
|
+
RateThrottleClient::ExponentialBackoff,
|
66
|
+
RateThrottleClient::ExponentialIncreaseGradualDecrease,
|
67
|
+
RateThrottleClient::ExponentialIncreaseProportionalDecrease,
|
68
|
+
RateThrottleClient::ExponentialIncreaseProportionalRemainingDecrease
|
69
|
+
]
|
70
|
+
clients.each do |klass|
|
71
|
+
begin
|
72
|
+
client = klass.new
|
73
|
+
demo = RateThrottleClient::Demo.new(client: client, duration: duration, time_scale: 10)
|
74
|
+
demo.call
|
75
|
+
ensure
|
76
|
+
demo.print_results
|
77
|
+
demo.chart
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module RateThrottleClient
|
2
|
+
class Chart
|
3
|
+
def initialize(log_dir:, name:, time_scale:)
|
4
|
+
@log_dir = log_dir
|
5
|
+
@time_scale = time_scale
|
6
|
+
@name = name
|
7
|
+
@label_hash = nil
|
8
|
+
@log_files = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def log_files
|
12
|
+
@log_files ||= @log_dir.entries.map do |entry|
|
13
|
+
@log_dir.join(entry)
|
14
|
+
end.select do |file|
|
15
|
+
file.basename.to_s.end_with?("-chart-data.txt")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_line_count
|
20
|
+
log_files.first.each_line.count
|
21
|
+
end
|
22
|
+
|
23
|
+
def label_hash(line_count = get_line_count)
|
24
|
+
return @label_hash if @label_hash
|
25
|
+
@label_hash = {}
|
26
|
+
|
27
|
+
lines_per_hour = (3600.0 / @time_scale).floor
|
28
|
+
|
29
|
+
line_tick = (line_count / 5.0).floor
|
30
|
+
|
31
|
+
@label_hash[0] = "0"
|
32
|
+
1.upto(5).each do |i|
|
33
|
+
line_number = i * line_tick
|
34
|
+
@label_hash[line_number - 1] = "%.2f" % (line_number.to_f / lines_per_hour)
|
35
|
+
end
|
36
|
+
|
37
|
+
@label_hash
|
38
|
+
end
|
39
|
+
|
40
|
+
def call(open_file = false)
|
41
|
+
require 'gruff'
|
42
|
+
|
43
|
+
graph = Gruff::Line.new()
|
44
|
+
graph.title_font_size = 24
|
45
|
+
|
46
|
+
graph.hide_legend = true if log_files.length > 10
|
47
|
+
graph.title = "#{@name}\nSleep Values for #{log_files.count} clients"
|
48
|
+
graph.x_axis_label = "Time duration in hours"
|
49
|
+
graph.y_axis_label = "Sleep time in seconds"
|
50
|
+
|
51
|
+
log_files.each do |entry|
|
52
|
+
graph.data entry.basename.to_s.gsub("-chart-data.txt", ""), entry.each_line.map(&:to_f)
|
53
|
+
end
|
54
|
+
|
55
|
+
graph.labels = label_hash
|
56
|
+
|
57
|
+
graph.write(file)
|
58
|
+
|
59
|
+
`open #{file}` if open_file
|
60
|
+
|
61
|
+
file
|
62
|
+
end
|
63
|
+
|
64
|
+
def file
|
65
|
+
@log_dir.join('chart.png')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -7,6 +7,8 @@ require 'timecop'
|
|
7
7
|
require 'wait_for_it'
|
8
8
|
require 'enumerable/statistics'
|
9
9
|
|
10
|
+
require_relative 'chart.rb'
|
11
|
+
|
10
12
|
Thread.abort_on_exception = true
|
11
13
|
|
12
14
|
# A class for simulating or "demoing" a rate throttle client
|
@@ -101,6 +103,11 @@ module RateThrottleClient
|
|
101
103
|
io.puts "```"
|
102
104
|
end
|
103
105
|
|
106
|
+
def chart(open_file)
|
107
|
+
chart = RateThrottleClient::Chart.new(log_dir: @log_dir, name: @client.class.to_s.gsub("RateThrottleClient::", ""), time_scale: @time_scale)
|
108
|
+
chart.call(open_file)
|
109
|
+
end
|
110
|
+
|
104
111
|
def results
|
105
112
|
result_hash = {}
|
106
113
|
|
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.description = %q{https://twitter.com/schneems/status/1138899094137651200}
|
11
11
|
spec.homepage = "https://github.com/zombocom/rate_throttle_client"
|
12
12
|
spec.license = "MIT"
|
13
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.2.0")
|
14
14
|
|
15
15
|
spec.metadata["homepage_uri"] = spec.homepage
|
16
16
|
spec.metadata["source_code_uri"] = "https://github.com/zombocom/rate_throttle_client"
|
@@ -30,5 +30,6 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_development_dependency "puma"
|
31
31
|
spec.add_development_dependency "timecop"
|
32
32
|
spec.add_development_dependency "excon"
|
33
|
+
spec.add_development_dependency "gruff"
|
33
34
|
spec.add_development_dependency "enumerable-statistics"
|
34
35
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rate_throttle_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- schneems
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-04-
|
11
|
+
date: 2020-04-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: wait_for_it
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: gruff
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: enumerable-statistics
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,6 +129,7 @@ files:
|
|
115
129
|
- bin/setup
|
116
130
|
- lib/rate_throttle_client.rb
|
117
131
|
- lib/rate_throttle_client/.DS_Store
|
132
|
+
- lib/rate_throttle_client/chart.rb
|
118
133
|
- lib/rate_throttle_client/clients/base.rb
|
119
134
|
- lib/rate_throttle_client/clients/exponential_backoff.rb
|
120
135
|
- lib/rate_throttle_client/clients/exponential_increase_gradual_decrease.rb
|
@@ -144,7 +159,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
144
159
|
requirements:
|
145
160
|
- - ">="
|
146
161
|
- !ruby/object:Gem::Version
|
147
|
-
version: 2.
|
162
|
+
version: 2.2.0
|
148
163
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
164
|
requirements:
|
150
165
|
- - ">="
|