blob: 9b98c67a79ba350ba1f8e7b8c1da4d88c072d2ac [file]
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# smoke-boot.sh -- launch a packaged Texera service from its unpacked dist and
# assert it boots (reaches a listening port) instead of crashing on startup with
# a runtime classpath/linkage failure (NoClassDefFoundError, LinkageError,
# Jackson/Scala-module version conflict, ...). That class of bug compiles and
# unit-tests clean but crashes the real `main`, so it slips through CI, which
# otherwise only builds + unit-tests each service and never starts it. See
# https://github.com/apache/texera/issues/6220.
#
# The verdict is based on the process's own behaviour, not on scanning its logs:
# * reaches LISTEN -> booted OK
# * exits before LISTEN -> crashed on boot (report its exit code)
# * neither, within timeout -> hung / failed to come up
# Scanning stdout/stderr for exception names was fragile -- any library that
# merely prints an exception name in prose (e.g. jOOQ's random "tip of the day")
# tripped it. See https://github.com/apache/texera/issues/6332.
#
# Usage:
# smoke-boot.sh <launcher-glob> <port> [timeout_secs]
# <launcher-glob> path (globbable) to the dist launcher, e.g.
# /tmp/dists/config-service-*/bin/config-service
# <port> application HTTP port to wait for (TCP LISTEN)
# [timeout_secs] how long to wait for LISTEN (default 60)
#
# Requirements:
# * TEXERA_HOME must point at the checkout root -- services resolve their
# config yaml from <TEXERA_HOME>/<service>/src/main/resources/...
# * the service's backing infra must already be up (postgres for every service,
# plus MinIO + LakeFS for file-service); the JVM connects via storage.conf
# defaults (postgres/postgres @ localhost:5432, MinIO :9000, LakeFS :8000).
set -euo pipefail
launcher_glob="${1:?usage: smoke-boot.sh <launcher-glob> <port> [timeout]}"
port="${2:?port required}"
timeout="${3:-60}"
# Resolve the (possibly globbed) launcher to exactly one concrete executable.
# Erroring on multiple matches avoids silently smoke-testing the wrong (e.g.
# lexicographically-first) binary if a dist dir ever accumulates versions.
matches="$(ls -d $launcher_glob 2>/dev/null || true)"
count="$(printf '%s' "$matches" | grep -c . || true)"
if [[ "$count" -eq 0 ]]; then
echo "::error::smoke-boot: launcher not found: $launcher_glob"
exit 1
fi
if [[ "$count" -gt 1 ]]; then
echo "::error::smoke-boot: launcher glob matched $count files, expected exactly 1: $launcher_glob"
exit 1
fi
launcher="$matches"
if [[ ! -x "$launcher" ]]; then
echo "::error::smoke-boot: launcher not executable: $launcher"
exit 1
fi
port_open() {
# Probe 127.0.0.1 explicitly (not "localhost", which can resolve to ::1 first
# while the JVM binds IPv4 0.0.0.0, giving a false "not listening").
if command -v nc >/dev/null 2>&1; then
nc -z 127.0.0.1 "$port" >/dev/null 2>&1
else
(exec 3<>"/dev/tcp/127.0.0.1/$port") 2>/dev/null
fi
}
# Fail fast if the port is already taken. The wait loop below treats "something
# is LISTENing on :port" as a healthy boot, so a leftover/unrelated listener
# could otherwise mask a crashed service. Requiring the port to be free at launch
# means a listener detected afterwards is the service we started.
if port_open; then
echo "::error::smoke-boot: port $port is already in use before launching '$launcher'"
exit 1
fi
log="$(mktemp)"
echo "smoke-boot: launching '$launcher' (port=$port timeout=${timeout}s)"
"$launcher" >"$log" 2>&1 &
pid=$!
# On any exit -- including an unexpected abort under `set -e` -- stop the service
# and remove the log, so a failing script can't orphan the JVM or leak temp files.
trap 'kill "$pid" 2>/dev/null || true; rm -f "$log"' EXIT
# Wait for the service to reach one of three terminal states: it opens its port
# (booted), it exits on its own (crashed), or neither happens in time (hung).
outcome="timeout"
for ((i = 0; i < timeout; i++)); do
if port_open; then outcome="listen"; break; fi
if ! kill -0 "$pid" 2>/dev/null; then outcome="exited"; break; fi
sleep 1
done
fail() {
echo "::error::smoke-boot: $*"
echo "----- last 80 log lines from '$launcher' -----"
tail -n 80 "$log" || true
exit 1
}
# Stop a still-running service. SIGTERM, then a bounded grace period, then
# SIGKILL -- so a service that ignores SIGTERM or hangs in shutdown can't leave
# the CI step running indefinitely.
stop_service() {
kill "$pid" 2>/dev/null || true
for _ in $(seq 1 10); do
if ! kill -0 "$pid" 2>/dev/null; then
wait "$pid" 2>/dev/null || true
return 0
fi
sleep 1
done
kill -9 "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
}
case "$outcome" in
listen)
stop_service
echo "smoke-boot: OK -- '$launcher' reached LISTEN on :$port"
;;
exited)
# The service died before it ever listened -- a boot crash. Its own exit
# code is the signal; reap it (the process has already exited) and report it.
code=0
wait "$pid" 2>/dev/null || code=$?
fail "'$launcher' exited on boot (exit code $code) before listening on :$port"
;;
*)
stop_service
fail "'$launcher' did not listen on :$port within ${timeout}s"
;;
esac