Skip to content

Commit daa9e7b

Browse files
authored
Fix submit_extrinsic timeout (#2497)
1 parent 7ca4d6d commit daa9e7b

File tree

3 files changed

+171
-55
lines changed

3 files changed

+171
-55
lines changed

bittensor/core/extrinsics/utils.py

Lines changed: 56 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Module with helper functions for extrinsics."""
22

3+
from concurrent.futures import ThreadPoolExecutor
4+
import os
35
import threading
46
from typing import TYPE_CHECKING
57

@@ -12,14 +14,15 @@
1214
from substrateinterface import SubstrateInterface, ExtrinsicReceipt
1315
from scalecodec.types import GenericExtrinsic
1416

17+
try:
18+
EXTRINSIC_SUBMISSION_TIMEOUT = float(os.getenv("EXTRINSIC_SUBMISSION_TIMEOUT", 200))
19+
except ValueError:
20+
raise ValueError(
21+
"EXTRINSIC_SUBMISSION_TIMEOUT environment variable must be a float."
22+
)
1523

16-
class _ThreadingTimeoutException(Exception):
17-
"""
18-
Exception raised for timeout. Different from TimeoutException because this also triggers
19-
a websocket failure. This exception should only be used with `threading` timer..
20-
"""
21-
22-
pass
24+
if EXTRINSIC_SUBMISSION_TIMEOUT < 0:
25+
raise ValueError("EXTRINSIC_SUBMISSION_TIMEOUT cannot be negative.")
2326

2427

2528
def submit_extrinsic(
@@ -50,55 +53,53 @@ def submit_extrinsic(
5053
extrinsic_hash = extrinsic.extrinsic_hash
5154
starting_block = substrate.get_block()
5255

53-
def _handler():
54-
"""
55-
Timeout handler for threading. Will raise a TimeoutError if timeout is exceeded.
56-
"""
57-
logging.error("Timed out waiting for extrinsic submission.")
58-
raise _ThreadingTimeoutException
59-
60-
# sets a timeout timer for the next call to 200 seconds
61-
# will raise a _ThreadingTimeoutException if it reaches this point
62-
timer = threading.Timer(200, _handler)
63-
64-
try:
65-
timer.start()
66-
response = substrate.submit_extrinsic(
67-
extrinsic,
68-
wait_for_inclusion=wait_for_inclusion,
69-
wait_for_finalization=wait_for_finalization,
70-
)
71-
except SubstrateRequestException as e:
72-
logging.error(format_error_message(e.args[0], substrate=substrate))
73-
# Re-rise the exception for retrying of the extrinsic call. If we remove the retry logic, the raise will need
74-
# to be removed.
75-
raise
76-
77-
except _ThreadingTimeoutException:
78-
after_timeout_block = substrate.get_block()
79-
56+
timeout = EXTRINSIC_SUBMISSION_TIMEOUT
57+
event = threading.Event()
58+
59+
def submit():
60+
try:
61+
response_ = substrate.submit_extrinsic(
62+
extrinsic,
63+
wait_for_inclusion=wait_for_inclusion,
64+
wait_for_finalization=wait_for_finalization,
65+
)
66+
except SubstrateRequestException as e:
67+
logging.error(format_error_message(e.args[0], substrate=substrate))
68+
# Re-raise the exception for retrying of the extrinsic call. If we remove the retry logic,
69+
# the raise will need to be removed.
70+
raise
71+
finally:
72+
event.set()
73+
return response_
74+
75+
with ThreadPoolExecutor(max_workers=1) as executor:
8076
response = None
81-
for block_num in range(
82-
starting_block["header"]["number"],
83-
after_timeout_block["header"]["number"] + 1,
84-
):
85-
block_hash = substrate.get_block_hash(block_num)
86-
try:
87-
response = substrate.retrieve_extrinsic_by_hash(
88-
block_hash, f"0x{extrinsic_hash.hex()}"
77+
future = executor.submit(submit)
78+
if not event.wait(timeout):
79+
logging.error("Timed out waiting for extrinsic submission.")
80+
after_timeout_block = substrate.get_block()
81+
82+
for block_num in range(
83+
starting_block["header"]["number"],
84+
after_timeout_block["header"]["number"] + 1,
85+
):
86+
block_hash = substrate.get_block_hash(block_num)
87+
try:
88+
response = substrate.retrieve_extrinsic_by_hash(
89+
block_hash, f"0x{extrinsic_hash.hex()}"
90+
)
91+
except ExtrinsicNotFound:
92+
continue
93+
if response:
94+
break
95+
if response is None:
96+
logging.error(
97+
f"Extrinsic '0x{extrinsic_hash.hex()}' not submitted. "
98+
f"Initially attempted to submit at block {starting_block['header']['number']}."
8999
)
90-
except ExtrinsicNotFound:
91-
continue
92-
if response:
93-
break
94-
finally:
95-
timer.cancel()
96-
97-
if response is None:
98-
logging.error(
99-
f"Extrinsic '0x{extrinsic_hash.hex()}' not submitted. "
100-
f"Initially attempted to submit at block {starting_block['header']['number']}."
101-
)
102-
raise SubstrateRequestException
100+
raise SubstrateRequestException
101+
102+
else:
103+
response = future.result()
103104

104105
return response

bittensor/utils/networking.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ def get_formatted_ws_endpoint_url(endpoint_url: Optional[str]) -> Optional[str]:
163163
def ensure_connected(func):
164164
"""Decorator ensuring the function executes with an active substrate connection."""
165165

166+
# TODO we need to rethink the logic in this
167+
166168
def is_connected(substrate) -> bool:
167169
"""Check if the substrate connection is active."""
168170
sock = substrate.websocket.socket
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import time
2+
from unittest.mock import MagicMock, patch
3+
import importlib
4+
import pytest
5+
from substrateinterface.base import (
6+
SubstrateInterface,
7+
GenericExtrinsic,
8+
SubstrateRequestException,
9+
)
10+
11+
from bittensor.core.extrinsics import utils
12+
13+
14+
@pytest.fixture
15+
def set_extrinsics_timeout_env(monkeypatch):
16+
monkeypatch.setenv("EXTRINSIC_SUBMISSION_TIMEOUT", "1")
17+
18+
19+
def test_submit_extrinsic_timeout():
20+
timeout = 1
21+
22+
def wait(extrinsic, wait_for_inclusion, wait_for_finalization):
23+
time.sleep(timeout + 0.01)
24+
return True
25+
26+
mock_substrate = MagicMock(autospec=SubstrateInterface)
27+
mock_substrate.submit_extrinsic = wait
28+
mock_extrinsic = MagicMock(autospec=GenericExtrinsic)
29+
with patch.object(utils, "EXTRINSIC_SUBMISSION_TIMEOUT", timeout):
30+
with pytest.raises(SubstrateRequestException):
31+
utils.submit_extrinsic(mock_substrate, mock_extrinsic, True, True)
32+
33+
34+
def test_submit_extrinsic_success():
35+
mock_substrate = MagicMock(autospec=SubstrateInterface)
36+
mock_substrate.submit_extrinsic.return_value = True
37+
mock_extrinsic = MagicMock(autospec=GenericExtrinsic)
38+
result = utils.submit_extrinsic(mock_substrate, mock_extrinsic, True, True)
39+
assert result is True
40+
41+
42+
def test_submit_extrinsic_timeout_env(set_extrinsics_timeout_env):
43+
importlib.reload(utils)
44+
timeout = utils.EXTRINSIC_SUBMISSION_TIMEOUT
45+
assert timeout < 5 # should be less than 5 seconds as taken from test env var
46+
47+
def wait(extrinsic, wait_for_inclusion, wait_for_finalization):
48+
time.sleep(timeout + 1)
49+
return True
50+
51+
mock_substrate = MagicMock(autospec=SubstrateInterface)
52+
mock_substrate.submit_extrinsic = wait
53+
mock_extrinsic = MagicMock(autospec=GenericExtrinsic)
54+
with pytest.raises(SubstrateRequestException):
55+
utils.submit_extrinsic(mock_substrate, mock_extrinsic, True, True)
56+
57+
58+
def test_submit_extrinsic_success_env(set_extrinsics_timeout_env):
59+
importlib.reload(utils)
60+
mock_substrate = MagicMock(autospec=SubstrateInterface)
61+
mock_substrate.submit_extrinsic.return_value = True
62+
mock_extrinsic = MagicMock(autospec=GenericExtrinsic)
63+
result = utils.submit_extrinsic(mock_substrate, mock_extrinsic, True, True)
64+
assert result is True
65+
66+
67+
def test_submit_extrinsic_timeout_env_float(monkeypatch):
68+
monkeypatch.setenv("EXTRINSIC_SUBMISSION_TIMEOUT", "1.45") # use float
69+
70+
importlib.reload(utils)
71+
timeout = utils.EXTRINSIC_SUBMISSION_TIMEOUT
72+
73+
assert timeout == 1.45 # parsed correctly
74+
75+
def wait(extrinsic, wait_for_inclusion, wait_for_finalization):
76+
time.sleep(timeout + 0.3) # sleep longer by float
77+
return True
78+
79+
mock_substrate = MagicMock(autospec=SubstrateInterface)
80+
mock_substrate.submit_extrinsic = wait
81+
mock_extrinsic = MagicMock(autospec=GenericExtrinsic)
82+
with pytest.raises(SubstrateRequestException):
83+
utils.submit_extrinsic(mock_substrate, mock_extrinsic, True, True)
84+
85+
86+
def test_import_timeout_env_parse(monkeypatch):
87+
# int
88+
monkeypatch.setenv("EXTRINSIC_SUBMISSION_TIMEOUT", "1")
89+
importlib.reload(utils)
90+
assert utils.EXTRINSIC_SUBMISSION_TIMEOUT == 1 # parsed correctly
91+
92+
# float
93+
monkeypatch.setenv("EXTRINSIC_SUBMISSION_TIMEOUT", "1.45") # use float
94+
importlib.reload(utils)
95+
assert utils.EXTRINSIC_SUBMISSION_TIMEOUT == 1.45 # parsed correctly
96+
97+
# invalid
98+
monkeypatch.setenv("EXTRINSIC_SUBMISSION_TIMEOUT", "not_an_int")
99+
with pytest.raises(ValueError) as e:
100+
importlib.reload(utils)
101+
assert "must be a float" in str(e.value)
102+
103+
# negative
104+
monkeypatch.setenv("EXTRINSIC_SUBMISSION_TIMEOUT", "-1")
105+
with pytest.raises(ValueError) as e:
106+
importlib.reload(utils)
107+
assert "cannot be negative" in str(e.value)
108+
109+
# default (not checking exact value, just that it's a value)
110+
monkeypatch.delenv("EXTRINSIC_SUBMISSION_TIMEOUT")
111+
importlib.reload(utils)
112+
assert isinstance(utils.EXTRINSIC_SUBMISSION_TIMEOUT, float) # has a default value
113+
assert utils.EXTRINSIC_SUBMISSION_TIMEOUT > 0 # is positive

0 commit comments

Comments
 (0)