Skip to content

Commit 19f37fc

Browse files
committed
Add custom list and backoff factor to http_retry_middleware
- Internal terminology change `whitelist` -> `allow_list` - Add a custom allow_list kwarg to allow passing in a custom list of RPC endpoints for retrying; closes #958 - Add a backoff factor to the sync version of retry middleware; closes #1911
1 parent f55177b commit 19f37fc

File tree

2 files changed

+78
-14
lines changed

2 files changed

+78
-14
lines changed

tests/core/middleware/test_http_request_retry.py

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,27 @@ def test_check_with_all_middlewares(make_post_request_mock):
9999
assert make_post_request_mock.call_count == 5
100100

101101

102+
@patch("web3.providers.rpc.make_post_request", side_effect=ConnectionError)
103+
def test_exception_retry_middleware_with_allow_list_kwarg(
104+
make_post_request_mock, exception_retry_request_setup
105+
):
106+
w3 = Mock()
107+
provider = HTTPProvider()
108+
errors = (ConnectionError, HTTPError, Timeout, TooManyRedirects)
109+
setup = exception_retry_middleware(
110+
provider.make_request, w3, errors, 5, allow_list=["test_userProvidedMethod"]
111+
)
112+
setup.w3 = w3
113+
with pytest.raises(ConnectionError):
114+
setup("test_userProvidedMethod", [])
115+
assert make_post_request_mock.call_count == 5
116+
117+
make_post_request_mock.reset_mock()
118+
with pytest.raises(ConnectionError):
119+
setup("eth_getBalance", [])
120+
assert make_post_request_mock.call_count == 1
121+
122+
102123
# -- async -- #
103124

104125

@@ -132,27 +153,56 @@ async def async_exception_retry_request_setup():
132153
aiohttp.ClientOSError,
133154
),
134155
)
135-
async def test_check_retry_middleware(error, async_exception_retry_request_setup):
156+
async def test_async_check_retry_middleware(error, async_exception_retry_request_setup):
136157
with patch(
137158
"web3.providers.async_rpc.async_make_post_request"
138-
) as make_post_request_mock:
139-
make_post_request_mock.side_effect = error
159+
) as async_make_post_request_mock:
160+
async_make_post_request_mock.side_effect = error
140161

141162
with pytest.raises(error):
142163
await async_exception_retry_request_setup("eth_getBalance", [])
143-
assert make_post_request_mock.call_count == ASYNC_TEST_RETRY_COUNT
164+
assert async_make_post_request_mock.call_count == ASYNC_TEST_RETRY_COUNT
144165

145166

146167
@pytest.mark.asyncio
147-
async def test_check_without_retry_middleware():
168+
async def test_async_check_without_retry_middleware():
148169
with patch(
149170
"web3.providers.async_rpc.async_make_post_request"
150-
) as make_post_request_mock:
151-
make_post_request_mock.side_effect = TimeoutError
171+
) as async_make_post_request_mock:
172+
async_make_post_request_mock.side_effect = TimeoutError
152173
provider = AsyncHTTPProvider()
153174
w3 = AsyncWeb3(provider)
154175
w3.provider._middlewares = ()
155176

156177
with pytest.raises(TimeoutError):
157178
await w3.eth.block_number
158-
assert make_post_request_mock.call_count == 1
179+
assert async_make_post_request_mock.call_count == 1
180+
181+
182+
@pytest.mark.asyncio
183+
async def test_async_exception_retry_middleware_with_allow_list_kwarg():
184+
w3 = Mock()
185+
provider = AsyncHTTPProvider()
186+
setup = await async_exception_retry_middleware(
187+
provider.make_request,
188+
w3,
189+
(TimeoutError, aiohttp.ClientError),
190+
retries=ASYNC_TEST_RETRY_COUNT,
191+
backoff_factor=0.1,
192+
allow_list=["test_userProvidedMethod"],
193+
)
194+
setup.w3 = w3
195+
196+
with patch(
197+
"web3.providers.async_rpc.async_make_post_request"
198+
) as async_make_post_request_mock:
199+
async_make_post_request_mock.side_effect = TimeoutError
200+
201+
with pytest.raises(TimeoutError):
202+
await setup("test_userProvidedMethod", [])
203+
assert async_make_post_request_mock.call_count == ASYNC_TEST_RETRY_COUNT
204+
205+
async_make_post_request_mock.reset_mock()
206+
with pytest.raises(TimeoutError):
207+
await setup("eth_getBalance", [])
208+
assert async_make_post_request_mock.call_count == 1

web3/middleware/exception_retry_request.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import asyncio
2+
import time
23
from typing import (
34
TYPE_CHECKING,
45
Any,
56
Callable,
67
Collection,
8+
List,
79
Optional,
810
Type,
911
)
@@ -28,7 +30,7 @@
2830
Web3,
2931
)
3032

31-
whitelist = [
33+
DEFAULT_ALLOWLIST = [
3234
"admin",
3335
"miner",
3436
"net",
@@ -87,11 +89,16 @@
8789
]
8890

8991

90-
def check_if_retry_on_failure(method: RPCEndpoint) -> bool:
92+
def check_if_retry_on_failure(
93+
method: str, allow_list: Optional[List[str]] = None
94+
) -> bool:
95+
if allow_list is None:
96+
allow_list = DEFAULT_ALLOWLIST
97+
9198
root = method.split("_")[0]
92-
if root in whitelist:
99+
if root in allow_list:
93100
return True
94-
elif method in whitelist:
101+
elif method in allow_list:
95102
return True
96103
else:
97104
return False
@@ -102,19 +109,22 @@ def exception_retry_middleware(
102109
_w3: "Web3",
103110
errors: Collection[Type[BaseException]],
104111
retries: int = 5,
112+
backoff_factor: float = 0.3,
113+
allow_list: Optional[List[str]] = None,
105114
) -> Callable[[RPCEndpoint, Any], RPCResponse]:
106115
"""
107116
Creates middleware that retries failed HTTP requests. Is a default
108117
middleware for HTTPProvider.
109118
"""
110119

111120
def middleware(method: RPCEndpoint, params: Any) -> Optional[RPCResponse]:
112-
if check_if_retry_on_failure(method):
121+
if check_if_retry_on_failure(method, allow_list):
113122
for i in range(retries):
114123
try:
115124
return make_request(method, params)
116125
except tuple(errors):
117126
if i < retries - 1:
127+
time.sleep(backoff_factor)
118128
continue
119129
else:
120130
raise
@@ -133,20 +143,24 @@ def http_retry_request_middleware(
133143
)
134144

135145

146+
# -- async -- #
147+
148+
136149
async def async_exception_retry_middleware(
137150
make_request: Callable[[RPCEndpoint, Any], Any],
138151
_async_w3: "AsyncWeb3",
139152
errors: Collection[Type[BaseException]],
140153
retries: int = 5,
141154
backoff_factor: float = 0.3,
155+
allow_list: Optional[List[str]] = None,
142156
) -> AsyncMiddlewareCoroutine:
143157
"""
144158
Creates middleware that retries failed HTTP requests.
145159
Is a default middleware for AsyncHTTPProvider.
146160
"""
147161

148162
async def middleware(method: RPCEndpoint, params: Any) -> Optional[RPCResponse]:
149-
if check_if_retry_on_failure(method):
163+
if check_if_retry_on_failure(method, allow_list):
150164
for i in range(retries):
151165
try:
152166
return await make_request(method, params)

0 commit comments

Comments
 (0)