#!/usr/bin/env bash set -euo pipefail ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) MODE=${1:-compose} OUTPUT=${2:-} RPC_EXPRESSION=' IO.puts( JSON.encode!(%{ node: to_string(node()), port_count: :erlang.system_info(:port_count), port_limit: :erlang.system_info(:port_limit), process_count: :erlang.system_info(:process_count), memory_total: :erlang.memory(:total), memory_system: :erlang.memory(:system), port_table: :erlang.system_info(:allocated_areas) |> Keyword.fetch!(:port_table) }) ) ' temporary=$(mktemp "${TMPDIR:-/tmp}/wnh-beam-runtime.XXXXXX") trap 'rm -f "$temporary"' EXIT HUP INT TERM record_probe() { local target=$1 local component=$2 local expected=$3 local runtime_json=$4 local cgroup_memory=$5 local rss_kb=$6 jq -cn \ --arg target "$target" \ --arg component "$component" \ --argjson expected_port_limit "$expected" \ --argjson cgroup_memory "$cgroup_memory" \ --argjson rss_kb "$rss_kb" \ --argjson runtime "$runtime_json" \ '{ target: $target, component: $component, expected_port_limit: $expected_port_limit, cgroup_memory: $cgroup_memory, rss_kb: $rss_kb } + $runtime' >>"$temporary" } case "$MODE" in compose) mapfile -t targets < <( docker compose -p "${COMPOSE_PROJECT_NAME:-who_need_help}" \ ps -q web worker | sort -u ) if [[ "${#targets[@]}" -eq 0 ]]; then echo "No running Compose web or worker replicas were found." >&2 exit 1 fi live_flags=$( for target in "${targets[@]}"; do docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \ "$target" | sed -n 's/^ERL_ZFLAGS=//p' done | sort -u ) if [[ ! "$live_flags" =~ ^\+Q[[:space:]]+([0-9]+)$ ]]; then echo "The live Compose replicas do not expose one unambiguous +Q limit." >&2 exit 1 fi expected=${BASH_REMATCH[1]} for target in "${targets[@]}"; do component=$( docker inspect --format \ '{{index .Config.Labels "com.docker.compose.service"}}' "$target" ) runtime_json=$( docker exec "$target" /app/bin/who_need_help rpc "$RPC_EXPRESSION" | tail -n 1 ) cgroup_memory=$(docker exec "$target" cat /sys/fs/cgroup/memory.current) rss_kb=$( docker exec "$target" awk "/^VmRSS:/ {print \$2}" /proc/1/status ) record_probe \ "$target" "$component" "$expected" "$runtime_json" \ "$cgroup_memory" "$rss_kb" done ;; kind) KUBECTL="$ROOT/.tools/bin/kubectl" CONTEXT=kind-who-need-help NAMESPACE=who-need-help if [[ ! -x "$KUBECTL" ]]; then echo "The project-owned kubectl is missing." >&2 exit 1 fi live_flags=$( "$KUBECTL" --context "$CONTEXT" --namespace "$NAMESPACE" \ get deployment \ who-need-help-who-need-help-web \ who-need-help-who-need-help-worker \ -o json | jq -r ' [ .items[].spec.template.spec.containers[].env[] | select(.name == "ERL_ZFLAGS") | .value ] | unique | if length == 1 then .[0] else empty end ' ) if [[ ! "$live_flags" =~ (^|[[:space:]])\+Q[[:space:]]+([0-9]+)($|[[:space:]]) ]]; then echo "The live deployments do not expose one unambiguous +Q limit." >&2 exit 1 fi expected=${BASH_REMATCH[2]} mapfile -t pods < <( "$KUBECTL" --context "$CONTEXT" --namespace "$NAMESPACE" \ get pods \ -l app.kubernetes.io/name=who-need-help \ -o json | jq -r ' .items[] | .metadata.labels["app.kubernetes.io/component"] as $component | select( ($component == "web" or $component == "worker") and .status.phase == "Running" and ([.status.containerStatuses[]?.ready] | all) ) | [.metadata.name, $component] | @tsv ' ) desired=$( "$KUBECTL" --context "$CONTEXT" --namespace "$NAMESPACE" \ get deployment \ who-need-help-who-need-help-web \ who-need-help-who-need-help-worker \ -o json | jq '[.items[].spec.replicas] | add' ) if [[ "${#pods[@]}" -ne "$desired" ]]; then echo "Expected $desired Ready application pods; observed ${#pods[@]}." >&2 exit 1 fi for entry in "${pods[@]}"; do IFS=$'\t' read -r pod component <<<"$entry" runtime_json=$( "$KUBECTL" --context "$CONTEXT" --namespace "$NAMESPACE" \ exec "$pod" --container "$component" -- \ /app/bin/who_need_help rpc "$RPC_EXPRESSION" | tail -n 1 ) cgroup_memory=$( "$KUBECTL" --context "$CONTEXT" --namespace "$NAMESPACE" \ exec "$pod" --container "$component" -- \ cat /sys/fs/cgroup/memory.current ) rss_kb=$( "$KUBECTL" --context "$CONTEXT" --namespace "$NAMESPACE" \ exec "$pod" --container "$component" -- \ awk "/^VmRSS:/ {print \$2}" /proc/1/status ) record_probe \ "$pod" "$component" "$expected" "$runtime_json" \ "$cgroup_memory" "$rss_kb" done ;; *) echo "Usage: $0 [compose|kind] [output.json]" >&2 exit 1 ;; esac summary=$(jq -s 'sort_by(.component, .target)' "$temporary") if ! printf '%s\n' "$summary" | jq -e \ --argjson expected "$expected" \ 'length > 0 and all(.port_limit == $expected)' >/dev/null; then echo "A running BEAM instance does not use the configured port limit." >&2 exit 1 fi if [[ -n "$OUTPUT" ]]; then printf '%s\n' "$summary" >"$OUTPUT" fi printf '%s\n' "$summary"