Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for rate limiting #21

Merged
merged 6 commits into from Aug 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Expand Up @@ -65,6 +65,18 @@ This throws error or returns `<Ping{data='pong!'}>`
You can also pass to the Config initializer:
* `request_timeout=` sets timeout for requests (seconds), default: none (see [requests docs](http://docs.python-requests.org/en/master/user/quickstart/#timeouts) for details)

### Rate Limits & Exponential Backoff
The library will keep retrying if the request exceeds the rate limit or if there's any network related error.
By default, the request will be retried for 20 times (approximately 15 minutes) before finally giving up.

You can change the retry count from the Config initializer:

* `max_retries=` sets the maximum number of retries for failed requests, default: 20
* `backoff_factor=` sets the exponential backoff factor, default: 2

Set max_retries 0 to disable it.
Set backoff_factor 0 to disable it.

## Usage

The library is based on [promises](https://pypi.python.org/pypi/promise) (mechanism similar to futures).
Expand Down
2 changes: 1 addition & 1 deletion chartmogul/__init__.py
Expand Up @@ -29,7 +29,7 @@
"""

__title__ = 'chartmogul'
__version__ = '1.1.8'
__version__ = '1.2.0'
__build__ = 0x000000
__author__ = 'ChartMogul Ltd'
__license__ = 'MIT'
Expand Down
4 changes: 3 additions & 1 deletion chartmogul/api/config.py
Expand Up @@ -5,6 +5,8 @@
class Config:
uri = API_BASE + "/" + VERSION

def __init__(self, account_token, secret_key, request_timeout=None):
def __init__(self, account_token, secret_key, request_timeout=None, max_retries=20, backoff_factor=2):
self.auth = (account_token, secret_key)
self.request_timeout = request_timeout
self.max_retries = max_retries
self.backoff_factor = backoff_factor
18 changes: 9 additions & 9 deletions chartmogul/resource.py
Expand Up @@ -2,6 +2,7 @@
from json import dumps
from promise import Promise
from uritemplate import URITemplate
from .retry_request import requests_retry_session
from .errors import APIError, ConfigurationError, ArgumentMissingError, annotateHTTPError
from .api.config import Config
from datetime import datetime, date
Expand Down Expand Up @@ -119,14 +120,14 @@ def _request(cls, config, method, http_verb, path, data=None, **kwargs):
data = dumps(data, default=json_serial)

return Promise(lambda resolve, _:
resolve(getattr(requests, http_verb)(
config.uri + path,
data=data,
headers={'content-type': 'application/json'},
params=params,
auth=config.auth,
timeout=config.request_timeout)
)).then(cls._load).catch(annotateHTTPError)
resolve(getattr(requests_retry_session(config.max_retries, config.backoff_factor), http_verb)(
config.uri + path,
data=data,
headers={'content-type': 'application/json'},
params=params,
auth=config.auth,
timeout=config.request_timeout)
)).then(cls._load).catch(annotateHTTPError)

@classmethod
def _expandPath(cls, path, kwargs):
Expand Down Expand Up @@ -160,7 +161,6 @@ def fc(cls, config, **kwargs):
return cls._request(config, method, http_verb, pathTemp, **kwargs)
return fc


def _add_method(cls, method, http_verb, path=None):
"""
Dynamically define all possible actions.
Expand Down
24 changes: 24 additions & 0 deletions chartmogul/retry_request.py
@@ -0,0 +1,24 @@
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

METHOD_WHITELIST = ['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']
STATUS_FORCELIST = (429, 500, 502, 503, 504, 520, 524)

def requests_retry_session(retries=20, backoff_factor=2, session=None,):
session = session or requests.Session()
adapter = _retry_adapter(retries, backoff_factor)
session.mount('https://', adapter)
return session

def _retry_adapter(retries, backoff_factor):
retry = Retry(
total=retries,
read=retries,
connect=retries,
status=retries,
method_whitelist=METHOD_WHITELIST,
status_forcelist=STATUS_FORCELIST,
backoff_factor=backoff_factor,
)
return HTTPAdapter(max_retries=retry)
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -20,7 +20,7 @@
'marshmallow>=2.12.1',
'future>=0.16.0',
]
test_requirements = ['mock>=1.0.1', 'requests-mock>=1.3.0', 'vcrpy>=1.11.1']
test_requirements = ['mock>=1.0.1', 'requests-mock>=1.3.0', 'vcrpy>=1.11.1', 'httpretty>=0.9.5']

with open('chartmogul/__init__.py', 'r') as fd:
version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
Expand Down
49 changes: 49 additions & 0 deletions test/api/test_retry_request.py
@@ -0,0 +1,49 @@
import unittest

import httpretty
import chartmogul
from chartmogul import Config, DataSource
from datetime import date, datetime
from requests.exceptions import RetryError
from chartmogul.retry_request import requests_retry_session

class RetryRequestTestCase(unittest.TestCase):

@httpretty.activate
def test_retry_request(self):
httpretty.register_uri(
httpretty.GET,
"https://example:444/testing",
responses=[
httpretty.Response(body='{}', status=500),
httpretty.Response(body='{}', status=200),
]
)

with self.assertRaises(RetryError):
requests_retry_session(0).get('https://example:444/testing')

response = requests_retry_session(2, 0).get('https://example:444/testing')
self.assertEqual(response.text, '{}')

@httpretty.activate
def test_requests_retry_session_on_resource(self):
httpretty.register_uri(
httpretty.POST,
"https://api.chartmogul.com/v1/data_sources",
responses=[
httpretty.Response(body='{}', status=500),
httpretty.Response(body='{}', status=500),
httpretty.Response(body='{}', status=500),
httpretty.Response(body='{}', status=500),
httpretty.Response(body='{}', status=200),
]
)

# max_retries set as 4
# backoff_factor set as 0 to avoid waiting while testing
config = Config("token", "secret", None, 4, 0)
try:
DataSource.create(config, data={ "test_date": date(2015, 1, 1) }).get()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a mock too right? Sorry I couldn't see where we mock it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is mocked by httpretty here

31         httpretty.register_uri(
32             httpretty.POST,
33             "https://api.chartmogul.com/v1/data_sources",
              ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

except RetryError:
self.fail("request raised retryError unexpectedly!")