Skip to content

Commit 3f916ba

Browse files
committed
[WIP] Proof of concept for fallback networks. Not tested or working with Async. Not perfect with sync.
1 parent 6b8a88a commit 3f916ba

File tree

2 files changed

+195
-4
lines changed

2 files changed

+195
-4
lines changed

bittensor/core/subtensor.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
SS58_FORMAT,
8282
TYPE_REGISTRY,
8383
)
84-
from bittensor.core.types import ParamWithTypes, SubtensorMixin
84+
from bittensor.core.types import ParamWithTypes, SubtensorMixin, RetrySubstrate
8585
from bittensor.utils import (
8686
Certificate,
8787
decode_hex_identity_dict,
@@ -92,6 +92,7 @@
9292
u16_normalized_float,
9393
u64_normalized_float,
9494
unlock_key,
95+
determine_chain_endpoint_and_network,
9596
)
9697
from bittensor.utils.balance import (
9798
Balance,
@@ -117,6 +118,8 @@ def __init__(
117118
config: Optional["Config"] = None,
118119
_mock: bool = False,
119120
log_verbose: bool = False,
121+
fallback_chains: Optional[list[str]] = None,
122+
retry_forever: bool = False,
120123
):
121124
"""
122125
Initializes an instance of the Subtensor class.
@@ -126,10 +129,13 @@ def __init__(
126129
config (Optional[Config]): Configuration object for the AsyncSubtensor instance.
127130
_mock: Whether this is a mock instance. Mainly just for use in testing.
128131
log_verbose (bool): Enables or disables verbose logging.
132+
fallback_chains: list of chain urls to try if the initial one fails
133+
retry_forever: whether to continuously try the chains indefinitely if timeout failure
129134
130135
Raises:
131136
Any exceptions raised during the setup, configuration, or connection process.
132137
"""
138+
fallback_chains_ = fallback_chains or []
133139
if config is None:
134140
config = self.config()
135141
self._config = copy.deepcopy(config)
@@ -143,13 +149,19 @@ def __init__(
143149
f"Connecting to network: [blue]{self.network}[/blue], "
144150
f"chain_endpoint: [blue]{self.chain_endpoint}[/blue]> ..."
145151
)
146-
self.substrate = SubstrateInterface(
147-
url=self.chain_endpoint,
152+
fallback_chain_urls = [
153+
determine_chain_endpoint_and_network(x)[1] for x in fallback_chains_
154+
]
155+
self.substrate = RetrySubstrate(
156+
substrate=SubstrateInterface,
157+
main_url=self.chain_endpoint,
148158
ss58_format=SS58_FORMAT,
149159
type_registry=TYPE_REGISTRY,
150160
use_remote_preset=True,
151161
chain_name="Bittensor",
152162
_mock=_mock,
163+
fallback_chains=fallback_chain_urls,
164+
retry_forever=retry_forever,
153165
)
154166
if self.log_verbose:
155167
logging.info(

bittensor/core/types.py

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
from abc import ABC
22
import argparse
3-
from typing import TypedDict, Optional
3+
from functools import partial
4+
from itertools import cycle
5+
from typing import TypedDict, Optional, Union, Type
6+
7+
from async_substrate_interface.sync_substrate import SubstrateInterface
8+
from async_substrate_interface.async_substrate import AsyncSubstrateInterface
9+
from async_substrate_interface.errors import MaxRetriesExceeded
410

511
from bittensor.utils import networking, Certificate
612
from bittensor.utils.btlogging import logging
@@ -9,6 +15,179 @@
915
from bittensor.core.chain_data import NeuronInfo, NeuronInfoLite
1016
from bittensor.utils import determine_chain_endpoint_and_network
1117

18+
SubstrateClass = Type[Union[SubstrateInterface, AsyncSubstrateInterface]]
19+
20+
21+
class RetrySubstrate:
22+
def __init__(
23+
self,
24+
substrate: SubstrateClass,
25+
main_url: str,
26+
ss58_format: int,
27+
type_registry: dict,
28+
use_remote_preset: bool,
29+
chain_name: str,
30+
_mock: bool,
31+
fallback_chains: Optional[list[str]] = None,
32+
retry_forever: bool = False,
33+
):
34+
self._substrate_class: SubstrateClass = substrate
35+
self.ss58_format: int = ss58_format
36+
self.type_registry: dict = type_registry
37+
self.use_remote_preset: bool = use_remote_preset
38+
self.chain_name: str = chain_name
39+
self._mock = _mock
40+
self.fallback_chains = (
41+
iter(fallback_chains)
42+
if not retry_forever
43+
else cycle(fallback_chains + [main_url])
44+
)
45+
initialized = False
46+
for chain_url in [main_url] + fallback_chains:
47+
try:
48+
self._substrate = self._substrate_class(
49+
url=chain_url,
50+
ss58_format=ss58_format,
51+
type_registry=type_registry,
52+
use_remote_preset=use_remote_preset,
53+
chain_name=chain_name,
54+
_mock=_mock,
55+
)
56+
initialized = True
57+
break
58+
except ConnectionError:
59+
continue
60+
if not initialized:
61+
raise ConnectionError(
62+
f"Unable to connect at any chains specified: {[main_url]+fallback_chains}"
63+
)
64+
65+
# retries
66+
67+
# TODO: properties that need retry logic
68+
# properties
69+
# version
70+
# token_decimals
71+
# token_symbol
72+
# name
73+
74+
self._get_block_handler = partial(self._retry, "_get_block_handler")
75+
self.apply_type_registry_presets = partial(
76+
self._retry, "apply_type_registry_presets"
77+
)
78+
self.close = partial(self._retry, "close")
79+
self.compose_call = partial(self._retry, "compose_call")
80+
self.connect = partial(self._retry, "connect")
81+
self.create_scale_object = partial(self._retry, "create_scale_object")
82+
self.create_signed_extrinsic = partial(self._retry, "create_signed_extrinsic")
83+
self.create_storage_key = partial(self._retry, "create_storage_key")
84+
self.decode_scale = partial(self._retry, "decode_scale")
85+
self.encode_scale = partial(self._retry, "encode_scale")
86+
self.extension_call = partial(self._retry, "extension_call")
87+
self.filter_events = partial(self._retry, "filter_events")
88+
self.filter_extrinsics = partial(self._retry, "filter_extrinsics")
89+
self.generate_signature_payload = partial(
90+
self._retry, "generate_signature_payload"
91+
)
92+
self.get_account_next_index = partial(self._retry, "get_account_next_index")
93+
self.get_account_nonce = partial(self._retry, "get_account_nonce")
94+
self.get_block = partial(self._retry, "get_block")
95+
self.get_block_hash = partial(self._retry, "get_block_hash")
96+
self.get_block_header = partial(self._retry, "get_block_header")
97+
self.get_block_metadata = partial(self._retry, "get_block_metadata")
98+
self.get_block_number = partial(self._retry, "get_block_number")
99+
self.get_block_runtime_info = partial(self._retry, "get_block_runtime_info")
100+
self.get_block_runtime_version_for = partial(
101+
self._retry, "get_block_runtime_version_for"
102+
)
103+
self.get_block_timestamp = partial(self._retry, "get_block_timestamp")
104+
self.get_chain_finalised_head = partial(self._retry, "get_chain_finalised_head")
105+
self.get_chain_head = partial(self._retry, "get_chain_head")
106+
self.get_constant = partial(self._retry, "get_constant")
107+
self.get_events = partial(self._retry, "get_events")
108+
self.get_extrinsics = partial(self._retry, "get_extrinsics")
109+
self.get_metadata_call_function = partial(
110+
self._retry, "get_metadata_call_function"
111+
)
112+
self.get_metadata_constant = partial(self._retry, "get_metadata_constant")
113+
self.get_metadata_error = partial(self._retry, "get_metadata_error")
114+
self.get_metadata_errors = partial(self._retry, "get_metadata_errors")
115+
self.get_metadata_module = partial(self._retry, "get_metadata_module")
116+
self.get_metadata_modules = partial(self._retry, "get_metadata_modules")
117+
self.get_metadata_runtime_call_function = partial(
118+
self._retry, "get_metadata_runtime_call_function"
119+
)
120+
self.get_metadata_runtime_call_functions = partial(
121+
self._retry, "get_metadata_runtime_call_functions"
122+
)
123+
self.get_metadata_storage_function = partial(
124+
self._retry, "get_metadata_storage_function"
125+
)
126+
self.get_metadata_storage_functions = partial(
127+
self._retry, "get_metadata_storage_functions"
128+
)
129+
self.get_parent_block_hash = partial(self._retry, "get_parent_block_hash")
130+
self.get_payment_info = partial(self._retry, "get_payment_info")
131+
self.get_storage_item = partial(self._retry, "get_storage_item")
132+
self.get_type_definition = partial(self._retry, "get_type_definition")
133+
self.get_type_registry = partial(self._retry, "get_type_registry")
134+
self.init_runtime = partial(self._retry, "init_runtime")
135+
self.initialize = partial(self._retry, "initialize")
136+
self.is_valid_ss58_address = partial(self._retry, "is_valid_ss58_address")
137+
self.load_runtime = partial(self._retry, "load_runtime")
138+
self.make_payload = partial(self._retry, "make_payload")
139+
self.query = partial(self._retry, "query")
140+
self.query_map = partial(self._retry, "query_map")
141+
self.query_multi = partial(self._retry, "query_multi")
142+
self.query_multiple = partial(self._retry, "query_multiple")
143+
self.reload_type_registry = partial(self._retry, "reload_type_registry")
144+
self.retrieve_extrinsic_by_hash = partial(
145+
self._retry, "retrieve_extrinsic_by_hash"
146+
)
147+
self.retrieve_extrinsic_by_identifier = partial(
148+
self._retry, "retrieve_extrinsic_by_identifier"
149+
)
150+
self.rpc_request = partial(self._retry, "rpc_request")
151+
self.runtime_call = partial(self._retry, "runtime_call")
152+
self.search_block_number = partial(self._retry, "search_block_number")
153+
self.serialize_constant = partial(self._retry, "serialize_constant")
154+
self.serialize_module_call = partial(self._retry, "serialize_module_call")
155+
self.serialize_module_error = partial(self._retry, "serialize_module_error")
156+
self.serialize_module_event = partial(self._retry, "serialize_module_event")
157+
self.serialize_storage_item = partial(self._retry, "serialize_storage_item")
158+
self.ss58_decode = partial(self._retry, "ss58_decode")
159+
self.ss58_encode = partial(self._retry, "ss58_encode")
160+
self.submit_extrinsic = partial(self._retry, "submit_extrinsic")
161+
self.subscribe_block_headers = partial(self._retry, "subscribe_block_headers")
162+
self.supports_rpc_method = partial(self._retry, "supports_rpc_method")
163+
self.ws = self._substrate.ws
164+
165+
def _retry(self, method, *args, **kwargs):
166+
try:
167+
method_ = getattr(self._substrate, method)
168+
return method_(*args, **kwargs)
169+
except MaxRetriesExceeded:
170+
try:
171+
next_network = next(self.fallback_chains)
172+
logging.error(
173+
f"Max retries exceeded with {self._substrate.url}. Retrying with {next_network}."
174+
)
175+
self._substrate = self._substrate_class(
176+
url=next_network,
177+
ss58_format=self.ss58_format,
178+
type_registry=self.type_registry,
179+
use_remote_preset=self.use_remote_preset,
180+
chain_name=self.chain_name,
181+
_mock=self._mock,
182+
)
183+
method_ = getattr(self._substrate, method)
184+
return self._retry(method_(*args, **kwargs))
185+
except StopIteration:
186+
logging.error(
187+
f"Max retries exceeded with {self._substrate.url}. No more fallback chains."
188+
)
189+
raise MaxRetriesExceeded
190+
12191

13192
class SubtensorMixin(ABC):
14193
network: str

0 commit comments

Comments
 (0)