#!/usr/bin/env bash
#
# MDEV-41706: Aria: a failed data-page write during FLUSH TABLES drops the dirty page;
# committed UPDATEs on a TRANSACTIONAL=1 table are silently lost, FLUSH TABLES returns OK.
#
# Usage:
#   DB_PORT=<port> ./mdev41706_repro.sh
#
# Environment (all optional except DB_PORT):
#   DB_HOST      default 127.0.0.1
#   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 re-checks the rows after the restart
#   KEEP         set to 1 to keep the test database
#
# Two cases are run, each with one EIO on the first pwrite64() to the table's .MAD file:
#   Case 1: FLUSH TABLES            Case 2: RENAME TABLE (which flushes the table)
# Needs: sudo, strace >= 4.22 (for -P and inject=), ptrace allowed on mariadbd,
#        read access to the datadir.

set -u
DB_HOST="${DB_HOST:-127.0.0.1}"
DB_USER="${DB_USER:-root}"
MARIADB_BIN="${MARIADB_BIN:-mariadb}"
DB="mdev41706"
OUT="${OUT_DIR:-/tmp/mdev41706_$(date +%Y%m%d_%H%M%S)}"
[ -n "${DB_PORT:-}" ] || { echo "Set DB_PORT (e.g. DB_PORT=12309 $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"; }
q() { "$MARIADB_BIN" -u"$DB_USER" ${DB_PASS:+-p"$DB_PASS"} -h"$DB_HOST" -P"$DB_PORT" -N -B -e "$1" 2>&1 | grep -v 'ssl-verify-server-cert'; }

# ---------------------------------------------------------------- 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")
[ -z "${SERVER_PID:-${pid:-}}" ] && pid=$(pgrep -f -- "mariadbd.*--port=${DB_PORT}( |\$)" | head -1)
PID="${SERVER_PID:-${pid:-}}"
[ -n "$PID" ] && [ -d "/proc/$PID" ] || { echo "Cannot find mariadbd PID; set SERVER_PID" >&2; exit 2; }
echo "mariadbd PID: $PID"
q "SELECT CONCAT('aria_recover_options=', @@aria_recover_options)" 2>/dev/null
sudo -v || { echo "sudo required"; exit 2; }
strace -V 2>&1 | head -1
LOG=$(q "SELECT @@log_error" | tail -1)

