Skip to content

Commit 2b7e669

Browse files
committed
feat(backend): Add persistence layer
- 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
1 parent ff36c96 commit 2b7e669

20 files changed

+442
-39
lines changed

.pre-commit-config.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ repos:
1111
- id: end-of-file-fixer
1212
- id: mixed-line-ending
1313
- repo: https://github.com/astral-sh/ruff-pre-commit
14-
rev: v0.6.9
14+
rev: v0.7.1
1515
hooks:
1616
- id: ruff
1717
args: [--fix]
1818
- id: ruff-format
1919
- repo: https://github.com/pre-commit/mirrors-mypy
20-
rev: v1.11.2
20+
rev: v1.13.0
2121
hooks:
2222
# See https://github.com/pre-commit/mirrors-mypy/blob/main/.pre-commit-hooks.yaml
2323
- id: mypy
@@ -31,7 +31,7 @@ repos:
3131
--install-types,
3232
]
3333
- repo: https://github.com/astral-sh/uv-pre-commit
34-
rev: 0.4.19
34+
rev: 0.4.28
3535
hooks:
3636
- id: uv-lock
3737
name: Lock project dependencies

backend/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Development database
2+
jobq.db

backend/Dockerfile

+21-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ ARG PYTHON_VERSION=3.12-slim
33
# Build stage
44
FROM python:${PYTHON_VERSION} AS build
55

