You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
MySQL source connector: Compute binlog positions instead of using the wrapped 32-bit log_pos #2508
The log_pos field in a binlog event header cannot represent positions beyond 4GiB.
It is an unsigned 32-bit value, while a binlog file can legitimately grow beyond 4GiB because
MySQL never splits a transaction across files. When the server writes the event header, it
truncates its internal 64-bit position to 32 bits
(log_event.cc):
As a result, log_pos wraps around for events past 4GiB.
The wrapped position cannot be corrected after the fact because it is indistinguishable from a
legitimate position below 4GiB. This is independent of gtid_mode.
The real position can instead be derived from the byte stream itself. The client can anchor
its position to the 8-byte value carried by the ROTATE event and then advance it by the length
of each event read from the binlog. This is also how mysqlbinlog produces its # at
offsets: those offsets remain correct even when the header-derived end_log_pos value has
wrapped.
The binlog client currently trusts the wrapped header value (EventHeaderV4Deserializer):
header.setNextPosition(inputStream.readLong(4));
Therefore, once a binlog file crosses 4GiB, every position exposed by the client is wrong even
though streaming itself continues normally.
This affects:
Committed connector offsets. On servers without GTIDs, a restart resumes from the
wrapped position. That position can point into the middle of an unrelated event, causing the
misleading error reported in Cannot resume reading binlog from a position after 4GB [DBZ-2869] #401: log event entry exceeded max_allowed_packet ... Error code: 1236.
source.pos. Every emitted change event carries the wrapped position, including on
GTID-enabled deployments.
Client reconnect state and metrics. The client's reconnect position and the BinlogPosition streaming metric also use the wrapped value.
Proposed fix
Track the real binlog position independently of the 32-bit event-header value.
BinaryLogClient will maintain a 64-bit position that:
is initialized from the position carried by a ROTATE event,
advances by the length of each in-band binlog event, and
replaces header.nextPosition before event listeners are notified.
Unlike the common header, the ROTATE event payload carries the position as 8 bytes
(log_event.cc, Rotate_log_event::write()), which is what makes it a reliable anchor:
int8store(buf + R_POS_OFFSET, pos);
This makes all existing consumers of nextPosition observe the real position without
requiring changes outside the client.
Events that do not correspond to bytes in the binlog file do not advance the tracked position.
This includes artificial events such as fake ROTATE events and heartbeats. The zero-position
FORMAT_DESCRIPTION event produced when a dump starts mid-file is handled similarly.
PR follows.
Compatibility
For events below 4GiB, behavior is unchanged.
For events past 4GiB, source.pos and committed offsets change from the wrapped position to
the real position (the field is already INT64-typed), making them consistent with what SHOW BINARY LOGS and mysqlbinlog report. For example:
wrapped: 318880927
real: 4613848223
The wrapped position is not a usable compatibility contract because it can collide with a
legitimate position below 4GiB. Consumers that compare positions for ordering will instead see
the correct ordering after this change.
This change should be released together with #2506 so that schema history recovery
also handles positions above Integer.MAX_VALUE correctly.
Remaining limitation
This change does not make restarting from a >4GiB position work on a server without GTIDs.
COM_BINLOG_DUMP still carries the requested position in 4 bytes (#401), so the
client cannot directly request an arbitrary position above 4GiB.
However, tracking the real position removes the current silent corruption and makes follow-up
solutions possible, such as:
failing fast when a non-GTID restart position exceeds the protocol limit, or
resuming from an earlier anchor and skipping events until the real position is reached.
Reproduction — MySQL 8.0.46, Debezium 3.7.0-SNAPSHOT, real 4.61GB binlog file, no offset manipulation
A 1,100 x 4MiB-row transaction plus one small transaction committed during its group-commit
flush produced a 4.61GB binlog file (4,613,848,353 bytes) with an event group starting past
4GiB (size-based rotation only happens on transaction boundaries, and the group-commit stages
are pipelined, so the small transaction lands after the large one in the same file).
With gtid_mode=ON, the event emitted for the small transaction carries a wrapped position
while its GTID is correct and restarts are unaffected:
source.pos = 318880927 // real position 4,613,848,223 = source.pos + 2^32
source.gtid = 7502a9eb-...:12 // correct
With gtid_mode=OFF the same wrap appears in the committed offset, and a restart then fails
with the #401 error because the connector resumes from the wrapped position:
committed offset: {"file":"mysql-bin.000003","pos":318880973, ...} // real - 2^32
restart: ... log event entry exceeded max_allowed_packet; the first event
'mysql-bin.000003' at 318880973 ... Error code: 1236
With a client patched as proposed above, the same scenario stores and emits the real positions
(pos=4613848046, source.pos=4613848183); positions below 4GiB are unchanged, and streaming
through the 4.61GB transaction behaves identically.
References
A binlog file larger than 4GiB is a legitimate state: max_binlog_size
— MySQL never splits a transaction across binary log files, so a single large transaction
grows one file past the 1GiB cap.
MySQL Bug #112189 — the log_pos overflow
itself, verified in 2023 and unresolved. The report demonstrates the wrap with mysqlbinlog
output, where the byte-counted # at offsets stay correct while the header-derived end_log_pos column wraps.
MySQL Bug #55231 — native replicas break on
reconnect past 4GiB with the same 1236 error, open since 2010; the server-side answer has
been GTID rather than a protocol fix.
gh-ost #1366 — the same trusted-header
pattern caused silent data loss in gh-ost; fixed by failing fast on detected overflow
(#1367).
Activity
debezium/dbz#2508 Compute binlog positions instead of using the wrapp…
debezium/dbz#2508 Compute binlog positions instead of using the wrapp…
debezium/dbz#2508 Compute binlog positions instead of using the wrapp…
debezium/dbz#2663 Revert "debezium/dbz#2508 Compute binlog positions …
debezium/dbz#2508 debezium/dbz#2359 Upgrade binlog client to 0.41.5
vjuranek commentedon Sep 21, 2026
Re-opening the issue as the related commit was reverted in debezium/mysql-binlog-connector-java@dea56b8 as it the position wasn't counted correctly and resulted into test failures in Debezium, see #2663.
FIY @minleejae ^^^
minleejae commentedon Sep 21, 2026
@vjuranek Thanks for the heads-up! I'll improve the binlog position tracking to correctly handle GTID-based resumes and open a follow-up PR.
debezium/dbz#2508 debezium/dbz#2359 Upgrade binlog client to 0.41.5
debezium/dbz#2508 debezium/dbz#2359 Upgrade binlog client to 0.41.5