Turning on MySQL GTIDs is one of those changes that feels like it should have consequences for whatever is replicating into the box. So it's a fair question to ask before committing to it: if a Tungsten Replicator is applying changes to a node, and you flip gtid_mode=ON on that node, does anything break? And what about the stricter companion setting, enforce_gtid_consistency=ON - does it quietly reject statements Tungsten needs to run?
The short answer is a reassuring one, and it comes down to a single architectural fact about how Tungsten tracks its position. But there are a couple of genuine edge cases worth knowing, plus a nice subtlety about how conditional DDL actually crosses the wire. Here's the whole picture.
The Setup
The environment is a shape we see a lot when a team wants Tungsten's clustering upstream and plain MySQL row-based replication downstream:
- A three-node Tungsten Cluster (one primary, two replicas) as the source.
- A separate standalone MySQL node subscribed to that cluster via Tungsten, receiving all changes.
- That node emitting ROW-format binlogs that Google BigQuery Datastream consumes for CDC.
Because Datastream reads the downstream node's binlog, the team had good reason to want GTIDs turned on there - and they'd already confirmed in a dev environment that enabling gtid_mode=ON and enforce_gtid_consistency=ON did not break Tungsten's apply. The remaining questions were about whether that's officially expected, whether it's safe long-term, and whether any edge cases lurk.
Why GTID Mode on the Target Is a MySQL Concern, Not a Tungsten One
Here's the key fact: Tungsten does not use MySQL GTIDs for positioning. It connects to the target as an ordinary MySQL client and applies changes over JDBC. It does not read, write, or track MySQL's GTID state, and it does not depend on GTIDs to know where it is.
Instead, Tungsten records its apply position in its own catalog table, trep_commit_seqno, on the target - keyed by its internal THL (Transaction History Log) sequence numbers. That bookkeeping is completely independent of MySQL's GTID machinery.
So when gtid_mode=ON causes MySQL to auto-assign a GTID to each transaction Tungsten applies, that's entirely transparent to the replicator. There is no Tungsten-side GTID handshake that could fall out of sync, because there is no Tungsten-side GTID handshake at all. This is expected behavior, and it's safe as a steady-state configuration.
ON vs ON_PERMISSIVE: pick ON
A natural instinct is to reach for gtid_mode=ON_PERMISSIVE as the "gentler" option, since it allows both GTID and anonymous transactions. For a stable target node, that instinct is misplaced. ON_PERMISSIVE is a migration state - it exists so you can roll GTIDs out across a topology without a flag day. It buys you nothing over plain ON on a settled node, and - importantly - it does not relax the enforce_gtid_consistency restrictions. Those two settings are independent. So ON_PERMISSIVE isn't a safer long-term mode; it's a transitional one. If the goal is GTIDs on the downstream node (and it should be, given Datastream reads its binlog), plain ON is the right end state.
One thing you can't opt out of: when gtid_mode=ON, MySQL requires enforce_gtid_consistency=ON. You can't keep GTIDs and skip the statement restrictions. Which brings us to the setting that can actually reject something.
Does enforce_gtid_consistency=ON Block Tungsten?
enforce_gtid_consistency=ON refuses a handful of statement patterns that MySQL can't safely assign a single GTID to. The three that matter:
-
CREATE TABLE ... SELECT -
CREATE/DROP TEMPORARY TABLEinside a transaction - Mixing transactional and non-transactional tables in one statement or transaction
The reassurance up front: Tungsten's own apply path uses none of these patterns. Any exposure comes purely from the application workload that Tungsten replays. And in a normal Tungsten setup - ROW format on the source, DML arriving as row events - most of that exposure evaporates. Rather than reason from theory, the behavior below was reproduced directly against MySQL 8.0 with gtid_mode=ON, enforce_gtid_consistency=ON, and binlog_format=ROW.
CREATE TABLE ... SELECT - effectively a non-issue on modern MySQL. MySQL 8.0.21 made CTAS atomic and crash-safe, which is exactly what made it GTID-legal. So the deciding factor is the target's MySQL version, not the binlog format. Under ROW, the source logs CTAS as a bare CREATE TABLE followed by row inserts - and a bare CREATE with no SELECT is always safe on any target. Only under STATEMENT/MIXED does the source log a single CREATE TABLE ... SELECT statement that Tungsten replays verbatim; that's safe on an 8.0.21+ target but rejected with ERROR 1786 on a pre-8.0.21 one. In practice: with modern MySQL at both ends, there's no CTAS exposure either way.
CREATE/DROP TEMPORARY TABLE in a transaction - never triggers under ROW. Temporary tables aren't written to the binlog in ROW format, so the restriction simply never fires. It only applies under STATEMENT/MIXED.
Mixing transactional and non-transactional engines - this one is genuinely rejected, with ERROR 1785: Statement violates GTID consistency. Two things keep this from being a real problem. First, replicated DML arrives from the source as row events, not statements, so ordinary multi-table transactions are applied as row changes and don't trip the rule. Second - and this is the part that makes it operationally safe - a genuine violation is a hard, visible apply error: it drops the service into OFFLINE:ERROR with the offending SQL right there in the log. It will not silently skip a statement or corrupt data. So the residual exposure is narrow: an application that writes to a non-transactional table (MyISAM, CSV, MEMORY) in the same statement or transaction as a transactional one. If your replicated schema has no such tables, you're clear.
The one thing worth confirming is the source's binlog_format. Tungsten's standard MySQL configuration is ROW, and with ROW on the source, CTAS always arrives as CREATE TABLE plus row inserts, the temporary-table restriction never applies, and the only case left to check is that mixed-engine pattern.
The Bice Subtlety: How Conditional DDL Actually Replicates
The team had a clever rolling ALTER planned - widening a text column to mediumtext - guarded by a conditional so it would only fire where needed:
SET @_sql = IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'table_name'
AND COLUMN_NAME = 'col' AND COLUMN_TYPE != 'mediumtext') > 0,
'ALTER TABLE `table_name` MODIFY COLUMN `col` mediumtext NULL, ALGORITHM=COPY, LOCK=SHARED',
'DO 0'
);
PREPARE _stmt FROM @_sql; EXECUTE _stmt; DEALLOCATE PREPARE _stmt;
Their question was a subtle and important one: when this crosses to the downstream node, does Tungsten replicate the whole SET/IF/PREPARE/EXECUTE dance, or just the resulting ALTER? Because if only the bare ALTER travels - stripped of its IF guard - you can't pre-apply the change downstream to turn the replicated DDL into a no-op.
Reproducing the exact block against MySQL 8.0 and inspecting the binlog gave a clean answer: the condition is fully resolved on the source, and only the materialized statement is logged. There's no SET, IF, SELECT, PREPARE, EXECUTE, or user-variable event in the binlog at all. When the column was still text, the binlog contained a single event under one GTID:
ALTER TABLE `table_name` MODIFY COLUMN `col` mediumtext NULL, ALGORITHM=COPY, LOCK=SHARED
And when the same block ran against a column that was already mediumtext, the IF collapsed to DO 0 and nothing was written to the binlog - no GTID consumed at all.
Tungsten replays exactly what MySQL logs, so the consequences for a rolling plan are:
-
The
IFguard is evaluated against the source's schema. If the source column is stilltext, the guard resolves to the realALTER, and that literal statement is what crosses to the downstream node. -
Because the guard already collapsed to a concrete
ALTERon the source, pre-applying the change downstream will not make the replicated DDL a no-op - Tungsten will run thatALTERon the downstream node regardless of its current column type. -
Re-running
ALTER ... MODIFY col mediumtext, ALGORITHM=COPYagainst an already-mediumtextcolumn isn't an error, butALGORITHM=COPYstill rebuilds the table under theSHAREDlock, and that DDL still lands in the downstream binlog - which means Datastream will see it.
The Tungsten-Recommended Way to Roll a Schema Change
The clean approach for schema changes on a Tungsten cluster is the rolling apply method: disable binary logging for the specific DDL statement so it never hits the binlog and therefore never replicates, then run the ALTER individually on each node. That keeps the change out of the downstream binlog entirely - so Datastream doesn't see a redundant table rebuild - at the cost of applying the statement node-by-node yourself. For a large ALGORITHM=COPY rebuild feeding a CDC pipeline, that trade is usually well worth it.
Takeaways
-
GTIDs on a Tungsten apply target are safe. Tungsten positions itself with THL sequence numbers and its own
trep_commit_seqnotable, not MySQL GTIDs, so auto-assigned GTIDs are transparent. Use plaingtid_mode=ONfor a stable target;ON_PERMISSIVEis a migration state, not a safer long-term one. -
enforce_gtid_consistency=ONwon't block Tungsten's own apply path. The only real exposure is an application that mixes transactional and non-transactional engines in one statement - and even then it fails loudly intoOFFLINE:ERROR, never silently. No MyISAM/CSV/MEMORY in the replicated schema means no exposure. -
Keep the source on ROW format. With ROW upstream, CTAS arrives as
CREATE TABLEplus row inserts, the temporary-table rule never fires, and the GTID-consistency surface shrinks to almost nothing. -
Conditional DDL is resolved at the source. Only the materialized statement is logged and replicated - the
IF/PREPARE/EXECUTEscaffolding never travels. Plan schema rollouts around what MySQL actually writes to the binlog, not around the wrapper you typed. -
Use the rolling apply method for big schema changes. Disabling binary logging per statement keeps a heavy
ALTERout of the downstream binlog and off your CDC consumer, at the cost of running it on each node yourself.
The through-line here is the same one that makes Tungsten pleasant to reason about: its positioning is entirely internal, so it stays out of the way of the target database's own features. GTIDs, consistency enforcement, and DDL all behave exactly the way MySQL says they will - and Tungsten simply replays what MySQL logged.
Comments
Add new comment