-
Notifications
You must be signed in to change notification settings - Fork 0
CI: Add behavioral validation tests for topology, problems, and operations #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
84ef389
cbda41b
a8afb3d
62b814e
1d88f3f
13ecfe5
5766a6c
47988d9
cd02379
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,222 @@ | ||||||||||||||||||||||||||
| #!/bin/bash | ||||||||||||||||||||||||||
| # Problem detection tests — verify orchestrator detects and clears | ||||||||||||||||||||||||||
| # replication problems correctly | ||||||||||||||||||||||||||
| set -uo pipefail # no -e: we handle failures ourselves | ||||||||||||||||||||||||||
| cd "$(dirname "$0")/../.." | ||||||||||||||||||||||||||
| source tests/functional/lib.sh | ||||||||||||||||||||||||||
|
Comment on lines
+5
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail fast when bootstrap steps fail. If Suggested fix-cd "$(dirname "$0")/../.."
-source tests/functional/lib.sh
+cd "$(dirname "$0")/../.." || { echo "FATAL: unable to cd to repository root"; exit 1; }
+source tests/functional/lib.sh || { echo "FATAL: unable to load tests/functional/lib.sh"; exit 1; }📝 Committable suggestion
Suggested change
🧰 Tools🪛 Shellcheck (0.11.0)[warning] 5-5: Use 'cd ... || exit' or 'cd ... || return' in case cd fails. (SC2164) [info] 6-6: Not following: tests/functional/lib.sh was not specified as input (see shellcheck -x). (SC1091) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| echo "=== PROBLEM DETECTION TESTS ===" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| COMPOSE="docker compose -f tests/functional/docker-compose.yml" | ||||||||||||||||||||||||||
| STOP_SQL=$(mysql_stop_replica_sql) | ||||||||||||||||||||||||||
| START_SQL=$(mysql_start_replica_sql) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| wait_for_orchestrator || { echo "FATAL: Orchestrator not reachable"; exit 1; } | ||||||||||||||||||||||||||
| discover_topology "mysql1" | ||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Treat topology discovery as a hard precondition. Line 15 ignores Suggested fix-discover_topology "mysql1"
+discover_topology "mysql1" || { echo "FATAL: initial topology discovery failed"; exit 1; }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # ---------------------------------------------------------------- | ||||||||||||||||||||||||||
| echo "" | ||||||||||||||||||||||||||
| echo "--- Test 1: Detect stopped replication ---" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Stop replication on mysql2 | ||||||||||||||||||||||||||
| echo "Stopping replication on mysql2..." | ||||||||||||||||||||||||||
| $COMPOSE exec -T mysql2 mysql -uroot -ptestpass -e "$STOP_SQL" 2>/dev/null | ||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check exit codes for state-changing MySQL commands. These commands mutate DB state but suppress stderr and do not verify success. If any command fails, subsequent assertions may incorrectly blame orchestrator behavior. Suggested pattern+# helper
+run_mysql() {
+ local host="$1"
+ local sql="$2"
+ if ! $COMPOSE exec -T "$host" mysql -uroot -ptestpass -e "$sql" >/dev/null; then
+ fail "MySQL command failed on ${host}" "$sql"
+ return 1
+ fi
+ return 0
+}-$COMPOSE exec -T mysql2 mysql -uroot -ptestpass -e "$STOP_SQL" 2>/dev/null
+run_mysql "mysql2" "$STOP_SQL" || exit 1Apply the same pattern to the other mutation calls. Also applies to: 81-81, 122-122, 150-150, 179-185, 216-220 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Force re-discovery so orchestrator refreshes instance state immediately | ||||||||||||||||||||||||||
| curl -s --max-time 10 "$ORC_URL/api/discover/mysql2/3306" > /dev/null 2>&1 | ||||||||||||||||||||||||||
| sleep 2 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Wait for orchestrator to detect the problem (poll up to 30s) | ||||||||||||||||||||||||||
| echo "Waiting for orchestrator to detect stopped replication..." | ||||||||||||||||||||||||||
| DETECTED=false | ||||||||||||||||||||||||||
| for i in $(seq 1 30); do | ||||||||||||||||||||||||||
| PROBLEMS=$(curl -s --max-time 10 "$ORC_URL/api/problems" 2>/dev/null) | ||||||||||||||||||||||||||
| if echo "$PROBLEMS" | python3 -c " | ||||||||||||||||||||||||||
| import json, sys | ||||||||||||||||||||||||||
| problems = json.load(sys.stdin) | ||||||||||||||||||||||||||
| for p in problems: | ||||||||||||||||||||||||||
| h = p.get('Key', {}).get('Hostname', '') | ||||||||||||||||||||||||||
| if 'mysql2' in h: | ||||||||||||||||||||||||||
| sys.exit(0) | ||||||||||||||||||||||||||
| sys.exit(1) | ||||||||||||||||||||||||||
| " 2>/dev/null; then | ||||||||||||||||||||||||||
| DETECTED=true | ||||||||||||||||||||||||||
| echo "Problem detected after ${i}s" | ||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
| sleep 1 | ||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if [ "$DETECTED" = "true" ]; then | ||||||||||||||||||||||||||
| pass "Orchestrator detected stopped replication on mysql2" | ||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||
| fail "Orchestrator did not detect stopped replication on mysql2 within 30s" | ||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Force another re-discovery to ensure instance data is fresh | ||||||||||||||||||||||||||
| curl -s --max-time 10 "$ORC_URL/api/discover/mysql2/3306" > /dev/null 2>&1 | ||||||||||||||||||||||||||
| sleep 2 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Verify the specific problem shows replication threads stopped | ||||||||||||||||||||||||||
| REPL_STATE=$(curl -s --max-time 10 "$ORC_URL/api/instance/mysql2/3306" 2>/dev/null | python3 -c " | ||||||||||||||||||||||||||
| import json, sys | ||||||||||||||||||||||||||
| inst = json.load(sys.stdin) | ||||||||||||||||||||||||||
| sql = inst.get('ReplicationSQLThreadRuning', 'unknown') | ||||||||||||||||||||||||||
| io = inst.get('ReplicationIOThreadRuning', 'unknown') | ||||||||||||||||||||||||||
| print(f'SQL={sql},IO={io}') | ||||||||||||||||||||||||||
| " 2>/dev/null || echo "unknown") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if echo "$REPL_STATE" | grep -q "SQL=False"; then | ||||||||||||||||||||||||||
| pass "Orchestrator reports SQL thread stopped on mysql2" | ||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||
| fail "Orchestrator replication state for mysql2: $REPL_STATE" | ||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # ---------------------------------------------------------------- | ||||||||||||||||||||||||||
| echo "" | ||||||||||||||||||||||||||
| echo "--- Test 1b: Clear stopped replication ---" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Restart replication | ||||||||||||||||||||||||||
| echo "Restarting replication on mysql2..." | ||||||||||||||||||||||||||
| $COMPOSE exec -T mysql2 mysql -uroot -ptestpass -e "$START_SQL" 2>/dev/null | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Force re-discovery so orchestrator refreshes instance state | ||||||||||||||||||||||||||
| curl -s --max-time 10 "$ORC_URL/api/discover/mysql2/3306" > /dev/null 2>&1 | ||||||||||||||||||||||||||
| sleep 2 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Wait for orchestrator to see replication running again | ||||||||||||||||||||||||||
| echo "Waiting for replication to recover..." | ||||||||||||||||||||||||||
| CLEARED=false | ||||||||||||||||||||||||||
| for i in $(seq 1 30); do | ||||||||||||||||||||||||||
| # Force re-discovery each iteration | ||||||||||||||||||||||||||
| curl -s --max-time 10 "$ORC_URL/api/discover/mysql2/3306" > /dev/null 2>&1 | ||||||||||||||||||||||||||
| REPL_STATE=$(curl -s --max-time 10 "$ORC_URL/api/instance/mysql2/3306" 2>/dev/null | python3 -c " | ||||||||||||||||||||||||||
| import json, sys | ||||||||||||||||||||||||||
| inst = json.load(sys.stdin) | ||||||||||||||||||||||||||
| sql = inst.get('ReplicationSQLThreadRuning', False) | ||||||||||||||||||||||||||
| io = inst.get('ReplicationIOThreadRuning', False) | ||||||||||||||||||||||||||
| print(f'{sql}:{io}') | ||||||||||||||||||||||||||
| " 2>/dev/null || echo "False:False") | ||||||||||||||||||||||||||
| SQL_RUNNING=$(echo "$REPL_STATE" | cut -d: -f1) | ||||||||||||||||||||||||||
| IO_RUNNING=$(echo "$REPL_STATE" | cut -d: -f2) | ||||||||||||||||||||||||||
| if [ "$SQL_RUNNING" = "True" ]; then | ||||||||||||||||||||||||||
| CLEARED=true | ||||||||||||||||||||||||||
| echo "Replication recovered after ${i}s (SQL=True, IO=${IO_RUNNING})" | ||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||
|
Comment on lines
+100
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Require both SQL and IO threads for “cleared” state. Current clear condition only checks Suggested fix- if [ "$SQL_RUNNING" = "True" ]; then
+ if [ "$SQL_RUNNING" = "True" ] && [ "$IO_RUNNING" = "True" ]; then
CLEARED=true
- echo "Replication recovered after ${i}s (SQL=True, IO=${IO_RUNNING})"
+ echo "Replication recovered after ${i}s (SQL=True, IO=True)"
break
fi📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
| sleep 1 | ||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if [ "$CLEARED" = "true" ]; then | ||||||||||||||||||||||||||
| pass "Stopped replication problem cleared after restart" | ||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||
| fail "Replication SQL thread not running on mysql2 after 30s (state: $REPL_STATE)" | ||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # ---------------------------------------------------------------- | ||||||||||||||||||||||||||
| echo "" | ||||||||||||||||||||||||||
| echo "--- Test 2: Detect read_only mismatch (writable replica) ---" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Make mysql2 writable (it should be read-only as a replica) | ||||||||||||||||||||||||||
| echo "Setting mysql2 read_only=0 (simulating writable replica)..." | ||||||||||||||||||||||||||
| $COMPOSE exec -T mysql2 mysql -uroot -ptestpass -e "SET GLOBAL read_only=0" 2>/dev/null | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Force re-discovery so orchestrator refreshes instance state | ||||||||||||||||||||||||||
| curl -s --max-time 10 "$ORC_URL/api/discover/mysql2/3306" > /dev/null 2>&1 | ||||||||||||||||||||||||||
| sleep 2 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Wait for orchestrator to detect the problem | ||||||||||||||||||||||||||
| echo "Waiting for orchestrator to detect writable replica..." | ||||||||||||||||||||||||||
| DETECTED=false | ||||||||||||||||||||||||||
| for i in $(seq 1 30); do | ||||||||||||||||||||||||||
| INST=$(curl -s --max-time 10 "$ORC_URL/api/instance/mysql2/3306" 2>/dev/null) | ||||||||||||||||||||||||||
| IS_RO=$(echo "$INST" | python3 -c "import json,sys; print(json.load(sys.stdin).get('ReadOnly', True))" 2>/dev/null || echo "True") | ||||||||||||||||||||||||||
| if [ "$IS_RO" = "False" ]; then | ||||||||||||||||||||||||||
| DETECTED=true | ||||||||||||||||||||||||||
| echo "Writable replica detected after ${i}s" | ||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
| sleep 1 | ||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if [ "$DETECTED" = "true" ]; then | ||||||||||||||||||||||||||
| pass "Orchestrator detected mysql2 is writable (read_only=false while replicating)" | ||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||
| fail "Orchestrator did not detect writable replica within 30s" | ||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Restore read_only | ||||||||||||||||||||||||||
| echo "Restoring read_only=1 on mysql2..." | ||||||||||||||||||||||||||
| $COMPOSE exec -T mysql2 mysql -uroot -ptestpass -e "SET GLOBAL read_only=1" 2>/dev/null | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Force re-discovery | ||||||||||||||||||||||||||
| curl -s --max-time 10 "$ORC_URL/api/discover/mysql2/3306" > /dev/null 2>&1 | ||||||||||||||||||||||||||
| sleep 2 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Wait for it to clear | ||||||||||||||||||||||||||
| CLEARED=false | ||||||||||||||||||||||||||
| for i in $(seq 1 15); do | ||||||||||||||||||||||||||
| IS_RO=$(curl -s --max-time 10 "$ORC_URL/api/instance/mysql2/3306" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin).get('ReadOnly', False))" 2>/dev/null || echo "False") | ||||||||||||||||||||||||||
| if [ "$IS_RO" = "True" ]; then | ||||||||||||||||||||||||||
| CLEARED=true | ||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
| sleep 1 | ||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if [ "$CLEARED" = "true" ]; then | ||||||||||||||||||||||||||
| pass "Writable replica problem cleared after restoring read_only" | ||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||
| fail "read_only still reported as false after 15s" | ||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # ---------------------------------------------------------------- | ||||||||||||||||||||||||||
| echo "" | ||||||||||||||||||||||||||
| echo "--- Test 3: Detect errant GTID ---" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Inject an errant transaction on mysql3 | ||||||||||||||||||||||||||
| echo "Injecting errant transaction on mysql3..." | ||||||||||||||||||||||||||
| $COMPOSE exec -T mysql3 mysql -uroot -ptestpass -e " | ||||||||||||||||||||||||||
| SET GLOBAL read_only=0; | ||||||||||||||||||||||||||
| SET GLOBAL super_read_only=0; | ||||||||||||||||||||||||||
| CREATE DATABASE IF NOT EXISTS errant_detect_test; | ||||||||||||||||||||||||||
| SET GLOBAL read_only=1; | ||||||||||||||||||||||||||
| SET GLOBAL super_read_only=1; | ||||||||||||||||||||||||||
| " 2>/dev/null | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Force re-discovery so orchestrator picks up the errant GTID | ||||||||||||||||||||||||||
| curl -s --max-time 10 "$ORC_URL/api/discover/mysql3/3306" > /dev/null 2>&1 | ||||||||||||||||||||||||||
| sleep 2 | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Wait for orchestrator to detect errant GTID | ||||||||||||||||||||||||||
| echo "Waiting for orchestrator to detect errant GTID..." | ||||||||||||||||||||||||||
| DETECTED=false | ||||||||||||||||||||||||||
| for i in $(seq 1 30); do | ||||||||||||||||||||||||||
| GTID_ERRANT=$(curl -s --max-time 10 "$ORC_URL/api/instance/mysql3/3306" 2>/dev/null | python3 -c " | ||||||||||||||||||||||||||
| import json, sys | ||||||||||||||||||||||||||
| inst = json.load(sys.stdin) | ||||||||||||||||||||||||||
| errant = inst.get('GtidErrant', '') | ||||||||||||||||||||||||||
| print(errant if errant else '') | ||||||||||||||||||||||||||
| " 2>/dev/null || echo "") | ||||||||||||||||||||||||||
| if [ -n "$GTID_ERRANT" ]; then | ||||||||||||||||||||||||||
| DETECTED=true | ||||||||||||||||||||||||||
| echo "Errant GTID detected after ${i}s: $GTID_ERRANT" | ||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
| sleep 1 | ||||||||||||||||||||||||||
| done | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if [ "$DETECTED" = "true" ]; then | ||||||||||||||||||||||||||
| pass "Orchestrator detected errant GTID on mysql3" | ||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||
| fail "Orchestrator did not detect errant GTID within 30s" | ||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Cleanup errant DB (GTID remains but that's OK) | ||||||||||||||||||||||||||
| $COMPOSE exec -T mysql3 mysql -uroot -ptestpass -e " | ||||||||||||||||||||||||||
| SET GLOBAL read_only=0; | ||||||||||||||||||||||||||
| DROP DATABASE IF EXISTS errant_detect_test; | ||||||||||||||||||||||||||
| SET GLOBAL read_only=1; | ||||||||||||||||||||||||||
| " 2>/dev/null | ||||||||||||||||||||||||||
|
Comment on lines
+216
to
+220
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cleanup for the errant GTID test will fail silently. After the injection at lines 158-164, The cleanup needs to disable
Suggested change
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| summary | ||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle cleanup failures explicitly to avoid cross-test contamination.
Line 130 suppresses stderr and does not check command success. If
DROP DATABASEfails, stale state can leak into later functional tests in the same environment.Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents