Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 53 additions & 48 deletions .github/workflows/check-sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ jobs:
runs-on: ubuntu-latest
if: always()
outputs:
run-sdk-tests: ${{ steps.get-labels.outputs.run-sdk-tests }}
run-sdk-tests: ${{ steps.check-manual.outputs.run-sdk-tests || steps.get-labels-pr.outputs.run-sdk-tests }}
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Skip label check for manual runs
id: get-labels
id: check-manual
if: ${{ github.event_name == 'workflow_dispatch' }}
run: |
echo "Manual workflow dispatch detected, skipping PR label check."
Expand All @@ -74,11 +74,13 @@ jobs:
if: ${{ github.event_name == 'pull_request' }}
run: |
sleep 5
LABELS=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels --jq '.[].name')
LABELS=$(gh pr view ${{ github.event.pull_request.number }} --json labels --jq '.labels[].name')
echo "Current labels: $LABELS"
if echo "$LABELS" | grep -q "run-bittensor-sdk-tests"; then
echo "run-sdk-tests=true" >> $GITHUB_ENV
echo "run-sdk-tests=true" >> $GITHUB_OUTPUT
else
echo "run-sdk-tests=false" >> $GITHUB_ENV
echo "run-sdk-tests=false" >> $GITHUB_OUTPUT
fi
env:
Expand Down Expand Up @@ -171,48 +173,51 @@ jobs:
- name: Check-out repository
uses: actions/checkout@v4

- name: Install dependencies
- name: Install system dependencies
run: |
sudo apt-get update &&
sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler

- name: Create Python virtual environment
working-directory: ${{ github.workspace }}
run: python3 -m venv ${{ github.workspace }}/venv
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Install uv
uses: astral-sh/setup-uv@v4

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

- name: Setup Bittensor SDK from cloned repo
- name: Checkout Bittensor branch
working-directory: ${{ github.workspace }}/bittensor
run: |
source ${{ github.workspace }}/venv/bin/activate
if ! git fetch origin $BITTENSOR_BRANCH; then
echo "❌ Error: Branch '$BITTENSOR_BRANCH' does not exist in opentensor/bittensor."
exit 1
fi
git checkout FETCH_HEAD
echo "✅ Using Bittensor branch: $BITTENSOR_BRANCH"
python3 -m pip install --upgrade pip uv
uv pip install '.[dev]'

- name: Clone Bittensor async-substrate-interface repo
- name: Install Bittensor SDK dependencies
working-directory: ${{ github.workspace }}/bittensor
run: uv pip install --system '.[dev]'

- name: Clone async-substrate-interface repo
run: git clone https://github.com/opentensor/async-substrate-interface.git

- name: Checkout PR branch in async-substrate-interface repo
- name: Checkout PR branch in async-substrate-interface
working-directory: ${{ github.workspace }}/async-substrate-interface
run: |
git fetch origin ${{ github.event.pull_request.head.ref }}
git checkout ${{ github.event.pull_request.head.ref }}
echo "Current branch: $(git rev-parse --abbrev-ref HEAD)"

- name: Install async-substrate-interface package
- name: Install async-substrate-interface with dev dependencies
working-directory: ${{ github.workspace }}/async-substrate-interface
run: |
source ${{ github.workspace }}/venv/bin/activate
python3 -m pip uninstall async-substrate-interface -y
uv pip install .
uv pip uninstall --system async-substrate-interface || true
uv pip install --system '.[dev]'

- name: Download Cached Docker Image
uses: actions/download-artifact@v4
Expand All @@ -222,10 +227,8 @@ jobs:
- name: Load Docker Image
run: docker load -i subtensor-localnet.tar

- name: Run tests
run: |
source ${{ github.workspace }}/venv/bin/activate
python3 -m pytest ${{ matrix.test-file }} -s
- name: Run e2e tests
run: pytest ${{ matrix.test-file }} -s


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

- name: Install dependencies
- name: Install system dependencies
run: |
sudo apt-get update &&
sudo apt-get install -y clang curl libssl-dev llvm libudev-dev protobuf-compiler

- name: Create Python virtual environment
working-directory: ${{ github.workspace }}
run: python3 -m venv venv
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Install uv
uses: astral-sh/setup-uv@v4

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

- name: Setup Bittensor SDK from cloned repo
- name: Checkout Bittensor branch
working-directory: ${{ github.workspace }}/bittensor
run: |
source ${{ github.workspace }}/venv/bin/activate
if ! git fetch origin $BITTENSOR_BRANCH; then
echo "❌ Error: Branch '$BITTENSOR_BRANCH' does not exist in opentensor/bittensor."
exit 1
fi
git checkout FETCH_HEAD
echo "✅ Using Bittensor branch: $BITTENSOR_BRANCH"
python3 -m pip install --upgrade pip uv
uv pip install '.[dev]'

- name: Checkout PR branch in async-substrate-interface repo
uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
path: async-substrate-interface
- name: Install Bittensor SDK dependencies
working-directory: ${{ github.workspace }}/bittensor
run: uv pip install --system '.[dev]'

- name: Install /async-substrate-interface package
- name: Clone async-substrate-interface repo
run: git clone https://github.com/opentensor/async-substrate-interface.git

- name: Checkout PR branch in async-substrate-interface
working-directory: ${{ github.workspace }}/async-substrate-interface
run: |
source ${{ github.workspace }}/venv/bin/activate
pip uninstall async-substrate-interface -y
uv pip install .
git fetch origin ${{ github.event.pull_request.head.ref }}
git checkout ${{ github.event.pull_request.head.ref }}
echo "Current branch: $(git rev-parse --abbrev-ref HEAD)"

