Files

Cluster discovery maintenance

2026-09-22: MySQL topology result ownership and fixture isolation

Failure and cause

TestMySQLReplicationAndGroupDiscovery intermittently reported no available primary, an unknown/incorrect cluster type, or a corrupted destination such as 1rimary.internal instead of primary.internal. Repeating the original test 100 times reproduced failures in both replication modes. This was not solely a timeout or a fixture scheduling problem: there were two independent defects.

  1. Production result ownership: pinned go-mysql v1.13.0's mysql.Resultset.GetString uses utils.ByteSliceToString, which borrows the result bytes. mysql.Result.Close returns that resultset to a global pool. probeMySQL retained hostnames (and potentially already-lowercase roles) in ClusterInfo.Nodes after closing the result. Subsequent queries or connections could overwrite the bytes while discovery still used those nodes.
  2. In-process fixture schema reuse: the client and fixture share that global resultset pool. Resultset.Reset retains field pointers. The dependency's BuildSimpleTextResultset initializes fields only when their entries are nil, so it can reuse the preceding client result's names, types and raw field data for an unrelated fixture response. That can produce invalid response schemas even after retained topology strings are fixed. A 500-repeat intermediate check with only the ownership fix still reproduced discovery failures.

These conclusions come from the pinned dependency source and local reproductions, not from a live MySQL server.

Changes and resulting behavior

  • mysqlTopologyNode clones host and role strings before a topology node escapes the result lifetime. Both group-replication and Source_Host/Master_Host discovery use it; role normalization remains unchanged.
  • The test fixture now creates independent MySQL field metadata and text-encoded rows using mysqlTopologyResult, avoiding the dependency's pooled simple-text builder. It still serves the actual MySQL wire protocol over net.Pipe.
  • The original selected-primary and topology-type assertions remain. Additional assertions require the selected port/role and exactly the expected primary and replica/secondary node roles.
  • TestMySQLTopologyOwnsResultStrings deterministically overwrites borrowed result buffers after closing the result and verifies the retained node is unchanged. It covers uppercase and already-lowercase roles, without assuming which object sync.Pool will return.

Scope and compatibility

Only internal/cluster is changed. There are no dependency upgrades, schema/data migrations, retry additions, timeout increases, weakened assertions, or changes to SQL commands, unknown-topology fallback, permissions or primary-selection rules. The safe-sql-updates review confirms discovery remains read-only: no transaction, lock or database write was added. The production change adds small owned string allocations per discovered MySQL node and prevents reuse-induced routing corruption.

Validation

Using GOTOOLCHAIN=go1.25.0:

  • go test ./internal/cluster -run 'TestMySQL(ReplicationAndGroupDiscovery|TopologyOwnsResultStrings)' -count=1000 -timeout=120s — passed.
  • go test ./internal/cluster -count=20 -timeout=120s — passed.
  • go vet ./internal/cluster — passed.
  • git diff --check -- internal/cluster — passed.
  • After the concurrent Vite build finished, go test ./... -count=1 -timeout=120s — all packages passed, including API and cluster (47.1 seconds wall time).
  • go vet ./... — passed.

These are isolated protocol fixtures, not live-server validation. Full-worktree validation ran after the concurrent Vite build finished, avoiding compilation of Go embedded assets while web/dist was being replaced. Shared rationale/testing documents remain parent-owned; this note provides the scoped change rationale.