diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 0000000..212ff2e --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,27 @@ +name: 'Run tests' + +on: + pull_request: + branches: + - main + +jobs: + run_tests: + name: Run Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Create venv + run: | + python3 -m venv venv + . ./venv/bin/activate + pip install -r requirements.txt + pip install black==23.7.0 pylint==2.17.5 + + - name: Run tests + run: | + set -e + . ./venv/bin/activate + black --check . + pylint *.py deps diff --git a/conf.py b/conf.py index e7bca57..59f835c 100644 --- a/conf.py +++ b/conf.py @@ -1,7 +1,10 @@ +""" Configuration settings """ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): + """Settings class, loaded from .env or environment vars""" + model_config = SettingsConfigDict(env_file=".env") arduino_cli_path: str = "arduino-cli" @@ -15,6 +18,8 @@ class Settings(BaseSettings): # Code cache settings max_code_caches: int = 100 code_cache_duration: int = 3600 + # Max number of concurrent compile tasks + max_concurrent_tasks: int = 10 settings = Settings() diff --git a/deps/cache.py b/deps/cache.py index f0e0fbc..3e3f631 100644 --- a/deps/cache.py +++ b/deps/cache.py @@ -1,3 +1,4 @@ +""" Code for caching compiled C++ code """ from hashlib import md5 from cachetools import TTLCache diff --git a/deps/session.py b/deps/session.py index fad4d8a..7e9cd94 100644 --- a/deps/session.py +++ b/deps/session.py @@ -1,3 +1,4 @@ +""" Manage session concurrency """ import uuid from typing import Annotated @@ -13,6 +14,7 @@ def get_session_id( response: Response, session_id: Annotated[str | None, Cookie()] = None ): + """Generate or get a consistent session ID for an anonymous user""" if not session_id: # First time user, create a new session session_id = uuid.uuid4().hex diff --git a/main.py b/main.py index c2d5ce7..d1ccde2 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +""" Leaphy compiler backend webservice """ import asyncio import tempfile from os import path @@ -20,6 +21,9 @@ allow_headers=["*"], ) +# Limit compiler concurrency to prevent overloading the vm +semaphore = asyncio.Semaphore(settings.max_concurrent_tasks) + async def _install_libraries(libraries: list[Library]): # Install required libraries @@ -46,8 +50,8 @@ async def _compile_sketch(sketch: Sketch) -> dict[str, str]: sketch_path = f"{dir_name}/{file_name}" # Write the sketch to a temp .ino file - async with aiofiles.open(sketch_path, "w+") as f: - await f.write(sketch.source_code) + async with aiofiles.open(sketch_path, "w+") as _f: + await _f.write(sketch.source_code) compiler = await asyncio.create_subprocess_exec( settings.arduino_cli_path, @@ -64,12 +68,13 @@ async def _compile_sketch(sketch: Sketch) -> dict[str, str]: if compiler.returncode != 0: raise HTTPException(500, stderr.decode() + stdout.decode()) - async with aiofiles.open(f"{sketch_path}.hex", "r") as f: - return {"hex": await f.read()} + async with aiofiles.open(f"{sketch_path}.hex", "r") as _f: + return {"hex": await _f.read()} @app.post("/compile/cpp") async def compile_cpp(sketch: Sketch, session_id: Session) -> dict[str, str]: + """Compile code and return the result in HEX format""" # Make sure there's no more than X compile requests per user sessions[session_id] += 1 @@ -81,9 +86,10 @@ async def compile_cpp(sketch: Sketch, session_id: Session) -> dict[str, str]: return compiled_code # Nope -> compile and store in cache - await _install_libraries(sketch.libraries) - result = await _compile_sketch(sketch) - code_cache[cache_key] = result - return result + async with semaphore: + await _install_libraries(sketch.libraries) + result = await _compile_sketch(sketch) + code_cache[cache_key] = result + return result finally: sessions[session_id] -= 1 diff --git a/models.py b/models.py index ba91378..4403533 100644 --- a/models.py +++ b/models.py @@ -1,3 +1,4 @@ +""" FastAPI models """ from typing import Annotated from pydantic import BaseModel, Field @@ -7,7 +8,8 @@ class Sketch(BaseModel): + """Model representing a arduino Sketch""" + source_code: str - # TODO: make this an enum with supported board types board: str libraries: list[Library] = [] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a62d1e8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.pylint.'MESSAGES CONTROL'] +max-line-length = 120 +disable = "too-few-public-methods, import-error"