Skip to content

fix: prevent false DeadMaster and drain MariaDB relay logs (#106) - #108

Merged
renecannao merged 10 commits into
masterfrom
fix/issue-106-mariadb-deadmaster-relay-drain
Jul 27, 2026
Merged

fix: prevent false DeadMaster and drain MariaDB relay logs (#106)#108
renecannao merged 10 commits into
masterfrom
fix/issue-106-mariadb-deadmaster-relay-drain

Conversation

@renecannao

@renecannao renecannao commented Jul 16, 2026

Copy link
Copy Markdown

Summary

Fixes #106

  • Detection: Count valid replicas with IO running; if any, classify as UnreachableMaster instead of DeadMaster when the master check fails (SQL stopped alone is not proof the master is dead).
  • MariaDB-safe drain: New DrainRelayLogs starts the SQL thread only (no IO stop/start), waits for catch-up, then stops replication. MariaDB nice-stop now uses this path so relay events are applied before promotion.
  • Skip undrainable candidates: GetCandidateReplica tries the next replica if drain fails (e.g. Error 1062); fails recovery if none can drain.
  • Validate before re-point: GTID dead-master recovery validates geo/lag/SQL-up-to-date before re-pointing siblings (RegroupReplicasGTIDWithValidation).
  • Tests/docs: Unit tests, functional test-mariadb-relay-drain.sh, MariaDB 10.6/10.11 CI job, docs updates.

Design / plan

  • docs/superpowers/specs/2026-07-16-mariadb-deadmaster-relay-drain-design.md
  • docs/superpowers/plans/2026-07-16-mariadb-deadmaster-relay-drain.md

Test plan

  • go test ./go/inst/ -count=1
  • go build ./go/cmd/orchestrator
  • CI: MySQL matrix includes relay-drain script
  • CI: functional-mariadb (10.6, 10.11)
  • Manual: SQL stopped + unapplied relay + master kill → promoted has relay data

Summary by CodeRabbit

  • New Features
    • Added MariaDB-aware failover logic, including relay-log draining during promotion preparation.
    • Introduced improved replication analysis to consider replica I/O thread health when deciding master reachability.
  • Bug Fixes
    • Reduces false “DeadMaster” outcomes when replica I/O remains active and distinguishes “unreachable” vs “dead” more accurately.
  • Documentation
    • Updated failure-detection scenario definitions and user guidance for master reachability outcomes.
  • Tests
    • Added MariaDB functional CI coverage and a relay-drain failover scenario, plus unit tests for the updated decision logic.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@renecannao, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e6ed92e7-bc20-4198-bd0e-9823fb6d4c01

📥 Commits

Reviewing files that changed from the base of the PR and between 0e3b596 and 305ee90.

📒 Files selected for processing (2)
  • tests/functional/setup-replication.sh
  • tests/functional/test-mariadb-relay-drain.sh
📝 Walkthrough

Walkthrough

The PR adds IO-aware master failure classification, MariaDB relay-log draining before promotion, candidate validation before sibling repointing, and MariaDB functional coverage across MySQL-compatible test infrastructure and CI.

Changes

MariaDB recovery behavior

Layer / File(s) Summary
IO-aware failure detection
go/inst/analysis.go, go/inst/analysis_classification.go, go/inst/analysis_dao.go, go/inst/analysis_classification_test.go, docs/failure-detection.md, docs/user-manual.md, docs/superpowers/...
Replication analysis now counts valid replicas with running IO threads and classifies affected masters as UnreachableMaster instead of DeadMaster; tests and documentation describe the updated criteria.
MariaDB relay-log draining
go/inst/instance_topology_dao.go, docs/superpowers/...
MariaDB replicas now start only their SQL thread, wait for relay progress, and then stop replication fully when a nice stop is requested.
Validated candidate recovery
go/inst/instance_topology.go, go/logic/topology_recovery.go, docs/superpowers/...
Rematch selection skips candidates that cannot drain, while GTID regroup validates the chosen candidate before sibling repointing and promotion.
MariaDB functional coverage
.github/workflows/functional.yml, tests/functional/docker-compose.mariadb.yml, tests/functional/mysql/*, tests/functional/lib.sh, tests/functional/setup-replication.sh, tests/functional/test-mariadb-relay-drain.sh
Adds MariaDB-specific compose, replication commands, version detection, SQL-stopped detection checks, relay-drain failover tests, and CI jobs for MariaDB 10.6 and 10.11.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MariaDBReplica
  participant Orchestrator
  participant CandidateReplica
  participant RecoveryTopology

  MariaDBReplica->>Orchestrator: report IO running and SQL stopped
  Orchestrator->>MariaDBReplica: classify master reachability
  Orchestrator->>CandidateReplica: drain relay logs
  CandidateReplica-->>Orchestrator: return SQL-aligned candidate
  Orchestrator->>RecoveryTopology: validate candidate
  RecoveryTopology-->>Orchestrator: repoint siblings and promote
Loading

Possibly related PRs

Poem

A rabbit watched the relay flow,
While SQL threads caught up below.
IO stayed bright, false deaths grew few,
Candidates drained before they flew.
“Hop safely, logs!” the bunny cheered.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main fix: preventing false DeadMaster detection and handling MariaDB relay-log draining.
Linked Issues check ✅ Passed The code and tests address #106 by fixing IO-only replicas, MariaDB relay draining, and candidate validation before sibling re-pointing.
Out of Scope Changes check ✅ Passed Changes stay focused on #106, adding matching code, tests, CI, and docs without unrelated behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-106-mariadb-deadmaster-relay-drain

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses false DeadMaster detections by treating replicas with running IO threads as UnreachableMaster and introduces a safe relay log draining mechanism (DrainRelayLogs) for MariaDB to prevent data loss during failover. The review feedback highlights several critical issues, including potential nil pointer dereferences when reading topology instances, inconsistent error return values in DrainRelayLogs, a logic bypass in candidate selection when no candidate is chosen, and unused helper code in analysis_classification.go.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread go/inst/instance_topology_dao.go Outdated
Comment on lines +320 to +322
instance, err = ReadTopologyInstance(instanceKey)
log.Infof("Drained relay logs on %+v, Self:%+v, Exec:%+v", *instanceKey, instance.SelfBinlogCoordinates, instance.ExecBinlogCoordinates)
return instance, err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If ReadTopologyInstance fails and returns a nil instance along with an error, accessing instance.SelfBinlogCoordinates and instance.ExecBinlogCoordinates in the log statement will cause a panic.

The error should be handled or checked before accessing the fields of instance.

	instance, err = ReadTopologyInstance(instanceKey)
	if err != nil {
		return instance, log.Errore(err)
	}
	log.Infof("Drained relay logs on %+v, Self:%+v, Exec:%+v", *instanceKey, instance.SelfBinlogCoordinates, instance.ExecBinlogCoordinates)
	return instance, nil

Comment on lines +300 to +302
if _, err := ExecInstance(instanceKey, instance.QSP.start_slave_sql_thread()); err != nil {
return nil, log.Errorf("%+v: DrainRelayLogs: start sql thread failed: %+v", *instanceKey, err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning nil on error is inconsistent with other error paths in this function (which return instance, err). Returning instance is safer and maintains consistency.

Suggested change
if _, err := ExecInstance(instanceKey, instance.QSP.start_slave_sql_thread()); err != nil {
return nil, log.Errorf("%+v: DrainRelayLogs: start sql thread failed: %+v", *instanceKey, err)
}
if _, err := ExecInstance(instanceKey, instance.QSP.start_slave_sql_thread()); err != nil {
return instance, log.Errorf("%+v: DrainRelayLogs: start sql thread failed: %+v", *instanceKey, err)
}

// classifyUnreachableMasterWhenIORunning returns UnreachableMaster when the
// master check failed but at least one replica still has IO running.
// Returns NoProblem when this rule does not apply (caller continues other branches).
func classifyUnreachableMasterWhenIORunning(isMaster bool, lastCheckValid bool, countValidIORunningReplicas uint) AnalysisCode {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The helper function classifyUnreachableMasterWhenIORunning is defined and tested but is never used in the actual classification logic in analysis_dao.go (which implements the same logic inline).

To improve maintainability and avoid dead code, either use this helper function in analysis_dao.go or remove it.

Comment on lines +2315 to +2317
if err != nil || candidateReplica == nil {
return candidateReplica, aheadReplicas, equalReplicas, laterReplicas, cannotReplicateReplicas, err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If chooseCandidateReplica returns candidateReplica == nil (with err == nil) on the first iteration, the function returns immediately. This bypasses the descriptive error at the end of the function for forRematchPurposes.

The check should handle candidateReplica == nil when forRematchPurposes is true by returning the appropriate error.

		if err != nil {
			return nil, aheadReplicas, equalReplicas, laterReplicas, cannotReplicateReplicas, err
		}
		if candidateReplica == nil {
			if forRematchPurposes {
				return nil, aheadReplicas, equalReplicas, laterReplicas, cannotReplicateReplicas, fmt.Errorf("GetCandidateReplica: no drainable candidate replica found for %+v", *masterKey)
			}
			return nil, aheadReplicas, equalReplicas, laterReplicas, cannotReplicateReplicas, nil
		}

Comment on lines +580 to +584
refreshed, readErr := inst.ReadTopologyInstance(&candidate.Key)
if readErr != nil {
return readErr
}
*candidate = *refreshed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If ReadTopologyInstance succeeds but returns a nil pointer for refreshed (along with a nil error), dereferencing it (*refreshed) will cause a panic.

A nil check on refreshed should be performed before dereferencing.

Suggested change
refreshed, readErr := inst.ReadTopologyInstance(&candidate.Key)
if readErr != nil {
return readErr
}
*candidate = *refreshed
refreshed, readErr := inst.ReadTopologyInstance(&candidate.Key)
if readErr != nil {
return readErr
}
if refreshed == nil {
return fmt.Errorf("DelayMasterPromotionIfSQLThreadNotUpToDate: refreshed instance is nil for %+v", candidate.Key)
}
*candidate = *refreshed

renecannao added a commit that referenced this pull request Jul 27, 2026
CI failures on #108 were test harness issues, not the product fix:

- Restore flat topology (mysql1 master) before the suite; earlier failover
  tests leave mysql2 promoted and break SHOW REPLICA STATUS assumptions
- Parse replica status with robust field splitters (MySQL 8/9 + MariaDB)
- Restart SQL_THREAD (not full START SLAVE) after the false-DeadMaster check
- Avoid deprecated VALUES() in ON DUPLICATE KEY UPDATE
- Honor COMPOSE env in lib.sh version detection for MariaDB overlays
@renecannao
renecannao force-pushed the fix/issue-106-mariadb-deadmaster-relay-drain branch from b86665f to a4f7386 Compare July 27, 2026 16:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/functional/lib.sh (2)

208-216: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Compose-command plumbing for the MariaDB override is inconsistent. Some helpers/steps honor ${COMPOSE} (with both compose files) while others hardcode or omit the base file; the MariaDB run currently only works because the derived compose project name happens to match.

  • tests/functional/lib.sh#L208-L216: use the ${COMPOSE:-docker compose -f tests/functional/docker-compose.yml} fallback in mysql_source_host instead of the hardcoded base-file command.
  • .github/workflows/functional.yml#L268-L272: add COMPOSE: docker compose -f tests/functional/docker-compose.yml -f tests/functional/docker-compose.mariadb.yml to the smoke-test and relay-drain steps, matching the "Setup replication" step.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/functional/lib.sh` around lines 208 - 216, Update mysql_source_host in
tests/functional/lib.sh:208-216 to use the ${COMPOSE:-docker compose -f
tests/functional/docker-compose.yml} fallback for both compose commands. In
.github/workflows/functional.yml:268-272, add COMPOSE with both the base and
MariaDB compose files to the smoke-test and relay-drain steps, matching Setup
replication.

159-166: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

mysql_change_source_channel_sql was not given a MariaDB branch.

Sibling helpers all gained mysql_is_mariadb handling; this one still emits CHANGE REPLICATION SOURCE ... FOR CHANNEL for MariaDB. MariaDB uses named connections (CHANGE MASTER 'name' TO ...), so any MariaDB run of the named-channels suite would fail. Not currently exercised by the MariaDB CI job, so this can be deferred — but worth a comment noting MySQL-only support.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/functional/lib.sh` around lines 159 - 166, Update
mysql_change_source_channel_sql to explicitly handle MariaDB, using MariaDB
named-connection syntax with the channel name instead of MySQL’s FOR CHANNEL
form; alternatively, if support is intentionally deferred, add a comment clearly
documenting that this helper is MySQL-only.
🧹 Nitpick comments (5)
go/inst/analysis_dao.go (1)

565-570: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the tested classification helper here.

The DAO duplicates classifyUnreachableMasterWhenIORunning, so its new unit tests do not protect the production branch. Route this condition through the helper to prevent drift.

Proposed fix
-} else if a.IsMaster && !a.LastCheckValid && a.CountValidIORunningReplicas > 0 {
+} else if analysis := classifyUnreachableMasterWhenIORunning(
+	a.IsMaster, a.LastCheckValid, a.CountValidIORunningReplicas,
+); analysis != NoProblem {
 	// IO thread running on a valid replica proves the master is accepting replica connections.
 	// Do not treat SQL-thread-stopped replicas as evidence of a dead master (issue `#106`).
-	a.Analysis = UnreachableMaster
+	a.Analysis = analysis
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/inst/analysis_dao.go` around lines 565 - 570, Replace the duplicated
classification logic in the Analysis branch for a.IsMaster, !a.LastCheckValid,
and a.CountValidIORunningReplicas > 0 with the tested
classifyUnreachableMasterWhenIORunning helper. Preserve the existing
UnreachableMaster result and description behavior while ensuring production uses
the helper covered by the unit tests.
.github/workflows/functional.yml (1)

183-194: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Consider a least-privilege permissions: block and persist-credentials: false.

zizmor flags the new job for default (broad) token permissions and credential persistence via checkout. permissions: contents: read plus persist-credentials: false on actions/checkout would close both. The rest of the file has the same gap, so this can be a follow-up sweep.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/functional.yml around lines 183 - 194, Update the
functional-mariadb job with a least-privilege permissions block granting only
contents: read, and set persist-credentials: false on its actions/checkout@v4
step; leave the remaining workflow jobs unchanged.

Source: Linters/SAST tools

tests/functional/lib.sh (1)

208-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

mysql_source_host still hardcodes the base compose file.

The version helpers now honor ${COMPOSE}, but this function does not. Use the same COMPOSE_CMD fallback so overrides apply uniformly.

♻️ Suggested change
 mysql_source_host() {
     local CONTAINER="$1"
+    local COMPOSE_CMD="${COMPOSE:-docker compose -f tests/functional/docker-compose.yml}"
     if mysql_is_57 || mysql_is_mariadb; then
-        docker compose -f tests/functional/docker-compose.yml exec -T "$CONTAINER" \
+        $COMPOSE_CMD exec -T "$CONTAINER" \
             mysql -uroot -ptestpass -Nse "SHOW SLAVE STATUS" 2>/dev/null | awk -F'\t' '{print $2; exit}'
     else
-        docker compose -f tests/functional/docker-compose.yml exec -T "$CONTAINER" \
+        $COMPOSE_CMD exec -T "$CONTAINER" \
             mysql -uroot -ptestpass -Nse "SHOW REPLICA STATUS" 2>/dev/null | awk -F'\t' '{print $2; exit}'
     fi
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/functional/lib.sh` around lines 208 - 216, Update mysql_source_host to
invoke Docker Compose through the shared COMPOSE_CMD fallback, matching the
version helper behavior, in both the MySQL 5.7/MariaDB and newer replica-status
branches. Preserve the existing compose file, container, credentials, and query
logic while allowing ${COMPOSE} overrides to apply.
go/inst/instance_topology_dao.go (1)

286-324: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the MaxScale-tolerant STOP SLAVE into a helper.

The same stop-and-swallow-1199 block now appears twice here and once in StopReplicationNicely (Lines 364-370).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/inst/instance_topology_dao.go` around lines 286 - 324, Extract the
duplicated MaxScale-tolerant STOP SLAVE logic from the current flow and
StopReplicationNicely into a shared helper. The helper should execute the
instance’s stop_slave command, clear the error only for MaxScale error 1199
(“Slave connection is not running”), and return all other errors unchanged;
replace each existing inline block with calls to this helper.
tests/functional/test-mariadb-relay-drain.sh (1)

21-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

STOP_REPLICA is unused.

Drop it, or use it in place of the raw STOP REPLICA; STOP SLAVE; at Line 73.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/functional/test-mariadb-relay-drain.sh` at line 21, Remove the unused
STOP_REPLICA assignment in the MariaDB relay drain test, or reuse STOP_REPLICA
at the existing raw stop-command location so the helper’s SQL is actually
executed.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-07-16-mariadb-deadmaster-relay-drain.md`:
- Line 15: Restore the heading hierarchy in the document around the “File map”
section by adding the required intermediate heading level between the document’s
H1 and the existing “### File map” heading, preserving the section’s content and
wording.

In `@go/inst/instance_topology_dao.go`:
- Around line 304-332: Update the DrainRelayLogs flow around the SQLDelay check
and final SQLThreadUpToDate assertion to handle delayed replicas explicitly.
When instance.SQLDelay is nonzero, return a distinct result that clearly
indicates the replica is delayed and should not be treated as a generic
undrainable or promotion failure; preserve the existing wait, stop, and
post-wait validation behavior for non-delayed replicas.

In `@go/inst/instance_topology.go`:
- Around line 2309-2330: The retry loop in GetCandidateReplica can spend
drainTimeout serially on every candidate, causing excessive failover latency.
Add an overall drain-time budget or bounded attempt count for the
forRematchPurposes path, and stop trying further candidates once that limit is
reached while preserving the existing candidate classification and error-return
behavior.
- Around line 2314-2333: Copy the replicas slice when initializing remaining so
candidate removal cannot mutate replicas through shared backing storage. Update
the remaining initialization in the chooseCandidateReplica loop, while
preserving the existing RemoveInstance behavior and use of replicas for the
warning context.

In `@tests/functional/setup-replication.sh`:
- Around line 42-55: Update both STATUS assignments in the MariaDB and
non-MariaDB branches of the replication retry loop to tolerate a non-zero
command-substitution pipeline by appending an unconditional success fallback,
allowing the loop to continue retrying while the replica starts.

In `@tests/functional/test-mariadb-relay-drain.sh`:
- Line 5: Guard the initial directory change in the test script by exiting
immediately if `cd "$(dirname "$0")/../.."` fails, before sourcing
`tests/functional/lib.sh`.
- Around line 73-79: Update the replica shutdown cleanup around RESET_REPLICA so
STOP REPLICA and STOP SLAVE execute as separate best-effort mysql statements,
allowing the supported command to run even when the other is unavailable; keep
the existing error suppression and subsequent reset/start sequence unchanged.

---

Outside diff comments:
In `@tests/functional/lib.sh`:
- Around line 208-216: Update mysql_source_host in
tests/functional/lib.sh:208-216 to use the ${COMPOSE:-docker compose -f
tests/functional/docker-compose.yml} fallback for both compose commands. In
.github/workflows/functional.yml:268-272, add COMPOSE with both the base and
MariaDB compose files to the smoke-test and relay-drain steps, matching Setup
replication.
- Around line 159-166: Update mysql_change_source_channel_sql to explicitly
handle MariaDB, using MariaDB named-connection syntax with the channel name
instead of MySQL’s FOR CHANNEL form; alternatively, if support is intentionally
deferred, add a comment clearly documenting that this helper is MySQL-only.

---

Nitpick comments:
In @.github/workflows/functional.yml:
- Around line 183-194: Update the functional-mariadb job with a least-privilege
permissions block granting only contents: read, and set persist-credentials:
false on its actions/checkout@v4 step; leave the remaining workflow jobs
unchanged.

In `@go/inst/analysis_dao.go`:
- Around line 565-570: Replace the duplicated classification logic in the
Analysis branch for a.IsMaster, !a.LastCheckValid, and
a.CountValidIORunningReplicas > 0 with the tested
classifyUnreachableMasterWhenIORunning helper. Preserve the existing
UnreachableMaster result and description behavior while ensuring production uses
the helper covered by the unit tests.

In `@go/inst/instance_topology_dao.go`:
- Around line 286-324: Extract the duplicated MaxScale-tolerant STOP SLAVE logic
from the current flow and StopReplicationNicely into a shared helper. The helper
should execute the instance’s stop_slave command, clear the error only for
MaxScale error 1199 (“Slave connection is not running”), and return all other
errors unchanged; replace each existing inline block with calls to this helper.

In `@tests/functional/lib.sh`:
- Around line 208-216: Update mysql_source_host to invoke Docker Compose through
the shared COMPOSE_CMD fallback, matching the version helper behavior, in both
the MySQL 5.7/MariaDB and newer replica-status branches. Preserve the existing
compose file, container, credentials, and query logic while allowing ${COMPOSE}
overrides to apply.

In `@tests/functional/test-mariadb-relay-drain.sh`:
- Line 21: Remove the unused STOP_REPLICA assignment in the MariaDB relay drain
test, or reuse STOP_REPLICA at the existing raw stop-command location so the
helper’s SQL is actually executed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b1c0877e-fbd4-4fa1-81cb-41dde95e7319

📥 Commits

Reviewing files that changed from the base of the PR and between b1ac4a2 and a4f7386.

📒 Files selected for processing (19)
  • .github/workflows/functional.yml
  • docs/failure-detection.md
  • docs/superpowers/plans/2026-07-16-mariadb-deadmaster-relay-drain.md
  • docs/superpowers/specs/2026-07-16-mariadb-deadmaster-relay-drain-design.md
  • docs/user-manual.md
  • go/inst/analysis.go
  • go/inst/analysis_classification.go
  • go/inst/analysis_classification_test.go
  • go/inst/analysis_dao.go
  • go/inst/instance_topology.go
  • go/inst/instance_topology_dao.go
  • go/logic/topology_recovery.go
  • tests/functional/docker-compose.mariadb.yml
  • tests/functional/lib.sh
  • tests/functional/mysql/mariadb-master.cnf
  • tests/functional/mysql/mariadb-replica.cnf
  • tests/functional/mysql/mariadb-replica2.cnf
  • tests/functional/setup-replication.sh
  • tests/functional/test-mariadb-relay-drain.sh


---

### File map

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Restore heading hierarchy.

### File map follows the H1 directly, triggering markdownlint MD001.

Proposed fix
-### File map
+## File map
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### File map
## File map
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 15-15: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-16-mariadb-deadmaster-relay-drain.md` at line
15, Restore the heading hierarchy in the document around the “File map” section
by adding the required intermediate heading level between the document’s H1 and
the existing “### File map” heading, preserving the section’s content and
wording.

Source: Linters/SAST tools

Comment thread go/inst/instance_topology_dao.go
Comment thread go/inst/instance_topology.go
Comment on lines +2314 to +2333
remaining := replicas
for len(remaining) > 0 {
candidateReplica, aheadReplicas, equalReplicas, laterReplicas, cannotReplicateReplicas, err = chooseCandidateReplica(remaining)
if err != nil || candidateReplica == nil {
return candidateReplica, aheadReplicas, equalReplicas, laterReplicas, cannotReplicateReplicas, err
}
if !forRematchPurposes {
break
}
// Skip candidates that cannot apply remaining relay logs (e.g. SQL error 1062).
drained, drainErr := ensureReplicaRelayDrained(candidateReplica, drainTimeout)
if drainErr != nil {
_ = log.Errorf("GetCandidateReplica: skipping %+v; cannot drain relay logs: %+v", candidateReplica.Key, drainErr)
remaining = RemoveInstance(remaining, &candidateReplica.Key)
candidateReplica = nil
continue
}
candidateReplica = drained
// Recompute sibling buckets against the drained candidate coordinates.
remaining = RemoveInstance(remaining, &candidateReplica.Key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep run --pattern $'func RemoveInstance($$$) $$$ {
  $$$
}' --lang go go/inst

Repository: ProxySQL/orchestrator

Length of output: 795


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- candidate replication snippet ---\n'
sed -n '2308,2366p' go/inst/instance_topology.go | nl -ba -v2308

printf '\n--- RemoveInstance implementation ---\n'
sed -n '168,184p' go/inst/instance_utils.go | nl -ba -v168

printf '\n--- relevant warnings/logs using replicas[0] ---\n'
sed -n '2350,2362p' go/inst/instance_topology.go | nl -ba -v2350

printf '\n--- Go slice mutation probe ---\n'
cat >/tmp/slice_alias_probe.go <<'EOF'
package main

import "fmt"

type Element struct{ ID int }

func RemoveInstance(in []*Element, id int) []*Element {
	for i := len(in) - 1; i >= 0; i-- {
		if in[i].ID == id {
			in = append(in[:i], in[i+1:]...)
		}
	}
	return in
}

func main() {
	replicas := []*Element{{1}, {2}, {3}}
	remaining := replicas
	remaining = RemoveInstance(remaining, 2)
	fmt.Printf("replicas len=%d, first=%d, last=%d\n", len(replicas), replicas[0].ID, replicas[len(replicas)-1].ID)
	fmt.Printf("remaining references same backing array? %t\n", &replicas[0] == &remaining[0])
}
EOF
go run /tmp/slice_alias_probe.go
rm /tmp/slice_alias_probe.go

Repository: ProxySQL/orchestrator

Length of output: 250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate replication snippet ---'
sed -n '2308,2366p' go/inst/instance_topology.go | nl -ba -v2308

printf '%s\n' '--- RemoveInstance implementation ---'
sed -n '168,184p' go/inst/instance_utils.go | nl -ba -v168

printf '%s\n' '--- relevant warnings/logs using replicas[0] ---'
sed -n '2350,2362p' go/inst/instance_topology.go | nl -ba -v2350

printf '%s\n' '--- Go slice mutation probe ---'
cat >/tmp/slice_alias_probe.go <<'EOF'
package main

import "fmt"

type Element struct{ ID int }

func RemoveInstance(in []*Element, id int) []*Element {
	for i := len(in) - 1; i >= 0; i-- {
		if in[i].ID == id {
			in = append(in[:i], in[i+1:]...)
		}
	}
	return in
}

func main() {
	replicas := []*Element{{1}, {2}, {3}}
	remaining := replicas
	remaining = RemoveInstance(remaining, 2)
	fmt.Printf("replicas len=%d, first=%d, last=%d\n", len(replicas), replicas[0].ID, replicas[len(replicas)-1].ID)
	fmt.Printf("remaining references same backing array? %t\n", &replicas[0] == &remaining[0])
}
EOF
go run /tmp/slice_alias_probe.go
rm /tmp/slice_alias_probe.go

Repository: ProxySQL/orchestrator

Length of output: 237


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- candidate replication snippet ---'
sed -n '2308,2366p' go/inst/instance_topology.go

echo '--- RemoveInstance implementation ---'
sed -n '168,184p' go/inst/instance_utils.go

echo '--- relevant warnings/logs using replicas[0] ---'
sed -n '2350,2362p' go/inst/instance_topology.go

echo '--- Go slice mutation probe ---'
cat >/tmp/slice_alias_probe.go <<'EOF'
package main

import "fmt"

type Element struct{ ID int }

func RemoveInstance(in []*Element, id int) []*Element {
	for i := len(in) - 1; i >= 0; i-- {
		if in[i].ID == id {
			in = append(in[:i], in[i+1:]...)
		}
	}
	return in
}

func main() {
	replicas := []*Element{{1}, {2}, {3}}
	remaining := replicas
	remaining = RemoveInstance(remaining, 2)
	fmt.Printf("replicas len=%d, first=%d, last=%d\n", len(replicas), replicas[0].ID, replicas[len(replicas)-1].ID)
	fmt.Printf("remaining references same backing array? %t\n", &replicas[0] == &remaining[0])
}
EOF
go run /tmp/slice_alias_probe.go
rm /tmp/slice_alias_probe.go

Repository: ProxySQL/orchestrator

Length of output: 4456


Don’t alias replicas through remaining.

remaining := replicas shares the backing array, and RemoveInstance compacts its slice in-place. If any candidate is skipped before success, replicas[0] used for the warning can point at the wrong/corrupted tail entry. Copy remaining into a separate slice before removing candidates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@go/inst/instance_topology.go` around lines 2314 - 2333, Copy the replicas
slice when initializing remaining so candidate removal cannot mutate replicas
through shared backing storage. Update the remaining initialization in the
chooseCandidateReplica loop, while preserving the existing RemoveInstance
behavior and use of replicas for the warning context.

Comment on lines +42 to +55
if echo "$MYSQL_FULL_VERSION" | grep -qi mariadb; then
STATUS=$($COMPOSE exec -T "$REPLICA" mysql -uroot -ptestpass -e \
"SHOW SLAVE STATUS\G" 2>/dev/null | awk '/Slave_IO_Running:/{print $2; exit}' | tr -d '[:space:]')
if [ "$STATUS" = "Yes" ]; then
echo "$REPLICA: replication OK (IO thread Yes)"
break
fi
else
STATUS=$($COMPOSE exec -T "$REPLICA" mysql -uroot -ptestpass -Nse \
"SELECT SERVICE_STATE FROM performance_schema.replication_connection_status" 2>/dev/null | tr -d '[:space:]')
if [ "$STATUS" = "ON" ]; then
echo "$REPLICA: replication OK (IO thread ON)"
break
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Retry loop can abort under set -e when the replica isn't ready yet.

STATUS=$(...) is a simple assignment, so a non-zero exit from the mysql pipeline (replica still starting, pipefail in effect) terminates the script instead of retrying. Append || true to both substitutions.

🛡️ Suggested fix
-            STATUS=$($COMPOSE exec -T "$REPLICA" mysql -uroot -ptestpass -e \
-                "SHOW SLAVE STATUS\G" 2>/dev/null | awk '/Slave_IO_Running:/{print $2; exit}' | tr -d '[:space:]')
+            STATUS=$($COMPOSE exec -T "$REPLICA" mysql -uroot -ptestpass -e \
+                "SHOW SLAVE STATUS\G" 2>/dev/null | awk '/Slave_IO_Running:/{print $2; exit}' | tr -d '[:space:]' || true)
@@
-            STATUS=$($COMPOSE exec -T "$REPLICA" mysql -uroot -ptestpass -Nse \
-                "SELECT SERVICE_STATE FROM performance_schema.replication_connection_status" 2>/dev/null | tr -d '[:space:]')
+            STATUS=$($COMPOSE exec -T "$REPLICA" mysql -uroot -ptestpass -Nse \
+                "SELECT SERVICE_STATE FROM performance_schema.replication_connection_status" 2>/dev/null | tr -d '[:space:]' || true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if echo "$MYSQL_FULL_VERSION" | grep -qi mariadb; then
STATUS=$($COMPOSE exec -T "$REPLICA" mysql -uroot -ptestpass -e \
"SHOW SLAVE STATUS\G" 2>/dev/null | awk '/Slave_IO_Running:/{print $2; exit}' | tr -d '[:space:]')
if [ "$STATUS" = "Yes" ]; then
echo "$REPLICA: replication OK (IO thread Yes)"
break
fi
else
STATUS=$($COMPOSE exec -T "$REPLICA" mysql -uroot -ptestpass -Nse \
"SELECT SERVICE_STATE FROM performance_schema.replication_connection_status" 2>/dev/null | tr -d '[:space:]')
if [ "$STATUS" = "ON" ]; then
echo "$REPLICA: replication OK (IO thread ON)"
break
fi
if echo "$MYSQL_FULL_VERSION" | grep -qi mariadb; then
STATUS=$($COMPOSE exec -T "$REPLICA" mysql -uroot -ptestpass -e \
"SHOW SLAVE STATUS\G" 2>/dev/null | awk '/Slave_IO_Running:/{print $2; exit}' | tr -d '[:space:]' || true)
if [ "$STATUS" = "Yes" ]; then
echo "$REPLICA: replication OK (IO thread Yes)"
break
fi
else
STATUS=$($COMPOSE exec -T "$REPLICA" mysql -uroot -ptestpass -Nse \
"SELECT SERVICE_STATE FROM performance_schema.replication_connection_status" 2>/dev/null | tr -d '[:space:]' || true)
if [ "$STATUS" = "ON" ]; then
echo "$REPLICA: replication OK (IO thread ON)"
break
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/functional/setup-replication.sh` around lines 42 - 55, Update both
STATUS assignments in the MariaDB and non-MariaDB branches of the replication
retry loop to tolerate a non-zero command-substitution pipeline by appending an
unconditional success fallback, allowing the loop to continue retrying while the
replica starts.

# Issue #106: MariaDB / SQL-stopped relay drain and false DeadMaster avoidance.
# Works on MariaDB and MySQL topologies (SQL_THREAD stop + unapplied relay + failover).
set -uo pipefail
cd "$(dirname "$0")/../.."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the cd.

Without || exit, a failed cd leaves the script running from an arbitrary directory and the subsequent source tests/functional/lib.sh will pick up nothing (or the wrong file).

🛡️ Suggested fix
-cd "$(dirname "$0")/../.."
+cd "$(dirname "$0")/../.." || exit 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cd "$(dirname "$0")/../.."
cd "$(dirname "$0")/../.." || exit 1
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 5-5: Use 'cd ... || exit' or 'cd ... || return' in case cd fails.

(SC2164)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/functional/test-mariadb-relay-drain.sh` at line 5, Guard the initial
directory change in the test script by exiting immediately if `cd "$(dirname
"$0")/../.."` fails, before sourcing `tests/functional/lib.sh`.

Source: Linters/SAST tools

Comment thread tests/functional/test-mariadb-relay-drain.sh Outdated
renecannao and others added 6 commits July 27, 2026 19:07
Spec for issue #106: IO-running-aware analysis, MariaDB-safe relay drain,
skip undrainable candidates, validate before re-point, and functional CI.

Signed-off-by: Rene Cannao <rene@proxysql.com>
Treat valid replicas with a running IO thread as UnreachableMaster rather
than DeadMaster when the master check fails. Add MariaDB-safe DrainRelayLogs
(SQL thread only), skip undrainable promotion candidates, and validate the
candidate before re-pointing siblings. Include unit tests, MariaDB CI job,
and docs updates.

Signed-off-by: Rene Cannao <rene@proxysql.com>
Signed-off-by: Rene Cannao <rene@proxysql.com>
Always re-read/drain candidates instead of trusting stale SQLThreadUpToDate.
Lengthen drain wait, require up-to-date after drain, and fix functional test
recovery API parsing plus wait-for-relay readiness before failover.

Signed-off-by: Rene Cannao <rene@proxysql.com>
CI failures on #108 were test harness issues, not the product fix:

- Restore flat topology (mysql1 master) before the suite; earlier failover
  tests leave mysql2 promoted and break SHOW REPLICA STATUS assumptions
- Parse replica status with robust field splitters (MySQL 8/9 + MariaDB)
- Restart SQL_THREAD (not full START SLAVE) after the false-DeadMaster check
- Avoid deprecated VALUES() in ON DUPLICATE KEY UPDATE
- Honor COMPOSE env in lib.sh version detection for MariaDB overlays
- Soft-path: skip rebuild when replicas already IO/SQL=Yes (MariaDB job)
- Prefer CHANGE+START over RESET (RESET left SQL=No on MariaDB)
- Explicitly START SQL_THREAD when IO=Yes and SQL=No
- Run relay-drain before failover suites in MySQL CI matrix so topology
  is still the clean setup-replication layout
@renecannao
renecannao force-pushed the fix/issue-106-mariadb-deadmaster-relay-drain branch from a4f7386 to ab1e44e Compare July 27, 2026 17:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/functional.yml:
- Around line 184-195: Add a job-level permissions block to functional-mariadb
granting only contents: read and actions: read, supporting its checkout and
artifact download steps while preventing inherited broader token permissions.

In `@tests/functional/test-mariadb-relay-drain.sh`:
- Around line 172-195: The Test 1 scenario must exercise an unreachable master
instead of leaving mysql1 reachable. After stopping the SQL thread on mysql2,
verify the replica remains IO=Yes/SQL=No, isolate the orchestrator from mysql1
while preserving that replica state, then query replication analysis and assert
mysql1’s entry is UnreachableMaster; restore connectivity afterward.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0695bff-945d-4cd7-ad6e-983f4eb9a20a

📥 Commits

Reviewing files that changed from the base of the PR and between a4f7386 and ab1e44e.

📒 Files selected for processing (19)
  • .github/workflows/functional.yml
  • docs/failure-detection.md
  • docs/superpowers/plans/2026-07-16-mariadb-deadmaster-relay-drain.md
  • docs/superpowers/specs/2026-07-16-mariadb-deadmaster-relay-drain-design.md
  • docs/user-manual.md
  • go/inst/analysis.go
  • go/inst/analysis_classification.go
  • go/inst/analysis_classification_test.go
  • go/inst/analysis_dao.go
  • go/inst/instance_topology.go
  • go/inst/instance_topology_dao.go
  • go/logic/topology_recovery.go
  • tests/functional/docker-compose.mariadb.yml
  • tests/functional/lib.sh
  • tests/functional/mysql/mariadb-master.cnf
  • tests/functional/mysql/mariadb-replica.cnf
  • tests/functional/mysql/mariadb-replica2.cnf
  • tests/functional/setup-replication.sh
  • tests/functional/test-mariadb-relay-drain.sh
🚧 Files skipped from review as they are similar to previous changes (15)
  • tests/functional/mysql/mariadb-replica.cnf
  • go/inst/analysis_classification_test.go
  • tests/functional/mysql/mariadb-master.cnf
  • tests/functional/docker-compose.mariadb.yml
  • tests/functional/mysql/mariadb-replica2.cnf
  • tests/functional/setup-replication.sh
  • go/inst/analysis_classification.go
  • go/inst/analysis.go
  • docs/user-manual.md
  • docs/failure-detection.md
  • go/inst/analysis_dao.go
  • go/logic/topology_recovery.go
  • go/inst/instance_topology_dao.go
  • tests/functional/lib.sh
  • go/inst/instance_topology.go

Comment on lines +184 to +195
functional-mariadb:
runs-on: ubuntu-latest
timeout-minutes: 30
needs: build

strategy:
fail-fast: false
matrix:
mariadb_version: ['10.6', '10.11']

steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow files =="
git ls-files '.github/workflows/*' | sed -n '1,120p'

echo
echo "== functional.yml relevant context =="
if [ -f .github/workflows/functional.yml ]; then
  wc -l .github/workflows/functional.yml
  sed -n '150,240p' .github/workflows/functional.yml | cat -n
else
  echo "missing .github/workflows/functional.yml"
fi

echo
echo "== GITHUB_TOKEN permissions in functional.yml =="
python3 - <<'PY'
from pathlib import Path
p=Path('.github/workflows/functional.yml')
if not p.exists():
    print('missing')
    raise SystemExit
for i,line in enumerate(p.read_text().splitlines(),1):
    if 'functional-mariadb:' in line or 'permissions:' in line or 'GITHUB_TOKEN' in line:
        print(f"{i}:{line}")
PY

echo
echo "== job steps around functional-mariadb =="
python3 - <<'PY'
from pathlib import Path
lines = Path('.github/workflows/functional.yml').read_text().splitlines()
for start,name in enumerate(lines):
    if name.rstrip().startswith('functional-mariadb:'):
        for j in range(start, min(len(lines), start+50)):
            print(f"{j+1}: {lines[j]}")
        break
PY

Repository: ProxySQL/orchestrator

Length of output: 4148


🌐 Web query:

GitHub Actions default GITHUB_TOKEN permissions workflow job permissions contents read artifacts

💡 Result:

The GITHUB_TOKEN is an automatically generated, short-lived secret used to authenticate API requests within GitHub Actions workflows [1][2]. Its default permissions are determined by enterprise, organization, or repository-level settings [3][4]. Currently, many new repositories default to a restricted, read-only configuration, often providing read access only to contents and packages [3][1][4]. To configure permissions for specific workflows or jobs, you use the permissions key in your YAML file [1][5]. Key points regarding GITHUB_TOKEN permissions: 1. Granular Control: You can specify permissions at the top-level (workflow-wide) or within individual jobs [5][6]. When defined at the job level, the job-level configuration entirely replaces any workflow-level permissions; they do not merge [5][6]. 2. Default Behavior: If you specify any permission within the permissions block, all other permissions not explicitly listed are automatically set to none [1][5]. 3. Least Privilege: It is a security best practice to explicitly define only the minimum permissions required for each job [7][1][6]. 4. Pull Requests from Forks: For security reasons, workflows triggered by pull requests from public forks are always granted read-only access to the GITHUB_TOKEN, regardless of your repository's configured default settings [1]. Example of setting read-only contents and no other access: permissions: contents: read If you need specific artifacts-related permissions, ensure you include them explicitly in your job definition, such as: jobs: job_name: runs-on: ubuntu-latest permissions: contents: read actions: read # If needed for artifact access steps: -... Always verify your required scopes, as write access includes read access [1][5].

Citations:


Restrict the MariaDB job token permissions.

functional-mariadb does not set a job-level permissions block, so it can inherit repository-default token permissions instead of least privilege. Scope the job to the read-only permissions it needs for checkout and artifacts; include both contents: read and actions: read because the job uses actions/checkout and actions/download-artifact.

Proposed fix
   functional-mariadb:
+    permissions:
+      contents: read
+      actions: read
     runs-on: ubuntu-latest
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
functional-mariadb:
runs-on: ubuntu-latest
timeout-minutes: 30
needs: build
strategy:
fail-fast: false
matrix:
mariadb_version: ['10.6', '10.11']
steps:
- uses: actions/checkout@v4
functional-mariadb:
permissions:
contents: read
actions: read
runs-on: ubuntu-latest
timeout-minutes: 30
needs: build
strategy:
fail-fast: false
matrix:
mariadb_version: ['10.6', '10.11']
steps:
- uses: actions/checkout@v4
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 195-195: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[warning] 184-295: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block

(excessive-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/functional.yml around lines 184 - 195, Add a job-level
permissions block to functional-mariadb granting only contents: read and
actions: read, supporting its checkout and artifact download steps while
preventing inherited broader token permissions.

Source: Linters/SAST tools

Comment on lines +172 to +195
echo "--- Test 1: SQL stopped + IO running is not DeadMaster while master is up ---"

$COMPOSE exec -T mysql2 mysql -uroot -ptestpass -e "$STOP_SQL_THREAD" 2>/dev/null
curl -s --max-time 10 "$ORC_URL/api/discover/mysql2/3306" >/dev/null 2>&1
sleep 3

ANALYSIS=$(curl -s --max-time 10 "$ORC_URL/api/replication-analysis" 2>/dev/null || echo "[]")
if echo "$ANALYSIS" | python3 -c "
import json, sys
data = json.load(sys.stdin)
if isinstance(data, dict):
data = data.get('Details') or data.get('data') or []
if not isinstance(data, list):
sys.exit(0)
for a in data:
code = a.get('AnalysisCode') or a.get('Analysis') or ''
if code == 'DeadMaster':
sys.exit(1)
sys.exit(0)
" 2>/dev/null; then
pass "No DeadMaster while master is reachable (SQL stopped on replica)"
else
fail "Unexpected DeadMaster while master is still up"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Exercise an unreachable-master condition here.

Keeping mysql1 reachable means this never reaches the old false-DeadMaster branch, so it passes on the pre-fix implementation too. Isolate orchestrator from mysql1 while preserving replica IO=Yes/SQL=No, then assert the analysis entry for mysql1 is UnreachableMaster (and verify the stopped-thread precondition first).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/functional/test-mariadb-relay-drain.sh` around lines 172 - 195, The
Test 1 scenario must exercise an unreachable master instead of leaving mysql1
reachable. After stopping the SQL thread on mysql2, verify the replica remains
IO=Yes/SQL=No, isolate the orchestrator from mysql1 while preserving that
replica state, then query replication analysis and assert mysql1’s entry is
UnreachableMaster; restore connectivity afterward.

Root causes of remaining CI red:
- MariaDB job did not export MYSQL_IMAGE/COMPOSE into the relay-drain
  step, so the suite mis-detected MySQL and issued START REPLICA / CHANGE
  REPLICATION SOURCE (leaving SQL=No).
- Running relay-drain early left mysql1 stopped and broke later topology
  tests even when the drain assertions themselves passed.

Export MYSQL_IMAGE+COMPOSE for MariaDB steps, detect mariadb via image
name, and keep the suite last in the MySQL matrix.
MariaDB relay-drain ensure_flat failed with errno 1950 (gtid strict
out-of-order) after prior local GTIDs on replicas. Disable strict mode
in test cnf, RESET + clear gtid_slave_pos on repoint, and require both
IO and SQL running in setup-replication.
…rain

MySQL 9.5/9.6 left SHOW REPLICA STATUS empty after ad-hoc RESET+CHANGE
following failover suites. Reuse setup-replication.sh (with super_read_only
cleared and STOP before CHANGE) when ensure_flat needs a rebuild.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/functional/test-mariadb-relay-drain.sh`:
- Around line 348-350: Replace the mysql1-only cleanup in the test’s
post-failover cleanup flow with a complete topology reset: either restore
verified flat replication and ProxySQL discovery/routing for every node, or tear
down and recreate the Compose project before summary generation. Ensure this
cleanup runs on mid-suite and CI failure paths so no promoted replica or stale
routing state remains for later tests.
- Line 83: Update the read-only reset command in the MariaDB relay-drain test to
avoid setting the unsupported super_read_only variable. Execute the
MariaDB-compatible read_only=0 reset before repointing, ensuring the reset
succeeds instead of being masked by || true.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e02d0ef-986d-414b-94dc-5ccaacf96245

📥 Commits

Reviewing files that changed from the base of the PR and between ab1e44e and 0e3b596.

📒 Files selected for processing (6)
  • .github/workflows/functional.yml
  • tests/functional/mysql/mariadb-master.cnf
  • tests/functional/mysql/mariadb-replica.cnf
  • tests/functional/mysql/mariadb-replica2.cnf
  • tests/functional/setup-replication.sh
  • tests/functional/test-mariadb-relay-drain.sh
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/functional/mysql/mariadb-replica2.cnf
  • tests/functional/mysql/mariadb-replica.cnf
  • tests/functional/mysql/mariadb-master.cnf

local r="$1"
local out
# Clear read-only so reconfiguration is allowed after a prior promotion.
$COMPOSE exec -T "$r" mysql -uroot -ptestpass -e "SET GLOBAL super_read_only=0; SET GLOBAL read_only=0;" 2>/dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate file and relevant section =="
if [ -f tests/functional/test-mariadb-relay-drain.sh ]; then
  wc -l tests/functional/test-mariadb-relay-relay-drain.sh 2>/dev/null || true
  sed -n '1,140p' tests/functional/test-mariadb-relay-drain.sh
else
  echo "file not found"
  fd -a 'test-mariadb-relay-drain.sh' .
fi

echo
echo "== Search super_read_only/read_only usages in tests/context =="
rg -n --hidden --glob '!*.lock' 'super_read_only|read_only|repoint_replica_to_mysql1' . || true

echo
echo "== Docker/compose hints around maria image =="
fd -a 'compose|docker-compose|Dockerfile|mariadb' . | head -100 | sed -n '1,160p'
rg -n --hidden --glob '!*.lock' 'mariadb|server_version|read_only|super_read_only' docker-compose* compose* . -g '!vendor' -g '!node_modules' | head -200 || true

Repository: ProxySQL/orchestrator

Length of output: 50378


🌐 Web query:

MariaDB super_read_only system variable MySQL MariaDB compatibility differences

💡 Result:

The super_read_only system variable is a feature specific to MySQL and Percona Server, and it is not available in MariaDB [1][2][3]. In MySQL, super_read_only extends the functionality of the standard read_only variable by prohibiting updates even from users who possess the SUPER privilege [2][4]. Because this variable does not exist in MariaDB, it is frequently listed in MariaDB documentation as a system variable difference between MariaDB and MySQL [1][3][5][6]. While feature requests to implement super_read_only in MariaDB have been submitted, it remains unimplemented as a system variable [2][7]. MariaDB users seeking similar functionality typically rely on the standard read_only variable or external access control and security configurations to restrict user write permissions [4].

Citations:


🌐 Web query:

MariaDB mysql client execute multiple statements -e error second statement executed after first error

💡 Result:

In the MariaDB and MySQL command-line clients, the behavior regarding errors when executing multiple statements depends on the execution mode and the options provided [1]. By default, if a statement fails during a batch execution, the client stops processing subsequent statements [2][3]. To control this behavior, consider the following points: 1. Use the --force option: The most direct way to ensure the client continues execution despite SQL errors is to use the --force command-line option [3][4][5]. This instructs the client to proceed with subsequent statements even if a previous one encounters an error. 2. C API and Programmatic Execution: If you are executing multiple statements programmatically (e.g., using the C API with CLIENT_MULTI_STATEMENTS), the application is responsible for iterating through the result sets [6][7][8]. The functions mysql_query or mysql_real_query only return the status of the first statement [2]. To detect errors in subsequent statements, you must loop through all results using mysql_next_result and check for errors after each call [2][8]. Simply checking the return value of the initial execution function is insufficient to guarantee that every statement in a multi-statement string succeeded [2]. 3. SQL-Level Handling: To prevent errors from occurring in the first place, you can use defensive SQL patterns such as DROP TABLE IF EXISTS or DROP USER IF EXISTS [3]. These statements will trigger a warning instead of a hard error if the object does not exist, allowing the script to continue naturally without requiring the --force flag [3]. In summary, for command-line scripts, --force is the standard solution to avoid stopping on errors [3]. For developers writing custom applications, you must implement a result-processing loop that inspects the return code of mysql_next_result for every statement in the batch [6][7][8].

Citations:


Use a MariaDB-compatible read-only reset.

super_read_only is MariaDB-unsupported, and the multi-statement -e execution stops on that error before read_only=0 runs; || true hides the failure, so the replica can remain read-only and repointing can fail. Split the reset or use only read_only=0 for MariaDB before re-pointing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/functional/test-mariadb-relay-drain.sh` at line 83, Update the
read-only reset command in the MariaDB relay-drain test to avoid setting the
unsupported super_read_only variable. Execute the MariaDB-compatible read_only=0
reset before repointing, ensuring the reset succeeds instead of being masked by
|| true.

Source: MCP tools

Comment on lines +348 to +350
# Best-effort restore so a mid-suite run does not poison later tests.
echo "--- Cleanup: restart mysql1 (best-effort) ---"
$COMPOSE start mysql1 >/dev/null 2>&1 || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file size =="
wc -l tests/functional/test-mariadb-relay-drain.sh

echo "== relevant sections =="
sed -n '1,120p' tests/functional/test-mariadb-relay-drain.sh | cat -n
echo "---"
sed -n '300,410p' tests/functional/test-mariadb-relay-drain.sh | cat -n

echo "== compose teardown / restart / summary usages =="
rg -n "\\b(start|stop|down|rm|restart|summary|COMPOSE_PROJECT_NAME|mysql)[A-Za-z0-9_\\-]*" tests/functional/test-mariadb-relay-drain.sh

Repository: ProxySQL/orchestrator

Length of output: 15235


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== functional lib summary and teardown helpers =="
sed -n '1,260p' tests/functional/lib.sh | cat -n

echo "== compose files =="
for f in tests/functional/docker-compose.yml tests/functional/docker-compose.mariadb.yml; do
  [ -f "$f" ] || continue
  echo "--- $f ---"
  sed -n '1,220p' "$f" | cat -n
done

echo "== summary usages in functional tests =="
rg -n '^[[:space:]]*(summary|ensure_flat_topology|teardown|tearDown|clean|down|rm)' tests/functional/lib.sh tests/functional -S || true

Repository: ProxySQL/orchestrator

Length of output: 19635


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== run-all functional execution =="
sed -n '1,130p' tests/functional/run-all.sh | cat -n

echo "== test names in functional subdir =="
git ls-files tests/functional | sed -n '1,160p'

echo "== orchestrator failover config snippets =="
for f in tests/functional/orchestrator-test.conf.json tests/functional/orchestrator-pg-test.conf.json; do
  [ -f "$f" ] || continue
  echo "--- $f ---"
  rg -n 'FailoverCommand|DeadMaster|mysql1|mysql2|mysql3|Command|Replace' "$f" || true
done

Repository: ProxySQL/orchestrator

Length of output: 5303


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== test-mariadb-relay-drain failover cleanup with context =="
sed -n '186,200p' tests/functional/test-mariadb-relay-drain.sh | cat -n
sed -n '280,352p' tests/functional/...-relay-drain.sh | cat -n 2>/dev/null || sed -n '280,352p' tests/functional/test-mariadb-relay-drain.sh | cat -n

echo "== other failover test teardown/cleanup patterns =="
for f in tests/functional/test-failover.sh tests/functional/test-failover-advanced.sh; do
  [ -f "$f" ] || continue
  echo "--- $f last 80 lines ---"
  sed -n '$(( $(wc -l < "$f") - 79 )),$ p' "$f" | cat -n
done

echo "== inspect failed if path to ensure_flat_topology call before failover =="
sed -n '170,190p' tests/functional/test-mariadb-relay-drain.sh | cat -n

Repository: ProxySQL/orchestrator

Length of output: 4232


Restore the full topology or tear it down after failover.

$COMPOSE start mysql1 only restarts the stopped mysql1 container; it leaves the promoted replica, sibling replication, and ProxySQL discovery/routing in the post-recovery state. Restore all paths to a verified flat topology, or tear down/recreate the Compose project before summary, so mid-suite or CI failure paths cannot pollute later runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/functional/test-mariadb-relay-drain.sh` around lines 348 - 350, Replace
the mysql1-only cleanup in the test’s post-failover cleanup flow with a complete
topology reset: either restore verified flat replication and ProxySQL
discovery/routing for every node, or tear down and recreate the Compose project
before summary generation. Ensure this cleanup runs on mid-suite and CI failure
paths so no promoted replica or stale routing state remains for later tests.

On MySQL 9.5/9.6, setup-replication reported IO ON via performance_schema
while replica_status_fields used SHOW REPLICA STATUS and got empty output
(status=|||), failing ensure_flat after a successful rebuild.

Read IO/SQL from performance_schema for MySQL 8+, keep SHOW SLAVE for
MariaDB/5.7, and relax the relay pre-check when positions are unavailable.
@renecannao
renecannao merged commit 7a92fae into master Jul 27, 2026
19 checks passed
@renecannao
renecannao deleted the fix/issue-106-mariadb-deadmaster-relay-drain branch July 27, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DeadMaster false detection and data loss on MariaDB when SQL thread is stopped

1 participant