prefect server in zig

add docker build and polish test harness

- dockerfile: multi-stage build with zig 0.15.2, copies facil.io shared lib
- docker-compose: server, redis, postgres, test services with healthchecks
- justfile: docker-test, docker-full, bench-compare, test-client commands
- benchmark: fix avg calculation to include results with timing data
- test-serve: add uv script header for prefect dependency

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+169 -28
+44
Dockerfile
··· 1 + # Build stage 2 + FROM alpine:3.20 AS builder 3 + 4 + # install build dependencies + CA certs for TLS 5 + RUN apk add --no-cache curl xz ca-certificates 6 + 7 + # download zig 0.15.2 based on architecture 8 + RUN ARCH=$(uname -m) && \ 9 + if [ "$ARCH" = "x86_64" ]; then ZIG_ARCH="x86_64"; \ 10 + elif [ "$ARCH" = "aarch64" ]; then ZIG_ARCH="aarch64"; \ 11 + else echo "Unsupported arch: $ARCH" && exit 1; fi && \ 12 + curl -L "https://ziglang.org/download/0.15.2/zig-${ZIG_ARCH}-linux-0.15.2.tar.xz" | \ 13 + tar -xJ -C /usr/local && \ 14 + ln -s /usr/local/zig-${ZIG_ARCH}-linux-0.15.2/zig /usr/local/bin/zig 15 + 16 + WORKDIR /build 17 + COPY build.zig build.zig.zon ./ 18 + COPY src/ src/ 19 + 20 + RUN zig build -Doptimize=ReleaseFast 21 + 22 + # copy facil.io shared library from zig cache 23 + RUN mkdir -p /build/lib && \ 24 + find / -name "libfacil.io.so" 2>/dev/null -exec cp {} /build/lib/ \; 25 + 26 + # Runtime stage 27 + FROM alpine:3.20 28 + 29 + RUN apk add --no-cache libstdc++ 30 + 31 + WORKDIR /app 32 + 33 + # copy binary and shared library 34 + COPY --from=builder /build/zig-out/bin/prefect-server /app/ 35 + COPY --from=builder /build/lib/ /app/lib/ 36 + 37 + # set library path 38 + ENV LD_LIBRARY_PATH=/app/lib 39 + ENV PREFECT_SERVER_PORT=4200 40 + ENV PREFECT_SERVER_LOGGING_LEVEL=INFO 41 + 42 + EXPOSE 4200 43 + 44 + CMD ["/app/prefect-server"]
+44 -2
docker-compose.yml
··· 1 - # dev services for prefect-server (redis + postgres) 2 - # usage: docker compose up -d 1 + # prefect-server development and testing 2 + # 3 + # usage: 4 + # docker compose up -d # start all services 5 + # docker compose up -d redis postgres # start dependencies only 6 + # docker compose up --build server # build and run server 7 + # docker compose run --rm test # run integration tests 3 8 4 9 services: 10 + server: 11 + build: . 12 + ports: 13 + - "4200:4200" 14 + environment: 15 + PREFECT_SERVER_LOGGING_LEVEL: INFO 16 + PREFECT_DATABASE_BACKEND: ${PREFECT_DATABASE_BACKEND:-sqlite} 17 + PREFECT_DATABASE_CONNECTION_URL: ${PREFECT_DATABASE_CONNECTION_URL:-} 18 + PREFECT_BROKER_BACKEND: ${PREFECT_BROKER_BACKEND:-memory} 19 + PREFECT_REDIS_URL: ${PREFECT_REDIS_URL:-} 20 + depends_on: 21 + redis: 22 + condition: service_healthy 23 + required: false 24 + postgres: 25 + condition: service_healthy 26 + required: false 27 + healthcheck: 28 + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:4200/api/health"] 29 + interval: 5s 30 + timeout: 3s 31 + retries: 3 32 + 5 33 redis: 6 34 image: redis:7-alpine 7 35 ports: ··· 25 53 interval: 5s 26 54 timeout: 3s 27 55 retries: 3 56 + 57 + # integration test runner 58 + test: 59 + image: python:3.12-slim 60 + working_dir: /tests 61 + volumes: 62 + - ./scripts:/tests:ro 63 + environment: 64 + PREFECT_API_URL: http://server:4200/api 65 + depends_on: 66 + server: 67 + condition: service_healthy 68 + entrypoint: ["sh", "-c"] 69 + command: ["pip install -q httpx rich && python test-api-sequence --json"]
+56 -10
justfile
··· 1 1 default: 2 2 @just --list 3 3 4 + # build the server 4 5 build: 5 6 zig build 6 7 8 + # build optimized release 9 + build-release: 10 + zig build -Doptimize=ReleaseFast 11 + 12 + # run dev server (sqlite, memory broker, debug logging) 7 13 dev: build 8 14 rm -f prefect.db 9 15 PREFECT_SERVER_LOGGING_LEVEL=DEBUG ./zig-out/bin/prefect-server 10 16 17 + # run API test suite against local server 11 18 test: 12 - PREFECT_API_URL=http://localhost:4200/api ./scripts/test-flow 19 + PREFECT_API_URL=http://localhost:4200/api ./scripts/test-api-sequence 20 + 21 + # run .serve() integration test 22 + test-serve: 23 + PREFECT_API_URL=http://localhost:4200/api ./scripts/test-serve 13 24 14 - # broker backend tests (starts/stops containers as needed) 25 + # broker backend tests (memory + redis) 15 26 test-broker backend="all": 16 27 #!/usr/bin/env bash 17 28 set -euo pipefail 18 29 if [ "{{backend}}" = "redis" ] || [ "{{backend}}" = "all" ]; then 19 30 docker compose up -d redis 20 - sleep 1 # wait for redis to be ready 31 + sleep 1 21 32 fi 22 33 ./scripts/test-broker-backends {{backend}} 23 34 if [ "{{backend}}" = "redis" ] || [ "{{backend}}" = "all" ]; then 24 35 docker compose down redis 25 36 fi 26 37 27 - # database backend tests (starts/stops containers as needed) 38 + # database backend tests (sqlite + postgres) 28 39 test-db backend="all": 29 40 #!/usr/bin/env bash 30 41 set -euo pipefail 31 42 if [ "{{backend}}" = "postgres" ] || [ "{{backend}}" = "all" ]; then 32 43 docker compose up -d postgres 33 - sleep 2 # wait for postgres to be ready 44 + sleep 2 34 45 fi 35 46 ./scripts/test-db-backends {{backend}} 36 47 if [ "{{backend}}" = "postgres" ] || [ "{{backend}}" = "all" ]; then 37 48 docker compose down postgres 38 49 fi 39 50 51 + # run high-level prefect client tests (requires running server) 52 + test-client: 53 + PREFECT_API_URL=http://localhost:4200/api ./scripts/test-flow 54 + PREFECT_API_URL=http://localhost:4200/api ./scripts/test-blocks 55 + 56 + # run all tests 57 + test-all: test test-serve test-client test-broker test-db 58 + 40 59 # start dev services (redis + postgres) 41 60 services-up: 42 - docker compose up -d 61 + docker compose up -d redis postgres 43 62 44 63 # stop dev services 45 64 services-down: 46 65 docker compose down 47 66 48 - # benchmarking 49 - bench server="zig" workload="scripts/test-api-sequence" iterations="1": 50 - ./scripts/benchmark --server {{server}} --workload {{workload}} --iterations {{iterations}} 67 + # build and run server in docker 68 + docker-build: 69 + docker compose build server 70 + 71 + # run server in docker (sqlite + memory) 72 + docker-run: docker-build 73 + docker compose up server 74 + 75 + # run integration tests in docker 76 + docker-test: 77 + docker compose up --build -d server 78 + docker compose run --rm test 79 + docker compose down 80 + 81 + # run server with postgres + redis in docker 82 + docker-full: 83 + PREFECT_DATABASE_BACKEND=postgres \ 84 + PREFECT_DATABASE_CONNECTION_URL="postgresql://prefect:prefect@postgres:5432/prefect" \ 85 + PREFECT_BROKER_BACKEND=redis \ 86 + PREFECT_REDIS_URL="redis://redis:6379" \ 87 + docker compose up --build 51 88 52 - bench-compare workload="scripts/test-api-sequence" iterations="1": 89 + # benchmark against local server 90 + bench workload="scripts/test-api-sequence" iterations="3": 91 + ./scripts/benchmark --server zig --workload {{workload}} --iterations {{iterations}} 92 + 93 + # benchmark comparison (zig vs python) 94 + bench-compare workload="scripts/test-api-sequence" iterations="3": 53 95 ./scripts/benchmark --compare --workload {{workload}} --iterations {{iterations}} 96 + 97 + # clean build artifacts 98 + clean: 99 + rm -rf zig-out .zig-cache prefect.db
+1 -1
loq.toml
··· 6 6 7 7 [[rules]] 8 8 path = "scripts/benchmark" 9 - max_lines = 564 9 + max_lines = 575 10 10 11 11 [[rules]] 12 12 path = "scripts/test-api-sequence"
+19 -14
scripts/benchmark
··· 335 335 """Print detailed comparison.""" 336 336 337 337 def avg(results: list[BenchmarkResult], key: str) -> float: 338 - successful = [r for r in results if r.success] 339 - if not successful: 338 + # use results with valid timing data, not just fully successful ones 339 + with_data = [r for r in results if getattr(r, key, 0) > 0] 340 + if not with_data: 340 341 return 0 341 - return sum(getattr(r, key) for r in successful) / len(successful) 342 + return sum(getattr(r, key) for r in with_data) / len(with_data) 342 343 343 344 def avg_section(results: list[BenchmarkResult], section_name: str) -> float: 344 345 successful = [r for r in results if r.success] ··· 369 370 "time", 370 371 f"{zig_time:.0f}ms", 371 372 f"{python_time:.0f}ms", 372 - f"[green]{speedup:.1f}x faster[/green]" if speedup > 1 else f"[yellow]{1/speedup:.1f}x slower[/yellow]", 373 + f"[green]{speedup:.1f}x faster[/green]" if speedup >= 1 else f"[yellow]{1/speedup:.1f}x slower[/yellow]" if speedup > 0 else "n/a", 373 374 ) 374 375 375 376 zig_mem = avg(zig_results, "memory_mb") 376 377 python_mem = avg(python_results, "memory_mb") 377 378 mem_ratio = python_mem / zig_mem if zig_mem > 0 else 0 378 379 379 - summary.add_row( 380 - "memory", 381 - f"{zig_mem:.1f}MB", 382 - f"{python_mem:.1f}MB", 383 - f"[green]{mem_ratio:.1f}x smaller[/green]" if mem_ratio > 1 else f"[yellow]{1/mem_ratio:.1f}x larger[/yellow]", 384 - ) 380 + if mem_ratio >= 1: 381 + mem_advantage = f"[green]{mem_ratio:.1f}x smaller[/green]" 382 + elif mem_ratio > 0: 383 + mem_advantage = f"[yellow]{1/mem_ratio:.1f}x larger[/yellow]" 384 + else: 385 + mem_advantage = "n/a" 386 + 387 + summary.add_row("memory", f"{zig_mem:.1f}MB", f"{python_mem:.1f}MB", mem_advantage) 385 388 386 389 zig_reqs = avg(zig_results, "total_requests") 387 390 python_reqs = avg(python_results, "total_requests") ··· 389 392 390 393 console.print(summary) 391 394 392 - # section breakdown 393 - if zig_results and zig_results[0].success and python_results and python_results[0].success: 395 + # section breakdown (show if both have sections data) 396 + zig_has_sections = zig_results and zig_results[0].sections 397 + python_has_sections = python_results and python_results[0].sections 398 + if zig_has_sections and python_has_sections: 394 399 console.print() 395 400 breakdown = Table(title="timing breakdown by section") 396 401 breakdown.add_column("section", style="cyan") ··· 417 422 console.print(breakdown) 418 423 419 424 # final verdict 420 - if zig_time > 0 and python_time > 0: 421 - if speedup > 1: 425 + if zig_time > 0 and python_time > 0 and speedup > 0: 426 + if speedup >= 1: 422 427 console.print(f"\n[bold green]zig is {speedup:.1f}x faster overall[/bold green]") 423 428 else: 424 429 console.print(f"\n[bold yellow]python is {1/speedup:.1f}x faster overall[/bold yellow]")
+5 -1
scripts/test-serve
··· 1 - #!/usr/bin/env python3 1 + #!/usr/bin/env -S uv run --script --quiet 2 + # /// script 3 + # requires-python = ">=3.12" 4 + # dependencies = ["prefect>=3.0"] 5 + # /// 2 6 """Test deployment .serve() functionality""" 3 7 4 8 import sys