Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
9baf44f
test: require Flask Gunicorn gthread profile
tappe9 Sep 27, 2026
9cf93dc
test: prove Flask Gunicorn acceptance red
tappe9 Sep 27, 2026
b10c85c
test: define Gunicorn process validation
tappe9 Sep 27, 2026
0143d38
test: cover Gunicorn process validation red
tappe9 Sep 27, 2026
7d0e54b
test: assert Gunicorn runtime profile at container boundary
tappe9 Sep 27, 2026
2343884
feat: replace Waitress with Gunicorn
tappe9 Sep 27, 2026
908797d
feat: add Gunicorn Flask application factory
tappe9 Sep 27, 2026
a9dbe44
feat: run Flask with one Gunicorn gthread worker
tappe9 Sep 27, 2026
9bb34ea
chore: record Gunicorn in Flask version metadata
tappe9 Sep 27, 2026
eb8d434
fix: validate Flask Gunicorn arbiter and worker
tappe9 Sep 27, 2026
019b5b2
test: validate Gunicorn runtime process model
tappe9 Sep 27, 2026
a7916f5
test: verify Gunicorn profile and generate lock
tappe9 Sep 27, 2026
6c01f3a
fix: preserve generic process validation fixtures
tappe9 Sep 27, 2026
b2f7f9d
test: pin pip for runtime lock generation
tappe9 Sep 27, 2026
4b22054
test: commit generated Flask runtime lock
tappe9 Sep 27, 2026
6a9d864
chore: refresh Flask runtime lock
github-actions[bot] Sep 27, 2026
cb2407e
test: cover Gunicorn worker-local Flask factory
tappe9 Sep 27, 2026
c906c75
chore: regenerate registry projection
tappe9 Sep 27, 2026
9fcb674
docs: describe Flask Gunicorn runtime
tappe9 Sep 27, 2026
f583897
test: run Flask Gunicorn real-container acceptance
tappe9 Sep 27, 2026
908b457
test: use repository unittest discovery path
tappe9 Sep 27, 2026
97c5624
fix: preserve historical Flask version compatibility
tappe9 Sep 27, 2026
1723648
chore: regenerate compatible registry projection
tappe9 Sep 27, 2026
3cfcf9a
test: confirm production Gunicorn profile without publishing
tappe9 Sep 27, 2026
0520e04
test: remove temporary Flask Gunicorn workflow
tappe9 Sep 27, 2026
59a0728
test: update current Flask runtime metadata expectation
tappe9 Sep 27, 2026
469f113
test: update Flask candidate runtime contract
tappe9 Sep 27, 2026
905a64f
test: fix Flask candidate assertion formatting
tappe9 Sep 27, 2026
f6b3649
style: apply shared Ruff formatting
tappe9 Sep 27, 2026
f6fe1a5
style: apply shared Ruff formatting
tappe9 Sep 27, 2026
56c539f
fix: model historical Flask server version alternatives
tappe9 Sep 27, 2026
c90a4f6
feat: validate alternative runtime version fields
tappe9 Sep 27, 2026
112a936
fix: validate current runtime version alternatives
tappe9 Sep 27, 2026
93b2e15
fix: validate historical and current Flask server provenance
tappe9 Sep 27, 2026
5f69a8e
refactor: reuse registry version validation
tappe9 Sep 27, 2026
49bd6fb
chore: regenerate runtime version alternatives
tappe9 Sep 27, 2026
90b7a01
fix: validate alternative Flask server versions in Pages
tappe9 Sep 27, 2026
11c09df
style: format registry imports
tappe9 Sep 27, 2026
f805bb7
refactor: keep Flask server metadata compatibility simple
tappe9 Sep 27, 2026
f80dfe8
refactor: keep Flask server metadata compatibility simple
tappe9 Sep 27, 2026
3877701
refactor: keep Flask server metadata compatibility simple
tappe9 Sep 27, 2026
89bbf56
refactor: make Flask server metadata additive
tappe9 Sep 27, 2026
c38ca3a
chore: regenerate Flask registry projection
tappe9 Sep 27, 2026
c32986c
refactor: scope Gunicorn process validation to Flask
tappe9 Sep 27, 2026
b611e7c
test: capture site compatibility failure
tappe9 Sep 27, 2026
b662ac5
test: reproduce site failure on CI Python
tappe9 Sep 27, 2026
eb70077
test: remove temporary site diagnostic
tappe9 Sep 27, 2026
70cdbbb
test: reproduce shared site failure order
tappe9 Sep 27, 2026
9c891d6
test: capture shared site failure output
tappe9 Sep 27, 2026
511032b
test: remove temporary shared-order diagnostic
tappe9 Sep 27, 2026
8e162e3
test: validate required stack version fields from registry
tappe9 Sep 27, 2026
75b108f
test: remove temporary CI failure capture
tappe9 Sep 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/python-flask/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ COPY --from=dependencies /opt/venv /opt/venv
COPY benchmark_api ./benchmark_api
USER 10001:10001
EXPOSE 8080
ENTRYPOINT ["python", "-m", "benchmark_api.server"]
ENTRYPOINT ["python", "-m", "gunicorn.app.wsgiapp"]
CMD ["--bind", "0.0.0.0:8080", "--workers", "1", "--worker-class", "gthread", "--threads", "1", "--keep-alive", "5", "--timeout", "30", "--graceful-timeout", "5", "--worker-tmp-dir", "/dev/shm", "benchmark_api.server:create_server_app()"]
44 changes: 6 additions & 38 deletions apps/python-flask/benchmark_api/server.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,13 @@
"""Single-process Waitress entry point with explicit graceful resource cleanup."""
"""Gunicorn application factory with worker-local database resources."""

