Query icon
Get icon
OutputValues icon
Log icon
Switch icon
Delete icon
SlackIncomingWebhook icon
Set icon
If icon
Pause icon
Sleep icon
Fail icon
Schedule icon

Detect a desynced MariaDB Galera node and clear wsrep_desync behind an approval

Detect a Galera node stuck at wsrep_desync ON, clear it behind an approval, re-read the wsrep state and fail the run if it is not Synced.

Categories
DataInfrastructure

Findings

  • A node left at wsrep_desync=ON answers connections, answers SELECT 1 and passes most health checks. It is not in the cluster's flow control. The cluster can therefore outrun it without ever slowing down.
  • SHOW GLOBAL STATUS LIKE 'wsrep_desync' returns nothing. There is no such status variable. The flag is the system variable @@global.wsrep_desync. The status side of it is the counter WSREP_DESYNC_COUNT.
  • wsrep_local_state_comment reads Donor/Desynced for an operator desync as well as for a node donating a state transfer. The state comment on its own cannot tell the two apart, so a guard that keys on it alone will clear the flag on a live donor.
  • WSREP_DESYNC_COUNT is how many desync holders are outstanding. An operator desync is count 1 with the variable at 1. Galera takes its own desync for the length of a transfer it donates, without touching the variable, so a donation puts the count above the variable. MariaDB's own SST scripts never set the variable, which is what makes the comparison sound.
  • A node that already has wsrep_desync=ON is not eligible to donate. Galera logs No donor candidates temporarily available in suitable state and the joiner keeps asking. So the count can only exceed the variable through a desync Galera took itself, which is what the refusal keys on.
  • A node in a non-primary component cannot be desynced at all. SET GLOBAL wsrep_desync=ON there fails with ERROR 1396 Operation 'desync' failed, so the quorum refusal is not caution, it is the only correct answer.

Safety

  • Read-only unless auto_remediate is true, dry_run is false and a human answers the approve_resync Pause with true. An unanswered Pause reads as false.
  • The flow refuses to write while a state transfer is in progress, even when the operator flag is set. That check runs before the desync bucket, so a node Galera is using never reaches the arm that writes.
  • It also refuses when the node is not in a primary component. Clearing desync cannot rejoin a node that has no quorum.
  • The write touches one node and one variable. It never changes cluster membership, never restarts anything, never touches data.
  • SET GLOBAL wsrep_desync = OFF runs with allowFailure. That is on purpose. A refused write reports success on plenty of systems, so the re-read is the authority. Letting the write end the run would skip the only step that knows the truth.

How it works

  1. read_wsrep reads the node's whole wsrep picture in one query through io.kestra.plugin.jdbc.mariadb.Query: @@global.wsrep_on, @@global.wsrep_desync, then WSREP_DESYNC_COUNT, WSREP_LOCAL_STATE, WSREP_LOCAL_STATE_COMMENT, WSREP_CLUSTER_STATUS, WSREP_CLUSTER_SIZE plus WSREP_READY from information_schema.GLOBAL_STATUS. It also returns the node's own UNIX_TIMESTAMP(), so the grace window is measured on the database clock.
  2. read_cursor reads the KV marker holding the epoch second this node was first seen out of sync. Missing means this run is the first sighting.
  3. classify puts the node in one bucket, in a fixed order: wsrep_off, transfer, nonprimary, desynced, synced. It also works out the desync age in seconds.
  4. route is a Switch over that bucket. synced deletes the marker then reports no findings. transfer logs the refusal with the numbers behind it then alerts Slack. nonprimary does the same for a node with no quorum. wsrep_off reports a standalone server.
  5. The desynced arm records the first-seen epoch, keeping any earlier value, then checks the age against grace_minutes. Inside the window it says so plainly. Past it, finding logs the statement that would fix the node, alert_finding posts to Slack.
  6. remediate_gate needs auto_remediate true with dry_run false. Then approve_resync pauses for a human.
  7. On approval clear_desync runs SET GLOBAL wsrep_desync = OFF, settle waits resync_settle, verify_resync reads the wsrep state again.
  8. assert_resync passes only when wsrep_desync is 0, wsrep_local_state is 4, the state comment is Synced and wsrep_ready is ON. On success it drops the marker and logs the before and after numbers side by side. Otherwise resync_failed fails the run with both readings in the message.

What you get

  • A detector that catches the failure port checks miss, with the before and after wsrep numbers in the log.
  • A refusal that protects a live state transfer, built on a counter comparison rather than on a state string that cannot tell donation from an operator flag.
  • A grace window on a KV marker with no TTL, so a short desync during normal operations never pages anyone.
  • A write that only ever happens behind two inputs plus a human answer.
  • A re-read that fails the run when the node did not come back, which is the difference between a guard that works and a guard that reports success.
  • Slack on detection as well as on failure, so the refusals are visible too.

