Skip to content

[MySQL] Fix idle keepalive LSN stall and multi-server-UUID GTID parsing - #706

Open
michaelbarnes wants to merge 13 commits into
mainfrom
fix/mysql-idle-keepalive-and-gtid-set-parsing
Open

[MySQL] Fix idle keepalive LSN stall and multi-server-UUID GTID parsing#706
michaelbarnes wants to merge 13 commits into
mainfrom
fix/mysql-idle-keepalive-and-gtid-set-parsing

Conversation

@michaelbarnes

@michaelbarnes michaelbarnes commented Jul 1, 2026

Copy link
Copy Markdown

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:

    • The source server UUID has not changed.
    • The binlog file and stored offset remain available.
    • Non-zero GTIDs are still present in gtid_executed.
  • 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

michaelbarnes and others added 2 commits July 1, 2026 17:21
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-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8b5cf9b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@powersync/service-module-mysql Minor
@powersync/service-schema Patch
@powersync/service-image Patch
@powersync/service-core Patch
@powersync/service-module-convex Patch
@powersync/service-module-core Patch
@powersync/service-module-mongodb-storage Patch
@powersync/service-module-mongodb Patch
@powersync/service-module-mssql Patch
@powersync/service-module-postgres-storage Patch
@powersync/service-module-postgres Patch
test-client Patch

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

@michaelbarnes
michaelbarnes requested a review from Rentacookie July 1, 2026 23:51
@Rentacookie

Copy link
Copy Markdown
Contributor

Hi @michaelbarnes
Thank you these changes look great! I just want to investigate the multi-server-UUID GTID sets issue a bit more.
Its good that we parse the GTID sets correctly now, but I am actually not sure what effect it has for our replication consistency were we to process GTIDs from multiple servers 😬

@michaelbarnes

Copy link
Copy Markdown
Author

@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?

@Rentacookie

Rentacookie commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

@michaelbarnes

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
Rentacookie marked this pull request as ready for review August 5, 2026 12:22

@Rentacookie Rentacookie 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.

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.

@bean1352

bean1352 commented Aug 6, 2026

Copy link
Copy Markdown

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.

@Rentacookie

Copy link
Copy Markdown
Contributor

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 Rentacookie 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.

Discussed strategy for selecting current server uuid for choosing which uuidset to use.

Comment thread modules/module-mysql/src/common/ReplicatedGTID.ts Outdated
Comment thread modules/module-mysql/src/common/ReplicatedGTID.ts
@Rentacookie
Rentacookie self-requested a review August 7, 2026 11:31
export class ReplicatedGTID {
private options: ReplicatedGTIDSpecification;

constructor(options: ReplicatedGTIDSpecification) {

@Rentacookie Rentacookie Aug 7, 2026

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.

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);

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.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants