Skip to content

Commit d5cb938

Browse files
committed
test: verify database workers across restart
1 parent efc2fa7 commit d5cb938

4 files changed

Lines changed: 146 additions & 2 deletions

File tree

‎CLAUDE.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ build of `~/Development/vgi-open-meteo`, and a `.tar.gz` containing the native
102102
Rust example worker plus a metadata file. It expects the sibling vgi-python,
103103
vgi-open-meteo, and vgi-rust repositories and the `uv`, `bun`, `cargo`, and
104104
`tar` tools. Repository paths and cross-build targets are overrideable; see
105-
`docs/database-worker-transport.md`.
105+
`docs/database-worker-transport.md`. The target finishes with a two-process
106+
restart test: one DuckDB process persists all three BLOBs, then a fresh process
107+
with an empty cache reopens that registry and executes every worker.
106108

107109
### HTTP Transport
108110

‎Makefile‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ VGI_RUST_DIR ?= $(HOME)/Development/vgi-rust
447447
VGI_CSHARP_DIR ?= $(HOME)/Development/vgi-csharp
448448

449449
.PHONY: test_python test_python_crash test_go test_typescript test_java test_rust test_csharp test_languages \
450-
build_database_workers test_database_workers
450+
build_database_workers test_database_workers test_database_worker_restart
451451

452452
# Real database-package demonstrations. These are intentionally separate from
453453
# the fast default suite: PyInstaller and Bun each produce a ~100 MiB native
@@ -473,6 +473,16 @@ test_database_workers: build_database_workers
473473
VGI_DATABASE_BUN_WORKER="$(VGI_DATABASE_WORKER_FIXTURE_DIR)/vgi-open-meteo" \
474474
VGI_DATABASE_RUST_WORKER="$(VGI_DATABASE_WORKER_FIXTURE_DIR)/vgi-rust-worker.tar.gz" \
475475
./build/release/test/unittest "test/sql/integration/database_worker/real_*"
476+
VGI_DATABASE_PYTHON_WORKER="$(VGI_DATABASE_WORKER_FIXTURE_DIR)/vgi-python-worker" \
477+
VGI_DATABASE_BUN_WORKER="$(VGI_DATABASE_WORKER_FIXTURE_DIR)/vgi-open-meteo" \
478+
VGI_DATABASE_RUST_WORKER="$(VGI_DATABASE_WORKER_FIXTURE_DIR)/vgi-rust-worker.tar.gz" \
479+
./scripts/test_database_worker_restart.sh
480+
481+
test_database_worker_restart: build_database_workers
482+
VGI_DATABASE_PYTHON_WORKER="$(VGI_DATABASE_WORKER_FIXTURE_DIR)/vgi-python-worker" \
483+
VGI_DATABASE_BUN_WORKER="$(VGI_DATABASE_WORKER_FIXTURE_DIR)/vgi-open-meteo" \
484+
VGI_DATABASE_RUST_WORKER="$(VGI_DATABASE_WORKER_FIXTURE_DIR)/vgi-rust-worker.tar.gz" \
485+
./scripts/test_database_worker_restart.sh
476486

477487
# Python uses this repo's default worker set. It does NOT just chain to
478488
# test_launcher, which invokes `unittest` bare: a lane that stops running tests

‎docs/database-worker-transport.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ make test_database_workers
149149
file in a `.tar.gz`, then proves VGI extracts `bin/vgi-rust-worker` and runs
150150
`sequence(7)`.
151151

