Schedule icon
Query icon
OutputValues icon
Set icon
Log icon
If icon
SlackIncomingWebhook icon
Pause icon
Loop icon
Fail icon

MySQL Orphaned XA Transaction Rollback Guard

Detect MySQL XA branches stuck in the prepared state, roll back only aged orphans behind a dry-run and approval gate, then re-read XA RECOVER to prove it.

Categories
DataInfrastructure

A distributed transaction that reaches the prepared state then loses its coordinator is left in limbo. It holds row and metadata locks, blocks history-list purge, stalls writers that touch the same rows. Nothing clears it on its own. XA RECOVER lists these prepared but not completed branches. This guard reads them, waits out a safety age so a branch from a live two-phase commit is never touched, rolls back only the orphaned ones behind a gate, then proves they are gone.

This blueprint was created by zkasuran.

Safety

  • Read-only by default. dry_run is true and auto_remediate is false, so a plain run reports and records what it saw. It never rolls anything back.
  • A rollback happens only when auto_remediate is true and dry_run is false. It then waits for a human to resume the Pause with approved set true. An unanswered gate reads as not approved.
  • The age gate is the core safety. A branch is targeted only after the guard has seen it prepared for at least min_age_minutes. A real two-phase commit prepares and commits in well under a second, so a legitimate in-flight branch is never old enough to be touched.
  • XA ROLLBACK discards the branch. If the coordinator chose commit, other participants keep it, so check the transaction manager first.
  • The run passes only when a second read of XA RECOVER shows every targeted branch gone.

How it works

  1. detect (io.kestra.plugin.jdbc.mysql.Query) runs XA RECOVER CONVERT XID. CONVERT XID returns the gtrid and bqual as a hex string so a binary xid rebuilds safely.
  2. compute (io.kestra.plugin.core.output.OutputValues) reads a KV cursor that holds the first time each branch was seen, keeps the entry for a branch still prepared, adds a timestamp for a new one, then targets only the branches first seen at least min_age_minutes ago. It rebuilds the XA ROLLBACK statement for each target from the hex gtrid, the hex bqual and the formatID.
  3. save_cursor (io.kestra.plugin.core.kv.Set) writes the pruned cursor back. It carries no ttl because an expired KV key throws on read. Pruning to the branches still prepared keeps it bounded.
  4. log_status (io.kestra.plugin.core.log.Log) records the prepared count, the targeted count and the mode.
  5. have_targets (io.kestra.plugin.core.flow.If) ends with a clean log when nothing is old enough. Otherwise alert_orphans posts to Slack in every mode.
  6. mode (io.kestra.plugin.core.flow.If) enters remediation only when auto_remediate is true and dry_run is false. Otherwise report_only logs what would run.
  7. approval_gate (io.kestra.plugin.core.flow.Pause) holds the run for a human. remediate_rollback (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs one XA ROLLBACK per targeted branch.
  8. reverify re-reads XA RECOVER. verify_result fails the run through io.kestra.plugin.core.execution.Fail when any targeted branch is still listed.

What you get

  • A detector for the stuck prepared XA branch, the failure mode that silently holds locks and stalls writers after a transaction manager dies.
  • An age gate that tells an abandoned branch apart from a branch a live commit is still finishing, so recovery never races a healthy transaction.
  • A gated rollback that rebuilds a binary-safe xid from the hex XA RECOVER CONVERT XID output.
  • A real verification step. The run passes only when the second XA RECOVER read shows every targeted branch gone.
  • Slack alerts whenever an orphan is present and on any failure.

Who it's for

  • Platform engineers who run MySQL behind a distributed transaction manager (JTA, a messaging bridge, a two-phase commit across services) where a crashed coordinator can abandon a prepared branch.
  • DBAs chasing a purge that will not advance or writers that block on locks with no visible owner.
  • Teams who want a safe, scheduled recovery for prepared XA branches instead of a manual XA RECOVER plus XA ROLLBACK at 3am.

Why orchestrate this with Kestra

MySQL has no scheduler, no age memory and no way to prove a rollback worked. Kestra adds each piece. It polls XA RECOVER on a schedule, remembers the first time each branch was seen in a KV cursor so the age gate holds across runs, gates the rollback behind dry-run and human approval, re-reads XA RECOVER to turn a hopeful rollback into a verified one. A Slack alert fires the moment an orphan appears or a step fails.

Tested end to end

Run on Kestra 2.0.5 against a MySQL 8.0 server with real prepared XA branches forced by opening a session, running XA START, XA END, XA PREPARE, then closing the connection with no commit.

  • Detector can fail: a server with zero prepared branches returned prepared 0, target 0 and took the clean path. The same server with three abandoned branches returned prepared 3.
  • Dry run on three orphans detected them, alerted and rolled back nothing. XA RECOVER showed the same three branches before and after.
  • Remediate with approval rolled back all three. The re-read of XA RECOVER returned empty and the seeded rows were gone, confirmed with the mysql client.
  • Age gate: with min_age_minutes set to 10, a branch prepared seconds earlier was reported but not targeted, so XA RECOVER still listed it after the run.
  • Re-verify can fail: a run whose rollback could not clear a targeted branch failed on the second XA RECOVER read rather than reporting success.
  • An unreachable host failed the detect step and fired the Slack alert through the errors block.

Prerequisites

A MySQL server reachable over JDBC and a user that can run XA RECOVER and XA ROLLBACK. On MySQL 8.0 that user needs the XA_RECOVER_ADMIN privilege.

Secrets

  • MYSQL_USERNAME: MySQL user, read when use_secret_auth is true.
  • MYSQL_PASSWORD: password for that user.
  • SLACK_WEBHOOK_URL: used by the alerts and the errors block.
  • In Kestra OSS, provide each secret as a base64-encoded environment variable prefixed SECRET_.

Inputs

Name Type Default Description
mysql_url STRING jdbc:mysql://localhost:3306/ JDBC URL of the MySQL server to guard.
use_secret_auth BOOL true Read credentials from secrets. Set false only for inline test credentials.
mysql_username STRING root Username used only when use_secret_auth is false.
mysql_password STRING `` Password used only when use_secret_auth is false.
min_age_minutes INT 15 Minimum time a branch must have been seen prepared before it is targeted.
cursor_key STRING xa_orphan_first_seen KV key for the first-seen map. Use a distinct key per guarded server.
dry_run BOOL true Report without rolling back.
auto_remediate BOOL false Roll back targeted branches behind the approval gate when dry_run is false.

Quick start

  1. Set MYSQL_USERNAME, MYSQL_PASSWORD and SLACK_WEBHOOK_URL as namespace secrets.
  2. Import this flow.
  3. Run it with mysql_url pointed at the server. The default dry run reports prepared branches and records first-seen.
  4. Enable the schedule trigger (safe mode) for min_age_minutes so the cursor ages the branches.
  5. To clear the orphans, run again with dry_run false and auto_remediate true, then resume the paused execution with approved true.

How to extend

  • Lower min_age_minutes for a staging server where no real distributed transactions run. Raise it where long-lived prepared branches are expected.
  • Correlate a target with its held locks by reading performance_schema.data_locks and information_schema.innodb_trx before the rollback for a richer alert.
  • Open a ticket instead of rolling back, for an environment where every XA rollback needs a change record.
  • Point cursor_key at a per-server value and schedule one copy of the flow per MySQL instance.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.