Source code for simstack.core.services.file_transfer_service

import asyncio
import logging
from pathlib import Path
from typing import Any

from simstack.core.context import context
from simstack.core.services.base_service import BaseService
from simstack.models.parameters import Resource
from simstack.util.file_transfer_client import (
    FileTransferClient,
    FileTransferError,
    resolve_instance_path,
)

logger = logging.getLogger("NodeRunner")


[docs] class FileTransferService(BaseService): """Poll the SimStack server for hidden file transfer uploads assigned to this runner.""" def __init__( self, resource: Resource, interval: int = 10, max_concurrent: int = 2, shutdown_event: asyncio.Event | None = None, ) -> None: super().__init__( "FileTransfer", resource, interval, shutdown_event=None ) self._semaphore = asyncio.Semaphore(max_concurrent) self._running_tasks: set[asyncio.Task[bool]] = set() self._client: FileTransferClient | None = None self._configuration_checked = False def _get_client(self) -> FileTransferClient | None: if self._client is not None: return self._client if self._configuration_checked: return None self._configuration_checked = True self._client = FileTransferClient.from_context(required=False) if self._client is None: logger.info( "File transfer service disabled; SIMSTACK_SERVER_URL or SIMSTACK_RUNNER_TOKEN is not configured." ) return self._client
[docs] async def execute(self) -> None: completed_tasks = {task for task in self._running_tasks if task.done()} for task in completed_tasks: try: await task except Exception as exc: logger.exception("File transfer task completed with error: %s", exc) self._running_tasks.remove(task) client = self._get_client() if client is None: return try: transfers = client.list_transfers(role="source", status="created", limit=10) except Exception as exc: logger.warning("Unable to poll file transfers: %s", exc) raise exc for transfer in transfers: transfer_id = str(transfer.get("transfer_id") or "") if not transfer_id: continue if any( getattr(task, "transfer_id", None) == transfer_id for task in self._running_tasks ): continue task = asyncio.create_task(self._run_with_semaphore(client, transfer)) setattr(task, "transfer_id", transfer_id) self._running_tasks.add(task)
async def _run_with_semaphore( self, client: FileTransferClient, transfer: dict[str, Any] ) -> bool: async with self._semaphore: return await asyncio.to_thread(self._upload_transfer, client, transfer) def _upload_transfer( self, client: FileTransferClient, transfer: dict[str, Any] ) -> bool: transfer_id = str(transfer.get("transfer_id")) source_path = transfer.get("source_path") if not source_path: client.fail_transfer( transfer_id, error_message="Transfer is missing source_path metadata.", error_code="SOURCE_PATH_MISSING", ) return False path = resolve_instance_path(str(source_path), Path(context.config.workdir)) try: client.upload_file(transfer_id, path) logger.info("Uploaded FileStack transfer %s from %s", transfer_id, path) return True except FileTransferError as exc: logger.warning("File transfer upload failed for %s: %s", transfer_id, exc) try: client.fail_transfer( transfer_id, error_message=str(exc), error_code="SOURCE_UPLOAD_FAILED", ) except Exception: logger.exception( "Failed to report failed file transfer %s", transfer_id ) return False