#!/usr/bin/env bash
#
# MDEV-41707: failed unlink() of the #sql-backup tablespace at the end of a copying
# ALTER TABLE leaves an orphan; later copying ALTERs on the SAME connection fail
# with errno 184 "Tablespace already exists".
#
# Usage:
#   DB_PORT=<port> ./mdev41707_repro.sh
#
# Environment (all optional except DB_PORT):
#   DB_HOST      default 127.0.0.1   ("localhost" would use a socket and ignore the port)
#   DB_USER      default root
#   DB_PASS      default none
#   MARIADB_BIN  default mariadb
#   SERVER_PID   mariadbd PID (default: from @@pid_file, then pgrep on --port)
#   RESTART_CMD  command that cleanly restarts the SAME server (same datadir); if set,
#                the script checks that the orphan file survives the restart
#   KEEP         set to 1 to keep the test database and the orphan file
#
# Needs: sudo, strace >= 4.22, ptrace allowed on mariadbd, read access to the datadir.
# The first ALTER and the second ALTER MUST run on one connection, so this script keeps
# a single mariadb client session open through a FIFO.

set -u
DB_HOST="${DB_HOST:-127.0.0.1}"
DB_USER="${DB_USER:-root}"
MARIADB_BIN="${MARIADB_BIN:-mariadb}"
DB="mdev41707"
OUT="${OUT_DIR:-/tmp/mdev41707_$(date +%Y%m%d_%H%M%S)}"
[ -n "${DB_PORT:-}" ] || { echo "Set DB_PORT (e.g. DB_PORT=11948 $0)" >&2; exit 2; }
mkdir -p "$OUT"

pass=0; fail=0
say()   { printf '\n=== %s ===\n' "$*"; }
check() { if [ "$2" -eq 0 ]; then echo "  [ok]   $1"; pass=$((pass+1)); else echo "  [MISS] $1"; fail=$((fail+1)); fi; }
has()   { grep -q -- "$2" <<<"$1"; }
cli_args() { printf '%s\n' "-u$DB_USER" ${DB_PASS:+-p"$DB_PASS"} "-h$DB_HOST" "-P$DB_PORT"; }
q() { "$MARIADB_BIN" $(cli_args) -N -B -e "$1" 2>&1; }          # one-shot, NEW connection

# ------------------------------------------------------------------ server / PID
say "Server"
q "SELECT @@version, @@port, @@datadir" | tee "$OUT/server.txt"
DATADIR=$(q "SELECT @@datadir" | tail -1)
pf=$(q "SELECT @@pid_file" | tail -1)
[ -r "$pf" ] && pid=$(cat "$pf")
if [ -z "${SERVER_PID:-${pid:-}}" ]; then
  pid=$(pgrep -f -- "mariadbd.*--port=${DB_PORT}( |\$)" | head -1)
fi
PID="${SERVER_PID:-${pid:-}}"
[ -n "$PID" ] && [ -d "/proc/$PID" ] || { echo "Cannot find mariadbd PID; set SERVER_PID" >&2; exit 2; }
echo "mariadbd PID: $PID"
[ -d "$DATADIR$DB" ] || true
sudo -v || { echo "sudo required"; exit 2; }
strace -V 2>&1 | head -1

orphans() { ls "$DATADIR$DB" 2>/dev/null | grep -E '^#sql-backup' ; }

# ------------------------------------------------------------------ setup
say "Setup"
q "DROP DATABASE IF EXISTS $DB; CREATE DATABASE $DB;
CREATE TABLE $DB.t1 (a INT PRIMARY KEY, b INT) ENGINE=InnoDB;
INSERT INTO $DB.t1 VALUES (1,1),(2,2);
CREATE TABLE $DB.t2 (a INT PRIMARY KEY) ENGINE=InnoDB;"
echo "created $DB.t1 and $DB.t2"

# ------------------------------------------------------------------ single session via FIFO
FIFO="$OUT/in.fifo"; SOUT="$OUT/session.out"
mkfifo "$FIFO"; : > "$SOUT"
"$MARIADB_BIN" $(cli_args) --force --unbuffered -N -B "$DB" < "$FIFO" > "$SOUT" 2>&1 &
CLI=$!
exec 7> "$FIFO"
sess() {   # sess "<sql>" -> prints the output the statements produced
  local m="__END_${RANDOM}_${RANDOM}__" start i=0
  start=$(wc -l < "$SOUT")
  printf '%s\nSELECT "%s";\n' "$1" "$m" >&7
  until grep -q "$m" "$SOUT"; do
    sleep 0.1; i=$((i+1)); [ $i -gt 300 ] && { echo "(session timeout)"; break; }
  done
  tail -n +$((start+1)) "$SOUT" | grep -v "$m"
}
CONN=$(sess "SELECT CONNECTION_ID();")
echo "session connection id: $CONN"

