#!/usr/bin/env bash
###############################################################################
# lacp-retrans-cause.sh  (HOST-NATIVE)
#
# Runs on tsys6 (receiver) or tsys7 (sender). Auto-detects role.
#
# Question this answers: are the ~56K retransmits we see on 8-stream iperf
# between tsys6 and tsys7 fixable (NIC ring drops, softnet drops, CPU
# saturation) or just unavoidable LACP reordering overhead?
#
# Method: snapshot drop/error counters + softnet_stat + TCP SNMP before and
# after a 12s iperf3 run, then print the deltas. The decisive columns are:
#   - NIC rx_dropped / rx_missed_errors / rx_no_dma_resources   -> ring too small
#   - /proc/net/softnet_stat drops                              -> ksoftirq starved
#   - /proc/net/snmp TCP retranst                               -> TCP-level retrans
# If NIC drop counters do NOT climb but TCP retrans does, the retrans are
# coming from LACP reordering (out-of-order segments triggering fast
# retransmit), not packet loss — and are NOT fixable by tuning.
###############################################################################
set -uo pipefail

DURATION="${LACP_TEST_SECS:-12}"
STREAMS="${LACP_STREAMS:-8}"
PEER_RX_IP="10.100.100.6"   # tsys6 storage IP (receiver)
PEER_TX_IP="10.100.100.7"   # tsys7 storage IP (sender)
LOG_DIR="/root"
BOND="bond0"

HOST="$(uname -n)"; HOST="${HOST%%.*}"
case "$HOST" in
    *tsys6) ROLE="receiver"; LOG="$LOG_DIR/lacp-retrans-receiver.log" ;;
    *tsys7) ROLE="sender";   LOG="$LOG_DIR/lacp-retrans-sender.log" ;;
    *) echo "ERROR: not on tsys6 or tsys7 (host=$HOST)"; exit 2 ;;
esac

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

# ---- per-slave NIC counter snapshot -----------------------------------------
# Captures rx_dropped, rx_missed_errors, rx_no_dma_resources, tx_retrans,
# plus all error-like counters. Falls back gracefully if a counter doesn't
# exist (different NIC drivers expose different names).
nic_snapshot() {
    # shellcheck disable=SC2013 # interface names contain no spaces; word-splitting is safe here
    for s in $(awk '/^Slave Interface:/{print $3}' "/proc/net/bonding/$BOND"); do
        [ -n "$s" ] || continue
        echo "[$s]"
        # ethtool -S may exist; show only drop/error/retrans lines
        if command -v ethtool >/dev/null 2>&1; then
            ethtool -S "$s" 2>/dev/null \
                | grep -iE 'drop|miss|error|no_dma|fifo|retrans|overflow' \
                || echo "(ethtool -S: no matching counters or unsupported)"
        else
            echo "(ethtool not installed)"
        fi
        # /sys counters (always available, driver-agnostic)
        for c in rx_dropped tx_dropped rx_errors tx_errors rx_missed_errors \
                 rx_length_errors rx_crc_errors rx_fifo_errors tx_fifo_errors \
                 multicast collisions; do
            v=$(cat "/sys/class/net/$s/statistics/$c" 2>/dev/null)
            [ -n "$v" ] && printf '  sysfs %-22s = %s\n' "$c" "$v"
        done
    done
}

# ---- softnet_stat snapshot (per-CPU RX softirq drops) -----------------------
# /proc/net/softnet_stat columns are HEX. Col1=processed, col2=dropped,
# col3=time_squeeze. We only care about dropped + squeeze (small numbers that
# look identical in hex and decimal). Processed is informational only and we
# don't try to delta it (hex arithmetic is awk-version-dependent).
softnet_snapshot() {
    awk '{ printf "cpu%s processed=%s dropped=%s squeezed=%s\n", \
              NR-1, $1, $2, $3 }' /proc/net/softnet_stat
}

# ---- TCP SNMP snapshot (the source of iperf's retransmit number) -----------
tcp_snapshot() {
    awk '/^Tcp:/{
        if (!seen) {
            seen=1
            # /proc/net/snmp Tcp line 1 = names, line 2 = values
            n=split($0, names, " ")
            # re-find the values line
            getline
            vals=$0
            split(vals, v, " ")
            for (i=1;i<=n;i++) printf "  %-22s = %s\n", names[i], v[i]
        }
    }' /proc/net/snmp
}

# ---- print deltas for selected counters ------------------------------------
# $1 = before file, $2 = after file
# softnet_snapshot output: "cpuN processed=HEX dropped=HEX squeezed=HEX"
# /proc/net/softnet_stat is hex, but dropped/squeezed are always small
# integers (typically 0 on a healthy host), so +0 coercion is correct for
# the values we care about. We deliberately DON'T report `processed` deltas
# because hex arithmetic on large numbers is awk-version-dependent.
softnet_delta() {
    awk '
        FNR==NR { if (match($0,/cpu[0-9]+/)) { id=substr($0,RSTART,RLENGTH);
                  split($0, p, /[= ]+/); drop[id]=p[5]+0; sqz[id]=p[7]+0 }
                  next }
        { if (match($0,/cpu[0-9]+/)) { id=substr($0,RSTART,RLENGTH);
              split($0, q, /[= ]+/);
              dd = (q[5]+0) - drop[id]
              sd = (q[7]+0) - sqz[id]
              if (dd != 0 || sd != 0)
                  printf "  %-8s dropped_delta=%-6s squeezed_delta=%-6s\n", id, dd, sd } }
    ' "$1" "$2" | sort
}

