Skip to content

Commit

Permalink
Do not retry on 502 (#147)
Browse files Browse the repository at this point in the history
  • Loading branch information
pnwpedro authored Sep 5, 2023
1 parent e143083 commit 616512f
Show file tree
Hide file tree
Showing 6 changed files with 41 additions and 66 deletions.
10 changes: 7 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,15 @@ When building queries, adapt your classes into dicts or lists prior to using the
Client Configuration
--------------------

Retry Policy
Max Attempts
------------
A retry policy can be set on the client. By default, this is configured with `max_attempts` of 3, inclusive of the initial call, and `max_backoff` of 20 seconds. The retry strategy implemented is a simple exponential backoff and will retry on 429s and 502s.
The maximum number of times a query will be attempted if a retryable exception is thrown (ThrottlingError). Default 3, inclusive of the initial call. The retry strategy implemented is a simple exponential backoff.

To disable retries, pass a RetryPolicy with max_attempts set to 1.
To disable retries, pass max_attempts less than or equal to 1.

Max Backoff
------------
The maximum backoff in seconds to be observed between each retry. Default 20 seconds.

Timeouts
--------
Expand Down
27 changes: 14 additions & 13 deletions fauna/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from typing import Any, Dict, Iterator, Mapping, Optional, List

import fauna
from fauna.client.retryable import RetryPolicy, Retryable
from fauna.client.retryable import Retryable
from fauna.errors import AuthenticationError, ClientError, ProtocolError, ServiceError, AuthorizationError, \
ServiceInternalError, ServiceTimeoutError, ThrottlingError, QueryTimeoutError, QueryRuntimeError, \
QueryCheckError, ContendedTransactionError, AbortError, InvalidRequestError, RetryableNetworkError
QueryCheckError, ContendedTransactionError, AbortError, InvalidRequestError
from fauna.client.headers import _DriverEnvironment, _Header, _Auth, Header
from fauna.http.http_client import HTTPClient
from fauna.query import Query, Page, fql
Expand Down Expand Up @@ -67,7 +67,8 @@ def __init__(
http_connect_timeout: Optional[timedelta] = DefaultHttpConnectTimeout,
http_pool_timeout: Optional[timedelta] = DefaultHttpPoolTimeout,
http_idle_timeout: Optional[timedelta] = DefaultIdleConnectionTimeout,
retry_policy: RetryPolicy = RetryPolicy(),
max_attempts: int = 3,
max_backoff: int = 20,
):
"""Initializes a Client.
Expand All @@ -86,11 +87,13 @@ def __init__(
:param http_connect_timeout: Set HTTP Connect timeout, default is :py:data:`DefaultHttpConnectTimeout`.
:param http_pool_timeout: Set HTTP Pool timeout, default is :py:data:`DefaultHttpPoolTimeout`.
:param http_idle_timeout: Set HTTP Idle timeout, default is :py:data:`DefaultIdleConnectionTimeout`.
:param retry_policy: A retry policy. The default policy sets max_attempts to 3 and max_backoff to 20.
:param max_attempts: The maximum number of times to attempt a query when a retryable exception is thrown. Defaults to 3.
:param max_backoff: The maximum backoff in seconds for an individual retry. Defaults to 20.
"""

self._set_endpoint(endpoint)
self._retry_policy = retry_policy
self._max_attempts = max_attempts
self._max_backoff = max_backoff

if secret is None:
self._auth = _Auth(_Environment.EnvFaunaSecret())
Expand Down Expand Up @@ -247,9 +250,8 @@ def query(
opts: Optional[QueryOptions] = None,
) -> QuerySuccess:
"""
Run a query on Fauna. A query will be retried with exponential backoff
up to the max_attempts set in the client's retry policy in the event
of a 429 or 502.
Run a query on Fauna. A query will be retried max_attempt times with exponential backoff
up to the max_backoff in the event of a 429.
:param fql: A Query
:param opts: (Optional) Query Options
Expand All @@ -274,11 +276,13 @@ def query(
raise ClientError("Failed to encode Query") from e

retryable = Retryable(
self._retry_policy,
self._max_attempts,
self._max_backoff,
self._query,
"/query/1",
fql=encoded_query,
opts=opts)
opts=opts,
)

r = retryable.run()
r.response.stats.attempts = r.attempts
Expand Down Expand Up @@ -338,9 +342,6 @@ def _query(
data=data,
) as response:
status_code = response.status_code()
if status_code == 502:
raise RetryableNetworkError(502, response.text())

response_json = response.json()
headers = response.headers()

Expand Down
16 changes: 4 additions & 12 deletions fauna/client/retryable.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,6 @@
from fauna.errors import RetryableFaunaException, ClientError


@dataclass
class RetryPolicy:
max_attempts: int = 3
"""An int. The maximum number of attempts."""

max_backoff: int = 20
"""An int. The maximum backoff in seconds."""


class RetryStrategy:

@abc.abstractmethod
Expand Down Expand Up @@ -52,13 +43,14 @@ class Retryable:

def __init__(
self,
policy: RetryPolicy,
max_attempts: int,
max_backoff: int,
func: Callable[..., QuerySuccess],
*args,
**kwargs,
):
self._max_attempts = policy.max_attempts
self._strategy = ExponentialBackoffStrategy(policy.max_backoff)
self._max_attempts = max_attempts
self._strategy = ExponentialBackoffStrategy(max_backoff)
self._func = func
self._args = args
self._kwargs = kwargs
Expand Down
2 changes: 1 addition & 1 deletion fauna/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
from .errors import ProtocolError, ServiceError
from .errors import AuthenticationError, AuthorizationError, QueryCheckError, QueryRuntimeError, \
QueryTimeoutError, ServiceInternalError, ServiceTimeoutError, ThrottlingError, ContendedTransactionError, \
InvalidRequestError, AbortError, RetryableFaunaException, RetryableNetworkError
InvalidRequestError, AbortError, RetryableFaunaException
18 changes: 0 additions & 18 deletions fauna/errors/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,6 @@ class NetworkError(FaunaException):
pass


class RetryableNetworkError(RetryableFaunaException):

@property
def status_code(self) -> int:
return self._status_code

@property
def message(self) -> str:
return self._message

def __init__(self, status_code: int, message: str):
self._status_code = status_code
self._message = message

def __str__(self):
return f"{self.status_code}: {self.message}"


class ProtocolError(FaunaException):
"""An error representing a HTTP failure - but one not directly emitted by Fauna."""

Expand Down
34 changes: 15 additions & 19 deletions tests/unit/test_retryable.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import pytest
from typing import List, Optional

from fauna.client.retryable import Retryable, RetryPolicy, ExponentialBackoffStrategy
from fauna.client.retryable import Retryable, ExponentialBackoffStrategy
from fauna.encoding import QuerySuccess, QueryStats
from fauna.errors import ThrottlingError, ServiceError, RetryableNetworkError
from fauna.errors import ThrottlingError, ServiceError


class Tester:
Expand All @@ -22,53 +22,49 @@ def f(self, _=""):
return QuerySuccess({}, None, None, QueryStats({}), None, None, None, None)


max_attempts = 3
max_backoff = 20


def test_retryable_no_retry():
tester = Tester([None])
policy = RetryPolicy()
retryable = Retryable(policy, tester.f)
retryable = Retryable(max_attempts, max_backoff, tester.f)
r = retryable.run()
assert r.attempts == 1


def test_retryable_throws_on_non_throttling_error():
tester = Tester([ServiceError(400, "oops", "not"), None])
policy = RetryPolicy()
retryable = Retryable(policy, tester.f)
retryable = Retryable(max_attempts, max_backoff, tester.f)
with pytest.raises(ServiceError):
retryable.run()


def test_retryable_retries_on_throttling_error():
tester = Tester([ThrottlingError(429, "oops", "throttled"), None])
policy = RetryPolicy()
retryable = Retryable(policy, tester.f)
r = retryable.run()
assert r.attempts == 2


def test_retryable_retries_on_502():
tester = Tester([RetryableNetworkError(502, "bad gateway"), None])
policy = RetryPolicy()
retryable = Retryable(policy, tester.f)
retryable = Retryable(max_attempts, max_backoff, tester.f)
r = retryable.run()
assert r.attempts == 2


def test_retryable_throws_when_exceeding_max_attempts():
err = ThrottlingError(429, "oops", "throttled")
tester = Tester([err, err, err, err])
policy = RetryPolicy()
retryable = Retryable(policy, tester.f)
retryable = Retryable(max_attempts, max_backoff, tester.f)
with pytest.raises(ThrottlingError):
retryable.run()


def test_strategy_backs_off():
strat = ExponentialBackoffStrategy(max_backoff=20)
strat = ExponentialBackoffStrategy(5)
b1 = strat.wait()
b2 = strat.wait()
b3 = strat.wait()
b4 = strat.wait()
b5 = strat.wait()

assert 0.0 <= b1 <= 1.0
assert 0.0 <= b2 <= 2.0
assert 0.0 <= b3 <= 4.0
assert 0.0 <= b4 <= 5.0
assert 0.0 <= b5 <= 5.0

0 comments on commit 616512f

Please sign in to comment.