Files
ITWorx-Pulse-Public/deploy/healthcheck-heartbeat.sh
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

64 lines
2.4 KiB
Bash

#!/bin/sh
# Heartbeat liveness check for pulse-worker and pulse-agent.
#
# Full contract: docs/operations/WORKER_AGENT_HEALTHCHECK_CONTRACT.md
#
# Summary: the running process must write/touch $PULSE_HEARTBEAT_FILE at
# least every ~10s as long as its main loop is alive and making forward
# progress. This script treats the file's mtime as the source of truth
# (not its contents) so it never needs to parse a timestamp format.
#
# `docker compose up` (non-swarm) does not restart a container solely
# because its HEALTHCHECK reports "unhealthy" -- that status is only
# informational under plain Compose. To actually recover a hung-but-alive
# process without an external watcher (which would need Docker socket
# access, forbidden host-wide per ADR-0005), this script self-terminates
# PID 1 once staleness is confirmed, which turns "unhealthy" into a real
# container exit that `restart: unless-stopped` then recovers from.
#
# It deliberately does NOT self-terminate merely because the heartbeat file
# is absent shortly after container start (that's the normal boot path,
# handled separately below using the entrypoint-recorded start time), which
# avoids a restart loop on legitimately slow startups (DB dial, migrations
# wait, etc.).
set -eu
HEARTBEAT_FILE="${PULSE_HEARTBEAT_FILE:-/tmp/healthy}"
STARTED_AT_FILE="${PULSE_STARTED_AT_FILE:-/tmp/.pulse-started-at}"
MAX_AGE_SECONDS="${PULSE_HEARTBEAT_MAX_AGE_SECONDS:-45}"
now=$(date +%s)
self_restart() {
reason="$1"
echo "pulse-healthcheck: $reason; forcing container exit for restart" >&2
# Same-UID SIGKILL to PID 1 requires no capabilities (cap_drop: [ALL] is
# fine): standard POSIX signal permission only requires a matching UID,
# not CAP_KILL, when sender and target share a UID.
kill -9 1 2>/dev/null || true
}
if [ ! -f "$HEARTBEAT_FILE" ]; then
started_at=$(cat "$STARTED_AT_FILE" 2>/dev/null || echo "$now")
uptime=$((now - started_at))
if [ "$uptime" -gt "$MAX_AGE_SECONDS" ]; then
self_restart "no heartbeat ${uptime}s after container start"
else
echo "pulse-healthcheck: heartbeat file not yet written (uptime ${uptime}s)" >&2
fi
exit 1
fi
mtime=$(stat -c %Y "$HEARTBEAT_FILE" 2>/dev/null) || {
echo "pulse-healthcheck: cannot stat heartbeat file $HEARTBEAT_FILE" >&2
exit 1
}
age=$((now - mtime))
if [ "$age" -gt "$MAX_AGE_SECONDS" ]; then
self_restart "heartbeat stale (${age}s > ${MAX_AGE_SECONDS}s)"
exit 1
fi
exit 0