web3 7.0.0b4__py3-none-any.whl → 7.0.0b6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- web3/_utils/batching.py +217 -0
- web3/_utils/caching.py +26 -2
- web3/_utils/compat/__init__.py +1 -0
- web3/_utils/contracts.py +5 -5
- web3/_utils/events.py +20 -20
- web3/_utils/filters.py +6 -6
- web3/_utils/method_formatters.py +0 -23
- web3/_utils/module_testing/__init__.py +0 -3
- web3/_utils/module_testing/eth_module.py +442 -373
- web3/_utils/module_testing/module_testing_utils.py +13 -0
- web3/_utils/module_testing/web3_module.py +438 -17
- web3/_utils/rpc_abi.py +0 -18
- web3/contract/async_contract.py +11 -11
- web3/contract/base_contract.py +19 -18
- web3/contract/contract.py +13 -13
- web3/contract/utils.py +112 -4
- web3/eth/async_eth.py +10 -8
- web3/eth/eth.py +7 -6
- web3/exceptions.py +75 -21
- web3/gas_strategies/time_based.py +2 -2
- web3/geth.py +0 -188
- web3/main.py +21 -13
- web3/manager.py +237 -74
- web3/method.py +29 -9
- web3/middleware/base.py +43 -0
- web3/middleware/filter.py +18 -6
- web3/middleware/signing.py +2 -2
- web3/module.py +47 -7
- web3/providers/async_base.py +55 -23
- web3/providers/base.py +59 -26
- web3/providers/eth_tester/defaults.py +0 -48
- web3/providers/eth_tester/main.py +36 -11
- web3/providers/eth_tester/middleware.py +3 -8
- web3/providers/ipc.py +23 -8
- web3/providers/legacy_websocket.py +26 -1
- web3/providers/persistent/async_ipc.py +60 -76
- web3/providers/persistent/persistent.py +134 -10
- web3/providers/persistent/request_processor.py +98 -14
- web3/providers/persistent/websocket.py +43 -66
- web3/providers/rpc/async_rpc.py +20 -2
- web3/providers/rpc/rpc.py +22 -2
- web3/providers/rpc/utils.py +1 -10
- web3/tools/benchmark/node.py +2 -8
- web3/types.py +8 -2
- {web3-7.0.0b4.dist-info → web3-7.0.0b6.dist-info}/LICENSE +1 -1
- {web3-7.0.0b4.dist-info → web3-7.0.0b6.dist-info}/METADATA +32 -21
- {web3-7.0.0b4.dist-info → web3-7.0.0b6.dist-info}/RECORD +49 -49
- web3/_utils/module_testing/go_ethereum_personal_module.py +0 -300
- {web3-7.0.0b4.dist-info → web3-7.0.0b6.dist-info}/WHEEL +0 -0
- {web3-7.0.0b4.dist-info → web3-7.0.0b6.dist-info}/top_level.txt +0 -0
|
@@ -10,9 +10,12 @@ from types import (
|
|
|
10
10
|
)
|
|
11
11
|
from typing import (
|
|
12
12
|
Any,
|
|
13
|
+
List,
|
|
13
14
|
Optional,
|
|
15
|
+
Tuple,
|
|
14
16
|
Type,
|
|
15
17
|
Union,
|
|
18
|
+
cast,
|
|
16
19
|
)
|
|
17
20
|
|
|
18
21
|
from eth_typing import (
|
|
@@ -25,6 +28,12 @@ from websockets.legacy.client import (
|
|
|
25
28
|
WebSocketClientProtocol,
|
|
26
29
|
)
|
|
27
30
|
|
|
31
|
+
from web3._utils.batching import (
|
|
32
|
+
sort_batch_response_by_response_ids,
|
|
33
|
+
)
|
|
34
|
+
from web3._utils.caching import (
|
|
35
|
+
handle_request_caching,
|
|
36
|
+
)
|
|
28
37
|
from web3.exceptions import (
|
|
29
38
|
Web3ValidationError,
|
|
30
39
|
)
|
|
@@ -91,6 +100,7 @@ class LegacyWebSocketProvider(JSONBaseProvider):
|
|
|
91
100
|
endpoint_uri: Optional[Union[URI, str]] = None,
|
|
92
101
|
websocket_kwargs: Optional[Any] = None,
|
|
93
102
|
websocket_timeout: int = DEFAULT_WEBSOCKET_TIMEOUT,
|
|
103
|
+
**kwargs: Any,
|
|
94
104
|
) -> None:
|
|
95
105
|
self.endpoint_uri = URI(endpoint_uri)
|
|
96
106
|
self.websocket_timeout = websocket_timeout
|
|
@@ -110,7 +120,7 @@ class LegacyWebSocketProvider(JSONBaseProvider):
|
|
|
110
120
|
f"in websocket_kwargs, found: {found_restricted_keys}"
|
|
111
121
|
)
|
|
112
122
|
self.conn = PersistentWebSocket(self.endpoint_uri, websocket_kwargs)
|
|
113
|
-
super().__init__()
|
|
123
|
+
super().__init__(**kwargs)
|
|
114
124
|
|
|
115
125
|
def __str__(self) -> str:
|
|
116
126
|
return f"WS connection {self.endpoint_uri}"
|
|
@@ -124,6 +134,7 @@ class LegacyWebSocketProvider(JSONBaseProvider):
|
|
|
124
134
|
await asyncio.wait_for(conn.recv(), timeout=self.websocket_timeout)
|
|
125
135
|
)
|
|
126
136
|
|
|
137
|
+
@handle_request_caching
|
|
127
138
|
def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
|
|
128
139
|
self.logger.debug(
|
|
129
140
|
f"Making request WebSocket. URI: {self.endpoint_uri}, " f"Method: {method}"
|
|
@@ -133,3 +144,17 @@ class LegacyWebSocketProvider(JSONBaseProvider):
|
|
|
133
144
|
self.coro_make_request(request_data), LegacyWebSocketProvider._loop
|
|
134
145
|
)
|
|
135
146
|
return future.result()
|
|
147
|
+
|
|
148
|
+
def make_batch_request(
|
|
149
|
+
self, requests: List[Tuple[RPCEndpoint, Any]]
|
|
150
|
+
) -> List[RPCResponse]:
|
|
151
|
+
self.logger.debug(
|
|
152
|
+
f"Making batch request WebSocket. URI: {self.endpoint_uri}, "
|
|
153
|
+
f"Methods: {requests}"
|
|
154
|
+
)
|
|
155
|
+
request_data = self.encode_batch_rpc_request(requests)
|
|
156
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
157
|
+
self.coro_make_request(request_data), LegacyWebSocketProvider._loop
|
|
158
|
+
)
|
|
159
|
+
response = cast(List[RPCResponse], future.result())
|
|
160
|
+
return sort_batch_response_by_response_ids(response)
|
|
@@ -11,9 +11,11 @@ from pathlib import (
|
|
|
11
11
|
import sys
|
|
12
12
|
from typing import (
|
|
13
13
|
Any,
|
|
14
|
+
List,
|
|
14
15
|
Optional,
|
|
15
16
|
Tuple,
|
|
16
17
|
Union,
|
|
18
|
+
cast,
|
|
17
19
|
)
|
|
18
20
|
|
|
19
21
|
from eth_utils import (
|
|
@@ -28,6 +30,10 @@ from web3.types import (
|
|
|
28
30
|
from . import (
|
|
29
31
|
PersistentConnectionProvider,
|
|
30
32
|
)
|
|
33
|
+
from ..._utils.batching import (
|
|
34
|
+
BATCH_REQUEST_ID,
|
|
35
|
+
sort_batch_response_by_response_ids,
|
|
36
|
+
)
|
|
31
37
|
from ..._utils.caching import (
|
|
32
38
|
async_handle_request_caching,
|
|
33
39
|
)
|
|
@@ -59,11 +65,12 @@ class AsyncIPCProvider(PersistentConnectionProvider):
|
|
|
59
65
|
|
|
60
66
|
_reader: Optional[asyncio.StreamReader] = None
|
|
61
67
|
_writer: Optional[asyncio.StreamWriter] = None
|
|
68
|
+
_decoder: json.JSONDecoder = json.JSONDecoder()
|
|
69
|
+
_raw_message: str = ""
|
|
62
70
|
|
|
63
71
|
def __init__(
|
|
64
72
|
self,
|
|
65
73
|
ipc_path: Optional[Union[str, Path]] = None,
|
|
66
|
-
max_connection_retries: int = 5,
|
|
67
74
|
# `PersistentConnectionProvider` kwargs can be passed through
|
|
68
75
|
**kwargs: Any,
|
|
69
76
|
) -> None:
|
|
@@ -74,7 +81,6 @@ class AsyncIPCProvider(PersistentConnectionProvider):
|
|
|
74
81
|
else:
|
|
75
82
|
raise Web3TypeError("ipc_path must be of type string or pathlib.Path")
|
|
76
83
|
|
|
77
|
-
self._max_connection_retries = max_connection_retries
|
|
78
84
|
super().__init__(**kwargs)
|
|
79
85
|
|
|
80
86
|
def __str__(self) -> str:
|
|
@@ -99,48 +105,16 @@ class AsyncIPCProvider(PersistentConnectionProvider):
|
|
|
99
105
|
)
|
|
100
106
|
return False
|
|
101
107
|
|
|
102
|
-
async def
|
|
103
|
-
|
|
104
|
-
_backoff_rate_change = 1.75
|
|
105
|
-
_backoff_time = 1.75
|
|
106
|
-
|
|
107
|
-
while _connection_attempts != self._max_connection_retries:
|
|
108
|
-
try:
|
|
109
|
-
_connection_attempts += 1
|
|
110
|
-
self._reader, self._writer = await async_get_ipc_socket(self.ipc_path)
|
|
111
|
-
self._message_listener_task = asyncio.create_task(
|
|
112
|
-
self._message_listener()
|
|
113
|
-
)
|
|
114
|
-
break
|
|
115
|
-
except OSError as e:
|
|
116
|
-
if _connection_attempts == self._max_connection_retries:
|
|
117
|
-
raise ProviderConnectionError(
|
|
118
|
-
f"Could not connect to: {self.ipc_path}. "
|
|
119
|
-
f"Retries exceeded max of {self._max_connection_retries}."
|
|
120
|
-
) from e
|
|
121
|
-
self.logger.info(
|
|
122
|
-
f"Could not connect to: {self.ipc_path}. Retrying in "
|
|
123
|
-
f"{round(_backoff_time, 1)} seconds.",
|
|
124
|
-
exc_info=True,
|
|
125
|
-
)
|
|
126
|
-
await asyncio.sleep(_backoff_time)
|
|
127
|
-
_backoff_time *= _backoff_rate_change
|
|
108
|
+
async def _provider_specific_connect(self) -> None:
|
|
109
|
+
self._reader, self._writer = await async_get_ipc_socket(self.ipc_path)
|
|
128
110
|
|
|
129
|
-
async def
|
|
111
|
+
async def _provider_specific_disconnect(self) -> None:
|
|
130
112
|
if self._writer and not self._writer.is_closing():
|
|
131
113
|
self._writer.close()
|
|
132
114
|
await self._writer.wait_closed()
|
|
133
115
|
self._writer = None
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
try:
|
|
137
|
-
self._message_listener_task.cancel()
|
|
138
|
-
await self._message_listener_task
|
|
116
|
+
if self._reader:
|
|
139
117
|
self._reader = None
|
|
140
|
-
except (asyncio.CancelledError, StopAsyncIteration):
|
|
141
|
-
pass
|
|
142
|
-
|
|
143
|
-
self._request_processor.clear_caches()
|
|
144
118
|
|
|
145
119
|
async def _reset_socket(self) -> None:
|
|
146
120
|
self._writer.close()
|
|
@@ -149,13 +123,12 @@ class AsyncIPCProvider(PersistentConnectionProvider):
|
|
|
149
123
|
|
|
150
124
|
@async_handle_request_caching
|
|
151
125
|
async def make_request(self, method: RPCEndpoint, params: Any) -> RPCResponse:
|
|
152
|
-
request_data = self.encode_rpc_request(method, params)
|
|
153
|
-
|
|
154
126
|
if self._writer is None:
|
|
155
127
|
raise ProviderConnectionError(
|
|
156
128
|
"Connection to ipc socket has not been initiated for the provider."
|
|
157
129
|
)
|
|
158
130
|
|
|
131
|
+
request_data = self.encode_rpc_request(method, params)
|
|
159
132
|
try:
|
|
160
133
|
self._writer.write(request_data)
|
|
161
134
|
await self._writer.drain()
|
|
@@ -172,43 +145,54 @@ class AsyncIPCProvider(PersistentConnectionProvider):
|
|
|
172
145
|
|
|
173
146
|
return response
|
|
174
147
|
|
|
175
|
-
async def
|
|
176
|
-
self
|
|
177
|
-
|
|
178
|
-
|
|
148
|
+
async def make_batch_request(
|
|
149
|
+
self, requests: List[Tuple[RPCEndpoint, Any]]
|
|
150
|
+
) -> List[RPCResponse]:
|
|
151
|
+
if self._writer is None:
|
|
152
|
+
raise ProviderConnectionError(
|
|
153
|
+
"Connection to ipc socket has not been initiated for the provider."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
request_data = self.encode_batch_rpc_request(requests)
|
|
157
|
+
try:
|
|
158
|
+
self._writer.write(request_data)
|
|
159
|
+
await self._writer.drain()
|
|
160
|
+
except OSError as e:
|
|
161
|
+
# Broken pipe
|
|
162
|
+
if e.errno == errno.EPIPE:
|
|
163
|
+
# one extra attempt, then give up
|
|
164
|
+
await self._reset_socket()
|
|
165
|
+
self._writer.write(request_data)
|
|
166
|
+
await self._writer.drain()
|
|
167
|
+
|
|
168
|
+
response = cast(
|
|
169
|
+
List[RPCResponse], await self._get_response_for_request_id(BATCH_REQUEST_ID)
|
|
179
170
|
)
|
|
180
|
-
|
|
181
|
-
decoder = json.JSONDecoder()
|
|
171
|
+
return response
|
|
182
172
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
# back to the event loop to share the loop with other tasks.
|
|
186
|
-
await asyncio.sleep(0)
|
|
173
|
+
async def _provider_specific_message_listener(self) -> None:
|
|
174
|
+
self._raw_message += to_text(await self._reader.read(4096)).lstrip()
|
|
187
175
|
|
|
176
|
+
while self._raw_message:
|
|
188
177
|
try:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"Exception caught in listener, error logging and keeping listener "
|
|
211
|
-
f"background task alive.\n error={e}"
|
|
212
|
-
)
|
|
213
|
-
# if only error logging, reset the ``raw_message`` buffer and continue
|
|
214
|
-
raw_message = ""
|
|
178
|
+
response, pos = self._decoder.raw_decode(self._raw_message)
|
|
179
|
+
except JSONDecodeError:
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
if isinstance(response, list):
|
|
183
|
+
response = sort_batch_response_by_response_ids(response)
|
|
184
|
+
|
|
185
|
+
is_subscription = (
|
|
186
|
+
response.get("method") == "eth_subscription"
|
|
187
|
+
if not isinstance(response, list)
|
|
188
|
+
else False
|
|
189
|
+
)
|
|
190
|
+
await self._request_processor.cache_raw_response(
|
|
191
|
+
response, subscription=is_subscription
|
|
192
|
+
)
|
|
193
|
+
self._raw_message = self._raw_message[pos:].lstrip()
|
|
194
|
+
|
|
195
|
+
def _error_log_listener_task_exception(self, e: Exception) -> None:
|
|
196
|
+
super()._error_log_listener_task_exception(e)
|
|
197
|
+
# reset the raw message buffer on exception when error logging
|
|
198
|
+
self._raw_message = ""
|
|
@@ -4,13 +4,23 @@ from abc import (
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import logging
|
|
6
6
|
from typing import (
|
|
7
|
+
Any,
|
|
8
|
+
List,
|
|
7
9
|
Optional,
|
|
10
|
+
Union,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from websockets import (
|
|
14
|
+
ConnectionClosed,
|
|
15
|
+
WebSocketException,
|
|
8
16
|
)
|
|
9
17
|
|
|
10
18
|
from web3._utils.caching import (
|
|
11
19
|
generate_cache_key,
|
|
12
20
|
)
|
|
13
21
|
from web3.exceptions import (
|
|
22
|
+
ProviderConnectionError,
|
|
23
|
+
TaskNotRunning,
|
|
14
24
|
TimeExhausted,
|
|
15
25
|
)
|
|
16
26
|
from web3.providers.async_base import (
|
|
@@ -35,19 +45,24 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
|
|
|
35
45
|
_message_listener_task: Optional["asyncio.Task[None]"] = None
|
|
36
46
|
_listen_event: asyncio.Event = asyncio.Event()
|
|
37
47
|
|
|
48
|
+
_batch_request_counter: Optional[int] = None
|
|
49
|
+
|
|
38
50
|
def __init__(
|
|
39
51
|
self,
|
|
40
52
|
request_timeout: float = DEFAULT_PERSISTENT_CONNECTION_TIMEOUT,
|
|
41
53
|
subscription_response_queue_size: int = 500,
|
|
42
54
|
silence_listener_task_exceptions: bool = False,
|
|
55
|
+
max_connection_retries: int = 5,
|
|
56
|
+
**kwargs: Any,
|
|
43
57
|
) -> None:
|
|
44
|
-
super().__init__()
|
|
58
|
+
super().__init__(**kwargs)
|
|
45
59
|
self._request_processor = RequestProcessor(
|
|
46
60
|
self,
|
|
47
61
|
subscription_response_queue_size=subscription_response_queue_size,
|
|
48
62
|
)
|
|
49
63
|
self.request_timeout = request_timeout
|
|
50
64
|
self.silence_listener_task_exceptions = silence_listener_task_exceptions
|
|
65
|
+
self._max_connection_retries = max_connection_retries
|
|
51
66
|
|
|
52
67
|
def get_endpoint_uri_or_ipc_path(self) -> str:
|
|
53
68
|
if hasattr(self, "endpoint_uri"):
|
|
@@ -61,16 +76,124 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
|
|
|
61
76
|
)
|
|
62
77
|
|
|
63
78
|
async def connect(self) -> None:
|
|
64
|
-
|
|
79
|
+
_connection_attempts = 0
|
|
80
|
+
_backoff_rate_change = 1.75
|
|
81
|
+
_backoff_time = 1.75
|
|
82
|
+
|
|
83
|
+
while _connection_attempts != self._max_connection_retries:
|
|
84
|
+
try:
|
|
85
|
+
_connection_attempts += 1
|
|
86
|
+
self.logger.info(
|
|
87
|
+
f"Connecting to: {self.get_endpoint_uri_or_ipc_path()}"
|
|
88
|
+
)
|
|
89
|
+
await self._provider_specific_connect()
|
|
90
|
+
self._message_listener_task = asyncio.create_task(
|
|
91
|
+
self._message_listener()
|
|
92
|
+
)
|
|
93
|
+
self._message_listener_task.add_done_callback(
|
|
94
|
+
self._message_listener_callback
|
|
95
|
+
)
|
|
96
|
+
self.logger.info(
|
|
97
|
+
f"Successfully connected to: {self.get_endpoint_uri_or_ipc_path()}"
|
|
98
|
+
)
|
|
99
|
+
break
|
|
100
|
+
except (WebSocketException, OSError) as e:
|
|
101
|
+
if _connection_attempts == self._max_connection_retries:
|
|
102
|
+
raise ProviderConnectionError(
|
|
103
|
+
f"Could not connect to: {self.get_endpoint_uri_or_ipc_path()}. "
|
|
104
|
+
f"Retries exceeded max of {self._max_connection_retries}."
|
|
105
|
+
) from e
|
|
106
|
+
self.logger.info(
|
|
107
|
+
f"Could not connect to: {self.get_endpoint_uri_or_ipc_path()}. "
|
|
108
|
+
f"Retrying in {round(_backoff_time, 1)} seconds.",
|
|
109
|
+
exc_info=True,
|
|
110
|
+
)
|
|
111
|
+
await asyncio.sleep(_backoff_time)
|
|
112
|
+
_backoff_time *= _backoff_rate_change
|
|
65
113
|
|
|
66
114
|
async def disconnect(self) -> None:
|
|
115
|
+
try:
|
|
116
|
+
if self._message_listener_task:
|
|
117
|
+
self._message_listener_task.cancel()
|
|
118
|
+
await self._message_listener_task
|
|
119
|
+
except (asyncio.CancelledError, StopAsyncIteration, ConnectionClosed):
|
|
120
|
+
pass
|
|
121
|
+
finally:
|
|
122
|
+
self._message_listener_task = None
|
|
123
|
+
self.logger.info("Message listener background task successfully shut down.")
|
|
124
|
+
|
|
125
|
+
await self._provider_specific_disconnect()
|
|
126
|
+
self._request_processor.clear_caches()
|
|
127
|
+
self.logger.info(
|
|
128
|
+
f"Successfully disconnected from: {self.get_endpoint_uri_or_ipc_path()}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# -- private methods -- #
|
|
132
|
+
|
|
133
|
+
async def _provider_specific_connect(self) -> None:
|
|
67
134
|
raise NotImplementedError("Must be implemented by subclasses")
|
|
68
135
|
|
|
69
|
-
async def
|
|
136
|
+
async def _provider_specific_disconnect(self) -> None:
|
|
70
137
|
raise NotImplementedError("Must be implemented by subclasses")
|
|
71
138
|
|
|
139
|
+
async def _provider_specific_message_listener(self) -> None:
|
|
140
|
+
raise NotImplementedError("Must be implemented by subclasses")
|
|
141
|
+
|
|
142
|
+
def _message_listener_callback(
|
|
143
|
+
self, message_listener_task: "asyncio.Task[None]"
|
|
144
|
+
) -> None:
|
|
145
|
+
# Puts a `TaskNotRunning` in the queue to signal the end of the listener task
|
|
146
|
+
# to any running subscription streams that are awaiting a response.
|
|
147
|
+
self._request_processor._subscription_response_queue.put_nowait(
|
|
148
|
+
TaskNotRunning(message_listener_task)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def _message_listener(self) -> None:
|
|
152
|
+
self.logger.info(
|
|
153
|
+
f"{self.__class__.__qualname__} listener background task started. Storing "
|
|
154
|
+
"all messages in appropriate request processor queues / caches to be "
|
|
155
|
+
"processed."
|
|
156
|
+
)
|
|
157
|
+
while True:
|
|
158
|
+
# the use of sleep(0) seems to be the most efficient way to yield control
|
|
159
|
+
# back to the event loop to share the loop with other tasks.
|
|
160
|
+
await asyncio.sleep(0)
|
|
161
|
+
try:
|
|
162
|
+
await self._provider_specific_message_listener()
|
|
163
|
+
except Exception as e:
|
|
164
|
+
if not self.silence_listener_task_exceptions:
|
|
165
|
+
raise e
|
|
166
|
+
else:
|
|
167
|
+
self._error_log_listener_task_exception(e)
|
|
168
|
+
|
|
169
|
+
def _error_log_listener_task_exception(self, e: Exception) -> None:
|
|
170
|
+
"""
|
|
171
|
+
When silencing listener task exceptions, this method is used to log the
|
|
172
|
+
exception and keep the listener task alive. Override this method to fine-tune
|
|
173
|
+
error logging behavior for the implementation class.
|
|
174
|
+
"""
|
|
175
|
+
self.logger.error(
|
|
176
|
+
"Exception caught in listener, error logging and keeping "
|
|
177
|
+
"listener background task alive."
|
|
178
|
+
f"\n error={e.__class__.__name__}: {e}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _handle_listener_task_exceptions(self) -> None:
|
|
182
|
+
"""
|
|
183
|
+
Should be called every time a `PersistentConnectionProvider` is polling for
|
|
184
|
+
messages in the main loop. If the message listener task has completed and an
|
|
185
|
+
exception was recorded, raise the exception in the main loop.
|
|
186
|
+
"""
|
|
187
|
+
msg_listener_task = getattr(self, "_message_listener_task", None)
|
|
188
|
+
if (
|
|
189
|
+
msg_listener_task
|
|
190
|
+
and msg_listener_task.done()
|
|
191
|
+
and msg_listener_task.exception()
|
|
192
|
+
):
|
|
193
|
+
raise msg_listener_task.exception()
|
|
194
|
+
|
|
72
195
|
async def _get_response_for_request_id(
|
|
73
|
-
self, request_id: RPCId, timeout: Optional[float] = None
|
|
196
|
+
self, request_id: Union[RPCId, List[RPCId]], timeout: Optional[float] = None
|
|
74
197
|
) -> RPCResponse:
|
|
75
198
|
if timeout is None:
|
|
76
199
|
timeout = self.request_timeout
|
|
@@ -79,10 +202,9 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
|
|
|
79
202
|
request_cache_key = generate_cache_key(request_id)
|
|
80
203
|
|
|
81
204
|
while True:
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
await asyncio.sleep(0)
|
|
205
|
+
# check if an exception was recorded in the listener task and raise it
|
|
206
|
+
# in the main loop if so
|
|
207
|
+
self._handle_listener_task_exceptions()
|
|
86
208
|
|
|
87
209
|
if request_cache_key in self._request_processor._request_response_cache:
|
|
88
210
|
self.logger.debug(
|
|
@@ -92,11 +214,13 @@ class PersistentConnectionProvider(AsyncJSONBaseProvider, ABC):
|
|
|
92
214
|
cache_key=request_cache_key,
|
|
93
215
|
)
|
|
94
216
|
return popped_response
|
|
217
|
+
else:
|
|
218
|
+
await asyncio.sleep(0)
|
|
95
219
|
|
|
96
220
|
try:
|
|
97
221
|
# Add the request timeout around the while loop that checks the request
|
|
98
|
-
# cache
|
|
99
|
-
#
|
|
222
|
+
# cache. If the request is not in the cache within the request_timeout,
|
|
223
|
+
# raise ``TimeExhausted``.
|
|
100
224
|
return await asyncio.wait_for(_match_response_id_to_request_id(), timeout)
|
|
101
225
|
except asyncio.TimeoutError:
|
|
102
226
|
raise TimeExhausted(
|
|
@@ -2,20 +2,32 @@ import asyncio
|
|
|
2
2
|
from copy import (
|
|
3
3
|
copy,
|
|
4
4
|
)
|
|
5
|
+
import sys
|
|
5
6
|
from typing import (
|
|
6
7
|
TYPE_CHECKING,
|
|
7
8
|
Any,
|
|
8
9
|
Callable,
|
|
9
10
|
Dict,
|
|
11
|
+
Generic,
|
|
10
12
|
Optional,
|
|
11
13
|
Tuple,
|
|
14
|
+
TypeVar,
|
|
15
|
+
Union,
|
|
12
16
|
)
|
|
13
17
|
|
|
18
|
+
from eth_utils.toolz import (
|
|
19
|
+
compose,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from web3._utils.batching import (
|
|
23
|
+
BATCH_REQUEST_ID,
|
|
24
|
+
)
|
|
14
25
|
from web3._utils.caching import (
|
|
15
26
|
RequestInformation,
|
|
16
27
|
generate_cache_key,
|
|
17
28
|
)
|
|
18
29
|
from web3.exceptions import (
|
|
30
|
+
TaskNotRunning,
|
|
19
31
|
Web3ValueError,
|
|
20
32
|
)
|
|
21
33
|
from web3.types import (
|
|
@@ -31,6 +43,34 @@ if TYPE_CHECKING:
|
|
|
31
43
|
PersistentConnectionProvider,
|
|
32
44
|
)
|
|
33
45
|
|
|
46
|
+
T = TypeVar("T")
|
|
47
|
+
|
|
48
|
+
# TODO: This is an ugly hack for python 3.8. Remove this after we drop support for it
|
|
49
|
+
# and use `asyncio.Queue[T]` type directly in the `TaskReliantQueue` class.
|
|
50
|
+
if sys.version_info >= (3, 9):
|
|
51
|
+
|
|
52
|
+
class _TaskReliantQueue(asyncio.Queue[T], Generic[T]):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
else:
|
|
56
|
+
|
|
57
|
+
class _TaskReliantQueue(asyncio.Queue, Generic[T]): # type: ignore
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TaskReliantQueue(_TaskReliantQueue[T]):
|
|
62
|
+
"""
|
|
63
|
+
A queue that relies on a task to be running to process items in the queue.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
async def get(self) -> T:
|
|
67
|
+
item = await super().get()
|
|
68
|
+
if isinstance(item, Exception):
|
|
69
|
+
# if the item is an exception, raise it so the task can handle this case
|
|
70
|
+
# more gracefully
|
|
71
|
+
raise item
|
|
72
|
+
return item
|
|
73
|
+
|
|
34
74
|
|
|
35
75
|
class RequestProcessor:
|
|
36
76
|
_subscription_queue_synced_with_ws_stream: bool = False
|
|
@@ -44,9 +84,9 @@ class RequestProcessor:
|
|
|
44
84
|
|
|
45
85
|
self._request_information_cache: SimpleCache = SimpleCache(500)
|
|
46
86
|
self._request_response_cache: SimpleCache = SimpleCache(500)
|
|
47
|
-
self._subscription_response_queue:
|
|
48
|
-
|
|
49
|
-
)
|
|
87
|
+
self._subscription_response_queue: TaskReliantQueue[
|
|
88
|
+
Union[RPCResponse, TaskNotRunning]
|
|
89
|
+
] = TaskReliantQueue(maxsize=subscription_response_queue_size)
|
|
50
90
|
|
|
51
91
|
@property
|
|
52
92
|
def active_subscriptions(self) -> Dict[str, Any]:
|
|
@@ -62,7 +102,11 @@ class RequestProcessor:
|
|
|
62
102
|
self,
|
|
63
103
|
method: RPCEndpoint,
|
|
64
104
|
params: Any,
|
|
65
|
-
response_formatters: Tuple[
|
|
105
|
+
response_formatters: Tuple[
|
|
106
|
+
Union[Dict[str, Callable[..., Any]], Callable[..., Any]],
|
|
107
|
+
Callable[..., Any],
|
|
108
|
+
Callable[..., Any],
|
|
109
|
+
],
|
|
66
110
|
) -> Optional[str]:
|
|
67
111
|
cached_requests_key = generate_cache_key((method, params))
|
|
68
112
|
if cached_requests_key in self._provider._request_cache._data:
|
|
@@ -76,12 +120,17 @@ class RequestProcessor:
|
|
|
76
120
|
)
|
|
77
121
|
return None
|
|
78
122
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
123
|
+
if self._provider._is_batching:
|
|
124
|
+
# the _batch_request_counter is set when entering the context manager
|
|
125
|
+
current_request_id = self._provider._batch_request_counter
|
|
126
|
+
self._provider._batch_request_counter += 1
|
|
127
|
+
else:
|
|
128
|
+
# copy the request counter and find the next request id without incrementing
|
|
129
|
+
# since this is done when / if the request is successfully sent
|
|
130
|
+
current_request_id = next(copy(self._provider.request_counter))
|
|
131
|
+
cache_key = generate_cache_key(current_request_id)
|
|
83
132
|
|
|
84
|
-
self._bump_cache_if_key_present(cache_key,
|
|
133
|
+
self._bump_cache_if_key_present(cache_key, current_request_id)
|
|
85
134
|
|
|
86
135
|
request_info = RequestInformation(
|
|
87
136
|
method,
|
|
@@ -89,7 +138,7 @@ class RequestProcessor:
|
|
|
89
138
|
response_formatters,
|
|
90
139
|
)
|
|
91
140
|
self._provider.logger.debug(
|
|
92
|
-
f"Caching request info:\n request_id={
|
|
141
|
+
f"Caching request info:\n request_id={current_request_id},\n"
|
|
93
142
|
f" cache_key={cache_key},\n request_info={request_info.__dict__}"
|
|
94
143
|
)
|
|
95
144
|
self._request_information_cache.cache(
|
|
@@ -153,9 +202,8 @@ class RequestProcessor:
|
|
|
153
202
|
# i.e. subscription request information remains in the cache
|
|
154
203
|
self._request_information_cache.get_cache_entry(cache_key)
|
|
155
204
|
)
|
|
156
|
-
|
|
157
205
|
else:
|
|
158
|
-
# retrieve the request info from the cache using the
|
|
206
|
+
# retrieve the request info from the cache using the response id
|
|
159
207
|
cache_key = generate_cache_key(response["id"])
|
|
160
208
|
if response in self._provider._request_cache._data.values():
|
|
161
209
|
request_info = (
|
|
@@ -184,6 +232,33 @@ class RequestProcessor:
|
|
|
184
232
|
|
|
185
233
|
return request_info
|
|
186
234
|
|
|
235
|
+
def append_result_formatter_for_request(
|
|
236
|
+
self, request_id: int, result_formatter: Callable[..., Any]
|
|
237
|
+
) -> None:
|
|
238
|
+
cache_key = generate_cache_key(request_id)
|
|
239
|
+
cached_request_info_for_id: RequestInformation = (
|
|
240
|
+
self._request_information_cache.get_cache_entry(cache_key)
|
|
241
|
+
)
|
|
242
|
+
if cached_request_info_for_id is not None:
|
|
243
|
+
(
|
|
244
|
+
current_result_formatters,
|
|
245
|
+
error_formatters,
|
|
246
|
+
null_result_formatters,
|
|
247
|
+
) = cached_request_info_for_id.response_formatters
|
|
248
|
+
cached_request_info_for_id.response_formatters = (
|
|
249
|
+
compose(
|
|
250
|
+
result_formatter,
|
|
251
|
+
current_result_formatters,
|
|
252
|
+
),
|
|
253
|
+
error_formatters,
|
|
254
|
+
null_result_formatters,
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
self._provider.logger.debug(
|
|
258
|
+
f"No cached request info for response id `{request_id}`. Cannot "
|
|
259
|
+
f"append response formatter for response."
|
|
260
|
+
)
|
|
261
|
+
|
|
187
262
|
def append_middleware_response_processor(
|
|
188
263
|
self,
|
|
189
264
|
response: RPCResponse,
|
|
@@ -218,7 +293,7 @@ class RequestProcessor:
|
|
|
218
293
|
) -> None:
|
|
219
294
|
if subscription:
|
|
220
295
|
if self._subscription_response_queue.full():
|
|
221
|
-
self._provider.logger.
|
|
296
|
+
self._provider.logger.debug(
|
|
222
297
|
"Subscription queue is full. Waiting for provider to consume "
|
|
223
298
|
"messages before caching."
|
|
224
299
|
)
|
|
@@ -229,6 +304,15 @@ class RequestProcessor:
|
|
|
229
304
|
f"Caching subscription response:\n response={raw_response}"
|
|
230
305
|
)
|
|
231
306
|
await self._subscription_response_queue.put(raw_response)
|
|
307
|
+
elif isinstance(raw_response, list):
|
|
308
|
+
# Since only one batch should be in the cache at all times, we use a
|
|
309
|
+
# constant cache key for the batch response.
|
|
310
|
+
cache_key = generate_cache_key(BATCH_REQUEST_ID)
|
|
311
|
+
self._provider.logger.debug(
|
|
312
|
+
f"Caching batch response:\n cache_key={cache_key},\n"
|
|
313
|
+
f" response={raw_response}"
|
|
314
|
+
)
|
|
315
|
+
self._request_response_cache.cache(cache_key, raw_response)
|
|
232
316
|
else:
|
|
233
317
|
response_id = raw_response.get("id")
|
|
234
318
|
cache_key = generate_cache_key(response_id)
|
|
@@ -289,6 +373,6 @@ class RequestProcessor:
|
|
|
289
373
|
"""Clear the request processor caches."""
|
|
290
374
|
self._request_information_cache.clear()
|
|
291
375
|
self._request_response_cache.clear()
|
|
292
|
-
self._subscription_response_queue =
|
|
376
|
+
self._subscription_response_queue = TaskReliantQueue(
|
|
293
377
|
maxsize=self._subscription_response_queue.maxsize
|
|
294
378
|
)
|