Skip to content

Commit

Permalink
feat(backend): Add persistence layer
Browse files Browse the repository at this point in the history
- SQLModel with SQLite database
- Add alembic for migrations, automatically invoked in container
- Pydantic settings for database connection
- Add volume for database persistence in k8s deployment
- Bump pre-commit hooks
  • Loading branch information
AdrianoKF committed Oct 30, 2024
1 parent ff36c96 commit 2b7e669
Show file tree
Hide file tree
Showing 20 changed files with 442 additions and 39 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ repos:
- id: end-of-file-fixer
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
rev: v0.7.1
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.11.2
rev: v1.13.0
hooks:
# See https://github.com/pre-commit/mirrors-mypy/blob/main/.pre-commit-hooks.yaml
- id: mypy
Expand All @@ -31,7 +31,7 @@ repos:
--install-types,
]
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.4.19
rev: 0.4.28
hooks:
- id: uv-lock
name: Lock project dependencies
2 changes: 2 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Development database
jobq.db
26 changes: 21 additions & 5 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ ARG PYTHON_VERSION=3.12-slim
# Build stage
FROM python:${PYTHON_VERSION} AS build

# Compile bytecode
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
ENV UV_COMPILE_BYTECODE=1

# uv Cache
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
ENV UV_LINK_MODE=copy

RUN apt-get update && \
apt-get install -y git && \
rm -rf /var/lib/apt/lists/*
Expand All @@ -13,20 +21,28 @@ RUN pip install --no-cache-dir --upgrade uv
ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_AAI_JOBQ_SERVER=0.0.0

WORKDIR /code
COPY ./uv.lock uv.lock
COPY ./pyproject.toml pyproject.toml
RUN uv sync --locked
COPY ./alembic.ini alembic.ini

RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project

COPY ./pyproject.toml ./uv.lock ./alembic.ini /code/
COPY ./src /code/src
RUN uv pip install --no-deps .

RUN --mount=type=cache,target=/root/.cache/uv \
uv sync

# Runtime stage
FROM python:${PYTHON_VERSION}

WORKDIR /code
COPY scripts/entrypoint.sh /entrypoint.sh
COPY --chown=nobody:nogroup --from=build /code /code

USER nobody

CMD ["/code/.venv/bin/uvicorn", "jobq_server.__main__:app", "--host", "0.0.0.0", "--port", "8000"]
ENV PYTHONUNBUFFERED=1

CMD ["/entrypoint.sh"]
71 changes: 71 additions & 0 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# A generic, single database configuration.

[alembic]
# path to migration scripts
script_location = src/jobq_server/alembic

# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =

# max length of characters to apply to the
# "slug" field
#truncate_slug_length = 40

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false

# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false

# version location specification; this defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat alembic/versions

# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8

# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
4 changes: 0 additions & 4 deletions backend/deploy/jobq-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,10 @@ Helm chart for the jobq backend server
| image.repository | string | `"ghcr.io/aai-institute/jobq-server"` | |
| image.tag | string | `""` | |
| imagePullSecrets | list | `[]` | |
| livenessProbe.httpGet.path | string | `"/health"` | |
| livenessProbe.httpGet.port | string | `"http"` | |
| nameOverride | string | `""` | |
| nodeSelector | object | `{}` | |
| podAnnotations | object | `{}` | |
| podLabels | object | `{}` | |
| readinessProbe.httpGet.path | string | `"/health"` | |
| readinessProbe.httpGet.port | string | `"http"` | |
| replicaCount | int | `1` | |
| resources | object | `{}` | |
| service.port | int | `8000` | |
Expand Down
25 changes: 20 additions & 5 deletions backend/deploy/jobq-server/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ metadata:
{{- include "jobq-server.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
# FIXME: Consider the Recreate deployment strategy to prevent concurrent access to the database
selector:
matchLabels:
{{- include "jobq-server.selectorLabels" . | nindent 6 }}
Expand Down Expand Up @@ -43,20 +44,34 @@ spec:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
env:
- name: DB_DSN
value: "sqlite:////data/jobq.db"
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
httpGet:
path: /health
port: http
initialDelaySeconds: 2
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
httpGet:
path: /health
port: http
initialDelaySeconds: 2
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.volumeMounts }}
volumeMounts:
- name: db-volume
mountPath: /data
{{- with .Values.volumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumes }}
volumes:
- name: db-volume
persistentVolumeClaim:
claimName: {{ include "jobq-server.fullname" . }}
{{- with .Values.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
Expand Down
10 changes: 10 additions & 0 deletions backend/deploy/jobq-server/templates/pvc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "jobq-server.fullname" . }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 256Mi
9 changes: 0 additions & 9 deletions backend/deploy/jobq-server/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,6 @@ resources:
# cpu: 100m
# memory: 128Mi

livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /health
port: http

# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
Expand Down
13 changes: 12 additions & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ maintainers = [
{ name = "Adrian Rumpold", email = "[email protected]" },
]
license = { text = "Apache-2.0" }
dependencies = ["fastapi", "uvicorn", "docker", "kubernetes", "aai-jobq"]
dependencies = [
"fastapi",
"uvicorn",
"docker",
"kubernetes",
"aai-jobq",
"sqlmodel>=0.0.22",
"alembic>=1.13.3",
"pydantic-settings>=2.6.0",
]
dynamic = ["version"]

[project.optional-dependencies]
Expand All @@ -38,6 +47,7 @@ root = ".."
[tool.ruff]
extend = "../pyproject.toml"
src = ["src"]
extend-exclude = ["src/jobq_server/alembic"]

[tool.mypy]
ignore_missing_imports = true
Expand All @@ -48,6 +58,7 @@ strict_optional = true
warn_unreachable = true
show_column_numbers = true
show_absolute_path = true
exclude = ["src/jobq_server/alembic"]

[tool.coverage.report]
exclude_also = ["@overload", "raise NotImplementedError", "if TYPE_CHECKING:"]
Expand Down
6 changes: 6 additions & 0 deletions backend/scripts/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash

set -eux

/code/.venv/bin/alembic upgrade head
/code/.venv/bin/uvicorn jobq_server.__main__:app --host 0.0.0.0 --port 8000
2 changes: 2 additions & 0 deletions backend/skaffold.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ build:
- image: ghcr.io/aai-institute/jobq-server
docker:
dockerfile: Dockerfile
local:
useBuildkit: true
deploy:
helm:
releases:
Expand Down
12 changes: 10 additions & 2 deletions backend/src/jobq_server/__main__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi import FastAPI, Response
from kubernetes import config
from sqlmodel import text

from jobq_server.db import engine
from jobq_server.routers import jobs


Expand All @@ -25,7 +27,13 @@ async def lifespan(app: FastAPI):

@app.get("/health", include_in_schema=False)
async def health():
return {"status": "ok"}
try:
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
return {"status": "ok"}
except Exception:
logging.error("Database connection failed", exc_info=True)
return Response(status_code=503)


# URLs to be excluded from Uvicorn access logging
Expand Down
Loading

0 comments on commit 2b7e669

Please sign in to comment.