mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 04:36:40 +00:00
On a cold container the docs Playwright suite triggers four serial Sphinx builds and waits for the last one. test-languages.js polls /deployed/de/ for 200 and got 202 for 55.6 minutes in job 110003713825 of run 36745666606, after which the suite budget expired and five further specs never ran. The suite has never passed on a first attempt; it used to go green only on the harness retry, which7127c6b96bremoved for budget timeouts, so a structural overrun became a red job. Measured cost per build, from run 36558500327 where the whole queue drained on a kept stack: container up 12:15:52, the spec green at 13:21:41 answering on its first poll, next container start only at 13:30:45. Four builds in 65 minutes 49 seconds, so at most 16.5 minutes each - not the sixty to ninety minutes40688e4dedestimated. Three of the four builds were none of the test's business. fetch() queued latest in the foreground although nothing waits on it at startup, and _refresh_stale_sites did the same for every servable version; both now use the background lane, which is what its own docstring describes. test-front-page.js reached GET /latest/, and reaching an unbuilt version queues its build - it now asserts the redirect from the Location header without following it, and checks the same page shape against /deployed/, which is built anyway. The release tag that test-on-demand-build.js clicks stays, and now runs behind the awaited work instead of in front of it. The translation could only be queued after its base finished, because the 202 branch returns before _translation is reachable, so it always sorted behind whatever the sibling specs had queued meanwhile. The 202 branch now carries the language segment along, admitted by translatable(), which answers from the snapshot catalog on disk before any build; build_language still re-checks the written index. Moving a version into the background lane exposed that status() read only the foreground one, so a waiting version would have been reported as missing rather than queued to the versions overview and to /api/versions. is_queued() now covers both lanes, mirroring _dequeue, which always removed from both. Two of the five unit failures this change first produced disappeared on that fix alone, which is what shows they pinned real behaviour; the three that pinned the lane itself were rewritten to assert the behaviour instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
180 lines
6.4 KiB
Python
180 lines
6.4 KiB
Python
"""HTTP front of the documentation library.
|
|
|
|
Environment:
|
|
DOCS_SOURCE_REPO: git URL of the documented repository.
|
|
DOCS_DATA_DIR: volume holding the mirror and the site of every version.
|
|
DOCS_BUILD_JOBS: parallel Sphinx processes per build.
|
|
DOCS_FETCH_INTERVAL: seconds between two fetches of new commits and tags.
|
|
DOCS_PORT: port to listen on.
|
|
DOCS_SNAPSHOT_DIR: the deployed working tree, served as version ``deployed``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
import json
|
|
import os
|
|
import threading
|
|
from functools import partial
|
|
from http import HTTPStatus
|
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
from urllib.parse import unquote, urlsplit
|
|
|
|
from infinito_docs.library import LATEST, Library
|
|
|
|
PACKAGE_DIR = Path(__file__).resolve().parent
|
|
SCRIPT = PACKAGE_DIR / "assets" / "js" / "versions.js"
|
|
|
|
PAGE = """<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{title}</title>
|
|
<script src="/_docs/versions.js" defer></script>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>{title}</h1>
|
|
{body}
|
|
</main>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
OVERVIEW = """<table>
|
|
<thead><tr><th>Version</th><th>Status</th><th>Progress</th></tr></thead>
|
|
<tbody data-versions></tbody>
|
|
</table>
|
|
"""
|
|
|
|
BUILDING = """<section data-build="{version}">
|
|
<p>This version is not built yet. The build started and this page reloads once it is done.</p>
|
|
<p data-phase>queued</p>
|
|
<progress max="100" value="0" aria-label="Build progress of {version}"></progress>
|
|
<pre></pre>
|
|
</section>
|
|
<p><a href="/versions/">All versions</a></p>
|
|
"""
|
|
|
|
|
|
class DocsHandler(SimpleHTTPRequestHandler):
|
|
def __init__(self, *args, library, **kwargs):
|
|
self.library = library
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def do_GET(self):
|
|
self._route(head=False)
|
|
|
|
def do_HEAD(self):
|
|
self._route(head=True)
|
|
|
|
def _send(self, status, content_type, body, head):
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", content_type)
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.send_header("Cache-Control", "no-store")
|
|
self.end_headers()
|
|
if not head:
|
|
self.wfile.write(body)
|
|
|
|
def _redirect(self, location):
|
|
self.send_response(HTTPStatus.FOUND)
|
|
self.send_header("Location", location)
|
|
self.send_header("Content-Length", "0")
|
|
self.end_headers()
|
|
|
|
def _page(self, status, title, body, head):
|
|
page = PAGE.format(title=html.escape(title), body=body)
|
|
self._send(status, "text/html; charset=utf-8", page.encode("utf-8"), head)
|
|
|
|
def _file(self, path, head):
|
|
self._send(HTTPStatus.OK, self.guess_type(str(path)), path.read_bytes(), head)
|
|
|
|
def _route(self, head):
|
|
path = unquote(urlsplit(self.path).path)
|
|
if path == "/":
|
|
self._redirect(f"/{LATEST}/")
|
|
elif path in {"/versions", "/versions/"}:
|
|
self._page(HTTPStatus.OK, "Documentation versions", OVERVIEW, head)
|
|
elif path == "/api/versions":
|
|
body = json.dumps(self.library.status()).encode("utf-8")
|
|
self._send(HTTPStatus.OK, "application/json", body, head)
|
|
elif path == "/_docs/versions.js":
|
|
self._file(SCRIPT, head)
|
|
elif path.startswith("/api/languages/"):
|
|
self._languages(path.removeprefix("/api/languages/"), head)
|
|
else:
|
|
self._version(path, head)
|
|
|
|
def _languages(self, version, head):
|
|
if version not in self.library.versions():
|
|
self.send_error(HTTPStatus.NOT_FOUND)
|
|
return
|
|
known, built = self.library.languages(version)
|
|
languages = [{"code": "en", "native": known.get("en", "English")}]
|
|
languages += [{"code": code, "native": known[code]} for code in built]
|
|
body = json.dumps({"version": version, "languages": languages}).encode("utf-8")
|
|
self._send(HTTPStatus.OK, "application/json", body, head)
|
|
|
|
def _version(self, path, head):
|
|
version, slash, rest = path.lstrip("/").partition("/")
|
|
if version not in self.library.versions():
|
|
self.send_error(HTTPStatus.NOT_FOUND)
|
|
elif not slash:
|
|
self._redirect(f"/{version}/")
|
|
elif not self.library.servable(version) or self.library.outdated(version):
|
|
self.library.request(version)
|
|
if wanted := rest.partition("/")[0]:
|
|
self.library.request(version, wanted)
|
|
body = BUILDING.format(version=html.escape(version))
|
|
self._page(HTTPStatus.ACCEPTED, f"Building {version}", body, head)
|
|
elif (code := rest.partition("/")[0]) in self.library.languages(version)[0]:
|
|
self._translation(version, code, path, head)
|
|
elif (target := self.library.resolve(version, rest)) is None:
|
|
self._redirect(f"/{version}/")
|
|
elif target.name == "index.html" and rest and not rest.endswith(("/", ".html")):
|
|
self._redirect(f"/{version}/{rest}/")
|
|
else:
|
|
self._file(target, head)
|
|
|
|
def _translation(self, version, code, path, head):
|
|
rest = path.lstrip("/").partition("/")[2].partition("/")[2]
|
|
if not self.library.translation_servable(version, code):
|
|
if not self.library.translates(version, code):
|
|
self.send_error(HTTPStatus.NOT_FOUND)
|
|
return
|
|
self.library.request(version, code)
|
|
body = BUILDING.format(version=html.escape(f"{version}/{code}"))
|
|
self._page(HTTPStatus.ACCEPTED, f"Building {version}/{code}", body, head)
|
|
elif (not rest and not path.endswith("/")) or (
|
|
target := self.library.resolve_translation(version, code, rest)
|
|
) is None:
|
|
self._redirect(f"/{version}/{code}/")
|
|
elif target.name == "index.html" and rest and not rest.endswith(("/", ".html")):
|
|
self._redirect(f"/{version}/{code}/{rest}/")
|
|
else:
|
|
self._file(target, head)
|
|
|
|
|
|
def main():
|
|
library = Library(
|
|
os.environ["DOCS_SOURCE_REPO"],
|
|
os.environ["DOCS_DATA_DIR"],
|
|
int(os.environ["DOCS_BUILD_JOBS"]),
|
|
PACKAGE_DIR,
|
|
os.environ["DOCS_SNAPSHOT_DIR"],
|
|
)
|
|
threading.Thread(
|
|
target=library.run_builder,
|
|
args=(int(os.environ["DOCS_FETCH_INTERVAL"]),),
|
|
daemon=True,
|
|
).start()
|
|
handler = partial(DocsHandler, library=library)
|
|
ThreadingHTTPServer(("", int(os.environ["DOCS_PORT"])), handler).serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|