forked from retoor/devplacepy
The workspace editor hung for 60s and then 504'd. Three independent faults were
stacked behind that one symptom.
Reachability: editor_target delegated to proxy_target, which returns
CONTAINER_PROXY_HOST plus the published host port and never falls back to the
container. From inside the app container that address crosses docker0 into the
host INPUT chain, whose policy is DROP with an allow-list that does not include
the published port range, so the packet was dropped and the request hung rather
than being refused. Measured from the app container: container_ip:8443 answers
302, gateway:20006 is dropped. One shared reachable_target now prefers the
direct container leg and falls back to the published port, and editor_target
uses tunnel_target as services/containers/CLAUDE.md already required. The same
defect affected /p/{slug} ingress and every tunnel, since all three resolved
through proxy_target.
The recorded measurement that motivated the old order (container_ip times out,
gateway connects) no longer holds: make docker-attach puts the app on the
instances' bridge network, which is what makes the direct leg work.
Duplicate response headers: the forwarding core relayed the upstream Date and
Server alongside the ones the serving layer generates, so every proxied
response carried two of each. Both are singleton headers and duplicating them
is malformed HTTP.
Serialization: WorkspaceViewOut declared flag_reason and three sibling strings
as str, so a NULL column made the workspace page 500 for JSON clients.
Documents the two public hostnames and the devplace.net SSH tunnel, so a future
session does not conclude the site is down after pointing curl --resolve at an
address the hostname does not resolve to, and adds the layered procedure for
diagnosing a production failure.
Verified on production with Playwright over both hostnames: the code-server
login renders and the workbench loads. Suite: 3345 passed.
191 lines
6.7 KiB
Makefile
191 lines
6.7 KiB
Makefile
LOCUST_PORT ?= 10502
|
|
LOCUST_WEB_PORT ?= 10503
|
|
LOCUST_DB_DIR ?= /tmp/devplace_locust
|
|
LOCUST_DB ?= $(LOCUST_DB_DIR)/datastore.db
|
|
LOCUST_USERS ?= 20
|
|
LOCUST_SPAWN_RATE ?= 5
|
|
LOCUST_RUN_TIME ?= 120s
|
|
LOCUST_WEB_WORKERS ?= 4
|
|
WEB_WORKERS ?= $(shell nproc 2>/dev/null || echo 2)
|
|
DEVPLACE_RATE_LIMIT ?= 1000000
|
|
|
|
PYTHONDONTWRITEBYTECODE := 1
|
|
export PYTHONDONTWRITEBYTECODE
|
|
|
|
.PHONY: install dev clean tree tree-loc zip test test-headed test-unit test-api test-e2e test-fast test-failed test-first-failure test-slowest test-cache-clean coverage coverage-headed coverage-html locust locust-headless
|
|
|
|
install:
|
|
pip install -e .
|
|
python -m playwright install chromium
|
|
|
|
dev:
|
|
uvicorn devplacepy.main:app --reload --reload-dir devplacepy --host 0.0.0.0 --port 10500 --backlog 4096
|
|
|
|
prod:
|
|
DEVPLACE_STATIC_VERSION=$$(date +%s) DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(WEB_WORKERS) uvicorn devplacepy.main:app --host 0.0.0.0 --port 10500 --workers $(WEB_WORKERS) --backlog 8192 --proxy-headers --forwarded-allow-ips '*'
|
|
|
|
delete-pyc:
|
|
find . -name "__pycache__" -type d -prune -exec rm -rf {} + 2>/dev/null || true
|
|
find . -name "*.pyc" -delete
|
|
|
|
tree:
|
|
git ls-files | tree --fromfile --noreport
|
|
|
|
tree-loc:
|
|
@git ls-files | while IFS= read -r f; do \
|
|
loc=$$(wc -l < "$$f" 2>/dev/null || echo 0); \
|
|
printf '%s [%s LOC]\n' "$$f" "$$loc"; \
|
|
done | tree --fromfile --noreport
|
|
|
|
zip:
|
|
@rm -f $(notdir $(CURDIR)).zip
|
|
@git ls-files -z | xargs -0 zip -q $(notdir $(CURDIR)).zip
|
|
@printf 'Wrote %s (%s files)\n' "$(notdir $(CURDIR)).zip" "$$(git ls-files | wc -l)"
|
|
|
|
test:
|
|
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/
|
|
|
|
test-headed:
|
|
PLAYWRIGHT_HEADLESS=0 python -m pytest tests/
|
|
|
|
test-unit:
|
|
python -m pytest tests/unit
|
|
|
|
test-api:
|
|
python -m pytest tests/api
|
|
|
|
test-e2e:
|
|
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/e2e
|
|
|
|
test-fast:
|
|
python -m pytest tests/unit tests/api
|
|
|
|
test-failed:
|
|
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ --last-failed --last-failed-no-failures none
|
|
|
|
test-first-failure:
|
|
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ -x
|
|
|
|
test-slowest:
|
|
PLAYWRIGHT_HEADLESS=1 python -m pytest tests/ --durations=40
|
|
|
|
coverage:
|
|
rm -f .coverage .coverage.*
|
|
COVERAGE_PROCESS_START=$(CURDIR)/.coveragerc PLAYWRIGHT_HEADLESS=1 \
|
|
python -m coverage run -m pytest tests/
|
|
python -m coverage combine
|
|
python -m coverage report
|
|
|
|
coverage-headed:
|
|
rm -f .coverage .coverage.*
|
|
COVERAGE_PROCESS_START=$(CURDIR)/.coveragerc PLAYWRIGHT_HEADLESS=0 \
|
|
python -m coverage run -m pytest tests/
|
|
python -m coverage combine
|
|
python -m coverage report
|
|
|
|
coverage-html: coverage
|
|
python -m coverage html
|
|
@echo "Report written to htmlcov/index.html"
|
|
|
|
locust:
|
|
export DEVPLACE_DATABASE_URL="sqlite:///$(LOCUST_DB)"; \
|
|
export DEVPLACE_RATE_LIMIT=$(DEVPLACE_RATE_LIMIT); \
|
|
fuser -k $(LOCUST_PORT)/tcp 2>/dev/null || true; \
|
|
sleep 1; \
|
|
mkdir -p $(LOCUST_DB_DIR); \
|
|
rm -f $(LOCUST_DB); \
|
|
DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(LOCUST_WEB_WORKERS) uvicorn devplacepy.main:app --host 127.0.0.1 --port $(LOCUST_PORT) --workers $(LOCUST_WEB_WORKERS) --backlog 8192 > /tmp/devplace_locust_server.log 2>&1 & \
|
|
PID=$$!; \
|
|
while kill -0 $$PID 2>/dev/null && ! curl -s http://127.0.0.1:$(LOCUST_PORT)/ > /dev/null 2>&1; do sleep 0.5; done; \
|
|
if ! kill -0 $$PID 2>/dev/null; then echo "Server failed to start (port $(LOCUST_PORT) busy?). See /tmp/devplace_locust_server.log"; exit 1; fi; \
|
|
locust --host http://127.0.0.1:$(LOCUST_PORT) --web-port $(LOCUST_WEB_PORT); \
|
|
kill $$PID 2>/dev/null || true; \
|
|
rm -rf $(LOCUST_DB_DIR)
|
|
|
|
locust-headless:
|
|
export DEVPLACE_DATABASE_URL="sqlite:///$(LOCUST_DB)"; \
|
|
export DEVPLACE_RATE_LIMIT=$(DEVPLACE_RATE_LIMIT); \
|
|
fuser -k $(LOCUST_PORT)/tcp 2>/dev/null || true; \
|
|
sleep 1; \
|
|
mkdir -p $(LOCUST_DB_DIR); \
|
|
rm -f $(LOCUST_DB); \
|
|
DEVPLACE_TEMPLATE_AUTO_RELOAD=0 DEVPLACE_WEB_WORKERS=$(LOCUST_WEB_WORKERS) uvicorn devplacepy.main:app --host 127.0.0.1 --port $(LOCUST_PORT) --workers $(LOCUST_WEB_WORKERS) --backlog 8192 > /tmp/devplace_locust_server.log 2>&1 & \
|
|
PID=$$!; \
|
|
while kill -0 $$PID 2>/dev/null && ! curl -s http://127.0.0.1:$(LOCUST_PORT)/ > /dev/null 2>&1; do sleep 0.5; done; \
|
|
if ! kill -0 $$PID 2>/dev/null; then echo "Server failed to start (port $(LOCUST_PORT) busy?). See /tmp/devplace_locust_server.log"; exit 1; fi; \
|
|
locust --host http://127.0.0.1:$(LOCUST_PORT) --headless -u $(LOCUST_USERS) -r $(LOCUST_SPAWN_RATE) --run-time $(LOCUST_RUN_TIME) --html=$(LOCUST_DB_DIR)/report.html; \
|
|
kill $$PID 2>/dev/null || true; \
|
|
rm -rf $(LOCUST_DB_DIR)
|
|
|
|
clean:
|
|
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
|
find . -type f -name '*.pyc' -delete
|
|
rm -rf devplacepy.egg-info
|
|
rm -rf .pytest_cache
|
|
rm -rf .venv
|
|
|
|
test-cache-clean:
|
|
rm -rf .pytest_cache
|
|
|
|
# Container Manager works out of the box: the overlay installs the docker CLI in
|
|
# the image and mounts the host socket. DOCKER_GID is read straight from the
|
|
# socket so the UID-1000 app can use it; the data dir is the project's own data/
|
|
# at its real host path, so the DooD bind-mount (host == container path) holds
|
|
# with no /srv dir and no sudo.
|
|
COMPOSE := docker compose -f docker-compose.yml -f docker-compose.containers.yml
|
|
DEVPLACE_DATA_DIR ?= $(CURDIR)/data
|
|
DOCKER_GID ?= $(shell stat -c '%g' /var/run/docker.sock 2>/dev/null)
|
|
DEVPLACE_CONTAINER_NETWORK ?= bridge
|
|
export DEVPLACE_DATA_DIR
|
|
export DOCKER_GID
|
|
|
|
.PHONY: docker-build docker-up docker-attach docker-reload docker-down docker-logs docker-clean docker-prep ppy
|
|
|
|
# Build the single shared container image every instance runs. Build once;
|
|
# rebuild only when ppy.Dockerfile, the sudo shim, or pagent change.
|
|
ppy:
|
|
docker build --network=host -f ppy.Dockerfile -t ppy:latest devplacepy/services/containers/files
|
|
|
|
docker-prep:
|
|
mkdir -p $(DEVPLACE_DATA_DIR)
|
|
|
|
docker-build: docker-prep
|
|
$(COMPOSE) build
|
|
|
|
docker-up: docker-prep
|
|
$(COMPOSE) up -d
|
|
$(MAKE) docker-attach
|
|
|
|
# Workspace tunnels reach a container port that was never published on the host,
|
|
# so the app must sit on the same docker network as the instances it runs. The
|
|
# default bridge rejects the network-scoped aliases compose always sends, so
|
|
# this cannot live in docker-compose.containers.yml and is wired here instead.
|
|
docker-attach:
|
|
@app=$$($(COMPOSE) ps -q app); \
|
|
test -n "$$app" || { echo "app container is not running"; exit 1; }; \
|
|
docker network connect $(DEVPLACE_CONTAINER_NETWORK) $$app 2>/dev/null \
|
|
&& echo "attached app to the $(DEVPLACE_CONTAINER_NETWORK) network" \
|
|
|| echo "app is already on the $(DEVPLACE_CONTAINER_NETWORK) network"
|
|
|
|
docker-reload:
|
|
$(COMPOSE) restart app
|
|
$(COMPOSE) up -d --wait
|
|
$(MAKE) docker-attach
|
|
|
|
docker-down:
|
|
$(COMPOSE) down
|
|
|
|
docker-logs:
|
|
$(COMPOSE) logs -f
|
|
|
|
docker-clean:
|
|
$(COMPOSE) down -v
|
|
|
|
docker-bup: docker-build docker-up
|
|
|
|
deploy:
|
|
git checkout production
|
|
git merge master
|
|
git push origin production
|
|
|