MySQL source connector: Compute binlog positions instead of using the wrapped 32-bit log_pos · Issue #2508 · debezium/dbz · GitHub
Skip to content

MySQL source connector: Compute binlog positions instead of using the wrapped 32-bit log_pos #2508

Open

Description

@minleejae

Description

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

int4store(buf + LOG_POS_OFFSET, static_cast<uint32>(common_header->log_pos));

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:

  1. is initialized from the position carried by a ROTATE event,
  2. advances by the length of each in-band binlog event, and
  3. 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.
  • mysqlbinlog --start-position
    — since MySQL 8.0.33 it accepts positions beyond the 32-bit range for local files.
  • 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

  1. moved this from To triage to In progress in Debezium Issue Trackeron Aug 27, 2026
  2. added 2 commits that reference this issue on Aug 27, 2026
  3. added a commit that references this issue on Sep 3, 2026
  4. moved this from In progress to Done in Debezium Issue Trackeron Sep 3, 2026
  5. closed this as completedby moving to Done in Debezium Issue Trackeron Sep 3, 2026
  6. moved this from Done to Released in Debezium Issue Trackeron Sep 16, 2026
  7. added 2 commits that reference this issue on Sep 21, 2026
  8. vjuranek commented on Sep 21, 2026

    @vjuranek
    Member

    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 ^^^

  9. minleejae commented on Sep 21, 2026

    @minleejae
    Author

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

  10. added a commit that references this issue on Sep 21, 2026
  11. added a commit that references this issue on Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions