Skip to content

Commit e3e055b

Browse files
authored
Merge pull request #235 from opentensor/release/1.5.11
Release/1.5.11
2 parents c1e0329 + 0d0ff5a commit e3e055b

File tree

10 files changed

+145
-57
lines changed

10 files changed

+145
-57
lines changed

.github/workflows/check-sdk-tests.yml

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ jobs:
5757
runs-on: ubuntu-latest
5858
if: always()
5959
outputs:
60-
run-sdk-tests: ${{ steps.get-labels.outputs.run-sdk-tests }}
60+
run-sdk-tests: ${{ steps.check-manual.outputs.run-sdk-tests || steps.get-labels-pr.outputs.run-sdk-tests }}
6161
steps:
6262
- name: Check out repository
6363
uses: actions/checkout@v4
6464

6565
- name: Skip label check for manual runs
66-
id: get-labels
66+
id: check-manual
6767
if: ${{ github.event_name == 'workflow_dispatch' }}
6868
run: |
6969
echo "Manual workflow dispatch detected, skipping PR label check."
@@ -74,11 +74,13 @@ jobs:
7474
if: ${{ github.event_name == 'pull_request' }}
7575
run: |
7676
sleep 5
77-
LABELS=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels --jq '.[].name')
77+
LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name')
7878
echo "Current labels: $LABELS"
7979
if echo "$LABELS" | grep -q "run-bittensor-sdk-tests"; then
80+
echo "run-sdk-tests=true" >> $GITHUB_ENV
8081
echo "run-sdk-tests=true" >> $GITHUB_OUTPUT
8182
else
83+
echo "run-sdk-tests=false" >> $GITHUB_ENV
8284
echo "run-sdk-tests=false" >> $GITHUB_OUTPUT
8385
fi
8486
env:
@@ -171,48 +173,51 @@ jobs:
171173
- name: Check-out repository
172174
uses: actions/checkout@v4
173175

174-
- name: Install dependencies
176+
- name: Install system dependencies
175177
run: |
176178
sudo apt-get update &&
177179
sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler
178180
179-
- name: Create Python virtual environment
180-
working-directory: ${{ github.workspace }}
181-
run: python3 -m venv ${{ github.workspace }}/venv
181+
- name: Set up Python
182+
uses: actions/setup-python@v5
183+
with:
184+
python-version: '3.13'
185+
186+
- name: Install uv
187+
uses: astral-sh/setup-uv@v4
182188

183189
- name: Clone Bittensor SDK repo
184-
working-directory: ${{ github.workspace }}
185190
run: git clone https://github.com/opentensor/bittensor.git
186191

187-
- name: Setup Bittensor SDK from cloned repo
192+
- name: Checkout Bittensor branch
188193
working-directory: ${{ github.workspace }}/bittensor
189194
run: |
190-
source ${{ github.workspace }}/venv/bin/activate
191195
if ! git fetch origin $BITTENSOR_BRANCH; then
192196
echo "❌ Error: Branch '$BITTENSOR_BRANCH' does not exist in opentensor/bittensor."
193197
exit 1
194198
fi
195199
git checkout FETCH_HEAD
196200
echo "✅ Using Bittensor branch: $BITTENSOR_BRANCH"
197-
python3 -m pip install --upgrade pip uv
198-
uv pip install '.[dev]'
199201
200-
- name: Clone Bittensor async-substrate-interface repo
202+
- name: Install Bittensor SDK dependencies
203+
working-directory: ${{ github.workspace }}/bittensor
204+
run: uv pip install --system '.[dev]'
205+
206+
- name: Clone async-substrate-interface repo
201207
run: git clone https://github.com/opentensor/async-substrate-interface.git
202208

203-
- name: Checkout PR branch in async-substrate-interface repo
209+
- name: Checkout PR branch in async-substrate-interface
204210
working-directory: ${{ github.workspace }}/async-substrate-interface
205211
run: |
206212
git fetch origin ${{ github.event.pull_request.head.ref }}
207213
git checkout ${{ github.event.pull_request.head.ref }}
208214
echo "Current branch: $(git rev-parse --abbrev-ref HEAD)"
209215
210-
- name: Install async-substrate-interface package
216+
- name: Install async-substrate-interface with dev dependencies
211217
working-directory: ${{ github.workspace }}/async-substrate-interface
212218
run: |
213-
source ${{ github.workspace }}/venv/bin/activate
214-
python3 -m pip uninstall async-substrate-interface -y
215-
uv pip install .
219+
uv pip uninstall --system async-substrate-interface || true
220+
uv pip install --system '.[dev]'
216221
217222
- name: Download Cached Docker Image
218223
uses: actions/download-artifact@v4
@@ -222,10 +227,8 @@ jobs:
222227
- name: Load Docker Image
223228
run: docker load -i subtensor-localnet.tar
224229