Who it's for

  • Teams running MariaDB Galera who have been bitten by a node that looked up while the cluster was already down to one writable copy.
  • Platform engineers who take backups with mariabackup against a desynced node, where the flag gets left behind when a backup dies.
  • Anyone who needs the resync to be an approved action with an audit trail rather than an ad hoc SET GLOBAL from a shell.

Why orchestrate this with Kestra

  • The grace window needs state between runs. Kestra KV holds the first-seen epoch without a sidecar database.
  • The refusal, the approval gate, the write, the settle then the re-read are five different decisions in one place, with the Switch and the If tasks making the routing readable.
  • Pause gives a real human gate with the numbers already in the log, so whoever approves can see what state the node is in.
  • A Schedule trigger in safe mode turns the detector into a standing check. The remediation stays a decision someone makes.
  • Failures land in Slack through the errors block rather than in a cron mailbox nobody reads.

Tested end to end

Run against MariaDB 11.8.9 with Galera 26.4.27, a single bootstrapped node on Kestra 2.0.5. Every state was read back with the mariadb client, not from the Kestra log.

  • Synced node, wsrep_desync 0, WSREP_DESYNC_COUNT 0, wsrep_local_state 4. Bucket synced, no findings.
  • SET GLOBAL wsrep_desync=ON applied by hand. Client then read wsrep_desync 1, WSREP_DESYNC_COUNT 1, wsrep_local_state 2, state comment Donor/Desynced. Bucket desynced, detection fired past the grace window.
  • dry_run=true reported the statement it would run. Client confirmed wsrep_desync still 1.
  • auto_remediate=true with dry_run=false plus an approved Pause cleared the flag. Client read wsrep_desync 0, WSREP_DESYNC_COUNT 0, wsrep_local_state 4, Synced.
  • A declined Pause left wsrep_desync at 1.
  • A real second node joining the cluster made the first node a donor. While the transfer ran the client read wsrep_desync 0 with WSREP_DESYNC_COUNT 1, wsrep_local_state 2 and cluster size 2, so the count sat above the variable with the same state comment an operator desync produces. Bucket transfer, the write refused with auto_remediate true and dry_run false.
  • Killing the joiner left the first node in a non-primary component, wsrep_ready OFF, wsrep_local_state 0. Bucket nonprimary, the write refused.
  • A user with SELECT but no SUPER reproduced the refused write. The SET GLOBAL task went green, the re-read still saw wsrep_desync 1 and resync_failed failed the run, which also fired the errors Slack block.

Prerequisites

  • MariaDB 10.4 or later with Galera, reachable over JDBC.
  • Kestra 2.0 or later with the plugin-jdbc-mariadb plugin.

Secrets

  • MARIADB_PASSWORD: password for the MariaDB user. The password input defaults to it.
  • SLACK_WEBHOOK_URL: used by the three alert tasks plus the errors block.
  • In Kestra OSS, provide each one as a base64-encoded environment variable prefixed SECRET_.

Inputs

Input Default Purpose
jdbc_url jdbc:mariadb://mariadb-galera-node:3306/mysql One Galera node, not a load balancer
username root MariaDB user
password {{ secret('MARIADB_PASSWORD') }} Password for that user
grace_minutes 10 How long a desync may last before it is a finding
resync_settle PT10S Wait between the write then the re-read
auto_remediate false Must be true before anything is written
dry_run true Must be false before anything is written

Quick start

  1. Add the MARIADB_PASSWORD plus SLACK_WEBHOOK_URL secrets.
  2. Import the flow and point jdbc_url at one Galera node.
  3. Run it with the defaults. Read the observed log line for the node's wsrep numbers.
  4. Set grace_minutes to how long your cluster normally takes to finish a transfer.
  5. When a node is genuinely stuck, run with auto_remediate: true and dry_run: false, then answer the Pause.
  6. Enable the every_fifteen_minutes trigger, which always runs in safe mode.

How to extend

  • Point one execution per node at the cluster by running the flow from a parent flow with a Subflow task per jdbc_url, so each node keeps its own KV marker.
  • Add WSREP_FLOW_CONTROL_PAUSED to read_wsrep to catch a node that is Synced yet pausing the cluster, which is the opposite failure.
  • Swap the Slack tasks for io.kestra.plugin.pagerduty or io.kestra.plugin.notifications.opsgenie where a stuck node is pageable.
  • Raise resync_settle on a busy cluster where the catch up after a desync takes minutes.
  • Replace the Pause with a LoopUntil on wsrep_local_state where you want the resync unattended on a node you know has no joiner.

Links

This blueprint was created by zkasuran.

See How

New to Kestra?

Use blueprints to kickstart your first workflows.