import signal

from waitress.server import create_server
import atexit

from benchmark_api.app import create_app
from benchmark_api.database import close_pool, open_pool


def serve() -> None:
def create_server_app():
"""Create one worker-local Flask app and close its pool when the worker exits."""
pool = open_pool()
server = None
previous = {}
try:
app = create_app(pool)
server = create_server(
app,
host="0.0.0.0",
port=8080,
threads=1,
channel_timeout=30,
cleanup_interval=10,
asyncore_use_poll=True,
)

def stop(_signum, _frame):
server.close()

for sig in (signal.SIGINT, signal.SIGTERM):
previous[sig] = signal.signal(sig, stop)
server.run()
finally:
for sig, handler in previous.items():
signal.signal(sig, handler)
if server is not None:
server.close()
dispatcher = getattr(server, "task_dispatcher", None)
if dispatcher is not None:
dispatcher.shutdown(cancel_pending=False, timeout=5)
close_pool(pool)


if __name__ == "__main__":
serve()
atexit.register(close_pool, pool)
return create_app(pool)
2 changes: 1 addition & 1 deletion apps/python-flask/requirements.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
flask==3.1.3
waitress==3.0.2
gunicorn==26.0.0
psycopg==3.3.5
psycopg-binary==3.3.5
psycopg-pool==3.3.1
14 changes: 9 additions & 5 deletions apps/python-flask/requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# This file is autogenerated by pip-compile with Python 3.14
# by the following command:
#
# pip-compile --generate-hashes --no-index --output-file=apps/python-flask/requirements.lock --strip-extras apps/python-flask/requirements.in
# pip-compile --generate-hashes --no-index --output-file=apps/python-flask/requirements.lock apps/python-flask/requirements.in
#
blinker==1.9.0 \
--hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \
Expand All @@ -16,6 +16,10 @@ flask==3.1.3 \
--hash=sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb \
--hash=sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c
# via -r apps/python-flask/requirements.in
gunicorn==26.0.0 \
--hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \
--hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf
# via -r apps/python-flask/requirements.in
itsdangerous==2.2.0 \
--hash=sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef \
--hash=sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173
Expand Down Expand Up @@ -118,6 +122,10 @@ markupsafe==3.0.3 \
# flask
# jinja2
# werkzeug
packaging==26.3 \
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
# via gunicorn
psycopg==3.3.5 \
--hash=sha256:ce5aa5cdb4f9379f00f487590e5890bfa7df9a164648c969ffa628505e21af4e \
--hash=sha256:d0a3d9ccf5788af054cbd745278cb02401b5c312aeaafbf2c6144460aec47da4
Expand Down Expand Up @@ -187,10 +195,6 @@ typing-extensions==4.16.0 \
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
# via psycopg-pool
waitress==3.0.2 \
--hash=sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f \
--hash=sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e
# via -r apps/python-flask/requirements.in
werkzeug==3.1.8 \
--hash=sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50 \
--hash=sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44
Expand Down
33 changes: 14 additions & 19 deletions apps/python-flask/tests/test_server.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
"""Waitress lifecycle tests for the approved one-process, one-thread profile."""
"""Gunicorn application-factory lifecycle tests."""