152+
After the per-worker SQL tests, the lane runs a two-process persistence test.
153+
The first DuckDB process writes all three packages to a disk-backed registry
154+
database and exits. A second process starts with an empty artifact cache,
155+
attaches the registry read-only, and executes all three workers. The reader is
156+
not given the original artifact paths, proving it reconstructs each worker from
157+
the persisted BLOB rather than from the build directory or a prior cache entry.
158+
Run only this restart check (building the fixtures first) with:
159+
160+
```bash
161+
make test_database_worker_restart
162+
```
163+
152164
Override `VGI_DATABASE_PYTHON_DIR`, `VGI_DATABASE_OPEN_METEO_DIR`, or
153165
`VGI_DATABASE_RUST_DIR` when the sibling repositories live elsewhere. Bun can
154166
cross-compile by setting `VGI_BUN_TARGET` (for example,
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env bash
2+
# Prove database-packaged workers survive a complete DuckDB process restart.
3+
4+
set -euo pipefail
5+
6+
project_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
7+
duckdb_bin=${VGI_DATABASE_WORKER_DUCKDB:-"${project_dir}/build/release/haybarn"}
8+
python_worker=${VGI_DATABASE_PYTHON_WORKER:?set VGI_DATABASE_PYTHON_WORKER}
9+
bun_worker=${VGI_DATABASE_BUN_WORKER:?set VGI_DATABASE_BUN_WORKER}
10+
rust_worker=${VGI_DATABASE_RUST_WORKER:?set VGI_DATABASE_RUST_WORKER}
11+
12+
for required_file in "${duckdb_bin}" "${python_worker}" "${bun_worker}" "${rust_worker}"; do
13+
if [[ ! -f "${required_file}" ]]; then
14+
echo "database-worker restart test input does not exist: ${required_file}" >&2
15+
exit 1
16+
fi
17+
done
18+
if [[ ! -x "${duckdb_bin}" ]]; then
19+
echo "database-worker restart test requires an executable DuckDB CLI: ${duckdb_bin}" >&2
20+
exit 1
21+
fi
22+
23+
# File paths become SQL string literals below. Doubling quotes is DuckDB's
24+
# portable escaping rule and keeps paths with spaces or apostrophes valid.
25+
sql_escape() {
26+
local value=$1
27+
printf '%s' "${value//\'/\'\'}"
28+
}
29+
30+
python_sql=$(sql_escape "${python_worker}")
31+
bun_sql=$(sql_escape "${bun_worker}")
32+
rust_sql=$(sql_escape "${rust_worker}")
33+
34+
runtime_dir=$(mktemp -d "${TMPDIR:-/tmp}/vgi-database-worker-restart.XXXXXX")
35+
registry_db="${runtime_dir}/worker_registry.duckdb"
36+
cache_dir="${runtime_dir}/fresh-reader-cache"
37+
registry_sql=$(sql_escape "${registry_db}")
38+
cache_sql=$(sql_escape "${cache_dir}")
39+
cleanup() {
40+
rm -rf "${runtime_dir}"
41+
}
42+
trap cleanup EXIT
43+
44+
echo "Phase 1: storing Python, Bun, and Rust workers in ${registry_db}"
45+
"${duckdb_bin}" -bail -batch -no-init -noheader -csv "${registry_db}" <<SQL
46+
LOAD vgi;
47+
CREATE TABLE worker_packages AS
48+
SELECT * FROM vgi_worker_package(
49+
'${python_sql}', 'python-fixture', 'restart-1')
50+
UNION ALL
51+
SELECT * FROM vgi_worker_package(
52+
'${bun_sql}', 'open-meteo', 'restart-1')
53+
UNION ALL
54+
SELECT * FROM vgi_worker_package(
55+
'${rust_sql}', 'rust-fixture', 'restart-1',
56+
entrypoint := 'bin/vgi-rust-worker');
57+
SELECT CASE
58+
WHEN count(*) = 3
59+
AND count(DISTINCT worker_name) = 3
60+
AND bool_and(octet_length(contents) > 1000000)
61+
AND bool_and(length(sha256) = 64)
62+
THEN 'registry-ok'
63+
ELSE error('persistent worker registry was not populated correctly')
64+
END
65+
FROM worker_packages;
66+
CHECKPOINT;
67+
SQL
68+
69+
# Do not leak the build inputs into the reader process through its environment.
70+
unset VGI_DATABASE_PYTHON_WORKER VGI_DATABASE_BUN_WORKER VGI_DATABASE_RUST_WORKER
71+
72+
# This is a distinct DuckDB process. It starts with an empty artifact cache and
73+
# receives only the registry database path: the original package paths above
74+
# are deliberately absent from this invocation.
75+
echo "Phase 2: opening the registry from a fresh DuckDB process and empty cache"
76+
TMPDIR="${runtime_dir}" XDG_CACHE_HOME="${cache_dir}" \
77+
"${duckdb_bin}" -bail -batch -no-init -noheader -csv :memory: <<SQL
78+
LOAD vgi;
79+
SET vgi_worker_cache_dir = '${cache_sql}';
80+
ATTACH '${registry_sql}' AS worker_registry (READ_ONLY);
81+
SELECT CASE WHEN count(*) = 0 THEN 'cache-empty' ELSE error('reader cache was not empty') END
82+
FROM vgi_worker_cache();
83+
84+
ATTACH 'python_package' AS python_worker (
85+
TYPE vgi,
86+
LOCATION 'database://worker_registry/main/worker_packages/python-fixture?package_version=restart-1');
87+
SELECT CASE
88+
WHEN (SELECT count(*) FROM python_worker.main.series(6)) = 6
89+
AND (SELECT sum(n) FROM python_worker.main.series(6)) = 15
90+
THEN 'python-ok'
91+
ELSE error('persisted Python worker returned the wrong result')
92+
END;
93+
94+
ATTACH 'open_meteo' AS meteo_worker (
95+
TYPE vgi,
96+
LOCATION 'database://worker_registry/main/worker_packages/open-meteo?package_version=restart-1');
97+
SELECT CASE
98+
WHEN (SELECT count(*) FROM meteo_worker.main.weather_codes) > 0
99+
THEN 'bun-ok'
100+
ELSE error('persisted Bun worker returned no weather codes')
101+
END;
102+
103+
ATTACH 'example' AS rust_worker (
104+
TYPE vgi,
105+
LOCATION 'database://worker_registry/main/worker_packages/rust-fixture?package_version=restart-1');
106+
SELECT CASE
107+
WHEN (SELECT count(*) FROM rust_worker.main.sequence(7)) = 7
108+
AND (SELECT sum(n) FROM rust_worker.main.sequence(7)) = 21
109+
THEN 'rust-ok'
110+
ELSE error('persisted Rust worker returned the wrong result')
111+
END;
112+
113+
SELECT CASE
114+
WHEN count(*) = 3 THEN 'cache-rebuilt-from-database'
115+
ELSE error('fresh process did not materialize all three database workers')
116+
END
117+
FROM vgi_worker_cache();
118+
SQL
119+
120+
echo "Database-worker restart test passed"

0 commit comments

Comments
 (0)