# ------------------------------------------------------------------ arm strace
say "Arm strace: EIO on the first unlink()"
TRACE="$OUT/trace.txt"; SPID="$OUT/strace.pid"
nohup sudo -n sh -c "echo \$\$ > '$SPID'; exec strace -f -qq -y -p $PID \
    -e trace=unlink,unlinkat,rename \
    -e inject=unlink:error=EIO:when=1 \
    -o '$TRACE'" >/dev/null 2>&1 &
i=0
while [ "$(awk '/^TracerPid:/{print $2}' /proc/$PID/status)" = "0" ]; do
  i=$((i+1)); [ $i -gt 100 ] && { echo "strace did not attach" >&2; exit 1; }
  sleep 0.1
done
echo "strace attached"

# ------------------------------------------------------------------ step 1
say "Step 1: copying ALTER on the session (EIO injected on the first unlink)"
R1=$(sess "ALTER TABLE t1 MODIFY b BIGINT, ALGORITHM=COPY; SHOW WARNINGS; SELECT * FROM t1;")
echo "$R1" | sed 's/^/  /'
sudo -n kill -INT "$(cat "$SPID")" 2>/dev/null
i=0
while [ "$(awk '/^TracerPid:/{print $2}' /proc/$PID/status)" != "0" ]; do
  i=$((i+1)); [ $i -gt 100 ] && break; sleep 0.1
done
echo "  trace (sql-backup / INJECTED lines):"
grep -E 'sql-backup|INJECTED' "$TRACE" | sed 's/^/    /'

# ------------------------------------------------------------------ step 2
say "Orphan file in the database directory"
ORPH=$(orphans)
echo "  ${ORPH:-<none>}" | sed 's/^/  /'

say "Step 2: next copying ALTER on the SAME session"
R2=$(sess "ALTER TABLE t2 ADD COLUMN c INT, ALGORITHM=COPY;")
echo "$R2" | sed 's/^/  /'

say "Step 3: the same kind of ALTER from ANOTHER connection"
R3=$(q "ALTER TABLE $DB.t2 FORCE, ALGORITHM=COPY; SELECT CONNECTION_ID();")
echo "$R3" | sed 's/^/  /'

# ------------------------------------------------------------------ optional restart
RESTART_ORPH="n/a"
if [ -n "${RESTART_CMD:-}" ]; then
  say "Restart (RESTART_CMD)"
  exec 7>&-; kill "$CLI" 2>/dev/null
  eval "$RESTART_CMD"
  for i in $(seq 1 90); do q "SELECT 1" | grep -q '^1$' && break; sleep 1; done
  q "SELECT @@version, @@port, @@datadir"
  if q "SHOW TABLES FROM $DB" | grep -q t1; then
    RESTART_ORPH=$(orphans)
    echo "  orphan files after restart: ${RESTART_ORPH:-<none>}"
    echo "  (the ticket's 'same thread id fails again' part needs the same pid as before, e.g. a container)"
  else
    echo "  WARNING: $DB is gone - datadir was probably re-initialised; restart check invalid."
    RESTART_ORPH="invalid"
  fi
fi

# ------------------------------------------------------------------ verdict
say "Checks"
grep -qE 'unlink\(.*sql-backup.*\.ibd.*\(INJECTED\)' "$TRACE";  check "EIO injected on unlink of the #sql-backup .ibd" $?
{ ! has "$R1" "ERROR" && grep -qE '^2[[:space:]]+2$' <<<"$R1"; }; check "step 1: ALTER succeeded and data intact (SELECT * FROM t1 returned rows)" $?
[ -n "$ORPH" ];                                                 check "orphan #sql-backup file left in the directory" $?
has "$R2" "errno: 184";                                         check "step 2: same-session ALTER fails with errno 184" $?
! has "$R3" "ERROR";                                            check "step 3: other connection's ALTER succeeds" $?
if [ -n "${RESTART_CMD:-}" ] && [ "$RESTART_ORPH" != "invalid" ]; then
  [ -n "$RESTART_ORPH" ];                                       check "orphan file still present after restart" $?
fi

# ------------------------------------------------------------------ cleanup
exec 7>&- 2>/dev/null; kill "$CLI" 2>/dev/null; rm -f "$FIFO"
if [ "${KEEP:-0}" != "1" ]; then
  say "Cleanup"
  [ -n "$ORPH" ] && rm -f "$DATADIR$DB"/'#sql-backup'* 2>/dev/null && echo "  removed orphan file(s)"
  q "DROP DATABASE IF EXISTS $DB" >/dev/null
  echo "  dropped $DB (KEEP=1 to retain)"
fi

say "Summary"
echo "  matched: $pass   not matched: $fail"
if [ "$fail" -eq 0 ]; then echo "  RESULT: BUG REPRODUCED (all ticket symptoms observed)"
else echo "  RESULT: NOT (fully) reproduced - see [MISS] lines and $OUT"; fi
echo "  Artifacts: $OUT"
[ "$fail" -eq 0 ]
