Skip to content

K8SPS-852 errantTransactionsPolicy: report quarantined members NotReady - #1412

Open
hors wants to merge 10 commits into
mainfrom
K8SPS-errant-transactions-policy
Open

K8SPS-852 errantTransactionsPolicy: report quarantined members NotReady#1412
hors wants to merge 10 commits into
mainfrom
K8SPS-errant-transactions-policy

Conversation

@hors

@hors hors commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Readiness: an async member that the bootstrap quarantines for errant transactions now reports NotReady. checkReadinessAsync stats the existing quarantine marker (/quarantine) and fails the probe when present, so the divergence surfaces in the cluster status and halts rollouts instead of a diverged member silently counting as ready. rebuild deletes the pod and inject-empty clears the marker on rejoin, so only the manual policy leaves a member persistently NotReady. (Reverses the prior "stay Ready" behavior; the OrderedReady scale/rollout stall while quarantined is the intended signal.)

E2e test (async-errant-transactions): exercises all three policies end to end plus the quarantine path:

  • manual: ErrantGTIDsDetected emitted, member untouched, errant DB survives
  • rebuild: ErrantMemberRebuild, cluster reconverges 3/3, errant DB gone
  • inject-empty: ErrantGTIDsInjectEmpty, member rejoins, errant DB kept locally
  • quarantine: forge errant txn + restart -> member returns NotReady with the quarantine marker (the same path a failed-over old primary hits)

CHANGE DESCRIPTION

Problem:
Short explanation of the problem.

Cause:
Short explanation of the root cause of the issue if applicable.

Solution:
Short explanation of the solution we are providing with this PR.

CHECKLIST

Jira

  • Is the Jira ticket created and referenced properly?
  • Does the Jira ticket have the proper statuses for documentation (Needs Doc) and QA (Needs QA)?
  • Does the Jira ticket link to the proper milestone (Fix Version field)?

Tests

  • Is an E2E test/test case added for the new feature/change?
  • Are unit tests added where appropriate?

Config/Logging/Testability

  • Are all needed new/changed options added to default YAML files?
  • Are all needed new/changed options added to the Helm Chart?
  • Did we add proper logging messages for operator actions?
  • Did we ensure compatibility with the previous version or cluster upgrade process?
  • Does the change support oldest and newest supported PS version?
  • Does the change support oldest and newest supported Kubernetes version?

Readiness: an async member that the bootstrap quarantines for errant
transactions now reports NotReady. checkReadinessAsync stats the existing
quarantine marker (<datadir>/quarantine) and fails the probe when present, so
the divergence surfaces in the cluster status and halts rollouts instead of a
diverged member silently counting as ready. rebuild deletes the pod and
inject-empty clears the marker on rejoin, so only the manual policy leaves a
member persistently NotReady. (Reverses the prior "stay Ready" behavior; the
OrderedReady scale/rollout stall while quarantined is the intended signal.)

E2e test (async-errant-transactions): exercises all three policies end to end
plus the quarantine path:
- manual: ErrantGTIDsDetected emitted, member untouched, errant DB survives
- rebuild: ErrantMemberRebuild, cluster reconverges 3/3, errant DB gone
- inject-empty: ErrantGTIDsInjectEmpty, member rejoins, errant DB kept locally
- quarantine: forge errant txn + restart -> member returns NotReady with the
  quarantine marker (the same path a failed-over old primary hits)
Copilot AI review requested due to automatic review settings June 19, 2026 05:38
@pull-request-size pull-request-size Bot added the size/XXL 1000+ lines label Jun 19, 2026
@github-actions github-actions Bot added the tests label Jun 19, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR makes async members that are quarantined due to errant transactions report NotReady (via a quarantine marker file), and adds operator-side reconciliation to resolve/quarantine/join such members according to spec.mysql.errantTransactionsPolicy, so divergence is surfaced in cluster status and rollouts are halted instead of silently proceeding.

