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
3 changes: 3 additions & 0 deletions .github/workflows/scan-codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
- opened
- ready_for_review
- reopened
# Without synchronize, pushes to an open PR are never rescanned: the
# last green result keeps showing while the code underneath changes.
- synchronize
pull_request_review:
types:
- submitted
Expand Down
27 changes: 23 additions & 4 deletions .github/workflows/unit-tests-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ jobs:
- run: npm run lint
- run: npm run build

backend-lint:
if: (github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]')
name: Backend lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- run: pip install ruff==0.15.22
- run: ruff check

backend-unit-tests:
if: (github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]')
runs-on: ubuntu-latest
Expand All @@ -51,7 +63,14 @@ jobs:
- run: pip install -r backend/requirements.txt
- run: pip install pytest-cov pytest-xdist
- run: pytest tests/unit --cov=backend --cov-report=xml --cov-branch -n auto
- name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@v7.0.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

# Backend coverage.xml previously had exactly one consumer (SonarCloud).
# Codecov measures the actual PR diff rather than a stale branch baseline.
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5.5.2
with:
verbose: true
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
files: coverage.xml
flags: backend
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ repos:
hooks:
- id: autoflake
args: ["--remove-all-unused-imports", "--in-place"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.22
hooks:
- id: ruff
args: ["--fix"]
files: ^backend/.*\.py$
# Copied from https://black.readthedocs.io/en/stable/integrations/source_version_control.html
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.4.2
Expand Down
1 change: 1 addition & 0 deletions backend/consensus/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,7 @@ async def process_validator_appeal(
"Appeal failed, no validators found to process the appeal",
{
"transaction_hash": context.transaction.hash,
"error": str(e),
},
transaction_hash=context.transaction.hash,
)
Expand Down
8 changes: 6 additions & 2 deletions backend/consensus/worker_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ def handle_signal(sig, frame):
# CRITICAL: Kill any orphaned GenVM processes from previous crashes
# These zombie processes can consume gigabytes of memory outside Docker limits
logger.info("Cleaning up orphaned GenVM processes from previous crashes...")
_pkill_rc = os.system("pkill -9 -f 'genvm (llm|web)' 2>/dev/null || true")
_pkill_rc = os.system( # noqa: ASYNC221 - one-shot startup orphan cleanup
"pkill -9 -f 'genvm (llm|web)' 2>/dev/null || true"
)
logger.info("GenVM cleanup complete")

# Database setup
Expand Down Expand Up @@ -479,7 +481,9 @@ async def run_worker_with_auto_restart():

# Final safety check: Kill any remaining genvm processes
logger.info("Final cleanup: killing any remaining GenVM processes...")
os.system("pkill -9 -f 'genvm (llm|web)' 2>/dev/null || true")
os.system( # noqa: ASYNC221 - one-shot shutdown orphan cleanup
"pkill -9 -f 'genvm (llm|web)' 2>/dev/null || true"
)
logger.info("GenVM cleanup complete")

print("Consensus Worker Service stopped")
Expand Down
1 change: 0 additions & 1 deletion backend/database_handler/snapshot_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ def create_snapshot(self) -> Snapshot:
"appealed": tx.appealed,
"appeal_undetermined": tx.appeal_undetermined,
"triggered_by_hash": tx.triggered_by_hash,
"appealed": tx.appealed,
"timestamp_awaiting_finalization": tx.timestamp_awaiting_finalization,
"num_of_initial_validators": tx.num_of_initial_validators,
"last_vote_timestamp": tx.last_vote_timestamp,
Expand Down
12 changes: 5 additions & 7 deletions backend/node/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,10 +472,10 @@ async def stop_module(self, module_type: typing.Literal["llm", "web"]):
body = await resp.json()
if resp.status != 200:
self.logger.error(
f"Failed to stop LLM module", body=body, status=resp.status
"Failed to stop LLM module", body=body, status=resp.status
)
else:
self.logger.info(f"Stopped LLM module", body=body, status=resp.status)
self.logger.info("Stopped LLM module", body=body, status=resp.status)

async def start_module(
self,
Expand All @@ -490,7 +490,7 @@ async def start_module(
body = await resp.json()
if resp.status != 200:
self.logger.error(
f"Failed to start module",
"Failed to start module",
module=module_type,
body=body,
status=resp.status,
Expand Down Expand Up @@ -518,9 +518,7 @@ async def try_llms(
async with aiohttp.request("POST", f"{self.url}/llm/check", json=data) as resp:
body = await resp.json()
if resp.status != 200:
self.logger.error(
f"Failed to check llms", body=body, status=resp.status
)
self.logger.error("Failed to check llms", body=body, status=resp.status)
# Return error response for each config when the check fails
return [
{
Expand Down Expand Up @@ -976,7 +974,7 @@ async def _run_genvm(
transaction_hash: str | None = None,
transaction_datetime: datetime.datetime | None,
state_status: str | None = None,
timeout: float = 10 * 60,
timeout: float = 10 * 60, # noqa: ASYNC109 - forwarded GenVM deadline
code: bytes | None = None,
value: int = 0,
origin_address: str | None = None,
Expand Down
4 changes: 2 additions & 2 deletions backend/node/genvm/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ def _leader_results_to_list(
async def run_genvm_host(
host_supplier: typing.Callable[[socket.socket], Host],
*,
timeout: float,
timeout: float, # noqa: ASYNC109 - retry budget spans multiple awaits
manager_uri: str = "http://127.0.0.1:3999",
logger: genvm_logger.Logger | None = None,
is_sync: bool,
Expand Down Expand Up @@ -859,7 +859,7 @@ async def run_genvm_host(
raise
except Exception as e:
logger.error(
f"GenVM execution attempt failed",
"GenVM execution attempt failed",
error=e,
retry_count=retry_count,
)
Expand Down
2 changes: 1 addition & 1 deletion backend/node/genvm/origin/base_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ async def _await_first_cancel_others(*it):
async def run_genvm(
handler: IHost,
*,
timeout: float | None = None,
timeout: float | None = None, # noqa: ASYNC109 - drives a process timeout task
manager_uri: str = "http://127.0.0.1:3999",
ctx: Context,
is_sync: bool,
Expand Down
2 changes: 1 addition & 1 deletion backend/protocol_rpc/app_lifespan.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ def get_session() -> Session:
# Register handler for validator change events
async def handle_validator_change(event_data):
"""Reload validators when they change."""
logger.info(f"RPC worker reloading validators due to change event")
logger.info("RPC worker reloading validators due to change event")
await validators_manager.restart()

redis_subscriber.register_handler("validator_created", handle_validator_change)
Expand Down
6 changes: 3 additions & 3 deletions backend/protocol_rpc/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,20 +469,20 @@ async def check_provider_is_available(

if len(res) != 1:
genvm_manager.logger.error(
f"LLM provider check failed", provider=provider, result=res
"LLM provider check failed", provider=provider, result=res
)
return False
res = res[0]
if (text_response := res.get("response")) is None:
genvm_manager.logger.error(
f"LLM provider check failed", provider=provider, result=res
"LLM provider check failed", provider=provider, result=res
)
return False

what_returned = text_response.strip().lower()
if what_returned != "ok":
genvm_manager.logger.error(
f"LLM provider check failed", provider=provider, text_response=text_response
"LLM provider check failed", provider=provider, text_response=text_response
)
return False
return True
Expand Down
17 changes: 16 additions & 1 deletion backend/protocol_rpc/message_handler/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import os
import json
import copy
Expand Down Expand Up @@ -47,8 +48,23 @@ def __init__(self, socketio: SocketIO, config: GlobalConfiguration):
self.socketio = socketio
self.config = config
self.client_session_id = None
self._pending_tasks: set[asyncio.Task] = set()
# Logging is configured at app startup

def _track_background_task(self, task: asyncio.Task) -> None:
"""Retain a background task and report failures until it completes."""
self._pending_tasks.add(task)
task.add_done_callback(self._background_task_done)

def _background_task_done(self, task: asyncio.Task) -> None:
self._pending_tasks.discard(task)
if task.cancelled():
return

exception = task.exception()
if exception is not None:
logger.opt(exception=exception).error("Background message delivery failed")

def with_client_session(self, client_session_id: str):
new_msg_handler = MessageHandler(self.socketio, self.config)
new_msg_handler.client_session_id = client_session_id
Expand Down Expand Up @@ -317,7 +333,6 @@ def setup_loguru_config():

# Get log level from environment
log_level = os.environ.get("LOG_LEVEL", "INFO").upper()
logging_env = os.environ.get("LOGCONFIG", "dev")

# Console handler with colors
logger.add(
Expand Down
21 changes: 20 additions & 1 deletion backend/protocol_rpc/message_handler/fastapi_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,25 @@ def __init__(self, broadcast: Broadcast, config: GlobalConfiguration):
self.broadcast = broadcast
self.config = config
self.client_session_id = None
self._pending_tasks: set[asyncio.Task] = set()

def _track_background_task(self, task: asyncio.Task) -> None:
"""Retain a background task and report failures until it completes."""
self._pending_tasks.add(task)
task.add_done_callback(self._background_task_done)

def _background_task_done(self, task: asyncio.Task) -> None:
self._pending_tasks.discard(task)
if task.cancelled():
return

exception = task.exception()
if exception is not None:
logger.opt(exception=exception).error("Background broadcast publish failed")

def with_client_session(self, client_session_id: str):
new_msg_handler = MessageHandler(self.broadcast, self.config)
new_msg_handler._pending_tasks = self._pending_tasks
new_msg_handler.client_session_id = client_session_id
return new_msg_handler

Expand Down Expand Up @@ -83,7 +99,10 @@ def _publish(self, channel: str, payload: dict[str, Any]) -> None:
if not loop.is_running():
return

loop.create_task(self.broadcast.publish(channel=channel, message=message))
task = loop.create_task(
self.broadcast.publish(channel=channel, message=message)
)
self._track_background_task(task)

def _socket_emit(self, log_event: LogEvent) -> None:
"""Emit a log event via broadcast channels.
Expand Down
3 changes: 2 additions & 1 deletion backend/protocol_rpc/message_handler/redis_worker_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ def _socket_emit(self, log_event: LogEvent):
loop = asyncio.get_event_loop()
if loop.is_running():
# Schedule the async send operation
asyncio.create_task(self._publish_to_redis(log_event))
task = asyncio.create_task(self._publish_to_redis(log_event))
self._track_background_task(task)
else:
# If no loop is running, run it synchronously
asyncio.run(self._publish_to_redis(log_event))
Expand Down
3 changes: 2 additions & 1 deletion backend/protocol_rpc/message_handler/worker_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ def _socket_emit(self, log_event: LogEvent):
loop = asyncio.get_event_loop()
if loop.is_running():
# Schedule the async send operation
asyncio.create_task(self._send_event_to_server(log_event))
task = asyncio.create_task(self._send_event_to_server(log_event))
self._track_background_task(task)
else:
# If no loop is running, try to run it synchronously
asyncio.run(self._send_event_to_server(log_event))
Expand Down
1 change: 1 addition & 0 deletions backend/protocol_rpc/validators_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ async def initialize_validators(
desired_hash = _desired_config_hash(validators_json)
except (TypeError, KeyError) as e:
# Bad config fields — fall through to the creation loop which gives a better error
logger.warning(f"Unable to hash validators config; validation will report: {e}")
desired_hash = None
current_hash = _current_config_hash(validators_manager.registry)

Expand Down
8 changes: 8 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
target-version = "py312"
include = ["backend/**/*.py"]
extend-exclude = ["backend/node/genvm/origin/keccak.py"]

[lint]
select = ["E9", "F", "ASYNC", "RUF006"]
# Star-import cleanup requires restructuring and is tracked separately.
ignore = ["F403", "F405"]
20 changes: 0 additions & 20 deletions sonar-project.properties

This file was deleted.

Loading