-
Notifications
You must be signed in to change notification settings - Fork 6
Fallback chains #100
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
Merged
Merged
Fallback chains #100
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
d6aa47d
[WIP] moving retry to async-substrate-interface
thewhaleking 743f708
moved around some stuff
thewhaleking 5e3d6f5
moved around some stuff
thewhaleking be2e1ed
Ruff
thewhaleking b544fb9
Merge remote-tracking branch 'origin/staging' into feat/thewhaleking/…
thewhaleking d80c1e6
New direction
thewhaleking a64def5
Add properties as well.
thewhaleking d13d282
Merge remote-tracking branch 'origin/staging' into feat/thewhaleking/…
thewhaleking 951d558
Update.
thewhaleking 91bfe7a
Sync substrate retry working.
thewhaleking 3f50aaf
Async also working.
thewhaleking 767e122
[WIP] tests
thewhaleking d3f6473
improved test a bit
thewhaleking 1baab0f
Add `chain_endpoint` and `url` prior to super init.
thewhaleking c9a13ff
More tests.
thewhaleking File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,325 @@ | ||
""" | ||
A number of "plugins" for SubstrateInterface (and AsyncSubstrateInterface). At initial creation, it contains only | ||
Retry (sync and async versions). | ||
""" | ||
|
||
import asyncio | ||
import logging | ||
import socket | ||
from functools import partial | ||
from itertools import cycle | ||
from typing import Optional | ||
|
||
from websockets.exceptions import ConnectionClosed | ||
|
||
from async_substrate_interface.async_substrate import AsyncSubstrateInterface, Websocket | ||
from async_substrate_interface.errors import MaxRetriesExceeded | ||
from async_substrate_interface.sync_substrate import SubstrateInterface | ||
|
||
logger = logging.getLogger("async_substrate_interface") | ||
|
||
|
||
RETRY_METHODS = [ | ||
"_get_block_handler", | ||
"close", | ||
"compose_call", | ||
"create_scale_object", | ||
"create_signed_extrinsic", | ||
"create_storage_key", | ||
"decode_scale", | ||
"encode_scale", | ||
"generate_signature_payload", | ||
"get_account_next_index", | ||
"get_account_nonce", | ||
"get_block", | ||
"get_block_hash", | ||
"get_block_header", | ||
"get_block_metadata", | ||
"get_block_number", | ||
"get_block_runtime_info", | ||
"get_block_runtime_version_for", | ||
"get_chain_finalised_head", | ||
"get_chain_head", | ||
"get_constant", | ||
"get_events", | ||
"get_extrinsics", | ||
"get_metadata_call_function", | ||
"get_metadata_constant", | ||
"get_metadata_error", | ||
"get_metadata_errors", | ||
"get_metadata_module", | ||
"get_metadata_modules", | ||
"get_metadata_runtime_call_function", | ||
"get_metadata_runtime_call_functions", | ||
"get_metadata_storage_function", | ||
"get_metadata_storage_functions", | ||
"get_parent_block_hash", | ||
"get_payment_info", | ||
"get_storage_item", | ||
"get_type_definition", | ||
"get_type_registry", | ||
"init_runtime", | ||
"initialize", | ||
"query", | ||
"query_map", | ||
"query_multi", | ||
"query_multiple", | ||
"retrieve_extrinsic_by_identifier", | ||
"rpc_request", | ||
"runtime_call", | ||
"submit_extrinsic", | ||
"subscribe_block_headers", | ||
"supports_rpc_method", | ||
] | ||
|
||
RETRY_PROPS = ["properties", "version", "token_decimals", "token_symbol", "name"] | ||
|
||
|
||
class RetrySyncSubstrate(SubstrateInterface): | ||
""" | ||
A subclass of SubstrateInterface that allows for handling chain failures by using backup chains. If a sustained | ||
network failure is encountered on a chain endpoint, the object will initialize a new connection on the next chain in | ||
the `fallback_chains` list. If the `retry_forever` flag is set, upon reaching the last chain in `fallback_chains`, | ||
the connection will attempt to iterate over the list (starting with `url`) again. | ||
|
||
E.g. | ||
``` | ||
substrate = RetrySyncSubstrate( | ||
"wss://entrypoint-finney.opentensor.ai:443", | ||
fallback_chains=["ws://127.0.0.1:9946"] | ||
) | ||
``` | ||
In this case, if there is a failure on entrypoint-finney, the connection will next attempt to hit localhost. If this | ||
also fails, a `MaxRetriesExceeded` exception will be raised. | ||
|
||
``` | ||
substrate = RetrySyncSubstrate( | ||
"wss://entrypoint-finney.opentensor.ai:443", | ||
fallback_chains=["ws://127.0.0.1:9946"], | ||
retry_forever=True | ||
) | ||
``` | ||
In this case, rather than a MaxRetriesExceeded exception being raised upon failure of the second chain (localhost), | ||
the object will again being to initialize a new connection on entrypoint-finney, and then localhost, and so on and | ||
so forth. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
url: str, | ||
use_remote_preset: bool = False, | ||
fallback_chains: Optional[list[str]] = None, | ||
retry_forever: bool = False, | ||
ss58_format: Optional[int] = None, | ||
type_registry: Optional[dict] = None, | ||
type_registry_preset: Optional[str] = None, | ||
chain_name: str = "", | ||
max_retries: int = 5, | ||
retry_timeout: float = 60.0, | ||
_mock: bool = False, | ||
): | ||
fallback_chains = fallback_chains or [] | ||
self.fallback_chains = ( | ||
iter(fallback_chains) | ||
if not retry_forever | ||
else cycle(fallback_chains + [url]) | ||
) | ||
self.use_remote_preset = use_remote_preset | ||
self.chain_name = chain_name | ||
self._mock = _mock | ||
self.retry_timeout = retry_timeout | ||
self.max_retries = max_retries | ||
thewhaleking marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.chain_endpoint = url | ||
self.url = url | ||
initialized = False | ||
for chain_url in [url] + fallback_chains: | ||
try: | ||
thewhaleking marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.chain_endpoint = chain_url | ||
self.url = chain_url | ||
super().__init__( | ||
url=chain_url, | ||
ss58_format=ss58_format, | ||
type_registry=type_registry, | ||
use_remote_preset=use_remote_preset, | ||
type_registry_preset=type_registry_preset, | ||
chain_name=chain_name, | ||
_mock=_mock, | ||
retry_timeout=retry_timeout, | ||
max_retries=max_retries, | ||
) | ||
initialized = True | ||
logger.info(f"Connected to {chain_url}") | ||
break | ||
except ConnectionError: | ||
logger.warning(f"Unable to connect to {chain_url}") | ||
if not initialized: | ||
raise ConnectionError( | ||
f"Unable to connect at any chains specified: {[url] + fallback_chains}" | ||
) | ||
# "connect" is only used by SubstrateInterface, not AsyncSubstrateInterface | ||
retry_methods = ["connect"] + RETRY_METHODS | ||
self._original_methods = { | ||
method: getattr(self, method) for method in retry_methods | ||
} | ||
for method in retry_methods: | ||
setattr(self, method, partial(self._retry, method)) | ||
|
||
def _retry(self, method, *args, **kwargs): | ||
method_ = self._original_methods[method] | ||
try: | ||
return method_(*args, **kwargs) | ||
except ( | ||
MaxRetriesExceeded, | ||
ConnectionError, | ||
EOFError, | ||
ConnectionClosed, | ||
TimeoutError, | ||
) as e: | ||
try: | ||
self._reinstantiate_substrate(e) | ||
return method_(*args, **kwargs) | ||
except StopIteration: | ||
logger.error( | ||
f"Max retries exceeded with {self.url}. No more fallback chains." | ||
) | ||
raise MaxRetriesExceeded | ||
|
||
def _reinstantiate_substrate(self, e: Optional[Exception] = None) -> None: | ||
next_network = next(self.fallback_chains) | ||
self.ws.close() | ||
if e.__class__ == MaxRetriesExceeded: | ||
logger.error( | ||
f"Max retries exceeded with {self.url}. Retrying with {next_network}." | ||
) | ||
else: | ||
logger.error(f"Connection error. Trying again with {next_network}") | ||
self.url = next_network | ||
self.chain_endpoint = next_network | ||
self.initialized = False | ||
self.ws = self.connect(init=True) | ||
if not self._mock: | ||
self.initialize() | ||
|
||
|
||
class RetryAsyncSubstrate(AsyncSubstrateInterface): | ||
""" | ||
A subclass of AsyncSubstrateInterface that allows for handling chain failures by using backup chains. If a | ||
sustained network failure is encountered on a chain endpoint, the object will initialize a new connection on | ||
the next chain in the `fallback_chains` list. If the `retry_forever` flag is set, upon reaching the last chain | ||
in `fallback_chains`, the connection will attempt to iterate over the list (starting with `url`) again. | ||
|
||
E.g. | ||
``` | ||
substrate = RetryAsyncSubstrate( | ||
"wss://entrypoint-finney.opentensor.ai:443", | ||
fallback_chains=["ws://127.0.0.1:9946"] | ||
) | ||
``` | ||
In this case, if there is a failure on entrypoint-finney, the connection will next attempt to hit localhost. If this | ||
also fails, a `MaxRetriesExceeded` exception will be raised. | ||
|
||
``` | ||
substrate = RetryAsyncSubstrate( | ||
"wss://entrypoint-finney.opentensor.ai:443", | ||
fallback_chains=["ws://127.0.0.1:9946"], | ||
retry_forever=True | ||
) | ||
``` | ||
In this case, rather than a MaxRetriesExceeded exception being raised upon failure of the second chain (localhost), | ||
the object will again being to initialize a new connection on entrypoint-finney, and then localhost, and so on and | ||
so forth. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
url: str, | ||
use_remote_preset: bool = False, | ||
fallback_chains: Optional[list[str]] = None, | ||
retry_forever: bool = False, | ||
ss58_format: Optional[int] = None, | ||
type_registry: Optional[dict] = None, | ||
type_registry_preset: Optional[str] = None, | ||
chain_name: str = "", | ||
max_retries: int = 5, | ||
retry_timeout: float = 60.0, | ||
_mock: bool = False, | ||
): | ||
fallback_chains = fallback_chains or [] | ||
self.fallback_chains = ( | ||
iter(fallback_chains) | ||
if not retry_forever | ||
else cycle(fallback_chains + [url]) | ||
) | ||
self.use_remote_preset = use_remote_preset | ||
self.chain_name = chain_name | ||
self._mock = _mock | ||
self.retry_timeout = retry_timeout | ||
self.max_retries = max_retries | ||
super().__init__( | ||
url=url, | ||
ss58_format=ss58_format, | ||
type_registry=type_registry, | ||
use_remote_preset=use_remote_preset, | ||
type_registry_preset=type_registry_preset, | ||
chain_name=chain_name, | ||
_mock=_mock, | ||
retry_timeout=retry_timeout, | ||
max_retries=max_retries, | ||
) | ||
self._original_methods = { | ||
method: getattr(self, method) for method in RETRY_METHODS | ||
} | ||
for method in RETRY_METHODS: | ||
setattr(self, method, partial(self._retry, method)) | ||
|
||
async def _reinstantiate_substrate(self, e: Optional[Exception] = None) -> None: | ||
next_network = next(self.fallback_chains) | ||
if e.__class__ == MaxRetriesExceeded: | ||
logger.error( | ||
f"Max retries exceeded with {self.url}. Retrying with {next_network}." | ||
) | ||
else: | ||
logger.error(f"Connection error. Trying again with {next_network}") | ||
try: | ||
await self.ws.shutdown() | ||
except AttributeError: | ||
pass | ||
if self._forgettable_task is not None: | ||
self._forgettable_task: asyncio.Task | ||
self._forgettable_task.cancel() | ||
try: | ||
await self._forgettable_task | ||
except asyncio.CancelledError: | ||
pass | ||
self.chain_endpoint = next_network | ||
self.url = next_network | ||
self.ws = Websocket( | ||
next_network, | ||
options={ | ||
"max_size": self.ws_max_size, | ||
"write_limit": 2**16, | ||
}, | ||
) | ||
self._initialized = False | ||
self._initializing = False | ||
await self.initialize() | ||
|
||
async def _retry(self, method, *args, **kwargs): | ||
method_ = self._original_methods[method] | ||
try: | ||
return await method_(*args, **kwargs) | ||
except ( | ||
MaxRetriesExceeded, | ||
ConnectionError, | ||
ConnectionClosed, | ||
EOFError, | ||
socket.gaierror, | ||
) as e: | ||
try: | ||
await self._reinstantiate_substrate(e) | ||
return await method_(*args, **kwargs) | ||
except StopAsyncIteration: | ||
logger.error( | ||
f"Max retries exceeded with {self.url}. No more fallback chains." | ||
) | ||
raise MaxRetriesExceeded |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.