#!/bin/sh
# One node = one worker + one broker. Start the worker, then hand PID 1 to the
# broker.
set -eu

: "${ZAKURO_NODE_NAME:?ZAKURO_NODE_NAME is required}"

# Refuse to start unowned. Without ZAKURO_API_KEY the broker still comes up
# perfectly happily -- is_billing_enabled() just returns false and every job
# runs free, so the node joins the mesh, serves work, and bills nobody. That
# failure is invisible in the logs and looks exactly like success, which is the
# whole reason this check is here rather than a comment.
if [ -z "${ZAKURO_API_KEY:-}" ]; then
  echo "[entrypoint] FATAL: ZAKURO_API_KEY is empty for ${ZAKURO_NODE_NAME}." >&2
  echo "[entrypoint] Run mesh/provision.sh to mint the four keys." >&2
  exit 1
fi

# A master key would resolve every caller to "admin" regardless of the four keys
# below, which defeats the point of the mesh. Fail loudly rather than run a
# mesh whose four identities silently collapse into one.
if [ -n "${ZAKURO_MASTER_KEY:-}" ]; then
  echo "[entrypoint] FATAL: ZAKURO_MASTER_KEY is set; it collapses all four" >&2
  echo "[entrypoint] identities into 'admin'. Unset it." >&2
  exit 1
fi

# Log the billing id, never the key. The id is the whole point -- it is what
# ties this node to an account -- and it is the one field that makes a
# mis-provisioned mesh visible in `compose logs`.
BILLING_ID=$(echo "$ZAKURO_API_KEY" | cut -d_ -f2)
echo "[entrypoint] ${ZAKURO_NODE_NAME}: owner billing id ${BILLING_ID} (key redacted)"

WORKER_PORT="${MW_PORT:-3960}"
HEALTH="http://127.0.0.1:${WORKER_PORT}/health"

python3 /usr/local/bin/worker.py &

# Do not hand off to the broker until the worker actually serves. A worker that
# dies at import -- a missing stdlib module, an unparseable env value -- leaves
# the broker running with an empty roster, and the node then reports healthy
# while computing nothing. Measured: python3-minimal ships no http.server, and
# the whole four-node mesh came up green with zero workers.
i=0
until curl -fsS --max-time 2 "$HEALTH" >/dev/null 2>&1; do
  i=$((i + 1))
  if [ "$i" -ge 30 ]; then
    echo "[entrypoint] FATAL: worker never answered $HEALTH after 30s." >&2
    exit 1
  fi
  sleep 1
done
echo "[entrypoint] worker ready on ${WORKER_PORT}"

# Supervise by health, not by PID.
#
# `kill -0 "$pid"` looks like the obvious check and is wrong here: the worker is
# a child of this shell, and the exec below replaces the shell, so nothing ever
# reaps it. A dead worker becomes a zombie whose PID still exists, `kill -0`
# keeps succeeding, and the supervisor never fires -- which is exactly how the
# workerless mesh above stayed up. Polling the port is the only check that
# reflects whether work can actually be done.
watch_worker() {
  fails=0
  while :; do
    if curl -fsS --max-time 2 "$HEALTH" >/dev/null 2>&1; then
      fails=0
    else
      fails=$((fails + 1))
      # Three strikes, not one: a single timeout under load is not a dead
      # worker, and bouncing the node on one would make the mesh flap.
      if [ "$fails" -ge 3 ]; then
        echo "[entrypoint] worker stopped answering; taking the node down" >&2
        kill -TERM 1 2>/dev/null || true
        return
      fi
    fi
    sleep 5
  done
}
watch_worker &

# exec: the broker becomes PID 1 so `docker stop` delivers SIGTERM to it
# directly. Without exec the shell holds PID 1, swallows the signal, and every
# shutdown waits out the 10s kill timeout -- and a broker killed rather than
# stopped leaves unsettled reservations in the WAL.
exec /usr/local/bin/zc broker
