Skip to content
Open
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
40 changes: 28 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,18 @@ jobs:
steps:
- name: Checkout leanSpec
uses: actions/checkout@v4

- name: Install uv and Python ${{ matrix.python-version }}

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
python-version: ${{ matrix.python-version }}


- name: Run all quality checks via tox
run: uvx tox -e all-checks

Expand All @@ -47,14 +51,18 @@ jobs:
steps:
- name: Checkout leanSpec
uses: actions/checkout@v4

- name: Install uv and Python ${{ matrix.python-version }}

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
python-version: ${{ matrix.python-version }}


- name: Run tests via tox
run: uvx tox -e pytest

Expand All @@ -65,12 +73,16 @@ jobs:
- name: Checkout leanSpec
uses: actions/checkout@v4

- name: Install uv and Python 3.14
- name: Set up Python 3.14
uses: actions/setup-python@v5
with:
python-version: "3.14"

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
python-version: "3.14"

- name: Sync dependencies
run: uv sync --no-progress
Expand All @@ -86,12 +98,16 @@ jobs:
- name: Checkout leanSpec
uses: actions/checkout@v4

- name: Install uv and Python 3.12
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
python-version: "3.12"

- name: Run interop tests
run: |
Expand Down
48 changes: 48 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Docker

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read
packages: write

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
docker:
name: Build Docker image (amd64 + arm64)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
target: node
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
tags: ghcr.io/leanethereum/leanspec-node:latest
cache-from: type=gha
cache-to: type=gha,mode=max
8 changes: 6 additions & 2 deletions .github/workflows/prod-vectors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ jobs:
- name: Checkout leanSpec
uses: actions/checkout@v4

- name: Install uv and Python 3.14
- name: Set up Python 3.14
uses: actions/setup-python@v5
with:
python-version: "3.14"

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
cache-dependency-glob: "pyproject.toml"
python-version: "3.14"

- name: Sync dependencies
run: uv sync --no-progress
Expand Down
6 changes: 5 additions & 1 deletion src/lean_spec/subspecs/api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,14 @@ async def run(self) -> None:
await asyncio.sleep(1)

def stop(self) -> None:
"""Request graceful shutdown."""
"""Request graceful shutdown (fire-and-forget). Prefer aclose() in async code."""
if self._runner is not None:
asyncio.create_task(self._async_stop())

async def aclose(self) -> None:
"""Gracefully stop the server. Await this in async code for clean shutdown."""
await self._async_stop()

async def _async_stop(self) -> None:
"""Gracefully stop the server."""
if self._runner:
Expand Down
17 changes: 14 additions & 3 deletions tests/api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,21 @@ def _create_server(self) -> ApiServer:
return ApiServer(config=config, store_getter=lambda: store)

def stop(self) -> None:
"""Stop the server and event loop."""
"""Stop the server and event loop, awaiting cleanup so _async_stop is run."""
if self.server and self.loop:
self.loop.call_soon_threadsafe(self.server.stop)
self.loop.call_soon_threadsafe(self.loop.stop)

async def shutdown() -> None:
if self.server:
await self.server.aclose()
if self.loop and self.loop.is_running():
self.loop.stop()

future = asyncio.run_coroutine_threadsafe(shutdown(), self.loop)
try:
future.result(timeout=5.0)
except TimeoutError:
if self.loop and self.loop.is_running():
self.loop.call_soon_threadsafe(self.loop.stop)


def _wait_for_server(url: str, timeout: float = 5.0) -> bool:
Expand Down
8 changes: 2 additions & 6 deletions tests/lean_spec/subspecs/api/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@

from __future__ import annotations

import asyncio

import httpx

from lean_spec.subspecs.api import ApiServer, ApiServerConfig
Expand Down Expand Up @@ -79,8 +77,7 @@ async def test_returns_503_when_store_not_initialized(self) -> None:
assert response.status_code == 503

finally:
server.stop()
await asyncio.sleep(0.1)
await server.aclose()


class TestJustifiedCheckpointEndpoint:
Expand All @@ -100,5 +97,4 @@ async def test_returns_503_when_store_not_initialized(self) -> None:
assert response.status_code == 503

finally:
server.stop()
await asyncio.sleep(0.1)
await server.aclose()
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import asyncio
from collections.abc import AsyncGenerator

import pytest
Expand Down Expand Up @@ -44,7 +45,10 @@ async def network() -> AsyncGenerator[GossipsubTestNetwork]:
"""
net = GossipsubTestNetwork()
yield net
await net.stop_all()
try:
await asyncio.wait_for(net.stop_all(), timeout=10.0)
except asyncio.TimeoutError:
pass


@pytest.fixture
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@


@pytest.mark.asyncio
@pytest.mark.timeout(60)
async def test_peer_churn(
network: GossipsubTestNetwork,
) -> None:
Expand Down
4 changes: 1 addition & 3 deletions tests/lean_spec/subspecs/sync/test_checkpoint_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import asyncio
from unittest.mock import MagicMock

from lean_spec.subspecs.api import ApiServer, ApiServerConfig
Expand Down Expand Up @@ -77,5 +76,4 @@ async def test_client_fetches_and_deserializes_state(self, base_store: Store) ->
assert is_valid is True

finally:
server.stop()
await asyncio.sleep(0.1)
await server.aclose()
Loading