|
| 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