# ---------------------------------------------------------------- helpers
# inject_run <label> <file> <sql>  -> INJ_OUT (client output), INJ_TRACE
inject_run() {
  local label="$1" file="$2" sql="$3" spid_file i=0
  INJ_TRACE="$OUT/trace_${label}.txt"; spid_file="$OUT/strace_${label}.pid"
  local f; f=$(readlink -f "$file" 2>/dev/null || echo "$file")
  nohup sudo -n sh -c "echo \$\$ > '$spid_file'; exec strace -f -qq -y -p $PID \
      -e trace=pwrite64 -e inject=pwrite64:error=EIO:when=1 -P '$f' \
      -o '$INJ_TRACE'" >/dev/null 2>&1 &
  while [ "$(awk '/^TracerPid:/{print $2}' /proc/$PID/status)" = "0" ]; do
    i=$((i+1)); [ $i -gt 100 ] && { echo "strace did not attach" >&2; return 1; }
    sleep 0.1
  done
  INJ_OUT=$(q "$sql")
  sudo -n kill -INT "$(cat "$spid_file")" 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 "  statement output: ${INJ_OUT:-<none> (reported OK)}"
  echo "  trace:"; sed 's/^/    /' "$INJ_TRACE" | cut -c1-160 | head -5
}
rows() {   # rows <table> -> "count old new", or ERR(<message>)
  local r
  r=$(q "SELECT COUNT(*), COALESCE(SUM(v LIKE 'old-%'),0), COALESCE(SUM(v LIKE 'new-%'),0) FROM $DB.$1" | tail -1)
  if [[ "$r" =~ ^[0-9]+[[:space:]]+[0-9]+[[:space:]]+[0-9]+$ ]]; then echo "$r"; else echo "ERR($r)"; fi
}
old_of() { [[ "$1" =~ ^[0-9]+[[:space:]]+([0-9]+)[[:space:]]+[0-9]+$ ]] && echo "${BASH_REMATCH[1]}" || echo -1; }
setup_table() {   # setup_table <name> ; aborts if the table is not 2000 rows 'new-%'
  local out r
  out=$(q "USE $DB;
CREATE TABLE $1 (id INT PRIMARY KEY, v VARCHAR(200)) ENGINE=Aria TRANSACTIONAL=1;
INSERT INTO $1 SELECT seq, CONCAT('old-', seq, '-', REPEAT('x',150)) FROM seq_1_to_2000;
FLUSH TABLES;
UPDATE $1 SET v=CONCAT('new-', id, '-', REPEAT('y',150));")
  if has "$out" "ERROR"; then echo "  SETUP FAILED for $1:"; echo "$out" | sed 's/^/    /'; exit 2; fi
  r=$(rows "$1"); echo "  rows in $1 (count old new): $r"
  [[ "$r" =~ ^2000[[:space:]]+0[[:space:]]+2000$ ]] || { echo "  SETUP FAILED: $1 must have 2000 rows, all 'new-...' (got: $r)"; exit 2; }
}

# ---------------------------------------------------------------- Case 1
say "Setup case 1: Aria TRANSACTIONAL=1 table t1, 2000 rows, committed UPDATE"
q "DROP DATABASE IF EXISTS $DB; CREATE DATABASE $DB;" | grep -i error && exit 2
setup_table t1

say "Case 1: FLUSH TABLES with EIO on the first write to t1.MAD"
inject_run flush "$DATADIR$DB/t1.MAD" "FLUSH TABLES"
FL1="$INJ_OUT"; TR1="$INJ_TRACE"
R1=$(rows t1)
echo "  rows after the failed flush (count old new): $R1"
OLD1=$(old_of "$R1")

RST1="n/a"
if [ -n "${RESTART_CMD:-}" ]; then
  say "Restart (RESTART_CMD)"
  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 [ -d "/proc/$PID" ]; then
    echo "  WARNING: the old mariadbd (PID $PID) is still running - RESTART_CMD did not restart THIS server."
    RST1="invalid"
  else
    RST1=$(rows t1); echo "  rows after restart (count old new): $RST1"
    PID=$(cat "$pf" 2>/dev/null || echo "$PID")
  fi
fi
CH=$(q "CHECK TABLE $DB.t1 EXTENDED"); echo "  CHECK TABLE t1 EXTENDED: $(echo "$CH" | tr '\t\n' '  ')"
[ -r "$LOG" ] && { echo "  error log:"; grep -E "marked as crashed|Recovering table|Got error '145" "$LOG" | tail -3 | sed 's/^/    /'; }

# ---------------------------------------------------------------- Case 2
say "Setup case 2: Aria table t2, 2000 rows, committed UPDATE"
setup_table t2
say "Case 2: RENAME TABLE t2 TO t7 with EIO on the first write to t2.MAD"
inject_run rename "$DATADIR$DB/t2.MAD" "RENAME TABLE $DB.t2 TO $DB.t7"
FL2="$INJ_OUT"; TR2="$INJ_TRACE"
TN=$(q "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA='$DB' AND TABLE_NAME IN ('t2','t7')" | head -1)
echo "  table after the rename: ${TN:-<none>}"
R2=$(rows "${TN:-t2}"); echo "  rows in ${TN:-t2} after the rename (count old new): $R2"
OLD2=$(old_of "$R2")

# ---------------------------------------------------------------- checks
say "Checks"
grep -q INJECTED "$TR1";                              check "case 1: EIO injected on a write to t1.MAD" $?
! has "$FL1" "ERROR";                                 check "case 1: FLUSH TABLES reported OK (no error to the client)" $?
[ "$OLD1" -gt 0 ];                                    check "case 1: committed UPDATE lost - $OLD1 row(s) show the old version (-1 = unreadable)" $?
if [ "$RST1" != "n/a" ] && [ "$RST1" != "invalid" ]; then
  OLDR=$(old_of "$RST1"); [ "$OLDR" -gt 0 ];          check "case 1: rows still old after the restart ($OLDR)" $?
fi
has "$CH" "OK";                                       check "case 1: CHECK TABLE EXTENDED reports OK" $?
grep -q INJECTED "$TR2";                              check "case 2: EIO injected on a write to t2.MAD" $?
! has "$FL2" "ERROR";                                 check "case 2: RENAME TABLE reported OK" $?
[ "$OLD2" -gt 0 ];                                    check "case 2: committed UPDATE lost - $OLD2 row(s) show the old version (-1 = unreadable)" $?

# ---------------------------------------------------------------- cleanup
if [ "${KEEP:-0}" != "1" ]; then
  say "Cleanup"; 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 ]
