Skip to content

Commit 758b35f

Browse files
authored
Merge pull request #1 from Query-farm/ci/multi-transport-sql
ci: multi-transport (subprocess/http/unix) SQL E2E matrix
2 parents c48c3bd + df6c694 commit 758b35f

6 files changed

Lines changed: 470 additions & 30 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,19 @@ jobs:
9797
GH_TOKEN: ${{ github.token }}
9898

9999
integration:
100-
name: End-to-end SQL on ${{ matrix.os }}
100+
name: SQL E2E (${{ matrix.transport }}) on ${{ matrix.os }}
101101
needs: resolve-haybarn
102102
strategy:
103103
fail-fast: false
104104
matrix:
105+
# Run the SAME sqllogictest suite over every VGI transport. The vgi
106+
# extension picks the transport from the ATTACH LOCATION string that
107+
# run-integration.sh builds per $TRANSPORT:
108+
# subprocess : `.venv/bin/python rerank_worker.py` (stdio; extension spawns it)
109+
# http : `http://127.0.0.1:<port>` (worker booted with --http)
110+
# unix : `unix:///tmp/rerank.sock` (worker booted with --unix)
111+
os: [ubuntu-latest, macos-latest]
112+
transport: [subprocess, http, unix]
105113
include:
106114
- { os: ubuntu-latest, asset: haybarn_unittest-linux-amd64.zip }
107115
- { os: macos-latest, asset: haybarn_unittest-osx-arm64.zip }
@@ -118,8 +126,10 @@ jobs:
118126
- name: Set up Python 3.13
119127
run: uv python install 3.13
120128

121-
- name: Install the worker (from the lockfile)
122-
run: uv sync --frozen --python 3.13
129+
- name: Install the worker (from the lockfile, with the http extra)
130+
# The `http` extra pulls in waitress so the worker can serve `--http`.
131+
# Harmless for the subprocess/unix legs; required for the http leg.
132+
run: uv sync --frozen --python 3.13 --extra http
123133

124134
# Pre-warm the fastembed/ONNX model cache so the first ATTACH+score isn't
125135
# paying the ~80 MB download inline.
@@ -147,7 +157,9 @@ jobs:
147157
UNITTEST="$PWD/$(find hb -name 'haybarn-unittest' -type f | head -1)"
148158
chmod +x "$UNITTEST"
149159
echo "HAYBARN_UNITTEST=$UNITTEST" >> "$GITHUB_ENV"
150-
echo "VGI_RERANK_WORKER=$PWD/.venv/bin/python $PWD/rerank_worker.py" >> "$GITHUB_ENV"
160+
echo "WORKER_CMD=$PWD/.venv/bin/python $PWD/rerank_worker.py" >> "$GITHUB_ENV"
151161
152-
- name: Run extension integration suite
162+
- name: Run extension integration suite (${{ matrix.transport }})
153163
run: ci/run-integration.sh
164+
env:
165+
TRANSPORT: ${{ matrix.transport }}

ci/README.md

Lines changed: 86 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# CI: the vgi-calendar worker integration suite
1+
# CI: the vgi-rerank worker integration suite
22

33
[`.github/workflows/ci.yml`](../.github/workflows/ci.yml) runs the unit tests
4-
and this repo's sqllogictest suite (`test/sql/*.test`) against the vgi-calendar
4+
and this repo's sqllogictest suite (`test/sql/*.test`) against the vgi-rerank
55
VGI worker through the **real DuckDB `vgi` extension** on every push / PR.
66

77
## How it works (no C++ build)
@@ -11,9 +11,9 @@ Rather than building the vgi DuckDB extension from source, CI drives a
1111
runner, published in Haybarn's releases) and installs the **signed** `vgi`
1212
extension from the Haybarn community channel:
1313