6+
# Compile bytecode
7+
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
8+
ENV UV_COMPILE_BYTECODE=1
9+
10+
# uv Cache
11+
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
12+
ENV UV_LINK_MODE=copy
13+
614
RUN apt-get update && \
715
apt-get install -y git && \
816
rm -rf /var/lib/apt/lists/*
@@ -13,20 +21,28 @@ RUN pip install --no-cache-dir --upgrade uv
1321
ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_AAI_JOBQ_SERVER=0.0.0
1422

1523
WORKDIR /code
16-
COPY ./uv.lock uv.lock
17-
COPY ./pyproject.toml pyproject.toml
18-
RUN uv sync --locked
24+
COPY ./alembic.ini alembic.ini
1925

26+
RUN --mount=type=cache,target=/root/.cache/uv \
27+
--mount=type=bind,source=uv.lock,target=uv.lock \
28+
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
29+
uv sync --frozen --no-install-project
2030

31+
COPY ./pyproject.toml ./uv.lock ./alembic.ini /code/
2132
COPY ./src /code/src
22-
RUN uv pip install --no-deps .
33+
34+
RUN --mount=type=cache,target=/root/.cache/uv \
35+
uv sync
2336

2437
# Runtime stage
2538
FROM python:${PYTHON_VERSION}
2639

2740
WORKDIR /code
41+
COPY scripts/entrypoint.sh /entrypoint.sh
2842
COPY --chown=nobody:nogroup --from=build /code /code
2943

3044
USER nobody
3145

32-
CMD ["/code/.venv/bin/uvicorn", "jobq_server.__main__:app", "--host", "0.0.0.0", "--port", "8000"]
46+
ENV PYTHONUNBUFFERED=1
47+
48+
CMD ["/entrypoint.sh"]

backend/alembic.ini

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# A generic, single database configuration.
2+
3+
[alembic]
4+
# path to migration scripts
5+
script_location = src/jobq_server/alembic
6+
7+
# template used to generate migration files
8+
# file_template = %%(rev)s_%%(slug)s
9+
10+
# timezone to use when rendering the date
11+
# within the migration file as well as the filename.
12+
# string value is passed to dateutil.tz.gettz()
13+
# leave blank for localtime
14+
# timezone =
15+
16+
# max length of characters to apply to the
17+
# "slug" field
18+
#truncate_slug_length = 40
19+
20+
# set to 'true' to run the environment during
21+
# the 'revision' command, regardless of autogenerate
22+
# revision_environment = false
23+
24+
# set to 'true' to allow .pyc and .pyo files without
25+
# a source .py file to be detected as revisions in the
26+
# versions/ directory
27+
# sourceless = false
28+
29+
# version location specification; this defaults
30+
# to alembic/versions. When using multiple version
31+
# directories, initial revisions must be specified with --version-path
32+
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
33+
34+
# the output encoding used when revision files
35+
# are written from script.py.mako
36+
# output_encoding = utf-8
37+
38+
# Logging configuration
39+
[loggers]
40+
keys = root,sqlalchemy,alembic
41+
42+
[handlers]
43+
keys = console
44+
45+
[formatters]
46+
keys = generic
47+
48+
[logger_root]
49+
level = WARN
50+
handlers = console
51+
qualname =
52+
53+
[logger_sqlalchemy]
54+
level = WARN
55+
handlers =
56+
qualname = sqlalchemy.engine
57+
58+
[logger_alembic]
59+
level = INFO
60+
handlers =
61+
qualname = alembic
62+
63+
[handler_console]
64+
class = StreamHandler
65+
args = (sys.stderr,)
66+
level = NOTSET
67+
formatter = generic
68+
69+
[formatter_generic]
70+
format = %(levelname)-5.5s [%(name)s] %(message)s
71+
datefmt = %H:%M:%S

backend/deploy/jobq-server/README.md

-4
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,10 @@ Helm chart for the jobq backend server
2020
| image.repository | string | `"ghcr.io/aai-institute/jobq-server"` | |
2121
| image.tag | string | `""` | |
2222
| imagePullSecrets | list | `[]` | |
23-
| livenessProbe.httpGet.path | string | `"/health"` | |
24-
| livenessProbe.httpGet.port | string | `"http"` | |
2523
| nameOverride | string | `""` | |
2624
| nodeSelector | object | `{}` | |
2725
| podAnnotations | object | `{}` | |
2826
| podLabels | object | `{}` | |
29-
| readinessProbe.httpGet.path | string | `"/health"` | |
30-
| readinessProbe.httpGet.port | string | `"http"` | |
3127
| replicaCount | int | `1` | |
3228
| resources | object | `{}` | |
3329
| service.port | int | `8000` | |

backend/deploy/jobq-server/templates/deployment.yaml

+20-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ metadata:
66
{{- include "jobq-server.labels" . | nindent 4 }}
77
spec:
88
replicas: {{ .Values.replicaCount }}
9+
# FIXME: Consider the Recreate deployment strategy to prevent concurrent access to the database
910
selector:
1011
matchLabels:
1112
{{- include "jobq-server.selectorLabels" . | nindent 6 }}
@@ -43,20 +44,34 @@ spec:
4344
- name: http
4445
containerPort: {{ .Values.service.port }}
4546
protocol: TCP
47+
env:
48+
- name: DB_DSN
49+
value: "sqlite:////data/jobq.db"
4650
livenessProbe:
47-
{{- toYaml .Values.livenessProbe | nindent 12 }}
51+
httpGet:
52+
path: /health
53+
port: http
54+
initialDelaySeconds: 2
4855
readinessProbe:
49-
{{- toYaml .Values.readinessProbe | nindent 12 }}
56+
httpGet:
57+
path: /health
58+
port: http
59+
initialDelaySeconds: 2
5060
resources:
5161
{{- toYaml .Values.resources | nindent 12 }}
52-
{{- with .Values.volumeMounts }}
5362
volumeMounts:
63+
- name: db-volume
64+
mountPath: /data
65+
{{- with .Values.volumeMounts }}
5466
{{- toYaml . | nindent 12 }}
5567
{{- end }}
56-
{{- with .Values.volumes }}
5768
volumes:
69+
- name: db-volume
70+
persistentVolumeClaim:
71+
claimName: {{ include "jobq-server.fullname" . }}
72+
{{- with .Values.volumes }}
5873
{{- toYaml . | nindent 8 }}
59-
{{- end }}
74+
{{- end }}
6075
{{- with .Values.nodeSelector }}
6176
nodeSelector:
6277
{{- toYaml . | nindent 8 }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: v1
2+
kind: PersistentVolumeClaim
3+
metadata:
4+
name: {{ include "jobq-server.fullname" . }}
5+
spec:
6+
accessModes:
7+
- ReadWriteOnce
8+
resources:
9+
requests:
10+
storage: 256Mi

backend/deploy/jobq-server/values.yaml

-9
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,6 @@ resources:
4141
# cpu: 100m
4242
# memory: 128Mi
4343

44-
livenessProbe:
45-
httpGet:
46-
path: /health
47-
port: http
48-
readinessProbe:
49-
httpGet:
50-
path: /health
51-
port: http
52-
5344
# Additional volumes on the output Deployment definition.
5445
volumes: []
5546
# - name: foo

backend/pyproject.toml

+12-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ maintainers = [
1616
{ name = "Adrian Rumpold", email = "a.rumpold@appliedai-institute.de" },
1717
]
1818
license = { text = "Apache-2.0" }
19-
dependencies = ["fastapi", "uvicorn", "docker", "kubernetes", "aai-jobq"]
19+
dependencies = [
20+
"fastapi",
21+
"uvicorn",
22+
"docker",
23+
"kubernetes",
24+
"aai-jobq",
25+
"sqlmodel>=0.0.22",
26+
"alembic>=1.13.3",
27+
"pydantic-settings>=2.6.0",
28+
]
2029
dynamic = ["version"]
2130

2231
[project.optional-dependencies]
@@ -38,6 +47,7 @@ root = ".."
3847
[tool.ruff]
3948
extend = "../pyproject.toml"
4049
src = ["src"]
50+
extend-exclude = ["src/jobq_server/alembic"]
4151

4252
[tool.mypy]
4353
ignore_missing_imports = true
@@ -48,6 +58,7 @@ strict_optional = true
4858
warn_unreachable = true
4959
show_column_numbers = true
5060
show_absolute_path = true
61+
exclude = ["src/jobq_server/alembic"]
5162

5263
[tool.coverage.report]
5364
exclude_also = ["@overload", "raise NotImplementedError", "if TYPE_CHECKING:"]

backend/scripts/entrypoint.sh

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env bash
2+
3+
set -eux
4+
5+
/code/.venv/bin/alembic upgrade head
6+
/code/.venv/bin/uvicorn jobq_server.__main__:app --host 0.0.0.0 --port 8000

backend/skaffold.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ build:
77
- image: ghcr.io/aai-institute/jobq-server
88
docker:
99
dockerfile: Dockerfile
10+
local:
11+
useBuildkit: true
1012
deploy:
1113
helm:
1214
releases:

backend/src/jobq_server/__main__.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import logging
22
from contextlib import asynccontextmanager
33

4-
from fastapi import FastAPI
4+
from fastapi import FastAPI, Response
55
from kubernetes import config
6+
from sqlmodel import text
67

8+
from jobq_server.db import engine
79
from jobq_server.routers import jobs
810

911

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

2628
@app.get("/health", include_in_schema=False)
2729
async def health():
28-
return {"status": "ok"}
30+
try:
31+
with engine.connect() as conn:
32+
conn.execute(text("SELECT 1"))
33+
return {"status": "ok"}
34+
except Exception:
35+
logging.error("Database connection failed", exc_info=True)
36+
return Response(status_code=503)
2937

3038

3139
# URLs to be excluded from Uvicorn access logging

0 commit comments

Comments
 (0)