225-
- name: Run tests
226-
run: |
227-
source ${{ github.workspace }}/venv/bin/activate
228-
python3 -m pytest ${{ matrix.test-file }} -s
230+
- name: Run e2e tests
231+
run: pytest ${{ matrix.test-file }} -s
229232

230233

231234
run-integration-and-unit-test:
@@ -237,52 +240,54 @@ jobs:
237240
- name: Check-out repository
238241
uses: actions/checkout@v4
239242

240-
- name: Install dependencies
243+
- name: Install system dependencies
241244
run: |
242245
sudo apt-get update &&
243246
sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler
244247
245-
- name: Create Python virtual environment
246-
working-directory: ${{ github.workspace }}
247-
run: python3 -m venv venv
248+
- name: Set up Python
249+
uses: actions/setup-python@v5
250+
with:
251+
python-version: '3.13'
252+
253+
- name: Install uv
254+
uses: astral-sh/setup-uv@v4
248255

249256
- name: Clone Bittensor SDK repo
250-
working-directory: ${{ github.workspace }}
251257
run: git clone https://github.com/opentensor/bittensor.git
252258

253-
- name: Setup Bittensor SDK from cloned repo
259+
- name: Checkout Bittensor branch
254260
working-directory: ${{ github.workspace }}/bittensor
255261
run: |
256-
source ${{ github.workspace }}/venv/bin/activate
257262
if ! git fetch origin $BITTENSOR_BRANCH; then
258263
echo "❌ Error: Branch '$BITTENSOR_BRANCH' does not exist in opentensor/bittensor."
259264
exit 1
260265
fi
261266
git checkout FETCH_HEAD
262267
echo "✅ Using Bittensor branch: $BITTENSOR_BRANCH"
263-
python3 -m pip install --upgrade pip uv
264-
uv pip install '.[dev]'
265268
266-
- name: Checkout PR branch in async-substrate-interface repo
267-
uses: actions/checkout@v4
268-
with:
269-
repository: ${{ github.event.pull_request.head.repo.full_name }}
270-
ref: ${{ github.event.pull_request.head.ref }}
271-
path: async-substrate-interface
269+
- name: Install Bittensor SDK dependencies
270+
working-directory: ${{ github.workspace }}/bittensor
271+
run: uv pip install --system '.[dev]'
272272

273-
- name: Install /async-substrate-interface package
273+
- name: Clone async-substrate-interface repo
274+
run: git clone https://github.com/opentensor/async-substrate-interface.git
275+
276+
- name: Checkout PR branch in async-substrate-interface
274277
working-directory: ${{ github.workspace }}/async-substrate-interface
275278
run: |
276-
source ${{ github.workspace }}/venv/bin/activate
277-
pip uninstall async-substrate-interface -y
278-
uv pip install .
279+
git fetch origin ${{ github.event.pull_request.head.ref }}
280+
git checkout ${{ github.event.pull_request.head.ref }}
281+
echo "Current branch: $(git rev-parse --abbrev-ref HEAD)"
279282
280-
- name: Run SDK integration tests
283+
- name: Install async-substrate-interface with dev dependencies
284+
working-directory: ${{ github.workspace }}/async-substrate-interface
281285
run: |
282-
source ${{ github.workspace }}/venv/bin/activate
283-
pytest ${{ github.workspace }}/bittensor/tests/integration_tests
286+
uv pip uninstall --system async-substrate-interface || true
287+
uv pip install --system '.[dev]'
284288
285-
- name: Run bittensor-sdk unit tests
286-
run: |
287-
source ${{ github.workspace }}/venv/bin/activate
288-
pytest ${{ github.workspace }}/bittensor/tests/unit_tests
289+
- name: Run SDK integration tests
290+
run: pytest ${{ github.workspace }}/bittensor/tests/integration_tests
291+
292+
- name: Run Bittensor SDK unit tests
293+
run: pytest ${{ github.workspace }}/bittensor/tests/unit_tests

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# Changelog
2+
## 1.5.11 /2025-11-14
3+
* Race Condition Bug fixes by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/234
4+
5+
**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.5.10...v1.5.11
6+
27
## 1.5.10 /2025-11-12
38
* bug fixes 1.5.10 by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/231
49
* no double-sleep in async-substrate-interface websocket querying

