Replication Internals: Decoding the MySQL Binary Log Part 11: GTID_TAGGED_LOG_EVENT — Tagged GTIDs and MySQL's New Serialization Framework | Readyset
MySQL
Replication Internals: Decoding the MySQL Binary Log Part 11: GTID_TAGGED_LOG_EVENT — Tagged GTIDs and MySQL's New Serialization Framework<br>16 min read<br>6 days ago
In this eleventh and final post of our series, we decode the GTID_TAGGED_LOG_EVENT — the event MySQL 8.4 introduced to carry user-defined tags alongside the classic UUID and GNO, and along the way meet the new mysql::serialization framework that encodes it.<br>Introduction<br>Back in Part 5 we deferred one event: the GTID_TAGGED_LOG_EVENT (event type 42, 0x2a). It was introduced in MySQL 8.4 to support tagged GTIDs , which extend the classic UUID:GNO form with an optional user-defined label:<br>55778904-0299-11f1-b1b8-4ef0c4956feb:mytag:3<br>└────────── SID (UUID) ───────────┘ └tag┘ └GNO
A tag is [a-z_][a-z0-9_]{0,31} — up to 32 lowercase characters. Two transactions that originate on the same server but carry different tags occupy independent GNO sequences : the server can have …:mytag:1-100 and …:other:1-50 at the same time, with no collision. That makes tagged GTIDs useful for multi-source replication and for separating administrative work from application traffic.<br>The on-disk format is also completely different from the untagged GTID_LOG_EVENT we decoded in Part 5. Instead of a fixed 42-byte post-header followed by a few packed integers, the entire payload is produced by MySQL's new mysql::serialization library — a forward-/backward-compatible TLV encoding using variable-length integers and explicit field IDs. We'll spend most of this post on that encoding, because once it clicks, every byte in the event falls into place.<br>For this post we'll use a different file from the rest of the series: binlog_gtid_tag.000001 , generated against MySQL 9.6.0 with one tagged transaction. It contains the same kinds of events we've already covered — magic number, FORMAT_DESCRIPTION_EVENT, PREVIOUS_GTIDS_LOG_EVENT, a TABLE_MAP/WRITE_ROWS pair, an XID_EVENT, and a closing ROTATE_EVENT — except the GTID at position 245 is a GTID_TAGGED_LOG_EVENT instead of the classic GTID_LOG_EVENT.<br>Event Location<br>Position 245: GTID_TAGGED_LOG_EVENT (83 bytes) ← Our subject<br>Position 328: QUERY_EVENT - BEGIN<br>Position 405: TABLE_MAP_EVENT<br>Position 461: WRITE_ROWS_EVENT<br>Position 510: XID_EVENT<br>Position 541: ROTATE_EVENT<br>Position 585: (end of file)
The transaction wrapped by this event is a single-row INSERT into test.orders, executed under SET GTID_NEXT = '55778904-...:mytag:3'.<br>Reading the Raw Bytes<br>$ xxd -s 245 -l 83 binlog_gtid_tag.000001<br>000000f5: afae 8569 2a01 0000 0053 0000 0048 0100 ...i*....S...H..<br>00000105: 0000 0002 7800 0000 02aa ee25 0208 0465 ....x......%...e<br>00000115: 0222 c503 c502 e102 9cc1 0311 0355 02de ."...........U..<br>00000125: ad03 040c 060a 6d79 7461 6708 000a 040c ......mytag.....<br>00000135: 7f1c f3b8 1424 4a06 10a1 0412 430f 0b78 .....$J.....C..x<br>00000145: 72ad 08 r..
The event is 83 bytes: 19-byte common header + 60-byte serialized payload + 4-byte checksum. Notice the ASCII mytag in the middle of the dump — the only part of the event readable without a decoder.<br>Common Header (19 bytes)<br>afae8569 2a 01000000 53000000 48010000 0000<br>│ │ │ │ │ │<br>│ │ │ │ │ └─→ Flags: 0x0000<br>│ │ │ │ └───────────→ Next Position: 328<br>│ │ │ └────────────────────→ Event Size: 83 bytes<br>│ │ └─────────────────────────────→ Server ID: 1<br>│ └────────────────────────────────→ Event Type: 42 (GTID_TAGGED_LOG_EVENT)<br>└─────────────────────────────────────────→ Timestamp: 1770368687
FieldBytesLittle-EndianValueTimestampafae85690x698585af1770368687 (2026-02-06 06:04:47)Event Type2a0x2a42 (GTID_TAGGED_LOG_EVENT)Server ID010000000x000000011Event Size530000000x0000005383 bytesNext Position480100000x00000148328Flags00000x0000No flags
The event type code 42 (0x2a) is the discriminator that tells the reader to take the new code path. It is defined alongside GTID_LOG_EVENT = 33 in Log_event_type, and the dispatch happens inside the shared Gtid_event constructor:<br>if (header()->type_code == GTID_TAGGED_LOG_EVENT) {<br>auto data_event_len = header()->data_written - fde->common_header_len;<br>if (footer()->checksum_alg != BINLOG_CHECKSUM_ALG_OFF) {<br>data_event_len -= BINLOG_CHECKSUM_LEN;<br>read_gtid_tagged_log_event(buf + fde->common_header_len, data_event_len);<br>BAPI_VOID_RETURN;
read_gtid_tagged_log_event() hands the payload to a Decoder_type (mysql::serialization::Serializer_default) and lets the framework do the work — there is no hand-rolled byte parser for this event.<br>A New Serialization Framework<br>Everything we decoded in Parts 2–10 used MySQL's classic event format: fixed-width fields, a few packed integers, byte offsets baked into the spec. GTID_TAGGED_LOG_EVENT breaks with that convention. The same Gtid_event C++ class produces both wire formats, but for the tagged variant the fields are routed through the mysql::serialization library, declared on the...