from unittest.mock import MagicMock

from benchmark_api import server


def test_server_uses_one_waitress_thread_and_closes_resources(monkeypatch):
def test_factory_opens_worker_local_pool_and_registers_cleanup(monkeypatch):
pool = object()
waitress = MagicMock()
waitress.run.return_value = None
waitress.task_dispatcher = MagicMock()
create_server = MagicMock(return_value=waitress)
app = object()
open_pool = MagicMock(return_value=pool)
close_pool = MagicMock()
monkeypatch.setattr(server, "open_pool", MagicMock(return_value=pool))
create_app = MagicMock(return_value=app)
register = MagicMock()

monkeypatch.setattr(server, "open_pool", open_pool)
monkeypatch.setattr(server, "close_pool", close_pool)
monkeypatch.setattr(server, "create_server", create_server)
monkeypatch.setattr(server, "create_app", MagicMock(return_value=object()))
monkeypatch.setattr(server.signal, "signal", MagicMock(return_value=server.signal.SIG_DFL))
monkeypatch.setattr(server, "create_app", create_app)
monkeypatch.setattr(server.atexit, "register", register)

server.serve()
assert server.create_server_app() is app

kwargs = create_server.call_args.kwargs
assert kwargs["host"] == "0.0.0.0"
assert kwargs["port"] == 8080
assert kwargs["threads"] == 1
waitress.run.assert_called_once_with()
waitress.close.assert_called()
waitress.task_dispatcher.shutdown.assert_called_once_with(cancel_pending=False, timeout=5)
close_pool.assert_called_once_with(pool)
open_pool.assert_called_once_with()
create_app.assert_called_once_with(pool)
register.assert_called_once_with(close_pool, pool)
9 changes: 6 additions & 3 deletions benchmark/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,15 @@ def normalized(arguments):
pieces = row.strip().split(None, 1)
require(len(pieces) == 2 and pieces[0].isdigit(), "invalid process list row")
commands.append(normalized(shlex.split(pieces[1])))
service = state.get("Config", {}).get("Labels", {}).get("com.docker.compose.service")
expected_servers = 2 if service == "python-flask" else 1
require(
commands.count(server) == 1 and all(command in allowed for command in commands),
commands.count(server) == expected_servers
and all(command in allowed for command in commands),
(
"expected one server process and only independent health probes"
f"expected {expected_servers} server process(es) and only independent health probes"
if allow_health_probe
else "expected exactly one server process"
else f"expected exactly {expected_servers} server process(es)"
),
)

