Skip to content

Commit

Permalink
Move to use rate_throttle_client Gem
Browse files Browse the repository at this point in the history
Instead of maintaining a single purpose rate throttle client wrapper, I ported the code over to a gem `rate_throttle_client` that has it's own set of tests and metrics generating code. This will make it easier to maintain and other libraries can use the code now as well.

I'm now also testing configuring the rate throttling logic.
  • Loading branch information
schneems committed Apr 21, 2020
1 parent e1bdf88 commit 3d212cf
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 335 deletions.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,26 @@ By default client requests from this library will respect Heroku's rate-limiting

Once a single request has been rate-limited, the client will auto-tune a sleep value so that future requests are less likely to be rate-limited by the server.

To disable this retry and sleep behavior set the env var`PLATFORM_API_DISABLE_RATE_THROTTLE=1`.
Rate throttle strategies are provided by [the Rate Throttle Client gem](https://github.com/zombocom/rate_throttle_client). By default the `RateThrottleClient::ExponentialIncreaseProportionalRemainingDecrease` strategy is used.

For more information about this algorithm and behavior see [Rate Limit GCRA client demo](https://github.com/schneems/rate-limit-gcra-client-demo).
To disable this retry-and-sleep behavior you can change the rate throttling strategy to any object that responds to `call` and yields to a block:

```ruby
PlatformAPI.rate_throttle = ->(&block) { block.call }

# or

PlatformAPI.rate_throttle = RateThrottleClient::Null.new
```

By default rate throttling will log to STDOUT when the sleep/retry behavior is triggered. To add your own custom logging, for example to librato or honeycomb, you can pass in a callable object that takes three arguments:

```ruby
PlatformAPI.rate_throttle.log = ->(event_type, request, throttle_object) {
PlatformAPI.rate_throttle.log = ->(info) {
# Your logic here

puts info.sleep_for
puts info.request
}
```

Expand Down
12 changes: 5 additions & 7 deletions config/client-config.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
# frozen_string_literal: true
require 'heroics'
require 'rate_throttle_client'

require File.join(File.expand_path('../..', __FILE__), 'lib', 'platform-api', 'version.rb')
require File.join(File.expand_path('../..', __FILE__), 'lib', 'platform-api', 'heroku_client_throttle.rb')

Heroics.default_configuration do |config|
config.base_url = 'https://api.heroku.com'
config.module_name = 'PlatformAPI'
config.schema_filepath = File.join(File.expand_path('../..', __FILE__), 'schema.json')

unless ENV['PLATFORM_API_DISABLE_RATE_THROTTLE']
PlatformAPI.rate_throttle = PlatformAPI::HerokuClientThrottle.new

config.rate_throttle = PlatformAPI.rate_throttle
config.acceptable_status_codes = [429]
end
PlatformAPI.rate_throttle = RateThrottleClient::ExponentialIncreaseProportionalRemainingDecrease.new
config.rate_throttle = PlatformAPI.rate_throttle
config.acceptable_status_codes = [429]

config.headers = {
'Accept' => 'application/vnd.heroku+json; version=3',
Expand Down
5 changes: 3 additions & 2 deletions lib/platform-api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
module PlatformAPI
def self.rate_throttle=(rate_throttle)
@rate_throttle = rate_throttle
Heroics.default_configuration do |config|
config.rate_throttle = @rate_throttle
end
end

# Get access to the rate throttling class object for configuration.
Expand All @@ -16,7 +19,5 @@ def self.rate_throttle
end

require_relative '../config/client-config'

require 'platform-api/client'
require 'platform-api/version'
require 'platform-api/heroku_client_throttle'
125 changes: 0 additions & 125 deletions lib/platform-api/heroku_client_throttle.rb

This file was deleted.

1 change: 1 addition & 0 deletions platform-api.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ Gem::Specification.new do |spec|

spec.add_dependency 'heroics', '~> 0.1.1'
spec.add_dependency 'moneta', '~> 1.0.0'
spec.add_dependency 'rate_throttle_client', '~> 0.1.0'
end
67 changes: 67 additions & 0 deletions spec/acceptance/config_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require 'platform-api'

describe 'Platform API config' do
describe 'rate limiting' do
before(:each) do
WebMock.enable!

rate_throttle = PlatformAPI.rate_throttle
@original_rate_throttle = rate_throttle.dup

# No junk in test dots
rate_throttle.log = ->(*_) {}

# Don't sleep in tests
def rate_throttle.sleep(value); end
end

after(:each) do
WebMock.disable!

PlatformAPI.rate_throttle = @original_rate_throttle
end

it "works even if first request is rate limited" do
stub_request(:get, "https://api.heroku.com/apps")
.to_return([
{status: 429},
{status: 200}
])

client.app.list

expect(WebMock).to have_requested(:get, "https://api.heroku.com/apps").twice
end

it "allows the rate throttling class to be modified" do
stub_request(:get, "https://api.heroku.com/apps")
.to_return([
{status: 429},
{status: 429},
{status: 200}
])

@retry_count = 0
PlatformAPI.rate_throttle.log = ->(*_) { @retry_count += 1 }
client.app.list

expect(WebMock).to have_requested(:get, "https://api.heroku.com/apps").times(3)
expect(@retry_count).to eq(2)
end

it "allows rate throttling logic to be changed" do
stub_request(:get, "https://api.heroku.com/apps")
.to_return([
{status: 429}
])

@times_called = 0
PlatformAPI.rate_throttle = ->(&block) { @times_called += 1; block.call }

client.app.list

expect(WebMock).to have_requested(:get, "https://api.heroku.com/apps").once
expect(@times_called).to eq(1)
end
end
end
61 changes: 2 additions & 59 deletions spec/acceptance/generated_client_spec.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,11 @@
require 'netrc'
require 'platform-api'
require 'hatchet'
require 'webmock/rspec'

include WebMock::API
WebMock.allow_net_connect!

describe 'The generated platform api client' do
before(:all) do
@app_name = ENV["TEST_APP_NAME"] || hatchet_app.name
end

it "works even if first request is rate limited" do
WebMock.enable!
url = "https://api.heroku.com/apps"
stub_request(:get, url)
.to_return([
{status: 429},
{status: 200}
])

client.app.list

expect(WebMock).to have_requested(:get, url).twice
ensure
WebMock.disable!
def app_name
@app_name
end

it "can get account info" do
Expand Down Expand Up @@ -74,42 +55,4 @@
it "can get app webhooks" do
expect(client.app_webhook.list(app_name)).not_to be_empty
end

def app_name
@app_name
end

def hatchet_app
@hatchet_app ||= begin
app = Hatchet::Runner.new("default_ruby", buildpacks: ["heroku/ruby"])
app.in_directory do
app.setup!
app.push_with_retry!
end
app.api_rate_limit.call.app_webhook.create(app.name, include: ["dyno"] , level: "notify", url: "https://example.com")
app.api_rate_limit.call.addon.create(app.name, plan: 'heroku-postgresql' )
app
end
end

def email
@email
end

def client
@client ||=
begin
entry = Netrc.read['api.heroku.com']
if entry
oauth_token = entry.password
@email = entry.login
else
oauth_token = ENV['OAUTH_TOKEN']
@email = ENV['ACCOUNT_EMAIL']
end
raise "Must set env vars or write a netrc" unless @email

PlatformAPI.connect_oauth(oauth_token)
end
end
end
37 changes: 37 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
require 'pry'
require 'webmock/rspec'
require 'platform-api'
require 'netrc'
require 'hatchet'

include WebMock::API
WebMock.allow_net_connect!

RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
Expand All @@ -18,3 +26,32 @@
config.order = :random
Kernel.srand config.seed
end

def hatchet_app
@hatchet_app ||= begin
app = Hatchet::Runner.new("default_ruby", buildpacks: ["heroku/ruby"])
app.in_directory do
app.setup!
app.push_with_retry!
end
app.api_rate_limit.call.app_webhook.create(app.name, include: ["dyno"] , level: "notify", url: "https://example.com")
app.api_rate_limit.call.addon.create(app.name, plan: 'heroku-postgresql' )
app
end
end

def client
@client ||= begin
entry = Netrc.read['api.heroku.com']
if entry
oauth_token = entry.password
@email = entry.login
else
oauth_token = ENV['OAUTH_TOKEN']
@email = ENV['ACCOUNT_EMAIL']
end
raise "Must set env vars or write a netrc" unless @email

PlatformAPI.connect_oauth(oauth_token)
end
end
Loading

0 comments on commit 3d212cf

Please sign in to comment.