| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- #!/bin/sh
- set -eu
- compose_file=${COMPOSE_FILE:-deploy/docker/docker-compose.yml}
- duration=${KESTRA_SOAK_SECONDS:-1800}
- interval=${KESTRA_SOAK_INTERVAL_SECONDS:-15}
- base_url=${KESTRA_SOAK_BASE_URL:-http://127.0.0.1:18080/api/v1/main}
- username=${KESTRA_USERNAME:-admin@dataops.local}
- password=${KESTRA_PASSWORD:-DataOpsKestra1!}
- evidence=${KESTRA_SOAK_EVIDENCE:-/tmp/dataops-kestra-soak-$(date +%Y%m%dT%H%M%S).jsonl}
- namespace=dataops_validation
- flow_id=queue_restart_soak_$(date +%s)
- deadline=$(( $(date +%s) + duration ))
- restart_at=$(( $(date +%s) + duration / 3 ))
- restarted=false
- post_restart_since=
- soak_started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
- checks=0
- executions=0
- postgres_container=$(docker compose -f "$compose_file" ps -q postgres)
- kestra_container=$(docker compose -f "$compose_file" ps -q kestra)
- postgres_restart_count_before=$(docker inspect -f '{{.RestartCount}}' "$postgres_container")
- kestra_restart_count_before=$(docker inspect -f '{{.RestartCount}}' "$kestra_container")
- jq -cn \
- --arg started_at "$soak_started_at" \
- --argjson duration_seconds "$duration" \
- --argjson postgres_restart_count "$postgres_restart_count_before" \
- --argjson kestra_restart_count "$kestra_restart_count_before" \
- '{kind:"soak_start",started_at:$started_at,
- duration_seconds:$duration_seconds,
- postgres_restart_count:$postgres_restart_count,
- kestra_restart_count:$kestra_restart_count}' >"$evidence"
- flow_file=$(mktemp "${TMPDIR:-/tmp}/dataops-kestra-soak.XXXXXX.yml")
- trap 'rm -f "$flow_file"' EXIT
- printf '%s\n' \
- "id: ${flow_id}" \
- "namespace: ${namespace}" \
- "description: DataOps controlled queue and restart soak" \
- "disabled: false" \
- "tasks:" \
- " - id: queue_probe" \
- " type: io.kestra.plugin.core.log.Log" \
- " message: DataOps queue soak probe" >"$flow_file"
- api() {
- method=$1
- path=$2
- shift 2
- curl -fsS --user "${username}:${password}" \
- -X "$method" "${base_url}/${path}" "$@"
- }
- api POST flows -H 'Content-Type: application/x-yaml' \
- --data-binary "@${flow_file}" >/dev/null
- while [ "$(date +%s)" -lt "$deadline" ]; do
- sample_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)
- execution=$(api POST "executions/${namespace}/${flow_id}")
- execution_id=$(printf '%s' "$execution" | jq -er '.id')
- state=UNKNOWN
- poll=0
- while [ "$poll" -lt 30 ]; do
- state=$(api GET "executions/${execution_id}" |
- jq -r '.state.current // .state // "UNKNOWN"')
- case "$state" in
- SUCCESS|FAILED|KILLED|CANCELLED|WARNING) break ;;
- esac
- poll=$((poll + 1))
- sleep 1
- done
- [ "$state" = "SUCCESS" ] || {
- printf 'Kestra soak execution %s ended as %s\n' "$execution_id" "$state" >&2
- exit 1
- }
- executions=$((executions + 1))
- docker compose -f "$compose_file" exec -T kestra \
- curl -fsS http://127.0.0.1:8081/health >/dev/null
- health='{"status":"UP"}'
- jq -cn \
- --arg kind "sample" \
- --arg sampled_at "$sample_time" \
- --arg execution_id "$execution_id" \
- --arg state "$state" \
- --argjson health "$health" \
- --argjson postgres_restarted "$restarted" \
- '{kind:$kind,sampled_at:$sampled_at,execution_id:$execution_id,state:$state,
- health:$health,controlled_restart_completed:$postgres_restarted}' \
- >>"$evidence"
- checks=$((checks + 1))
- if [ "$restarted" = false ] && [ "$(date +%s)" -ge "$restart_at" ]; then
- docker compose -f "$compose_file" restart postgres
- ready_attempt=0
- until docker compose -f "$compose_file" exec -T postgres \
- pg_isready -U dataops -d dataops >/dev/null 2>&1; do
- ready_attempt=$((ready_attempt + 1))
- [ "$ready_attempt" -lt 30 ] || {
- echo "PostgreSQL did not recover during controlled restart" >&2
- exit 1
- }
- sleep 2
- done
- docker compose -f "$compose_file" run --rm -T kestra-db-init
- docker compose -f "$compose_file" restart kestra
- docker compose -f "$compose_file" up -d --wait kestra
- post_restart_since=$(date -u +%Y-%m-%dT%H:%M:%SZ)
- restarted=true
- fi
- sleep "$interval"
- done
- [ "$restarted" = true ] || {
- echo "Controlled PostgreSQL/Kestra restart did not run" >&2
- exit 1
- }
- postgres_restart_count_after=$(docker inspect -f '{{.RestartCount}}' "$postgres_container")
- kestra_restart_count_after=$(docker inspect -f '{{.RestartCount}}' "$kestra_container")
- # Docker's explicit `docker restart` does not consume RestartCount; that field
- # counts daemon/restart-policy recoveries. Any delta therefore proves an
- # uncontrolled extra restart during the observation window.
- if [ "$postgres_restart_count_after" != "$postgres_restart_count_before" ] ||
- [ "$kestra_restart_count_after" != "$kestra_restart_count_before" ]; then
- echo "Unexpected automatic container restart during soak" >&2
- exit 1
- fi
- log_file=$(mktemp "${TMPDIR:-/tmp}/dataops-kestra-soak-logs.XXXXXX")
- docker compose -f "$compose_file" logs --since "$soak_started_at" kestra >"$log_file"
- if grep -Eiq 'outofmemory' "$log_file"; then
- echo "Kestra emitted a fatal database/queue signature during soak" >&2
- exit 1
- fi
- # PostgreSQL emits SQLSTATE 57P01 / "terminating connection due to
- # administrator command" for the one deliberate database restart above. It is
- # retained and counted as controlled evidence, while every other FATAL line
- # across the full observation window remains a release-gate failure.
- if grep -Ei '(^|[^a-z])fatal([^a-z]|$)' "$log_file" |
- grep -Eiv 'fatal: terminating connection due to administrator command' \
- >/dev/null; then
- echo "Kestra emitted an unexpected fatal database/queue signature" >&2
- exit 1
- fi
- known_restart_disconnects=$(grep -Eic \
- 'broken pipe|connection is closed|connection reset|hikaripool.*failed|terminating connection due to administrator command' \
- "$log_file" || true)
- soak_completed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
- jq -cn \
- --arg completed_at "$soak_completed_at" \
- --arg log_since "$soak_started_at" \
- --argjson executions "$executions" \
- --argjson samples "$checks" \
- --argjson known_restart_disconnects "$known_restart_disconnects" \
- --argjson postgres_restart_count "$postgres_restart_count_after" \
- --argjson kestra_restart_count "$kestra_restart_count_after" \
- '{kind:"soak_complete",completed_at:$completed_at,log_since:$log_since,
- executions:$executions,samples:$samples,
- known_controlled_restart_disconnects:$known_restart_disconnects,
- postgres_restart_count:$postgres_restart_count,
- kestra_restart_count:$kestra_restart_count}' >>"$evidence"
- rm -f "$log_file"
- printf 'Kestra soak passed: %s executions, %s samples, evidence=%s\n' \
- "$executions" "$checks" "$evidence"
|