Expand Down
2 changes: 1 addition & 1 deletion benchmark/implementations.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{"id":"node-fastify","language":"Node.js","framework":"Fastify","display_name":"Node.js / Fastify","source_path":"apps/node-fastify","version_fields":["node","fastify","pg"],"acceptance_test":"tests/test_node_fastify_service.py","failure_test":"tests/test_node_fastify_acceptance.py"},
{"id":"node-express","language":"Node.js","framework":"Express","display_name":"Node.js / Express","source_path":"apps/node-express","version_fields":["node","express","pg"],"acceptance_test":"tests/test_node_express_service.py","failure_test":"tests/test_node_express_acceptance.py"},
{"id":"python-fastapi","language":"Python","framework":"FastAPI","display_name":"Python / FastAPI","source_path":"apps/python-fastapi","version_fields":["python","fastapi","uvicorn","asyncpg"],"acceptance_test":"tests/test_python_fastapi_service.py","failure_test":"tests/test_python_fastapi_acceptance.py"},
{"id":"python-flask","language":"Python","framework":"Flask","display_name":"Python / Flask","source_path":"apps/python-flask","version_fields":["python","flask","waitress","psycopg","psycopg-binary","psycopg-pool"],"acceptance_test":"tests/test_python_flask_service.py","failure_test":"tests/test_python_flask_acceptance.py"},
{"id":"python-flask","language":"Python","framework":"Flask","display_name":"Python / Flask","source_path":"apps/python-flask","version_fields":["python","flask","psycopg","psycopg-binary","psycopg-pool"],"acceptance_test":"tests/test_python_flask_service.py","failure_test":"tests/test_python_flask_acceptance.py"},
{"id":"java-spring-boot","language":"Java","framework":"Spring Boot","display_name":"Java / Spring Boot","source_path":"apps/java-spring-boot","version_fields":["java","spring-boot","tomcat","postgresql","hikaricp","jackson","gradle"],"acceptance_test":"tests/test_java_spring_boot_service.py","failure_test":"tests/test_java_spring_boot_acceptance.py"}
],
"cohorts": {
Expand Down
12 changes: 6 additions & 6 deletions docs/FLASK.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

- CPython 3.14.7, matching the FastAPI baseline.
- Flask 3.1.3.
- Waitress 3.0.2 as the production WSGI server.
- One server OS process and exactly one Waitress worker thread.
- Gunicorn 26.0.0 as the production WSGI server, using one `gthread` request worker with one thread.
- One Gunicorn arbiter plus exactly one request-processing worker and one request thread.
- psycopg 3.3.5 with the binary implementation package pinned to the same version.
- psycopg-pool 3.3.1 with `min_size=1`, `max_size=10`.
- The same PostgreSQL fixture, parameterized lookup SQL, 1 CPU / 512 MiB container budget, loopback-only host publication, dropped capabilities, no-new-privileges policy, and non-root runtime used by the other implementations.

Flask's development server, Gunicorn master/worker processes, gevent/event-loop monkey patching, extra middleware, response caches, and precomputed CPU results are not used.
Flask's development server, gevent/event-loop monkey patching, extra middleware, response caches, and precomputed CPU results are not used.

## HTTP behavior

Expand All @@ -27,15 +27,15 @@ The implementation provides the unchanged shared contract:

## Startup and shutdown

PostgreSQL pool creation and a real `SELECT 1` readiness query complete before Waitress begins accepting HTTP traffic. SIGINT and SIGTERM close the listener, Waitress drains/shuts down its single task-dispatcher thread with a bounded timeout, and the PostgreSQL pool is then closed.
Each Gunicorn worker creates its own PostgreSQL pool and proves readiness with a real `SELECT 1` before its Flask application is returned. Gunicorn handles SIGTERM and bounded graceful worker shutdown; worker-local pool cleanup is registered with Python `atexit`.

The production image starts directly with:

```text
python -m benchmark_api.server
python -m gunicorn.app.wsgiapp --workers 1 --worker-class gthread --threads 1 … 'benchmark_api.server:create_server_app()'
```

There is no supervisor or second worker process.
Gunicorn uses one arbiter process and one request-processing worker; it does not add request workers beyond the benchmark profile's single worker.

## Verification

Expand Down
1 change: 0 additions & 1 deletion site/registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ export const REGISTRY = {
"version_fields": [
"python",
"flask",
"waitress",
"psycopg",
"psycopg-binary",
"psycopg-pool"
Expand Down
5 changes: 4 additions & 1 deletion tests/eight_stack_cases.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import test from 'node:test';

import { REGISTRY } from '../site/registry.mjs';

const members = ['go-gin', 'go-echo', 'rust-actix', 'rust-axum', 'node-fastify', 'node-express', 'python-fastapi', 'python-flask'];
const legacyMembers = ['go-gin', 'rust-actix', 'node-fastify', 'python-fastapi'];

Expand Down Expand Up @@ -91,7 +93,8 @@ export function registerEightStackTests() {
r => { r.implementations[index].endpoints.reverse(); },
r => { r.implementations[index].endpoints[0].runs.pop(); },
);
for (const field of Object.keys(eight.metadata.versions[members[index]])) {
const spec = REGISTRY.implementations.find(item => item.id === members[index]);
for (const field of spec.version_fields) {
changes.push(r => { delete r.metadata.versions[members[index]][field]; });
}
}
Expand Down
26 changes: 26 additions & 0 deletions tests/test_benchmark_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,32 @@ def execute(arguments, *, timeout, tick=None, cwd=environment.ROOT):
self.assertEqual(result["memory_samples"], 1)


class ProcessValidationTests(unittest.TestCase):
def test_flask_gunicorn_allows_exactly_one_arbiter_and_one_worker(self):
value = state()
value["Config"]["Labels"]["com.docker.compose.service"] = "python-flask"
value["Path"] = "python"
value["Args"] = ["-m", "benchmark_api.server"]
output = (
"PID COMMAND\n10 python -m benchmark_api.server\n11 python -m benchmark_api.server\n"
)
environment.validate_processes(value, output, allow_health_probe=False)

def test_flask_gunicorn_rejects_extra_worker(self):
value = state()
value["Config"]["Labels"]["com.docker.compose.service"] = "python-flask"
value["Path"] = "python"
value["Args"] = ["-m", "benchmark_api.server"]
output = (
"PID COMMAND\n"
"10 python -m benchmark_api.server\n"
"11 python -m benchmark_api.server\n"
"12 python -m benchmark_api.server\n"
)
with self.assertRaises(BenchmarkFailure):
environment.validate_processes(value, output, allow_health_probe=False)


class InstallerTests(unittest.TestCase):
def test_cached_checksum_is_checked_before_executing_binary(self):
with tempfile.TemporaryDirectory() as directory:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_benchmark_java_candidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"python-flask": {
"python": "3.14.7",
"flask": "3.1.3",
"waitress": "3.0.2",
"gunicorn": "26.0.0",
"psycopg": "3.3.5",
"psycopg-binary": "3.3.5",
"psycopg-pool": "3.3.1",
Expand Down
14 changes: 8 additions & 6 deletions tests/test_benchmark_python_flask_candidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@


class PythonFlaskCandidateTests(unittest.TestCase):
def test_flask_profile_is_explicit_and_single_process(self):
def test_flask_profile_is_explicit_and_single_request_worker(self):
self.assertEqual((APP / ".python-version").read_text().strip(), "3.14.7")
requirements = set((APP / "requirements.in").read_text().splitlines())
self.assertIn("flask==3.1.3", requirements)
self.assertIn("waitress==3.0.2", requirements)
self.assertIn("gunicorn==26.0.0", requirements)
self.assertNotIn("waitress==3.0.2", requirements)
self.assertIn("psycopg==3.3.5", requirements)
self.assertIn("psycopg-pool==3.3.1", requirements)
server = (APP / "benchmark_api" / "server.py").read_text()
self.assertIn("threads=1", server)
self.assertIn("create_server", server)
self.assertNotIn("gunicorn", server.lower())
dockerfile = (APP / "Dockerfile").read_text()
self.assertIn('"--workers", "1"', dockerfile)
self.assertIn('"--worker-class", "gthread"', dockerfile)
self.assertIn('"--threads", "1"', dockerfile)
self.assertIn('"--keep-alive", "5"', dockerfile)

def test_flask_is_registered_without_mutating_legacy_cohort(self):
registry = json.loads((ROOT / "benchmark" / "implementations.json").read_text())
Expand Down
15 changes: 9 additions & 6 deletions tests/test_python_flask_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,25 @@ def test_approved_wsgi_profile_is_pinned(self):
self.assertEqual((APP / ".python-version").read_text().strip(), "3.14.7")
requirements = set((APP / "requirements.in").read_text().splitlines())
self.assertIn("flask==3.1.3", requirements)
self.assertIn("waitress==3.0.2", requirements)
self.assertIn("gunicorn==26.0.0", requirements)
self.assertNotIn("waitress==3.0.2", requirements)
self.assertIn("psycopg==3.3.5", requirements)
self.assertIn("psycopg-binary==3.3.5", requirements)
self.assertIn("psycopg-pool==3.3.1", requirements)
server = (APP / "benchmark_api" / "server.py").read_text()
self.assertIn("threads=1", server)
self.assertIn("create_server", server)
self.assertNotIn("gunicorn", server.lower())
dockerfile = (APP / "Dockerfile").read_text()
self.assertIn('"--workers", "1"', dockerfile)
self.assertIn('"--threads", "1"', dockerfile)
self.assertIn('"--worker-class", "gthread"', dockerfile)
self.assertIn('"--keep-alive", "5"', dockerfile)
self.assertNotIn("waitress", dockerfile.lower())

def test_pool_and_container_limits_are_not_weakened(self):
database = (APP / "benchmark_api" / "database.py").read_text()
self.assertIn('"max_size": 10', database)
dockerfile = (APP / "Dockerfile").read_text()
self.assertEqual(dockerfile.count("python:3.14.7-slim-bookworm@sha256:"), 2)
self.assertIn("USER 10001:10001", dockerfile)
self.assertIn('ENTRYPOINT ["python", "-m", "benchmark_api.server"]', dockerfile)
self.assertIn('ENTRYPOINT ["python", "-m", "gunicorn.app.wsgiapp"]', dockerfile)

def test_registration_preserves_the_frozen_legacy_cohort(self):
registry = json.loads((ROOT / "benchmark" / "implementations.json").read_text())
Expand Down
18 changes: 14 additions & 4 deletions tests/test_python_flask_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,15 @@ def running_service() -> str:
require(state["HostConfig"]["Memory"] == 536_870_912, "memory limit differs")
require(state["Config"]["User"] == "10001:10001", "runtime user differs")
require(
state["Path"] == "python" and state["Args"] == ["-m", "benchmark_api.server"],
"unexpected server wrapper",
state["Path"] == "python"
and state["Args"][:2] == ["-m", "gunicorn.app.wsgiapp"]
and "--workers" in state["Args"]
and state["Args"][state["Args"].index("--workers") + 1] == "1"
and "--worker-class" in state["Args"]
and state["Args"][state["Args"].index("--worker-class") + 1] == "gthread"
and "--threads" in state["Args"]
and state["Args"][state["Args"].index("--threads") + 1] == "1",
"unexpected Gunicorn runtime profile",
)
require(state["HostConfig"]["CapDrop"] == ["ALL"], "capabilities not dropped")
require(
Expand All @@ -133,8 +140,11 @@ def running_service() -> str:
["docker", "top", container, "-eo", "pid,args"]
).stdout.splitlines()[1:]
commands = [line.split(maxsplit=1)[1] for line in processes if line.strip()]
servers = [command for command in commands if "benchmark_api.server" in command]
require(len(servers) == 1, f"expected one Waitress server process, got {servers!r}")
servers = [command for command in commands if "gunicorn" in command]
require(
len(servers) == 2,
f"expected one Gunicorn arbiter and one request worker, got {servers!r}",
)
return container


Expand Down
Loading