[MySQL] Fix idle keepalive LSN stall and multi-server-UUID GTID parsing - #706
[MySQL] Fix idle keepalive LSN stall and multi-server-UUID GTID parsing#706michaelbarnes wants to merge 13 commits into
Conversation
Heartbeat keepalives re-sent the LSN from the start of the last
transaction, while checkpoints store the LSN from the end of the same
transaction. On an idle server this blocked checkpoint creation
("Waiting before creating checkpoint" every ~30s) until the next
transaction arrived.
All commit paths (Xid, DDL auto-commit, non-transactional query) now
advance the current GTID position to the commit position, so keepalive
LSNs are never behind the last checkpoint LSN. The listener also no
longer mutates the caller's startGTID position object.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ReplicatedGTID.comparable assumed a single server UUID in the raw GTID. A gtid_executed containing multiple server UUIDs (e.g. after a failover or restore) was mis-parsed into a NaN transaction id, producing LSNs like "0000000000000NaN|...". On servers with a low transaction count this permanently blocked checkpoint creation, and the corrupted LSN could not be recovered by a service restart. The comparable LSN now parses full GTID sets (multiple UUIDs joined with ",\n", multiple intervals per UUID) and uses the maximum transaction id across the set. Unparseable segments are skipped instead of poisoning the result. deserialize now validates the binlog offset instead of silently producing NaN. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 8b5cf9b The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Hi @michaelbarnes |
|
@Rentacookie, when you mention "am actually not sure what effect it has for our replication consistency were we to process GTIDs from multiple servers" Do you mean multiple servers connecting to a single PowerSync Service instance? |
No, I mean more that events from multiple MySQL servers are appearing on binlog for the MySQL server that PowerSync is actually connected to. I believe this can happen if the MySQL DB we are connecting to is a replica itself, replicating from other MySQL servers. |
Rentacookie
left a comment
There was a problem hiding this comment.
Since the multi server uuid is unsupported, for now we'll add a check that does not allow replication to continue if multiple server uuids are found on the binlog.
I went with a warning rather than a hard stop. Happy to make it a hard error instead if you'd prefer. |
I reckon that is fine for now until we implement the full multi server uuid handling. |
Rentacookie
left a comment
There was a problem hiding this comment.
Discussed strategy for selecting current server uuid for choosing which uuidset to use.
…or consistency and added validation for single GTID constraints
…ntax handling and tests
…t cross-server transaction processing errors and handle GTID changes more robustly.
| export class ReplicatedGTID { | ||
| private options: ReplicatedGTIDSpecification; | ||
|
|
||
| constructor(options: ReplicatedGTIDSpecification) { |
There was a problem hiding this comment.
The main focus here is to ensure that a ReplicatedGTID only represents a single GTID since that preserves the integrity of our checkpoint LSNs.
That means no GTID sets and no transaction ranges are allowed. GTIDs read from events on the Binlog were always already in this format.
When reading the executed GTID set to determine the replication head and for snapshot checkpoints we now determine the correct single GTID based on the active server uuid.
| return false; | ||
| } | ||
|
|
||
| const isAvailable = await common.isGtidPositionStillAvailable(connection, lastKnowGTID); |
There was a problem hiding this comment.
In addition to checking if the binlog is still available, we now do the following validation on the resume LSN's GTID as well:
- Confirm that the server uuid from the GTID matches the active server uuid
- That the transaction id from the GTID is actually still available on the server
If these checks fail a full re-snapshot is required.
Fixes #704
Fixes #705
Background
These fixes address two MySQL replication issues found in the same support case: idle replication could leave checkpoints blocked, and GTID sets containing historical server UUIDs could produce invalid NaN LSNs.
Changes
Keepalive LSNs now use the end position of the last committed transaction across Xid, DDL auto-commit, and non-transactional commit paths.
Replication heads are derived from the active MySQL server UUID instead of storing the complete Executed_Gtid_Set.
Multi-interval and tagged GTID entries are reduced to a single uuid:transaction_id, preventing invalid or NaN comparable LSNs.
Servers with no transactions for the active UUID use a synthetic uuid:0 GTID while retaining the current binlog coordinates.
Resume validation now checks:
Invalid or rewound resume positions trigger a fresh snapshot.
MySQL replicas are rejected during source configuration checks.
Transactions originating from a server UUID other than the active source are rejected until multi-origin ordering is supported.
Serialized GTIDs and binlog offsets are now validated rather than silently accepting malformed values.
Tests
Added and updated unit coverage for GTID parsing, zero GTIDs, resume validation, replica detection, malformed serialization, and committed keepalive positions.
Current limitations
PowerSync still does not support replication directly from a MySQL replica or binlogs containing transactions from multiple active origins. MySQL 8.4 GTID tags are not preserved; tagged entries are normalized to uuid:transaction_id.
🤖 AI disclosure: Investigated and reproduced issues with Claude Code working from the customer's logs.
Implementation, tests and review were done using Claude Opus 5 and ChatGPT 5.6. Directed and reviewed by @michaelbarnes, @Rentacookie @bean1352