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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a98e4aae0d6770a20d784d63888ac487ad537bc27c2cb00127498a4afd41fb20
4
- data.tar.gz: 9c6b04db20202345cbe2c9995c09fbd3b1f79521e5c33d5337851f0c93455d5e
3
+ metadata.gz: b93186de79a4c1b41605182c30ba093f37b569e1d44a23e6d9dbd7b634827f4e
4
+ data.tar.gz: 034a009ee78065955c40fe0b5da17c9384deefc961bff812b7b2c61fe2086219
5
5
  SHA512:
6
- metadata.gz: 5a32f254cb2c857c4e50cf83fdbece7e9c60c8031f9ffa1d5957e7778899e68e2ccc4565cf6a6cc96ef685a8af05681527de61ea7b0c80ac59077e904850b88a
7
- data.tar.gz: 8a8e0cd1b0809ff00c51c2bd986e4f270a054c6ae1f99ef67ef2634b0e2cb8c659bb02727f9179c9767ad4bc22d3bd28f84333f559d140404b44ab93d0db3012
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
@@ -9,4 +9,6 @@ Gemfile.lock
9
9
  /spec/reports/
10
10
  /tmp/
11
11
 
12
+ test/fixtures/logs/prop_dec/chart.png
13
+
12
14
  logs/*/**
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## Master (Unreleased)
2
2
 
3
+ ## 0.1.1
4
+
5
+ - Supports Ruby 2.2
6
+ - Chart support
7
+
3
8
  ## 0.1.0
4
9
 
5
10
  - First
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 in simulated environments. Essentially, 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.
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
- 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) and a `headers` method.
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 limiting is triggered #{info.sleep_for} #{info.request}" }
42
- config.max_limit = 4500.to_f # Maximum number of requests per hour
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 algorithm, and then you need some benchmarks.
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 high workload 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.
64
- - Handle a change in work load to either slow down or speed up rate throttling: If the workload is light, then clients should not wait/sleep much. If workload is heavy, then clients should sleep/wait enough. The algorithm should adjust to a changing workload as quickly as possible.
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. Which throttle strategy you use depends on your needs.
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
- 76.18 seconds
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
- 65.50 seconds
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
- 489.24 seconds
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
- 66.92 seconds
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
 
@@ -1,3 +1,3 @@
1
1
  module RateThrottleClient
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -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.3.0")
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.0
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 00:00:00.000000000 Z
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.3.0
162
+ version: 2.2.0
148
163
  required_rubygems_version: !ruby/object:Gem::Requirement
149
164
  requirements:
150
165
  - - ">="