14-
1. **Install the worker**`uv sync --frozen` into a venv. `calendar_worker.py`
15-
is a self-contained PEP 723 stdio worker the extension can spawn via
16-
`uv run calendar_worker.py`.
14+
1. **Install the worker**`uv sync --frozen --extra http` into a venv.
15+
`rerank_worker.py` is a self-contained PEP 723 worker the extension can spawn
16+
via stdio, or that the harness can boot over HTTP / an AF_UNIX socket.
1717
2. **Download the runner** — the matching `haybarn_unittest-*` asset per
1818
platform from the latest Haybarn release.
1919
3. **Preprocess** — the standalone runner links none of the extensions the
@@ -24,20 +24,92 @@ extension from the Haybarn community channel:
2424
`INSTALL vgi FROM community;` right before each bare `LOAD vgi;`. `require-env`
2525
and everything else pass through untouched.
2626
4. **Run**[`run-integration.sh`](run-integration.sh) stages the preprocessed
27-
tree, points `VGI_CALENDAR_WORKER` at `uv run calendar_worker.py`, warms the
28-
extension cache once, then runs the suite in a single `haybarn-unittest`
29-
invocation. Any failed assertion exits non-zero and fails the job.
27+
tree, resolves `VGI_RERANK_WORKER` (the ATTACH `LOCATION`) per the
28+
`$TRANSPORT` it's run with (see below), warms the extension cache once, then
29+
runs the suite in a single `haybarn-unittest` invocation. Any failed
30+
assertion exits non-zero and fails the job.
31+
32+
## Transport matrix (subprocess | http | unix)
33+
34+
The same `test/sql/*.test` suite is run over all three VGI transports — the
35+
extension picks the transport from the `LOCATION` string the `.test` files
36+
`ATTACH`, and `run-integration.sh` builds that string from `$TRANSPORT`:
37+
38+
| `TRANSPORT` | `VGI_RERANK_WORKER` (LOCATION) | How the worker is reached |
39+
|--------------|---------------------------------------|---------------------------|
40+
| `subprocess` | `.venv/bin/python rerank_worker.py` | extension spawns the worker per query; Arrow IPC over stdin/stdout (default) |
41+
| `http` | `http://127.0.0.1:<port>` | harness boots `rerank_worker.py --http --port 0 --port-file <f>`, waits for the port-file, then ATTACHes that URL |
42+
| `unix` | `unix:///tmp/rerank-<pid>.sock` | harness boots `rerank_worker.py --unix <sock>`, waits for the socket to appear, then ATTACHes it |
43+
44+
The CI `integration` job is a `transport: [subprocess, http, unix]` × `os`
45+
matrix; each leg runs `ci/run-integration.sh` with `TRANSPORT=<t>`. Run a single
46+
transport locally with e.g. `TRANSPORT=http ci/run-integration.sh`.
47+
48+
### Port / readiness discovery
49+
50+
- **http**: the worker writes its auto-selected port to `--port-file`
51+
atomically (tmp + rename), so the harness watches for that file to appear and
52+
reads the port from it — it does **not** parse stdout. Boot line:
53+
`rerank_worker.py --http --port 0 --port-file <f>`.
54+
- **unix**: the worker binds the AF_UNIX socket and prints `UNIX:<abs-path>`;
55+
the harness polls for the socket file (`test -S`) to appear. Boot line:
56+
`rerank_worker.py --unix <sock>`.
57+
58+
Both out-of-band server processes are booted with cwd = the repo root (so they
59+
resolve the worker's relative resources / model cache) and are trap-killed on
60+
exit.
61+
62+
### HTTP transport needs the `httpfs` extension (resolved, not gated)
63+
64+
The vgi extension implements HTTP transport on top of DuckDB's **httpfs**
65+
extension, so an `http://` ATTACH binds with
66+
67+
> `Binder Error: VGI HTTP transport requires the httpfs extension. Install it with: INSTALL httpfs; LOAD httpfs;`
68+
69+
unless httpfs is loaded first. This is a **dependency**, not a protocol
70+
limitation, so we resolve it rather than gate: the http leg of
71+
`run-integration.sh` injects a signed `INSTALL httpfs FROM core; LOAD httpfs;`
72+
into each staged `.test` (right after the awk-injected `LOAD vgi;`). The
73+
`.test` files themselves stay transport-agnostic.
74+
75+
The http leg also needs the worker's `http` extra (waitress): `pyproject.toml`
76+
ships an `http` extra (`vgi-python[http]`), the PEP 723 header lists
77+
`vgi-python[http]`, and CI runs `uv sync --frozen --extra http`.
78+
79+
> **Sharp edge — the runner silently SKIPs HTTP errors.** The haybarn/DuckDB
80+
> sqllogictest runner's default skip list skips any statement whose error
81+
> message contains `"HTTP"` or `"Unable to connect"`. Without the httpfs load,
82+
> *every* HTTP-leg test SKIPs (the httpfs binder error contains "HTTP") and the
83+
> suite reports "All tests were skipped" — a green-looking **fake pass**, not a
84+
> real one. `run-integration.sh` therefore fails the leg unless the runner
85+
> reports `All tests passed (N assertions …)` with N > 0 and reports zero
86+
> skips.
87+
88+
### Per-transport status
89+
90+
- **subprocess**: GREEN — 21 assertions.
91+
- **http**: GREEN — 25 assertions (21 + the injected httpfs INSTALL/LOAD
92+
statements).
93+
- **unix**: GREEN — 21 assertions. No extra deps; `--unix` is built into the
94+
worker's `Worker.main()`.
95+
96+
The suite here is stateless scalar scoring + a static discovery table function
97+
(`supported_models()`), so none of the inherent HTTP limitations (streaming
98+
partition-local state, etc.) apply — nothing needed gating.
3099

31100
## Run it locally
32101

33102
```bash
34-
uv sync --python 3.13 # install the worker + deps
103+
uv sync --python 3.13 --extra http # install the worker + deps (http extra for the http leg)
35104
# point HAYBARN_UNITTEST at a haybarn-unittest binary (or a local DuckDB
36-
# `unittest` built with the vgi extension), and the worker at the stdio command:
105+
# `unittest` built with the vgi extension). WORKER_CMD is the stdio command that
106+
# runs the worker; the harness uses it directly for subprocess and boots it with
107+
# --http / --unix for the other transports.
37108
HAYBARN_UNITTEST=/path/to/haybarn-unittest \
38-
VGI_CALENDAR_WORKER="uv run --python 3.13 calendar_worker.py" \
39-
ci/run-integration.sh
109+
WORKER_CMD="uv run --python 3.13 rerank_worker.py" \
110+
TRANSPORT=subprocess ci/run-integration.sh # or TRANSPORT=http / TRANSPORT=unix
40111
```
41112

42-
Or use the Makefile target `make test-sql`, which installs `haybarn-unittest`
43-
as a uv tool and points the worker at `uv run --python 3.13 calendar_worker.py`.
113+
`TRANSPORT` defaults to `subprocess`, and `WORKER_CMD` defaults to
114+
`uv run --python 3.13 <repo>/rerank_worker.py`, so a bare
115+
`HAYBARN_UNITTEST=… ci/run-integration.sh` runs the subprocess leg.

ci/run-integration.sh

Lines changed: 176 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,171 @@
55
# VGI worker, using a prebuilt standalone `haybarn-unittest` and the signed
66
# community `vgi` extension — no C++ build from source. See ci/README.md.
77
#
8+
# The SAME suite is exercised over three VGI transports, selected by $TRANSPORT.
9+
# The vgi extension picks the transport from the LOCATION string the .test files
10+
# ATTACH (`${VGI_RERANK_WORKER}`):
11+
#
12+
# subprocess : a bare stdio command (`.venv/bin/python rerank_worker.py`) — the
13+
# extension spawns the worker per query and talks Arrow IPC over
14+
# stdin/stdout. Default; current behavior.
15+
# http : the worker is started out-of-band in `--http` mode on an auto
16+
# port; LOCATION becomes `http://127.0.0.1:<port>`.
17+
# unix : the worker is started out-of-band on an AF_UNIX socket;
18+
# LOCATION becomes `unix:///path/to.sock`.
19+
#
820
# Required environment:
9-
# HAYBARN_UNITTEST path to the haybarn-unittest binary
10-
# VGI_RERANK_WORKER worker LOCATION the .test files ATTACH (a stdio command
11-
# such as `uv run rerank_worker.py`, or an http:// URL)
21+
# HAYBARN_UNITTEST path to the haybarn-unittest binary
22+
# TRANSPORT subprocess | http | unix (default: subprocess)
23+
# WORKER_CMD the stdio command that runs the worker. Used directly
24+
# as the LOCATION for subprocess, and as the process to
25+
# boot the server for http/unix. Defaults to
26+
# `uv run --python 3.13 <repo>/rerank_worker.py`.
1227
# Optional:
13-
# STAGE scratch dir for the preprocessed test tree (default: mktemp)
28+
# STAGE scratch dir for the preprocessed test tree (default: mktemp)
1429
set -euo pipefail
1530

1631
: "${HAYBARN_UNITTEST:?path to the haybarn-unittest binary}"
17-
: "${VGI_RERANK_WORKER:?worker LOCATION (stdio command or http:// URL)}"
1832

1933
HERE="$(cd "$(dirname "$0")" && pwd)"
2034
REPO="$(cd "$HERE/.." && pwd)"
2135
STAGE="${STAGE:-$(mktemp -d)}"
36+
TRANSPORT="${TRANSPORT:-subprocess}"
37+
WORKER_CMD="${WORKER_CMD:-uv run --python 3.13 $REPO/rerank_worker.py}"
2238

2339
echo "Staging preprocessed tests into $STAGE ..."
2440
mkdir -p "$STAGE/test/sql"
2541
for f in "$REPO"/test/sql/*.test; do
2642
awk -f "$HERE/preprocess-require.awk" "$f" > "$STAGE/test/sql/$(basename "$f")"
2743
done
2844

45+
# ---------------------------------------------------------------------------
46+
# Per-transport: resolve VGI_RERANK_WORKER (the LOCATION) and, for the
47+
# out-of-band transports, boot the worker server + arrange trap-cleanup.
48+
# ---------------------------------------------------------------------------
49+
SERVER_PID=""
50+
SOCK=""
51+
PORT_FILE=""
52+
53+
cleanup() {
54+
# Capture the real exit status FIRST: an EXIT trap whose last command returns
55+
# non-zero (e.g. a short-circuited `[[ -n "" ]] && …` on the subprocess/unix
56+
# legs where nothing needs cleaning) would otherwise become the script's exit
57+
# status under `set -e` and fail an already-passing run.
58+
local rc=$?
59+
if [[ -n "$SERVER_PID" ]]; then
60+
kill "$SERVER_PID" 2>/dev/null || true
61+
wait "$SERVER_PID" 2>/dev/null || true
62+
fi
63+
if [[ -n "$SOCK" ]]; then rm -f "$SOCK"; fi
64+
if [[ -n "$PORT_FILE" ]]; then rm -f "$PORT_FILE"; fi
65+
return "$rc"
66+
}
67+
trap cleanup EXIT
68+
69+
case "$TRANSPORT" in
70+
subprocess)
71+
export VGI_RERANK_WORKER="$WORKER_CMD"
72+
;;
73+
74+
http)
75+
# The vgi extension's HTTP transport is implemented on top of DuckDB's
76+
# httpfs extension, so an `http://` ATTACH binds with
77+
# "Binder Error: VGI HTTP transport requires the httpfs extension."
78+
# unless httpfs is loaded first. (The haybarn sqllogictest runner's default
79+
# skip list swallows any error containing "HTTP", so without this the whole
80+
# suite would silently SKIP rather than fail — a fake pass.) The .test files
81+
# are transport-agnostic; inject a signed `INSTALL httpfs FROM core; LOAD
82+
# httpfs;` right after the awk-injected `LOAD vgi;` in each staged file, so
83+
# httpfs is present only when we actually run over HTTP.
84+
echo "Injecting httpfs load into staged tests (HTTP transport needs it) ..."
85+
for sf in "$STAGE"/test/sql/*.test; do
86+
awk '
87+
{ print }
88+
/^LOAD[ \t]+vgi[ \t]*;[ \t]*$/ && !done {
89+
print "";
90+
print "statement ok";
91+
print "INSTALL httpfs FROM core;";
92+
print "";
93+
print "statement ok";
94+
print "LOAD httpfs;";
95+
done = 1
96+
}
97+
' "$sf" > "$sf.tmp" && mv "$sf.tmp" "$sf"
98+
done
99+
100+
# Boot the worker in HTTP mode on an auto-selected port. The worker writes
101+
# the chosen port to --port-file atomically (tmp + rename), so we watch for
102+
# the file to appear rather than parsing stdout. HTTP mode needs the `http`
103+
# extra (waitress); WORKER_CMD must resolve it — CI runs
104+
# `uv sync --extra http` and the PEP 723 header lists `vgi-python[http]`.
105+
PORT_FILE="$(mktemp -u "${TMPDIR:-/tmp}/rerank-port.XXXXXX")"
106+
LOG_FILE="${TMPDIR:-/tmp}/rerank-http-server.log"
107+
echo "Starting HTTP worker: $WORKER_CMD --http --port 0 --port-file $PORT_FILE"
108+
# shellcheck disable=SC2086
109+
( cd "$REPO" && exec $WORKER_CMD --http --port 0 --port-file "$PORT_FILE" ) > "$LOG_FILE" 2>&1 &
110+
SERVER_PID=$!
111+
112+
PORT=""
113+
for _ in $(seq 1 240); do
114+
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
115+
echo "ERROR: HTTP worker exited before reporting a port. Log:" >&2
116+
cat "$LOG_FILE" >&2
117+
exit 1
118+
fi
119+
if [[ -s "$PORT_FILE" ]]; then
120+
PORT="$(tr -d '[:space:]' < "$PORT_FILE")"
121+
[[ -n "$PORT" ]] && break
122+
fi
123+
sleep 0.5
124+
done
125+
if [[ -z "$PORT" ]]; then
126+
echo "ERROR: timed out waiting for HTTP worker port-file. Log:" >&2
127+
cat "$LOG_FILE" >&2
128+
exit 1
129+
fi
130+
echo "HTTP worker ready on port $PORT (pid $SERVER_PID)"
131+
export VGI_RERANK_WORKER="http://127.0.0.1:$PORT"
132+
;;
133+
134+
unix)
135+
# Boot the worker bound to an AF_UNIX socket. The worker prints
136+
# `UNIX:<abs-path>` once bound; we poll for the socket file to appear.
137+
SOCK="${TMPDIR:-/tmp}/rerank-$$.sock"
138+
rm -f "$SOCK"
139+
LOG_FILE="${TMPDIR:-/tmp}/rerank-unix-server.log"
140+
echo "Starting unix worker: $WORKER_CMD --unix $SOCK"
141+
# shellcheck disable=SC2086
142+
( cd "$REPO" && exec $WORKER_CMD --unix "$SOCK" ) > "$LOG_FILE" 2>&1 &
143+
SERVER_PID=$!
144+
145+
READY=""
146+
for _ in $(seq 1 240); do
147+
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
148+
echo "ERROR: unix worker exited before binding the socket. Log:" >&2
149+
cat "$LOG_FILE" >&2
150+
exit 1
151+
fi
152+
if [[ -S "$SOCK" ]]; then
153+
READY=1
154+
break
155+
fi
156+
sleep 0.5
157+
done
158+
if [[ -z "$READY" ]]; then
159+
echo "ERROR: timed out waiting for unix worker socket. Log:" >&2
160+
cat "$LOG_FILE" >&2
161+
exit 1
162+
fi
163+
echo "unix worker ready on $SOCK (pid $SERVER_PID)"
164+
export VGI_RERANK_WORKER="unix://$SOCK"
165+
;;
166+
167+
*)
168+
echo "ERROR: unknown TRANSPORT '$TRANSPORT' (want subprocess|http|unix)" >&2
169+
exit 2
170+
;;
171+
esac
172+
29173
cd "$STAGE"
30174

31175
# Warm the extension cache once: vgi from the signed community channel. A miss
@@ -42,7 +186,30 @@ EOF
42186
"$HAYBARN_UNITTEST" "test/_warm.test" >/dev/null 2>&1 || echo "::warning::extension warm step did not fully succeed"
43187
rm -f "$STAGE/test/_warm.test"
44188

45-
# Run the whole suite in one invocation, streaming the runner's native
46-
# sqllogictest report. Any failed assertion exits non-zero and fails the job.
47-
echo "Running suite (worker: $VGI_RERANK_WORKER) ..."
48-
"$HAYBARN_UNITTEST" "test/sql/*"
189+
# Run the whole suite in one invocation, capturing the runner's native
190+
# sqllogictest report so we can both stream it AND assert on the summary line.
191+
echo "Running suite (transport: $TRANSPORT, worker: $VGI_RERANK_WORKER) ..."
192+
RUN_LOG="$STAGE/run.log"
193+
set +e
194+
"$HAYBARN_UNITTEST" "test/sql/*" 2>&1 | tee "$RUN_LOG"
195+
status=${PIPESTATUS[0]}
196+
set -e
197+
198+
# SILENT-SKIP GUARD (critical for the http leg). DuckDB's sqllogictest runner
199+
# auto-SKIPS (exit 0!) any test whose error message contains "HTTP" or "Unable
200+
# to connect" — so a broken http setup reports "All tests were skipped" and the
201+
# job goes GREEN while testing nothing. Fail the leg unless the runner reports a
202+
# real pass with N>0 assertions and reported zero skips.
203+
if [[ $status -ne 0 ]]; then
204+
echo "ERROR: haybarn-unittest exited $status" >&2
205+
exit "$status"
206+
fi
207+
if grep -Eqi 'were skipped|tests were skipped' "$RUN_LOG"; then
208+
echo "ERROR: tests were SKIPPED (likely a masked $TRANSPORT transport error — see above)." >&2
209+
exit 1
210+
fi
211+
if ! grep -Eq 'All tests passed \([1-9][0-9]* assertions' "$RUN_LOG"; then
212+
echo "ERROR: did not find an 'All tests passed (N assertions ...)' summary with N>0." >&2
213+
exit 1
214+
fi
215+
echo "Suite GREEN over transport: $TRANSPORT"

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ dev = [
4141
"mypy>=1.10",
4242
"pydoclint>=0.5",
4343
]
44+
# HTTP transport: the worker can serve over HTTP (`rerank_worker.py --http`) in
45+
# addition to stdio/AF_UNIX. That path needs vgi-python's `http` extra
46+
# (waitress). CI's http transport leg installs this via `uv sync --extra http`.
47+
http = [
48+
"vgi-python[http]>=0.8.3",
49+
]
4450

4551
[project.urls]
4652
Homepage = "https://query.farm"

0 commit comments

Comments
 (0)