- name: Run SDK integration tests
- name: Install async-substrate-interface with dev dependencies
working-directory: ${{ github.workspace }}/async-substrate-interface
run: |
source ${{ github.workspace }}/venv/bin/activate
pytest ${{ github.workspace }}/bittensor/tests/integration_tests
uv pip uninstall --system async-substrate-interface || true
uv pip install --system '.[dev]'

- name: Run bittensor-sdk unit tests
run: |
source ${{ github.workspace }}/venv/bin/activate
pytest ${{ github.workspace }}/bittensor/tests/unit_tests
- name: Run SDK integration tests
run: pytest ${{ github.workspace }}/bittensor/tests/integration_tests

- name: Run Bittensor SDK unit tests
run: pytest ${{ github.workspace }}/bittensor/tests/unit_tests
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# Changelog
## 1.5.11 /2025-11-14
* Race Condition Bug fixes by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/234

**Full Changelog**: https://github.com/opentensor/async-substrate-interface/compare/v1.5.10...v1.5.11

## 1.5.10 /2025-11-12
* bug fixes 1.5.10 by @thewhaleking in https://github.com/opentensor/async-substrate-interface/pull/231
* no double-sleep in async-substrate-interface websocket querying
Expand Down
42 changes: 38 additions & 4 deletions async_substrate_interface/async_substrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ def __init__(
self._max_retries = max_retries
self._last_activity = asyncio.Event()
self._last_activity.set()
self._waiting_for_response = 0

@property
def state(self):
Expand All @@ -599,6 +600,24 @@ async def __aenter__(self):
await self.connect()
return self

async def mark_waiting_for_response(self):
"""
Mark that a response is expected. This will cause the websocket to not automatically close.

Note: you must mark as response received once you have received the response.
"""
async with self._lock:
self._waiting_for_response += 1

async def mark_response_received(self):
"""
Mark that the expected response has been received. Automatic shutdown of websocket will proceed normally.

Note: only do this if you have previously marked as waiting for response
"""
async with self._lock:
self._waiting_for_response -= 1

@staticmethod
async def loop_time() -> float:
return asyncio.get_running_loop().time()
Expand Down Expand Up @@ -738,7 +757,10 @@ async def _handler(self, ws: ClientConnection) -> Union[None, Exception]:
task_res = task.result()

# If ConnectionClosedOK, graceful shutdown - don't reconnect
if isinstance(task_res, websockets.exceptions.ConnectionClosedOK):
if (
isinstance(task_res, websockets.exceptions.ConnectionClosedOK)
and self._waiting_for_response <= 0
):
logger.debug("Graceful shutdown detected, not reconnecting")
return None # Clean exit

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

async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.shutdown_timer is not None:
if self.state != State.CONNECTING:
if (
self.state != State.CONNECTING
and self._sending.qsize() == 0
and not self._received_subscriptions
and self._waiting_for_response <= 0
):
if self._exit_task is not None:
self._exit_task.cancel()
try:
Expand All @@ -812,6 +839,7 @@ async def _exit_with_timer(self):
try:
if self.shutdown_timer is not None:
await asyncio.sleep(self.shutdown_timer)
logger.debug("Exiting with timer")
await self.shutdown()
except asyncio.CancelledError:
pass
Expand Down Expand Up @@ -2495,6 +2523,7 @@ async def _make_rpc_request(
logger.debug(
f"Submitted payload ID {payload['id']} with websocket ID {item_id}: {output_payload}"
)
await ws.mark_waiting_for_response()

while True:
for item_id in request_manager.unresponded():
Expand Down Expand Up @@ -2552,6 +2581,7 @@ async def _make_rpc_request(
)

if request_manager.is_complete:
await ws.mark_response_received()
break
else:
await asyncio.sleep(0.01)
Expand Down Expand Up @@ -3948,6 +3978,7 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]:
}

if "finalized" in message_result and wait_for_finalization:
logger.debug("Extrinsic finalized. Unsubscribing.")
async with self.ws as ws:
await ws.unsubscribe(subscription_id)
return {
Expand All @@ -3956,14 +3987,17 @@ async def result_handler(message: dict, subscription_id) -> tuple[dict, bool]:
"finalized": True,
}, True
elif (
"inblock" in message_result
any(x in message_result for x in ["inblock", "inBlock"])
and wait_for_inclusion
and not wait_for_finalization
):
logger.debug("Extrinsic included. Unsubscribing.")
async with self.ws as ws:
await ws.unsubscribe(subscription_id)
return {
"block_hash": message_result["inblock"],
"block_hash": message_result.get(
"inblock", message_result.get("inBlock")
),
"extrinsic_hash": "0x{}".format(extrinsic.extrinsic_hash.hex()),
"finalized": False,
}, True
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "async-substrate-interface"
version = "1.5.10"
version = "1.5.11"
description = "Asyncio library for interacting with substrate. Mostly API-compatible with py-substrate-interface"
readme = "README.md"
license = { file = "LICENSE" }
Expand Down
4 changes: 3 additions & 1 deletion tests/helpers/proxy_server.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
import logging
import time

Expand Down Expand Up @@ -26,7 +27,8 @@ def connect(self):
def close(self):
if self.upstream_connection:
self.upstream_connection.close()
self.server.shutdown()
with contextlib.suppress(AttributeError):
self.server.shutdown()

def proxy_request(self, websocket: ServerConnection):
for message in websocket:
Expand Down
Loading
Loading