Changes:

  • Add a quarantine marker (/var/lib/mysql/quarantine) and update async readiness to fail when it exists.
  • Extend async replication reconciliation to (a) repair broken replicas after takeover, and (b) resolve “quarantined / unjoined” members via rebuild/inject-empty/manual policy flows.
  • Add an end-to-end KUTTL test (async-errant-transactions) covering manual/rebuild/inject-empty and quarantine behavior; register it in the PR test suite.

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pkg/orchestrator/client.go Add Orchestrator instance fields for errant GTIDs/thread states and an API wrapper for gtid-errant-inject-empty.
pkg/mysql/mysql.go Introduce a shared constant for the quarantine marker file path.
pkg/db/replication.go Add helpers for read-only detection, GTID set operations, and empty-GTID injection (pod-exec based).
pkg/controller/ps/controller.go Enhance async replication reconciliation: repair broken replicas and manage quarantined/unjoined members per policy.
pkg/controller/ps/upgrade.go Avoid rollout deadlock by recreating unready outdated pods during SmartUpdate.
cmd/healthcheck/main.go Make async readiness fail when quarantine marker exists.
cmd/bootstrap/async/async_replication.go Quarantine members with errant GTIDs instead of cloning over local transactions; clear marker on successful join.
cmd/internal/db/db.go Ensure CHANGE REPLICATION SOURCE sets GET_SOURCE_PUBLIC_KEY=1; return source host even when replication is stopped; add GTID_SUBTRACT helper.
api/v1/perconaservermysql_types.go Add errantTransactionsPolicy API field with enum + default.
api/v1/perconaservermysql_types_test.go Add/adjust a defaults/validation test case (backup disabled with image).
deploy/crd.yaml, deploy/bundle.yaml, deploy/cw-bundle.yaml, config/crd/bases/... CRD schema updates for errantTransactionsPolicy (enum + default).
deploy/cr.yaml Document errantTransactionsPolicy in the example CR.
cmd/example-gen/scripts/lib/ps.sh Exclude errantTransactionsPolicy from commented example fields.
cmd/example-gen/pkg/defaults/manual.go Set manual default for generated example manifests.
e2e-tests/functions Add helper to find an async replica by @@super_read_only.
e2e-tests/tests/async-errant-transactions/* New KUTTL E2E test steps for errant transaction policies and quarantine NotReady behavior.
e2e-tests/run-pr.csv Register the new E2E test for PR runs (8.4).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/db/replication.go
Comment on lines +149 to +150
q := fmt.Sprintf("SELECT GTID_SUBTRACT('%s', '%s') AS diff", a, b)
err := m.query(ctx, q, &rows)
Comment thread pkg/db/replication.go
Comment on lines +192 to +194
// gtidSetRe matches one uuid:intervals element of a GTID set,
// e.g. 3e11fa47-71ca-11e1-9e33-c80aa9429562:1-5:11.
var gtidSetRe = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
Comment thread cmd/healthcheck/main.go
Comment on lines +104 to +106
if _, err := os.Stat(mysql.QuarantineFile); err == nil {
return errors.New("member is quarantined due to errant transactions; see the ErrantGTIDsDetected event")
}
Comment thread pkg/controller/ps/controller.go Outdated
Comment thread cmd/bootstrap/async/async_replication.go Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i think it'd better to move all new functions to new file called async.go

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

@hors hors changed the title errantTransactionsPolicy: report quarantined members NotReady K8SPS-852 errantTransactionsPolicy: report quarantined members NotReady Aug 5, 2026
@hors
hors requested a review from Copilot August 5, 2026 17:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

cmd/healthcheck/main.go:106

  • os.Stat errors other than "file does not exist" are currently ignored, which can incorrectly mark the pod Ready if the quarantine marker can’t be accessed (e.g. permission/IO errors). Treat unexpected stat failures as readiness failures so the condition is visible.
	if _, err := os.Stat(mysql.QuarantineFile); err == nil {
		return errors.New("member is quarantined due to errant transactions; see the ErrantGTIDsDetected event")
	}

pkg/controller/ps/async.go:365

  • Same PVC-deletion safety concern here: deleting by name without UID preconditions can delete a freshly reprovisioned PVC if this reconcile path repeats. Fetch the PVC and delete it with UID preconditions to avoid rebuild loops and accidental data loss.
		pvc := &corev1.PersistentVolumeClaim{
			ObjectMeta: metav1.ObjectMeta{
				Name:      fmt.Sprintf("%s-%s", mysql.DataVolumeName, mysqlPod.Name),
				Namespace: cr.Namespace,
			},
		}
		if err := r.Delete(ctx, pvc); client.IgnoreNotFound(err) != nil {
			return errors.Wrapf(err, "delete PVC %s", pvc.Name)
		}
		if err := r.Delete(ctx, mysqlPod); client.IgnoreNotFound(err) != nil {
			return errors.Wrapf(err, "delete pod %s", mysqlPod.Name)
		}

pkg/db/replication.go:150

  • Building the GTID_SUBTRACT query via string interpolation can break on unexpected characters (and is harder to reason about from a security perspective). At minimum, escape single quotes before embedding the GTID sets.

Comment thread pkg/controller/ps/async.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated no new comments.

Suppressed comments (4)

pkg/db/replication.go:150

  • GTIDSubtract builds SQL via fmt.Sprintf with unescaped string literals. Even if inputs are usually server-generated, a malformed/trimmed GTID string containing a quote would break the query. Escape quotes before interpolation (or switch to a parameterized query path if available).
    pkg/db/replication.go:194
  • The gtidSetRe comment says it matches a full "uuid:intervals" element, but the regex only matches the UUID prefix. Adjust the comment to match actual behavior to avoid confusion for future edits.
    cmd/healthcheck/main.go:106
  • os.Stat() errors other than nil (e.g. permission/IO errors) are currently ignored, which could let a quarantined member report Ready even though the marker exists but can't be stat'ed. Treat non-IsNotExist errors as probe failures (or at least surface them).
	if _, err := os.Stat(mysql.QuarantineFile); err == nil {
		return errors.New("member is quarantined due to errant transactions; see the ErrantGTIDsDetected event")
	}

pkg/controller/ps/async.go:67

  • In the initial pod scan, primaryPod is only set after IsReadonly() succeeds. If IsReadonly() fails for the primary pod, the loop continues and primaryPod stays nil, causing this function to return early and skip primary confirmation/quarantine reconciliation. Set primaryPod before the read-only check, and log the read-only check error instead of silently skipping.
		um := database.NewReplicationManager(pod, r.ClientCmd, apiv1.UserOperator, operatorPass, "127.0.0.1")
		readOnly, err := um.IsReadonly(ctx)
		if err != nil {
			continue
		}
		if !readOnly {
			writableExists = true
		}
		if mysql.PodFQDN(cr, pod) == primary.Key.Hostname {
			primaryPod = pod
		}

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated no new comments.

Suppressed comments (3)

cmd/healthcheck/main.go:106

  • checkReadinessAsync treats the quarantine marker as present only when os.Stat returns nil, but it silently ignores other Stat errors (e.g. permission/IO). In those cases the readiness probe would incorrectly report Ready even though it can’t reliably determine quarantine state.
	// A quarantined member (errant transactions, not joined to the cluster) is
	// reported NotReady so the divergence surfaces in the cluster status and
	// halts rollouts. rebuild/inject-empty clear the marker quickly; only the
	// manual policy leaves it in place.
	if _, err := os.Stat(mysql.QuarantineFile); err == nil {
		return errors.New("member is quarantined due to errant transactions; see the ErrantGTIDsDetected event")
	}

pkg/db/replication.go:151

  • GTIDSubtract builds the SQL string by interpolating raw GTID set strings into single-quoted literals. If the GTID strings ever contain quotes or backslashes (unexpected formatting, corrupted output, etc.), this can break the query or be interpreted as additional SQL. It’s safer to escape before embedding into a SQL literal.
    pkg/db/replication.go:194
  • The gtidSetRe comment says it matches a full "uuid:intervals" element, but the regexp only matches the UUID portion. This is misleading when maintaining/using expandGTIDSet.

@hors
hors marked this pull request as ready for review August 5, 2026 19:57
@hors
hors requested a review from mayankshah1607 as a code owner August 5, 2026 19:57
@hors
hors marked this pull request as draft August 5, 2026 22:41
@JNKPercona

Copy link
Copy Markdown
Collaborator
Test Name Result Time
async-ignore-annotations-8-4 passed 00:00:00
async-global-metadata-8-4 passed 00:10:54
async-upgrade-8-0 passed 00:00:00
async-upgrade-8-4 passed 00:00:00
auto-config-8-4 passed 00:00:00
config-8-4 passed 00:23:35
config-router-8-0 passed 00:00:00
config-router-8-4 passed 00:00:00
custom-users-8-4 passed 00:00:00
demand-backup-8-0 passed 00:00:00
demand-backup-8-4 passed 00:00:00
gr-pitr-minio-8-4 passed 00:00:00
gr-pitr-encrypted-minio-8-4 passed 00:00:00
gr-pitr-one-pod-8-4 passed 00:00:00
async-pitr-minio-8-4 passed 00:00:00
demand-backup-cloud-8-4 passed 00:00:00
demand-backup-retry-8-4 passed 00:00:00
demand-backup-incremental-8-0 passed 00:00:00
demand-backup-incremental-8-4 passed 00:00:00
async-data-at-rest-encryption-8-0 passed 00:00:00
async-data-at-rest-encryption-8-4 passed 00:00:00
async-errant-transactions-8-4 passed 00:00:00
gr-cross-cluster-8-0 passed 00:00:00
gr-cross-cluster-8-4 passed 00:00:00
gr-cross-cluster-backup-8-0 passed 00:00:00
gr-cross-cluster-backup-8-4 passed 00:00:00
gr-global-metadata-8-4 passed 00:00:00
gr-data-at-rest-encryption-8-0 passed 00:00:00
gr-data-at-rest-encryption-8-4 passed 00:00:00
gr-demand-backup-8-4 passed 00:00:00
gr-demand-backup-cloud-8-4 passed 00:00:00
gr-demand-backup-haproxy-8-4 passed 00:00:00
gr-demand-backup-incremental-8-0 passed 00:00:00
gr-demand-backup-incremental-8-4 passed 00:00:00
gr-demand-backup-incremental-compressed-8-0 passed 00:00:00
gr-demand-backup-incremental-compressed-8-4 passed 00:00:00
gr-demand-backup-incremental-encrypted-8-0 passed 00:00:00
gr-demand-backup-incremental-encrypted-8-4 passed 00:00:00
gr-finalizer-8-4 passed 00:00:00
gr-haproxy-8-0 passed 00:00:00
gr-haproxy-8-4 passed 00:00:00
gr-ignore-annotations-8-4 passed 00:00:00
gr-init-deploy-8-0 passed 00:00:00
gr-init-deploy-8-4 passed 00:00:00
gr-one-pod-8-4 passed 00:00:00
gr-recreate-8-4 passed 00:00:00
gr-scaling-8-4 passed 00:00:00
gr-scheduled-backup-8-4 passed 00:00:00
gr-scheduled-backup-incremental-8-4 passed 00:00:00
gr-security-context-8-4 passed 00:00:00
gr-self-healing-8-4 passed 00:00:00
gr-tls-cert-manager-8-4 passed 00:00:00
gr-users-8-4 passed 00:00:00
gr-upgrade-8-0 passed 00:00:00
gr-upgrade-8-4 passed 00:00:00
haproxy-8-0 passed 00:00:00
haproxy-8-4 passed 00:00:00
init-deploy-8-0 passed 00:00:00
init-deploy-8-4 passed 00:00:00
limits-8-4 passed 00:00:00
monitoring-8-4 passed 00:00:00
one-pod-8-0 passed 00:00:00
one-pod-8-4 passed 00:00:00
operator-self-healing-8-4 passed 00:00:00
pvc-auto-resize-8-4 passed 00:00:00
pvc-resize-8-4 passed 00:00:00
recreate-8-4 passed 00:00:00
scaling-8-4 passed 00:00:00
scheduled-backup-8-0 passed 00:00:00
scheduled-backup-8-4 passed 00:00:00
scheduled-backup-incremental-8-0 passed 00:00:00
scheduled-backup-incremental-8-4 passed 00:00:00
service-per-pod-8-4 passed 00:00:00
sidecars-8-4 passed 00:00:00
smart-update-8-4 passed 00:00:00
storage-8-4 passed 00:00:00
switch-cluster-type-8-4 passed 00:00:00
telemetry-8-4 passed 00:00:00
tls-cert-manager-8-4 passed 00:00:00
users-8-0 passed 00:00:00
users-8-4 passed 00:00:00
version-service-8-4 passed 00:00:00
Summary Value
Tests Run 82/82
Job Duration 00:57:56
Total Test Time 00:34:29

commit: 33fbcb6
image: perconalab/percona-server-mysql-operator:PR-1412-33fbcb60

@hors
hors marked this pull request as ready for review August 21, 2026 21:05
@hors
hors requested a review from egegunes August 21, 2026 21:05
@egegunes egegunes added this to the v1.4.0 milestone Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants