From 2dfdad2f6bf1af0a6f441790c659e17c5dc57d4f Mon Sep 17 00:00:00 2001 From: Toby Jennings Date: Thu, 12 Dec 2024 11:21:43 -0600 Subject: [PATCH] WIP --- .github/workflows/ci.yaml | 14 +++++++- docker-compose.yaml | 14 ++++++++ docker/Dockerfile | 24 +++++++++---- src/lsst/cmservice/config.py | 3 ++ src/lsst/cmservice/daemon.py | 70 +++++++++++++++++++++++++++++++++--- 5 files changed, 113 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9567eee22..bf3a59d55 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -92,10 +92,22 @@ jobs: with: fetch-depth: 0 - - uses: lsst-sqre/build-and-push-to-ghcr@v1 + - uses: lsst-sqre/build-and-push-to-ghcr@tickets/DM-48192 id: build-service with: dockerfile: docker/Dockerfile + target: cmservice + image: ${{ github.repository }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - uses: lsst-sqre/build-and-push-to-ghcr@tickets/DM-48192 + id: build-worker + with: + dockerfile: docker/Dockerfile + target: cmworker + build-args: | + PYTHON_VERSION=3.11 + UV_VERSION=0.5 image: ${{ github.repository }} github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/docker-compose.yaml b/docker-compose.yaml index 9d3a05eee..9da82adbb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,6 +22,8 @@ services: CM_DATABASE_ECHO: true CM_DATABASE_URL: postgresql://cm-service@postgresql:5432/cm-service CM_DATABASE_PASSWORD: INSECURE-PASSWORD + networks: + - cmservice depends_on: postgresql: condition: service_healthy @@ -33,6 +35,7 @@ services: build: context: . dockerfile: docker/Dockerfile + target: cmservice entrypoint: - uvicorn command: @@ -44,6 +47,8 @@ services: environment: *cmenv ports: - "8080:8080" + networks: + - cmservice depends_on: init-db: condition: service_completed_successfully @@ -55,12 +60,15 @@ services: build: context: . dockerfile: docker/Dockerfile + target: cmworker entrypoint: - /opt/venv/bin/python3 - -m command: - lsst.cmservice.daemon environment: *cmenv + networks: + - cmservice depends_on: init-db: condition: service_completed_successfully @@ -74,6 +82,8 @@ services: POSTGRES_DB: "cm-service" ports: - "5432" + networks: + - cmservice volumes: - "pgsql:/var/lib/postgresql/data" healthcheck: @@ -87,3 +97,7 @@ services: volumes: pgsql: + +networks: + cmservice: + driver: bridge diff --git a/docker/Dockerfile b/docker/Dockerfile index a0be4123b..8e3d96863 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,13 +1,18 @@ # syntax=docker/dockerfile:1 -ARG PYTHON_VERSION=3.11 -ARG UV_VERSION=0.5 +ARG PYTHON_VERSION +ARG UV_VERSION #============================================================================== # UV SOURCE IMAGE FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv +# HTCONDOR SOURCE IMAGE +FROM continuumio/miniconda3:latest AS htcondor +RUN conda create -p /opt/htcondor -c conda-forge htcondor-utils --no-default-packages + + #============================================================================== # BASE IMAGE FROM python:${PYTHON_VERSION}-slim-bookworm AS base-image @@ -69,8 +74,8 @@ ENDRUN #============================================================================== -# RUNTIME IMAGE -FROM base-image AS runtime-image +# RUNTIME IMAGE - CM-Service +FROM base-image AS cmservice # Expose the API port EXPOSE 8080 @@ -105,5 +110,12 @@ EOF ENTRYPOINT ["./docker-entrypoint.sh"] CMD ["lsst.cmservice.main:app", "--host", "0.0.0.0", "--port", "8080"] -# ENTRYPOINT ["/opt/venv/bin/python3", "-m"] -# CMD ["lsst.cmservice.daemon"] + +#============================================================================== +# RUNTIME IMAGE - Daemon +FROM cmservice AS cmworker + +COPY --from=htcondor /opt/htcondor /opt/htcondor + +ENTRYPOINT ["/opt/venv/bin/python3", "-m"] +CMD ["lsst.cmservice.daemon"] diff --git a/src/lsst/cmservice/config.py b/src/lsst/cmservice/config.py index 334d0d90e..06862e351 100644 --- a/src/lsst/cmservice/config.py +++ b/src/lsst/cmservice/config.py @@ -1,9 +1,12 @@ +from dotenv import load_dotenv from pydantic import Field from pydantic_settings import BaseSettings from safir.logging import LogLevel, Profile __all__ = ["Configuration", "config"] +load_dotenv() + class Configuration(BaseSettings): """Configuration for cm-service.""" diff --git a/src/lsst/cmservice/daemon.py b/src/lsst/cmservice/daemon.py index deb237199..d221329b0 100644 --- a/src/lsst/cmservice/daemon.py +++ b/src/lsst/cmservice/daemon.py @@ -1,14 +1,68 @@ -import asyncio +from asyncio import create_task, sleep +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager from datetime import datetime, timedelta -from time import sleep import structlog +import uvicorn +from fastapi import APIRouter, FastAPI, HTTPException from safir.database import create_async_session, create_database_engine +from . import __version__ from .common.daemon import daemon_iteration from .config import config +class State: + """A class defining an application State object. + + Attributes + ---------- + tasks : set + Set of coroutine tasks that have been added to the event loop. + """ + + tasks: set + + def __init__(self) -> None: + self.tasks = set() + + +health_router = APIRouter() +"""An API Router for a health endpoint""" + +state = State() +"""A global State object""" + + +@health_router.get("/healthz", tags=["internal", "health"]) +async def get_healthz() -> dict: + server_ok = True + health_response = {} + + for task in state.tasks: + task_response: dict[str, bool | str | None] = {"task_running": True, "task_exception": None} + if task.done(): + server_ok = False + task_response["task_running"] = False + task_response["task_exception"] = str(task.exception()) + health_response[task.get_name()] = task_response + + if not server_ok: + raise HTTPException(status_code=500, detail=health_response) + else: + return health_response + + +@asynccontextmanager +async def lifespan(_: FastAPI) -> AsyncGenerator: + # start + daemon = create_task(main_loop(), name="daemon") + state.tasks.add(daemon) + yield + # stop + + async def main_loop() -> None: logger = structlog.get_logger(config.logger_name) engine = create_database_engine(config.database_url, config.database_password) @@ -26,12 +80,18 @@ async def main_loop() -> None: logger.info(f"Daemon completed {iteration_count} iterations in {delta_seconds} seconds.") last_log_time = datetime.now() await daemon_iteration(session) - sleep(15) + await sleep(15) def main() -> None: - loop = asyncio.get_event_loop() - loop.run_until_complete(main_loop()) + app = FastAPI( + lifespan=lifespan, + version=__version__, + ) + + app.include_router(health_router) + + uvicorn.run(app, host="0.0.0.0", port=8081) if __name__ == "__main__":