From 9baf44f0db14cb427de3a911665d061fb1bfff00 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:03:10 +0900 Subject: [PATCH 01/52] test: require Flask Gunicorn gthread profile --- tests/test_python_flask_acceptance.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_python_flask_acceptance.py b/tests/test_python_flask_acceptance.py index d773850..62b50e8 100644 --- a/tests/test_python_flask_acceptance.py +++ b/tests/test_python_flask_acceptance.py @@ -13,14 +13,17 @@ 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()) + self.assertIn('workers = 1', server) + self.assertIn('threads = 1', server) + self.assertIn('worker_class = "gthread"', server) + self.assertIn('keepalive = 5', server) + self.assertNotIn("waitress", server.lower()) def test_pool_and_container_limits_are_not_weakened(self): database = (APP / "benchmark_api" / "database.py").read_text() From 9cf93dc3c9b12804a30f66c5411e483565915fe3 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:03:15 +0900 Subject: [PATCH 02/52] test: prove Flask Gunicorn acceptance red --- .github/workflows/flask-gunicorn-tdd.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/flask-gunicorn-tdd.yml diff --git a/.github/workflows/flask-gunicorn-tdd.yml b/.github/workflows/flask-gunicorn-tdd.yml new file mode 100644 index 0000000..3ba7e2f --- /dev/null +++ b/.github/workflows/flask-gunicorn-tdd.yml @@ -0,0 +1,15 @@ +name: Temporary Flask Gunicorn TDD red +on: + push: + branches: [fix/flask-gunicorn-70] +permissions: + contents: read +jobs: + red: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version-file: apps/python-flask/.python-version + - run: python -m unittest tests.test_python_flask_acceptance From b10c85c98fe1ff847dc60cde3dcf53df47b3b65e Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:04:16 +0900 Subject: [PATCH 03/52] test: define Gunicorn process validation --- tests/test_benchmark_environment.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_benchmark_environment.py b/tests/test_benchmark_environment.py index 3041e21..1129020 100644 --- a/tests/test_benchmark_environment.py +++ b/tests/test_benchmark_environment.py @@ -163,6 +163,34 @@ 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\n" + "10 python -m benchmark_api.server\n" + "11 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: From 0143d380ec1a5e1e7621eabcbca237465556f613 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:04:23 +0900 Subject: [PATCH 04/52] test: cover Gunicorn process validation red --- .github/workflows/flask-gunicorn-tdd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flask-gunicorn-tdd.yml b/.github/workflows/flask-gunicorn-tdd.yml index 3ba7e2f..79c4e21 100644 --- a/.github/workflows/flask-gunicorn-tdd.yml +++ b/.github/workflows/flask-gunicorn-tdd.yml @@ -12,4 +12,4 @@ jobs: - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: python-version-file: apps/python-flask/.python-version - - run: python -m unittest tests.test_python_flask_acceptance + - run: python -m unittest tests.test_python_flask_acceptance tests.test_benchmark_environment From 7d0e54b0758fe1a30d708d81449db1465bf66067 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:04:45 +0900 Subject: [PATCH 05/52] test: assert Gunicorn runtime profile at container boundary --- tests/test_python_flask_acceptance.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_python_flask_acceptance.py b/tests/test_python_flask_acceptance.py index 62b50e8..2695aa3 100644 --- a/tests/test_python_flask_acceptance.py +++ b/tests/test_python_flask_acceptance.py @@ -18,12 +18,12 @@ def test_approved_wsgi_profile_is_pinned(self): 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('workers = 1', server) - self.assertIn('threads = 1', server) - self.assertIn('worker_class = "gthread"', server) - self.assertIn('keepalive = 5', server) - self.assertNotIn("waitress", 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() @@ -31,7 +31,7 @@ def test_pool_and_container_limits_are_not_weakened(self): 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()) From 23438846b1f49f3b2d1c53a269eb35ccb24fc63c Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:04:54 +0900 Subject: [PATCH 06/52] feat: replace Waitress with Gunicorn --- apps/python-flask/requirements.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/python-flask/requirements.in b/apps/python-flask/requirements.in index 691945c..a3e98fe 100644 --- a/apps/python-flask/requirements.in +++ b/apps/python-flask/requirements.in @@ -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 From 908797def38c1dc50519ae4ee4bb58d994b3071c Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:04:57 +0900 Subject: [PATCH 07/52] feat: add Gunicorn Flask application factory --- apps/python-flask/benchmark_api/server.py | 44 ++++------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/apps/python-flask/benchmark_api/server.py b/apps/python-flask/benchmark_api/server.py index 9a264a1..2b018f6 100644 --- a/apps/python-flask/benchmark_api/server.py +++ b/apps/python-flask/benchmark_api/server.py @@ -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) From a9dbe4405c5dc78be312eb1510775ead8110b954 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:04:59 +0900 Subject: [PATCH 08/52] feat: run Flask with one Gunicorn gthread worker --- apps/python-flask/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/python-flask/Dockerfile b/apps/python-flask/Dockerfile index a5c61dd..443c516 100644 --- a/apps/python-flask/Dockerfile +++ b/apps/python-flask/Dockerfile @@ -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()"] From 9bb34ead5b3ff4a83e30b56ddec496ba75e1598e Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:05:06 +0900 Subject: [PATCH 09/52] chore: record Gunicorn in Flask version metadata --- benchmark/implementations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/implementations.json b/benchmark/implementations.json index 6b66854..89f6be5 100644 --- a/benchmark/implementations.json +++ b/benchmark/implementations.json @@ -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","gunicorn","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": { From eb8d434ef4d90eabd796bc43b684a7fb71893ecd Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:05:09 +0900 Subject: [PATCH 10/52] fix: validate Flask Gunicorn arbiter and worker --- benchmark/environment.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/benchmark/environment.py b/benchmark/environment.py index f18e66f..e28f1b6 100644 --- a/benchmark/environment.py +++ b/benchmark/environment.py @@ -128,12 +128,14 @@ 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["Config"]["Labels"]["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)" ), ) From 019b5b29b394f15314e46d5ea2bd4bf6fae4f7d1 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:05:12 +0900 Subject: [PATCH 11/52] test: validate Gunicorn runtime process model --- tests/test_python_flask_service.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/test_python_flask_service.py b/tests/test_python_flask_service.py index 47ae42e..0110b83 100644 --- a/tests/test_python_flask_service.py +++ b/tests/test_python_flask_service.py @@ -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( @@ -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 From a7916f5ffeae6e91ee60a92907f939a1afab458a Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:05:20 +0900 Subject: [PATCH 12/52] test: verify Gunicorn profile and generate lock --- .github/workflows/flask-gunicorn-tdd.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/flask-gunicorn-tdd.yml b/.github/workflows/flask-gunicorn-tdd.yml index 79c4e21..36ecb0a 100644 --- a/.github/workflows/flask-gunicorn-tdd.yml +++ b/.github/workflows/flask-gunicorn-tdd.yml @@ -1,15 +1,25 @@ -name: Temporary Flask Gunicorn TDD red +name: Temporary Flask Gunicorn TDD on: push: branches: [fix/flask-gunicorn-70] permissions: contents: read jobs: - red: + verify: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: python-version-file: apps/python-flask/.python-version - - run: python -m unittest tests.test_python_flask_acceptance tests.test_benchmark_environment + - name: Unit acceptance + run: python -m unittest tests.test_python_flask_acceptance tests.test_benchmark_environment + - name: Generate runtime lock + run: | + python -m pip install pip-tools==7.5.2 + python -m piptools compile --generate-hashes --output-file=apps/python-flask/requirements.lock apps/python-flask/requirements.in + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: flask-gunicorn-lock + path: apps/python-flask/requirements.lock + if-no-files-found: error From 6c01f3a32de2a838cb3aa61b946a6b68bbab3413 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:05:49 +0900 Subject: [PATCH 13/52] fix: preserve generic process validation fixtures --- benchmark/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/environment.py b/benchmark/environment.py index e28f1b6..160e269 100644 --- a/benchmark/environment.py +++ b/benchmark/environment.py @@ -128,7 +128,7 @@ 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["Config"]["Labels"]["com.docker.compose.service"] + service = state.get("Config", {}).get("Labels", {}).get("com.docker.compose.service") expected_servers = 2 if service == "python-flask" else 1 require( commands.count(server) == expected_servers and all(command in allowed for command in commands), From b2f7f9d61962ee12bd80a59424e79bb65b0f5006 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:06:20 +0900 Subject: [PATCH 14/52] test: pin pip for runtime lock generation --- .github/workflows/flask-gunicorn-tdd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flask-gunicorn-tdd.yml b/.github/workflows/flask-gunicorn-tdd.yml index 36ecb0a..706e388 100644 --- a/.github/workflows/flask-gunicorn-tdd.yml +++ b/.github/workflows/flask-gunicorn-tdd.yml @@ -16,7 +16,7 @@ jobs: run: python -m unittest tests.test_python_flask_acceptance tests.test_benchmark_environment - name: Generate runtime lock run: | - python -m pip install pip-tools==7.5.2 + python -m pip install pip==25.3 pip-tools==7.5.2 python -m piptools compile --generate-hashes --output-file=apps/python-flask/requirements.lock apps/python-flask/requirements.in - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: From 4b220540c33c3514aa3c855e9db471a3b146cd7b Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:07:11 +0900 Subject: [PATCH 15/52] test: commit generated Flask runtime lock --- .github/workflows/flask-gunicorn-tdd.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flask-gunicorn-tdd.yml b/.github/workflows/flask-gunicorn-tdd.yml index 706e388..36a8d6e 100644 --- a/.github/workflows/flask-gunicorn-tdd.yml +++ b/.github/workflows/flask-gunicorn-tdd.yml @@ -3,7 +3,7 @@ on: push: branches: [fix/flask-gunicorn-70] permissions: - contents: read + contents: write jobs: verify: runs-on: ubuntu-24.04 @@ -18,8 +18,12 @@ jobs: run: | python -m pip install pip==25.3 pip-tools==7.5.2 python -m piptools compile --generate-hashes --output-file=apps/python-flask/requirements.lock apps/python-flask/requirements.in - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: flask-gunicorn-lock - path: apps/python-flask/requirements.lock - if-no-files-found: error + - name: Commit generated runtime lock + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add apps/python-flask/requirements.lock + if ! git diff --cached --quiet; then + git commit -m "chore: refresh Flask runtime lock" + git push origin HEAD:fix/flask-gunicorn-70 + fi From 6a9d86498b32d475c785588ad6e9837533947923 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 02:07:25 +0000 Subject: [PATCH 16/52] chore: refresh Flask runtime lock --- apps/python-flask/requirements.lock | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/apps/python-flask/requirements.lock b/apps/python-flask/requirements.lock index b5be15f..46f0d5f 100644 --- a/apps/python-flask/requirements.lock +++ b/apps/python-flask/requirements.lock @@ -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 \ @@ -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 @@ -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 @@ -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 From cb2407e5909dfef7d57a4aff6012c9aafbc40333 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:07:44 +0900 Subject: [PATCH 17/52] test: cover Gunicorn worker-local Flask factory --- apps/python-flask/tests/test_server.py | 33 +++++++++++--------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/apps/python-flask/tests/test_server.py b/apps/python-flask/tests/test_server.py index ad148a4..a1c3a79 100644 --- a/apps/python-flask/tests/test_server.py +++ b/apps/python-flask/tests/test_server.py @@ -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) From c906c75e18455c38d0eab86073c7d48f7a888fb6 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:07:47 +0900 Subject: [PATCH 18/52] chore: regenerate registry projection --- site/registry.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/registry.mjs b/site/registry.mjs index 089a8cd..027d943 100644 --- a/site/registry.mjs +++ b/site/registry.mjs @@ -145,7 +145,7 @@ export const REGISTRY = { "version_fields": [ "python", "flask", - "waitress", + "gunicorn", "psycopg", "psycopg-binary", "psycopg-pool" From 9fcb674d9190a16979fcbf759a2ffac955b60bee Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:07:49 +0900 Subject: [PATCH 19/52] docs: describe Flask Gunicorn runtime --- docs/FLASK.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/FLASK.md b/docs/FLASK.md index 26b5db6..86435f6 100644 --- a/docs/FLASK.md +++ b/docs/FLASK.md @@ -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 @@ -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 From f5838979862c96216ddc13069fc4eba221417535 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:07:57 +0900 Subject: [PATCH 20/52] test: run Flask Gunicorn real-container acceptance --- .github/workflows/flask-gunicorn-tdd.yml | 26 ++++++++---------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/.github/workflows/flask-gunicorn-tdd.yml b/.github/workflows/flask-gunicorn-tdd.yml index 36a8d6e..e8723c2 100644 --- a/.github/workflows/flask-gunicorn-tdd.yml +++ b/.github/workflows/flask-gunicorn-tdd.yml @@ -1,29 +1,19 @@ -name: Temporary Flask Gunicorn TDD +name: Temporary Flask Gunicorn acceptance on: push: branches: [fix/flask-gunicorn-70] permissions: - contents: write + contents: read jobs: - verify: + acceptance: runs-on: ubuntu-24.04 + timeout-minutes: 20 steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: python-version-file: apps/python-flask/.python-version - - name: Unit acceptance - run: python -m unittest tests.test_python_flask_acceptance tests.test_benchmark_environment - - name: Generate runtime lock - run: | - python -m pip install pip==25.3 pip-tools==7.5.2 - python -m piptools compile --generate-hashes --output-file=apps/python-flask/requirements.lock apps/python-flask/requirements.in - - name: Commit generated runtime lock - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add apps/python-flask/requirements.lock - if ! git diff --cached --quiet; then - git commit -m "chore: refresh Flask runtime lock" - git push origin HEAD:fix/flask-gunicorn-70 - fi + - name: Static and shared regression tests + run: python -m unittest tests.test_python_flask_acceptance tests.test_benchmark_environment tests.test_benchmark_registry + - name: Real Flask container acceptance + run: python tests/test_python_flask_service.py From 908b457ec386941c0b3d433727798cef92cfbc85 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:08:22 +0900 Subject: [PATCH 21/52] test: use repository unittest discovery path --- .github/workflows/flask-gunicorn-tdd.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flask-gunicorn-tdd.yml b/.github/workflows/flask-gunicorn-tdd.yml index e8723c2..22ad000 100644 --- a/.github/workflows/flask-gunicorn-tdd.yml +++ b/.github/workflows/flask-gunicorn-tdd.yml @@ -14,6 +14,8 @@ jobs: with: python-version-file: apps/python-flask/.python-version - name: Static and shared regression tests - run: python -m unittest tests.test_python_flask_acceptance tests.test_benchmark_environment tests.test_benchmark_registry + run: | + python -m unittest tests.test_python_flask_acceptance tests.test_benchmark_environment + python -m unittest discover -s tests -p 'test_benchmark_registry.py' - name: Real Flask container acceptance run: python tests/test_python_flask_service.py From 97c56241fd1c31842e3cee023fbc0968820558d2 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:09:13 +0900 Subject: [PATCH 22/52] fix: preserve historical Flask version compatibility --- benchmark/implementations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/implementations.json b/benchmark/implementations.json index 89f6be5..bab355b 100644 --- a/benchmark/implementations.json +++ b/benchmark/implementations.json @@ -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","gunicorn","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": { From 1723648dd8f384174b55f3f7a825de8021ed2d85 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:09:15 +0900 Subject: [PATCH 23/52] chore: regenerate compatible registry projection --- site/registry.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/site/registry.mjs b/site/registry.mjs index 027d943..24ae24a 100644 --- a/site/registry.mjs +++ b/site/registry.mjs @@ -145,7 +145,6 @@ export const REGISTRY = { "version_fields": [ "python", "flask", - "gunicorn", "psycopg", "psycopg-binary", "psycopg-pool" From 3cfcf9ae8d540e35f8e16a0d948fcb231105b0c5 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:10:38 +0900 Subject: [PATCH 24/52] test: confirm production Gunicorn profile without publishing --- .github/workflows/flask-gunicorn-tdd.yml | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/flask-gunicorn-tdd.yml b/.github/workflows/flask-gunicorn-tdd.yml index 22ad000..66cb407 100644 --- a/.github/workflows/flask-gunicorn-tdd.yml +++ b/.github/workflows/flask-gunicorn-tdd.yml @@ -19,3 +19,33 @@ jobs: python -m unittest discover -s tests -p 'test_benchmark_registry.py' - name: Real Flask container acceptance run: python tests/test_python_flask_service.py + - name: Non-publishing production-profile confirmation + run: | + python - <<'PY' + import json + from pathlib import Path + from benchmark.contract_test import run_contract + from benchmark.environment import DockerEnvironment + from benchmark.healthcheck import EXTERNAL_READINESS + from benchmark.install_oha import ensure_oha + + out = Path(".cache/flask-gunicorn-confirmation") + env = DockerEnvironment(ensure_oha(), out, health_policy=EXTERNAL_READINESS) + try: + env.build("python-flask") + identity = env.start("python-flask") + checks = run_contract("http://127.0.0.1:8080", implementation="python-flask") + results = { + endpoint: env.measure(endpoint, 10, 1) + for endpoint in ("/json", "/db/42", "/cpu") + } + print("FLASK_GUNICORN_CONFIRMATION=" + json.dumps({ + "official": False, + "publishable": False, + "contract_checks": checks, + "container": identity, + "results": results, + }, sort_keys=True)) + finally: + env.cleanup() + PY From 0520e048b38c9218a3d46c88ee0a450ea93b1857 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:12:53 +0900 Subject: [PATCH 25/52] test: remove temporary Flask Gunicorn workflow --- .github/workflows/flask-gunicorn-tdd.yml | 51 ------------------------ 1 file changed, 51 deletions(-) delete mode 100644 .github/workflows/flask-gunicorn-tdd.yml diff --git a/.github/workflows/flask-gunicorn-tdd.yml b/.github/workflows/flask-gunicorn-tdd.yml deleted file mode 100644 index 66cb407..0000000 --- a/.github/workflows/flask-gunicorn-tdd.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Temporary Flask Gunicorn acceptance -on: - push: - branches: [fix/flask-gunicorn-70] -permissions: - contents: read -jobs: - acceptance: - runs-on: ubuntu-24.04 - timeout-minutes: 20 - steps: - - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 - with: - python-version-file: apps/python-flask/.python-version - - name: Static and shared regression tests - run: | - python -m unittest tests.test_python_flask_acceptance tests.test_benchmark_environment - python -m unittest discover -s tests -p 'test_benchmark_registry.py' - - name: Real Flask container acceptance - run: python tests/test_python_flask_service.py - - name: Non-publishing production-profile confirmation - run: | - python - <<'PY' - import json - from pathlib import Path - from benchmark.contract_test import run_contract - from benchmark.environment import DockerEnvironment - from benchmark.healthcheck import EXTERNAL_READINESS - from benchmark.install_oha import ensure_oha - - out = Path(".cache/flask-gunicorn-confirmation") - env = DockerEnvironment(ensure_oha(), out, health_policy=EXTERNAL_READINESS) - try: - env.build("python-flask") - identity = env.start("python-flask") - checks = run_contract("http://127.0.0.1:8080", implementation="python-flask") - results = { - endpoint: env.measure(endpoint, 10, 1) - for endpoint in ("/json", "/db/42", "/cpu") - } - print("FLASK_GUNICORN_CONFIRMATION=" + json.dumps({ - "official": False, - "publishable": False, - "contract_checks": checks, - "container": identity, - "results": results, - }, sort_keys=True)) - finally: - env.cleanup() - PY From 59a0728136d4110e65633a51311eecf2bddf67ca Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:14:54 +0900 Subject: [PATCH 26/52] test: update current Flask runtime metadata expectation --- tests/test_benchmark_java_candidate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_benchmark_java_candidate.py b/tests/test_benchmark_java_candidate.py index ccd885e..d6c52a9 100644 --- a/tests/test_benchmark_java_candidate.py +++ b/tests/test_benchmark_java_candidate.py @@ -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", From 469f1137b1539a49f8b1aade9512f681a6a0e9bd Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:14:56 +0900 Subject: [PATCH 27/52] test: update Flask candidate runtime contract --- tests/test_benchmark_python_flask_candidate.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_benchmark_python_flask_candidate.py b/tests/test_benchmark_python_flask_candidate.py index d4bb9bf..1b7d019 100644 --- a/tests/test_benchmark_python_flask_candidate.py +++ b/tests/test_benchmark_python_flask_candidate.py @@ -9,17 +9,18 @@ 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)\n 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()) From 905a64f97bbe57bfd5d17bed327832faa0f93bc0 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:17:32 +0900 Subject: [PATCH 28/52] test: fix Flask candidate assertion formatting --- tests/test_benchmark_python_flask_candidate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_benchmark_python_flask_candidate.py b/tests/test_benchmark_python_flask_candidate.py index 1b7d019..8508aae 100644 --- a/tests/test_benchmark_python_flask_candidate.py +++ b/tests/test_benchmark_python_flask_candidate.py @@ -13,7 +13,8 @@ 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("gunicorn==26.0.0", requirements)\n self.assertNotIn("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) dockerfile = (APP / "Dockerfile").read_text() From f6b3649231f0790e71660e455c877f989538a326 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:20:53 +0900 Subject: [PATCH 29/52] style: apply shared Ruff formatting --- benchmark/environment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/benchmark/environment.py b/benchmark/environment.py index 160e269..c688689 100644 --- a/benchmark/environment.py +++ b/benchmark/environment.py @@ -131,7 +131,8 @@ def normalized(arguments): service = state.get("Config", {}).get("Labels", {}).get("com.docker.compose.service") expected_servers = 2 if service == "python-flask" else 1 require( - commands.count(server) == expected_servers and all(command in allowed for command in commands), + commands.count(server) == expected_servers + and all(command in allowed for command in commands), ( f"expected {expected_servers} server process(es) and only independent health probes" if allow_health_probe From f6fe1a569d4c6270d59de716c6a9febcf34880cc Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:20:56 +0900 Subject: [PATCH 30/52] style: apply shared Ruff formatting --- tests/test_benchmark_environment.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_benchmark_environment.py b/tests/test_benchmark_environment.py index 1129020..8c1aec0 100644 --- a/tests/test_benchmark_environment.py +++ b/tests/test_benchmark_environment.py @@ -170,9 +170,7 @@ def test_flask_gunicorn_allows_exactly_one_arbiter_and_one_worker(self): 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" + "PID COMMAND\n10 python -m benchmark_api.server\n11 python -m benchmark_api.server\n" ) environment.validate_processes(value, output, allow_health_probe=False) From 56c539f85f8d90a03477a8791d03981d175588d1 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:25:23 +0900 Subject: [PATCH 31/52] fix: model historical Flask server version alternatives --- benchmark/implementations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/implementations.json b/benchmark/implementations.json index bab355b..97afecb 100644 --- a/benchmark/implementations.json +++ b/benchmark/implementations.json @@ -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","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"],"version_any_of":[["waitress","gunicorn"]],"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": { From c90a4f6130923f09454b355064e95ede4486898f Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:25:26 +0900 Subject: [PATCH 32/52] feat: validate alternative runtime version fields --- benchmark/registry.py | 51 ++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/benchmark/registry.py b/benchmark/registry.py index 8c8f6ce..607d026 100644 --- a/benchmark/registry.py +++ b/benchmark/registry.py @@ -29,19 +29,19 @@ def validate_registry(data: dict) -> dict: identifiers = [] sources = [] for spec in specs: - object_fields( - spec, - ( - "id", - "language", - "framework", - "display_name", - "source_path", - "version_fields", - "acceptance_test", - "failure_test", - ), - "implementation registration", + required_spec_fields = { + "id", + "language", + "framework", + "display_name", + "source_path", + "version_fields", + "acceptance_test", + "failure_test", + } + require( + set(spec) in (required_spec_fields, required_spec_fields | {"version_any_of"}), + "unexpected implementation registration fields", ) identifier = spec["id"] require( @@ -78,6 +78,20 @@ def validate_registry(data: dict) -> dict: "invalid version field", ) require(len(fields) == len(set(fields)), "duplicate version fields") + alternatives = spec.get("version_any_of", []) + require(type(alternatives) is list, "invalid version alternatives") + for group in alternatives: + require( + type(group) is list + and len(group) >= 2 + and len(group) == len(set(group)) + and all( + type(key) is str and re.fullmatch(r"[a-z][a-z0-9_-]*", key) + for key in group + ) + and not (set(group) & set(fields)), + "invalid version alternative group", + ) require(len(identifiers) == len(set(identifiers)), "duplicate implementation ID") require(len(sources) == len(set(sources)), "implementations must have independent source paths") cohorts = data["cohorts"] @@ -123,6 +137,17 @@ def implementation(identifier: str) -> dict: raise BenchmarkFailure("unknown implementation ID") +def validate_version_fields(identifier: str, values: dict) -> None: + spec = implementation(identifier) + require(type(values) is dict, "missing stack versions") + require(set(spec["version_fields"]) <= set(values), "missing stack versions") + for group in spec.get("version_any_of", []): + require( + sum(key in values for key in group) == 1, + "expected exactly one alternative stack version", + ) + + def active_members() -> tuple[str, ...]: return tuple(REGISTRY["cohorts"][REGISTRY["active_cohort"]]["members"]) From 112a936366b122e44b446d03b2ce3cd629e40bd6 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:25:31 +0900 Subject: [PATCH 33/52] fix: validate current runtime version alternatives --- benchmark/environment.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/benchmark/environment.py b/benchmark/environment.py index c688689..ea27025 100644 --- a/benchmark/environment.py +++ b/benchmark/environment.py @@ -224,10 +224,9 @@ def match(pattern, text): "version extractors must cover registered implementations", ) for identifier, values in versions.items(): - require( - set(implementation(identifier)["version_fields"]) <= set(values), - "missing required stack versions", - ) + from .registry import validate_version_fields + + validate_version_fields(identifier, values) require( all( type(value) is str From 93b2e15e76a00d7413767645649032a4dda2fca8 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:25:34 +0900 Subject: [PATCH 34/52] fix: validate historical and current Flask server provenance --- benchmark/report.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/benchmark/report.py b/benchmark/report.py index dcafc7c..9ac3290 100644 --- a/benchmark/report.py +++ b/benchmark/report.py @@ -180,9 +180,8 @@ def validate_report(report: dict, *, expected_context: dict | None = None) -> No text(metadata.get("docker_compose"), "Docker Compose version required") versions = object_fields(metadata.get("versions"), members, "versions") for implementation in members: - keys = registry.implementation(implementation)["version_fields"] values = versions[implementation] - require(type(values) is dict and set(keys) <= set(values), "missing stack versions") + registry.validate_version_fields(implementation, values) for value in values.values(): require( type(value) is str and re.fullmatch(r"\d+\.\d+\.\d+", value) is not None, From 5f69a8e76b3d7680eaa1a56f25a94dce7108cc08 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:25:39 +0900 Subject: [PATCH 35/52] refactor: reuse registry version validation --- benchmark/environment.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/benchmark/environment.py b/benchmark/environment.py index ea27025..b2b6828 100644 --- a/benchmark/environment.py +++ b/benchmark/environment.py @@ -25,7 +25,7 @@ from .java_versions import JAVA_VERSION from .java_versions import pinned_versions as java_pinned_versions from .process import ROOT, execute -from .registry import active_members, implementation, implementation_ids +from .registry import active_members, implementation, implementation_ids, validate_version_fields from .results import BenchmarkFailure, require, strict_json @@ -224,8 +224,6 @@ def match(pattern, text): "version extractors must cover registered implementations", ) for identifier, values in versions.items(): - from .registry import validate_version_fields - validate_version_fields(identifier, values) require( all( From 49bd6fb12307aad0515825c740f17518333fe0e5 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:25:45 +0900 Subject: [PATCH 36/52] chore: regenerate runtime version alternatives --- site/registry.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/site/registry.mjs b/site/registry.mjs index 24ae24a..4cb4ad8 100644 --- a/site/registry.mjs +++ b/site/registry.mjs @@ -142,6 +142,12 @@ export const REGISTRY = { "id": "python-flask", "language": "Python", "source_path": "apps/python-flask", + "version_any_of": [ + [ + "waitress", + "gunicorn" + ] + ], "version_fields": [ "python", "flask", From 90b7a01adc54aec5e962676e39947c924c498bfe Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 11:25:50 +0900 Subject: [PATCH 37/52] fix: validate alternative Flask server versions in Pages --- site/app.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/site/app.mjs b/site/app.mjs index 7521ca2..16a00df 100644 --- a/site/app.mjs +++ b/site/app.mjs @@ -150,6 +150,10 @@ function validateReport(report) { for (const key of STACKS[id].version_fields) { assert(typeof versionSet[key] === "string" && /^\d+\.\d+\.\d+$/.test(versionSet[key]), `invalid ${id} ${key} version`); } + for (const group of STACKS[id].version_any_of ?? []) { + const present = group.filter((key) => Object.hasOwn(versionSet, key)); + assert(present.length === 1, `expected exactly one ${id} alternative stack version`); + } assert(Object.values(versionSet).every((value) => typeof value === "string" && /^\d+\.\d+\.\d+$/.test(value)), "invalid stack version"); assert(Array.isArray(backend.endpoints) && backend.endpoints.length === ENDPOINTS.length, "three endpoints required"); backend.endpoints.forEach((entry, endpointIndex) => { From 11c09df9c61fea697065ce992c1e07a3539706d7 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:44:40 +0900 Subject: [PATCH 38/52] style: format registry imports --- benchmark/environment.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/benchmark/environment.py b/benchmark/environment.py index b2b6828..93097d1 100644 --- a/benchmark/environment.py +++ b/benchmark/environment.py @@ -25,7 +25,12 @@ from .java_versions import JAVA_VERSION from .java_versions import pinned_versions as java_pinned_versions from .process import ROOT, execute -from .registry import active_members, implementation, implementation_ids, validate_version_fields +from .registry import ( + active_members, + implementation, + implementation_ids, + validate_version_fields, +) from .results import BenchmarkFailure, require, strict_json From f805bb7ed5db426d4e54e897eefd9d0ab10af59c Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:45:14 +0900 Subject: [PATCH 39/52] refactor: keep Flask server metadata compatibility simple --- benchmark/registry.py | 51 +++++++++++-------------------------------- 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/benchmark/registry.py b/benchmark/registry.py index 607d026..8c8f6ce 100644 --- a/benchmark/registry.py +++ b/benchmark/registry.py @@ -29,19 +29,19 @@ def validate_registry(data: dict) -> dict: identifiers = [] sources = [] for spec in specs: - required_spec_fields = { - "id", - "language", - "framework", - "display_name", - "source_path", - "version_fields", - "acceptance_test", - "failure_test", - } - require( - set(spec) in (required_spec_fields, required_spec_fields | {"version_any_of"}), - "unexpected implementation registration fields", + object_fields( + spec, + ( + "id", + "language", + "framework", + "display_name", + "source_path", + "version_fields", + "acceptance_test", + "failure_test", + ), + "implementation registration", ) identifier = spec["id"] require( @@ -78,20 +78,6 @@ def validate_registry(data: dict) -> dict: "invalid version field", ) require(len(fields) == len(set(fields)), "duplicate version fields") - alternatives = spec.get("version_any_of", []) - require(type(alternatives) is list, "invalid version alternatives") - for group in alternatives: - require( - type(group) is list - and len(group) >= 2 - and len(group) == len(set(group)) - and all( - type(key) is str and re.fullmatch(r"[a-z][a-z0-9_-]*", key) - for key in group - ) - and not (set(group) & set(fields)), - "invalid version alternative group", - ) require(len(identifiers) == len(set(identifiers)), "duplicate implementation ID") require(len(sources) == len(set(sources)), "implementations must have independent source paths") cohorts = data["cohorts"] @@ -137,17 +123,6 @@ def implementation(identifier: str) -> dict: raise BenchmarkFailure("unknown implementation ID") -def validate_version_fields(identifier: str, values: dict) -> None: - spec = implementation(identifier) - require(type(values) is dict, "missing stack versions") - require(set(spec["version_fields"]) <= set(values), "missing stack versions") - for group in spec.get("version_any_of", []): - require( - sum(key in values for key in group) == 1, - "expected exactly one alternative stack version", - ) - - def active_members() -> tuple[str, ...]: return tuple(REGISTRY["cohorts"][REGISTRY["active_cohort"]]["members"]) From f80dfe8238ccea816fc8665f373f11f668073a5c Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:45:19 +0900 Subject: [PATCH 40/52] refactor: keep Flask server metadata compatibility simple --- benchmark/report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/benchmark/report.py b/benchmark/report.py index 9ac3290..dcafc7c 100644 --- a/benchmark/report.py +++ b/benchmark/report.py @@ -180,8 +180,9 @@ def validate_report(report: dict, *, expected_context: dict | None = None) -> No text(metadata.get("docker_compose"), "Docker Compose version required") versions = object_fields(metadata.get("versions"), members, "versions") for implementation in members: + keys = registry.implementation(implementation)["version_fields"] values = versions[implementation] - registry.validate_version_fields(implementation, values) + require(type(values) is dict and set(keys) <= set(values), "missing stack versions") for value in values.values(): require( type(value) is str and re.fullmatch(r"\d+\.\d+\.\d+", value) is not None, From 3877701657d83aa76104ddacc9de2b859a8ae73d Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:45:26 +0900 Subject: [PATCH 41/52] refactor: keep Flask server metadata compatibility simple --- site/app.mjs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/site/app.mjs b/site/app.mjs index 16a00df..7521ca2 100644 --- a/site/app.mjs +++ b/site/app.mjs @@ -150,10 +150,6 @@ function validateReport(report) { for (const key of STACKS[id].version_fields) { assert(typeof versionSet[key] === "string" && /^\d+\.\d+\.\d+$/.test(versionSet[key]), `invalid ${id} ${key} version`); } - for (const group of STACKS[id].version_any_of ?? []) { - const present = group.filter((key) => Object.hasOwn(versionSet, key)); - assert(present.length === 1, `expected exactly one ${id} alternative stack version`); - } assert(Object.values(versionSet).every((value) => typeof value === "string" && /^\d+\.\d+\.\d+$/.test(value)), "invalid stack version"); assert(Array.isArray(backend.endpoints) && backend.endpoints.length === ENDPOINTS.length, "three endpoints required"); backend.endpoints.forEach((entry, endpointIndex) => { From 89bbf56166dceaca067b4a19336d8db98131c670 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:45:29 +0900 Subject: [PATCH 42/52] refactor: make Flask server metadata additive --- benchmark/implementations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/implementations.json b/benchmark/implementations.json index 97afecb..bab355b 100644 --- a/benchmark/implementations.json +++ b/benchmark/implementations.json @@ -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","psycopg","psycopg-binary","psycopg-pool"],"version_any_of":[["waitress","gunicorn"]],"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": { From c38ca3adc61e9167fe13af431a212d810331a86b Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:45:35 +0900 Subject: [PATCH 43/52] chore: regenerate Flask registry projection --- site/registry.mjs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/site/registry.mjs b/site/registry.mjs index 4cb4ad8..24ae24a 100644 --- a/site/registry.mjs +++ b/site/registry.mjs @@ -142,12 +142,6 @@ export const REGISTRY = { "id": "python-flask", "language": "Python", "source_path": "apps/python-flask", - "version_any_of": [ - [ - "waitress", - "gunicorn" - ] - ], "version_fields": [ "python", "flask", From c32986c5b1b470199c3451cba1e3b0b6b0517273 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:46:06 +0900 Subject: [PATCH 44/52] refactor: scope Gunicorn process validation to Flask --- benchmark/environment.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/benchmark/environment.py b/benchmark/environment.py index 93097d1..c688689 100644 --- a/benchmark/environment.py +++ b/benchmark/environment.py @@ -25,12 +25,7 @@ from .java_versions import JAVA_VERSION from .java_versions import pinned_versions as java_pinned_versions from .process import ROOT, execute -from .registry import ( - active_members, - implementation, - implementation_ids, - validate_version_fields, -) +from .registry import active_members, implementation, implementation_ids from .results import BenchmarkFailure, require, strict_json @@ -229,7 +224,10 @@ def match(pattern, text): "version extractors must cover registered implementations", ) for identifier, values in versions.items(): - validate_version_fields(identifier, values) + require( + set(implementation(identifier)["version_fields"]) <= set(values), + "missing required stack versions", + ) require( all( type(value) is str From b611e7cf3e64516ea95efce9bcc7dccea83bdcba Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:49:43 +0900 Subject: [PATCH 45/52] test: capture site compatibility failure --- .github/workflows/site-test-diagnostic.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/site-test-diagnostic.yml diff --git a/.github/workflows/site-test-diagnostic.yml b/.github/workflows/site-test-diagnostic.yml new file mode 100644 index 0000000..36e5d36 --- /dev/null +++ b/.github/workflows/site-test-diagnostic.yml @@ -0,0 +1,22 @@ +name: Temporary site failure diagnostic +on: + push: + branches: [fix/flask-gunicorn-70] +permissions: + contents: read +jobs: + diagnose: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: '3.10' + - run: | + mkdir -p .cache + python -m unittest discover -s tests -p 'test_site.py' -v > .cache/site-test.log 2>&1 || true + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: site-test-diagnostic + path: .cache/site-test.log + retention-days: 1 From b662ac556f8ff192fc712257c1d094261cf15e93 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:50:24 +0900 Subject: [PATCH 46/52] test: reproduce site failure on CI Python --- .github/workflows/site-test-diagnostic.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/site-test-diagnostic.yml b/.github/workflows/site-test-diagnostic.yml index 36e5d36..4b6f74b 100644 --- a/.github/workflows/site-test-diagnostic.yml +++ b/.github/workflows/site-test-diagnostic.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: - python-version: '3.10' + python-version-file: apps/python-fastapi/.python-version - run: | mkdir -p .cache python -m unittest discover -s tests -p 'test_site.py' -v > .cache/site-test.log 2>&1 || true From eb7007706b94931c921400ccdaa2b54621a4e047 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:51:04 +0900 Subject: [PATCH 47/52] test: remove temporary site diagnostic --- .github/workflows/site-test-diagnostic.yml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 .github/workflows/site-test-diagnostic.yml diff --git a/.github/workflows/site-test-diagnostic.yml b/.github/workflows/site-test-diagnostic.yml deleted file mode 100644 index 4b6f74b..0000000 --- a/.github/workflows/site-test-diagnostic.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Temporary site failure diagnostic -on: - push: - branches: [fix/flask-gunicorn-70] -permissions: - contents: read -jobs: - diagnose: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 - with: - python-version-file: apps/python-fastapi/.python-version - - run: | - mkdir -p .cache - python -m unittest discover -s tests -p 'test_site.py' -v > .cache/site-test.log 2>&1 || true - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: site-test-diagnostic - path: .cache/site-test.log - retention-days: 1 From 70cdbbba75b9b75dec131316fef675d406b995af Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:53:03 +0900 Subject: [PATCH 48/52] test: reproduce shared site failure order --- .github/workflows/shared-order-diagnostic.yml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/shared-order-diagnostic.yml diff --git a/.github/workflows/shared-order-diagnostic.yml b/.github/workflows/shared-order-diagnostic.yml new file mode 100644 index 0000000..6f7e124 --- /dev/null +++ b/.github/workflows/shared-order-diagnostic.yml @@ -0,0 +1,28 @@ +name: Temporary shared-order diagnostic +on: + push: + branches: [fix/flask-gunicorn-70] +permissions: + contents: read +jobs: + diagnose: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version: '3.10' + - run: make test-benchmark + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + with: + python-version-file: apps/python-fastapi/.python-version + - run: python -m pip install --require-hashes --only-binary=:all: -r apps/python-fastapi/requirements-dev.lock + - run: make test-registry + - run: | + mkdir -p .cache/diag + python -m unittest discover -s tests -p 'test_site.py' -v > .cache/diag/site.log 2>&1 || true + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: shared-order-diagnostic + path: .cache/diag/site.log + retention-days: 1 From 9c891d6495054a102e6a03f619f1ed1812b514c4 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:53:48 +0900 Subject: [PATCH 49/52] test: capture shared site failure output --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c8610a..269195e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,7 @@ jobs: - name: make test-registry run: make test-registry - name: make test-site - run: make test-site + run: make test-site > .cache/ci/site.log 2>&1 - name: make test-db run: make test-db - name: make test-benchmark @@ -99,7 +99,9 @@ jobs: uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: formatting-${{ github.run_id }}-${{ github.run_attempt }} - path: .cache/ci/format.patch + path: | + .cache/ci/format.patch + .cache/ci/site.log include-hidden-files: true retention-days: 7 if-no-files-found: ignore From 511032bf8067b3aa9851b7697f909713697489ac Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:53:53 +0900 Subject: [PATCH 50/52] test: remove temporary shared-order diagnostic --- .github/workflows/shared-order-diagnostic.yml | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/shared-order-diagnostic.yml diff --git a/.github/workflows/shared-order-diagnostic.yml b/.github/workflows/shared-order-diagnostic.yml deleted file mode 100644 index 6f7e124..0000000 --- a/.github/workflows/shared-order-diagnostic.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Temporary shared-order diagnostic -on: - push: - branches: [fix/flask-gunicorn-70] -permissions: - contents: read -jobs: - diagnose: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 - with: - python-version: '3.10' - - run: make test-benchmark - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 - with: - python-version-file: apps/python-fastapi/.python-version - - run: python -m pip install --require-hashes --only-binary=:all: -r apps/python-fastapi/requirements-dev.lock - - run: make test-registry - - run: | - mkdir -p .cache/diag - python -m unittest discover -s tests -p 'test_site.py' -v > .cache/diag/site.log 2>&1 || true - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 - with: - name: shared-order-diagnostic - path: .cache/diag/site.log - retention-days: 1 From 8e162e376c2eac225901aa720c51c48eda687fd2 Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:58:03 +0900 Subject: [PATCH 51/52] test: validate required stack version fields from registry --- tests/eight_stack_cases.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/eight_stack_cases.mjs b/tests/eight_stack_cases.mjs index a1db6ee..0362f91 100644 --- a/tests/eight_stack_cases.mjs +++ b/tests/eight_stack_cases.mjs @@ -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']; @@ -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]; }); } } From 75b108fc475672554c249daeb29f25ea8d56e9dd Mon Sep 17 00:00:00 2001 From: Yuma Takino <36617106+tappe9@users.noreply.github.com> Date: Sun, 27 Sep 2026 18:58:09 +0900 Subject: [PATCH 52/52] test: remove temporary CI failure capture --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 269195e..9c8610a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,7 @@ jobs: - name: make test-registry run: make test-registry - name: make test-site - run: make test-site > .cache/ci/site.log 2>&1 + run: make test-site - name: make test-db run: make test-db - name: make test-benchmark @@ -99,9 +99,7 @@ jobs: uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: formatting-${{ github.run_id }}-${{ github.run_attempt }} - path: | - .cache/ci/format.patch - .cache/ci/site.log + path: .cache/ci/format.patch include-hidden-files: true retention-days: 7 if-no-files-found: ignore