async_substrate_interface/async_substrate.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@ def __init__(
586586
self._max_retries = max_retries
587587
self._last_activity = asyncio.Event()
588588
self._last_activity.set()
589+
self._waiting_for_response = 0
589590

590591
@property
591592
def state(self):
@@ -599,6 +600,24 @@ async def __aenter__(self):
599600
await self.connect()
600601
return self
601602

603+
async def mark_waiting_for_response(self):
604+
"""
605+
Mark that a response is expected. This will cause the websocket to not automatically close.
606+
607+
Note: you must mark as response received once you have received the response.
608+
"""
609+
async with self._lock:
610+
self._waiting_for_response += 1
611+
612+
async def mark_response_received(self):
613+
"""
614+
Mark that the expected response has been received. Automatic shutdown of websocket will proceed normally.
615+
616+
Note: only do this if you have previously marked as waiting for response
617+
"""
618+
async with self._lock:
619+
self._waiting_for_response -= 1
620+
602621
@staticmethod
603622
async def loop_time() -> float:
604623
return asyncio.get_running_loop().time()
@@ -738,7 +757,10 @@ async def _handler(self, ws: ClientConnection) -> Union[None, Exception]:
738757
task_res = task.result()
739758

740759
# If ConnectionClosedOK, graceful shutdown - don't reconnect
741-
if isinstance(task_res, websockets.exceptions.ConnectionClosedOK):
760+
if (
761+
isinstance(task_res, websockets.exceptions.ConnectionClosedOK)
762+
and self._waiting_for_response <= 0
763+
):
742764
logger.debug("Graceful shutdown detected, not reconnecting")
743765
return None # Clean exit
744766

@@ -793,7 +815,12 @@ async def _handler(self, ws: ClientConnection) -> Union[None, Exception]:
793815

794816
async def __aexit__(self, exc_type, exc_val, exc_tb):
795817
if self.shutdown_timer is not None:
796-
if self.state != State.CONNECTING:
818+
if (
819+
self.state != State.CONNECTING
820+
and self._sending.qsize() == 0
821+
and not self._received_subscriptions
822+
and self._waiting_for_response <= 0
823+
):
797824
if self._exit_task is not None:
798825
self._exit_task.cancel()
799826
try:
@@ -812,6 +839,7 @@ async def _exit_with_timer(self):
812839
try:
813840
if self.shutdown_timer is not None:
814841
await asyncio.sleep(self.shutdown_timer)
842+
logger.debug("Exiting with timer")
815843
await self.shutdown()
816844
except asyncio.CancelledError:
817845
pass
@@ -2495,6 +2523,7 @@ async def _make_rpc_request(
24952523
logger.debug(
24962524
f"Submitted payload ID {payload['id']} with websocket ID {item_id}: {output_payload}"
24972525
)
2526+
await ws.mark_waiting_for_response()
24982527

24992528
while True:
25002529
for item_id in request_manager.unresponded():
@@ -2552,6 +2581,7 @@ async def _make_rpc_request(
25522581
)
25532582

25542583
if request_manager.is_complete:
2584+
await ws.mark_response_received()
25552585
break
25562586
else:
25572587
await asyncio.sleep(0.01)
@@ -3948,6 +3978,7 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]:
39483978
}
39493979

39503980
if "finalized" in message_result and wait_for_finalization:
3981+
logger.debug("Extrinsic finalized. Unsubscribing.")
39513982
async with self.ws as ws:
39523983
await ws.unsubscribe(subscription_id)
39533984
return {
@@ -3956,14 +3987,17 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]:
39563987
"finalized": True,
39573988
}, True
39583989
elif (
3959-
"inblock" in message_result
3990+
any(x in message_result for x in ["inblock", "inBlock"])
39603991
and wait_for_inclusion
39613992
and not wait_for_finalization
39623993
):
3994+
logger.debug("Extrinsic included. Unsubscribing.")
39633995
async with self.ws as ws:
39643996
await ws.unsubscribe(subscription_id)
39653997
return {
3966-
"block_hash": message_result["inblock"],
3998+
"block_hash": message_result.get(
3999+
"inblock", message_result.get("inBlock")
4000+
),
39674001
"extrinsic_hash": "0x{}".format(extrinsic.extrinsic_hash.hex()),
39684002
"finalized": False,
39694003
}, True

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "async-substrate-interface"
3-
version = "1.5.10"
3+
version = "1.5.11"
44
description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface"
55
readme = "README.md"
66
license = { file = "LICENSE" }

tests/helpers/proxy_server.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import logging
23
import time
34

@@ -26,7 +27,8 @@ def connect(self):
2627
def close(self):
2728
if self.upstream_connection:
2829
self.upstream_connection.close()
29-
self.server.shutdown()
30+
with contextlib.suppress(AttributeError):
31+
self.server.shutdown()
3032

3133
def proxy_request(self, websocket: ServerConnection):
3234
for message in websocket:

0 commit comments

Comments
 (0)