# $1 = before file, $2 = after file, $3 = section label prefix
# File format: slave header line "[nic1]", then "  counter_name = value" lines.
# Output only counters whose value changed.
nic_delta() {
    awk '
        FNR==NR { if ($0 ~ /^\[/) slave=$0
                  else if ($0 ~ /=/) {
                      # Everything before " = " is the counter name (trim spaces)
                      pos = index($0, "=")
                      name = substr($0, 1, pos-1)
                      gsub(/^ +| +$/, "", name)
                      val  = substr($0, pos+1); gsub(/^ +| +$/, "", val)
                      before[slave SUBSEP name] = val
                  }
                  next }
        { if ($0 ~ /^\[/) slave=$0
          else if ($0 ~ /=/) {
              pos = index($0, "=")
              name = substr($0, 1, pos-1); gsub(/^ +| +$/, "", name)
              val  = substr($0, pos+1); gsub(/^ +| +$/, "", val)
              b = before[slave SUBSEP name]+0
              d = val+0 - b
              if (d != 0) printf "  %-8s %-30s %12s -> %12s  delta=%d\n", slave, name, b, val, d
          } }
    ' "$1" "$2"
}

{
echo "=== LACP retransmit cause investigation ($ROLE) ==="
echo "Host: $HOST   Role: $ROLE"
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Bond: $BOND"
echo ""
echo "--- bond0 hash + driver ---"
grep -E "Bonding Mode|Transmit Hash|Number of ports|Partner Mac" /proc/net/bonding/$BOND
# shellcheck disable=SC2013 # interface names contain no spaces; word-splitting is safe here
for s in $(awk '/^Slave Interface:/{print $3}' /proc/net/bonding/$BOND); do
    drv=$(ethtool -i "$s" 2>/dev/null | awk -F: '/^driver:/{print $2}' | sed 's/^ *//')
    speed=$(cat "/sys/class/net/$s/speed" 2>/dev/null)
    ring=$(ethtool -g "$s" 2>/dev/null | awk '/RX:/{print $2; exit}')
    printf "  %-8s driver=%-20s speed=%-6s current RX ring=%s\n" "$s" "$drv" "$speed" "$ring"
done
echo ""
} | tee "$LOG"

echo "--- BEFORE snapshots ---"
nic_snapshot      > "$TMP/nic_before"
softnet_snapshot  > "$TMP/softnet_before"
tcp_snapshot      > "$TMP/tcp_before"

if [ "$ROLE" = "receiver" ]; then
    echo "[receiver] starting iperf3 -s -1 on $PEER_RX_IP ..."
    pkill -x iperf3 2>/dev/null; sleep 0.5
    nohup iperf3 -s -1 -B "$PEER_RX_IP" > "$TMP/iperf.out" 2>&1 &
    SRV_PID=$!
    for _ in $(seq 1 60); do kill -0 "$SRV_PID" 2>/dev/null || break; sleep 1; done
    wait "$SRV_PID" 2>/dev/null
else
    echo "[sender] waiting 3s for receiver to listen, then running iperf3 -P $STREAMS -t $DURATION ..."
    sleep 3
    iperf3 -c "$PEER_RX_IP" -B "$PEER_TX_IP" -P "$STREAMS" -t "$DURATION" -l 128k -O 2 \
        > "$TMP/iperf.out" 2>&1
fi

echo "--- AFTER snapshots ---"
nic_snapshot      > "$TMP/nic_after"
softnet_snapshot  > "$TMP/softnet_after"
tcp_snapshot      > "$TMP/tcp_after"

{
echo ""
echo "--- iperf3 summary ---"
grep -E '\[SUM\].*(sender|receiver)' "$TMP/iperf.out" | tail -2
echo ""
echo "--- TCP SNMP deltas (/proc/net/snmp) ---"
# Show only the counters that changed
paste "$TMP/tcp_before" "$TMP/tcp_after" \
    | awk '{
        a=$3; b=$(NF)
        if (a+0 != b+0) printf "  %-22s %12s -> %12s  delta=%d\n", $1, a, b, b-a
    }'
echo ""
echo "--- softnet_stat deltas (look for non-zero dropped_delta/squeezed_delta) ---"
softnet_delta "$TMP/softnet_before" "$TMP/softnet_after"
echo ""
echo "--- NIC counter deltas (only non-zero) ---"
nic_delta "$TMP/nic_before" "$TMP/nic_after" "$HOST"
echo ""
echo "--- raw iperf3 tail (last 12 lines) ---"
tail -12 "$TMP/iperf.out"
echo ""
echo "==================================================================="
echo " HOW TO READ THIS"
echo "==================================================================="
echo " - If NIC rx_dropped / rx_missed_errors / rx_fifo_errors climbed:"
echo "     -> ring buffers too small. Fix: ethtool -G \$IF rx 4096 (or max)."
echo " - If softnet_stat 'dropped' or 'squeezed' climbed on any CPU:"
echo "     -> softirq starvation. Fix: increase net.core.netdev_budget,"
echo "        check IRQ affinity, consider RPS."
echo " - If NEITHER of the above climbed but TCP RetransSegs did:"
echo "     -> the retransmits are LACP reordering (out-of-order segments"
echo "        triggering fast retransmit), NOT packet loss. NOT fixable."
echo "==================================================================="
} | tee -a "$LOG"

echo ""
echo "Log: $LOG"
