Problem
Every other MySQL transaction remains in a [pending] state in SHOW READYSET STATUS, even though the data is fully applied. This is a cosmetic/reporting issue — there is no delay in applying events, rows are committed to noria immediately. The problem is only that the persisted replication offset still shows a [pending] marker.
Observed behavior
-- Insert 1: finalized correctly
readyset> insert into t1 values (3);
readyset> SHOW READYSET STATUS;
Maximum Replication Offset: ...a2fc8caa:1-17 -- no pending, good
-- Insert 2: stuck as pending (but data IS applied)
readyset> insert into t1 values (4);
readyset> SHOW READYSET STATUS;
Maximum Replication Offset: ...a2fc8caa:1-17 [pending: a2fc8caa:18@1] -- cosmetic
The pattern repeats: odd transactions finalize, even ones stay pending until the next transaction arrives.
Root Cause
The issue is in connector.rs in the XID_EVENT handler:
EventType::XID_EVENT => {
self.finalize_current_gtid(); // finalizes in connector memory
if self.report_position_elapsed() || is_last {
return Ok((vec![ReplicationAction::LogPosition], self.current_offset()));
}
continue; // <-- finalized offset is NEVER persisted to noria
}
The flow for each transaction is:
- WRITE_ROWS_EVENT → returns
TableAction + offset with pending GTID. The rows are applied to noria immediately, and this offset (with pending) is stored per-table via SetReplicationOffset.
- XID_EVENT → calls
finalize_current_gtid() which promotes the pending GTID to committed in the connector's in-memory state only. But it only returns LogPosition (which would persist the finalized offset to noria) if report_position_elapsed() returns true (>10 seconds since last report).
When report_position_elapsed() returns false, the finalized offset is never persisted. The table in noria keeps the old offset with the pending GTID, even though the data itself was fully applied at step 1.
Why "every other"
last_reported_pos_ts is initialized to Instant::now() - MAX_POSITION_TIME (line 430), so the first call to report_position_elapsed() always returns true and resets the timer.
- The second transaction's XID arrives within <10s, so
report_position_elapsed() returns false → finalized offset is not persisted.
- After >10s passes (user does another insert), the timer elapses again → first XID reports, second doesn't, etc.
The same issue exists in the COMMIT query handler (process_event_query):
Ok(SqlQuery::Commit(_)) => {
self.finalize_current_gtid();
Err(ReadySetError::SkipEvent) // <-- also never persists
}
Constraints on the fix
The naive fix of "always return LogPosition from XID" is problematic because handle_log_position updates the replication offset for all tables, not just the one modified in this transaction.
Possible approaches
- Buffer row actions until XID: Don't return from
next_action_inner on WRITE_ROWS_EVENT. Instead buffer the TableAction and only return it when XID_EVENT arrives, using the finalized offset. Pro: offset is always clean. Con: changes streaming semantics, rows are applied later.
- Targeted offset update from XID: Track which table(s) were modified in the current transaction and return a targeted offset update that only updates the affected tables.
- Return
LogPosition from XID only when a pending GTID was actually finalized: Less frequent than "always return", but still updates all tables.
- Don't include pending GTID in the offset stored per-table: Only store the committed ranges in
SetReplicationOffset. Keep the pending GTID only in the connector's state for crash recovery.
Reproduction
- Start Readyset with MySQL upstream (GTID mode)
- Create a table and insert two rows in quick succession (<10s apart)
- Check
SHOW READYSET STATUS after each insert
- First insert shows finalized offset, second shows
[pending: ...]
Problem
Every other MySQL transaction remains in a
[pending]state inSHOW READYSET STATUS, even though the data is fully applied. This is a cosmetic/reporting issue — there is no delay in applying events, rows are committed to noria immediately. The problem is only that the persisted replication offset still shows a[pending]marker.Observed behavior
The pattern repeats: odd transactions finalize, even ones stay pending until the next transaction arrives.
Root Cause
The issue is in
connector.rsin theXID_EVENThandler:The flow for each transaction is:
TableAction+ offset withpendingGTID. The rows are applied to noria immediately, and this offset (with pending) is stored per-table viaSetReplicationOffset.finalize_current_gtid()which promotes the pending GTID to committed in the connector's in-memory state only. But it only returnsLogPosition(which would persist the finalized offset to noria) ifreport_position_elapsed()returns true (>10 seconds since last report).When
report_position_elapsed()returns false, the finalized offset is never persisted. The table in noria keeps the old offset with the pending GTID, even though the data itself was fully applied at step 1.Why "every other"
last_reported_pos_tsis initialized toInstant::now() - MAX_POSITION_TIME(line 430), so the first call toreport_position_elapsed()always returns true and resets the timer.report_position_elapsed()returns false → finalized offset is not persisted.The same issue exists in the
COMMITquery handler (process_event_query):Constraints on the fix
The naive fix of "always return
LogPositionfrom XID" is problematic becausehandle_log_positionupdates the replication offset for all tables, not just the one modified in this transaction.Possible approaches
next_action_inneronWRITE_ROWS_EVENT. Instead buffer theTableActionand only return it whenXID_EVENTarrives, using the finalized offset. Pro: offset is always clean. Con: changes streaming semantics, rows are applied later.LogPositionfrom XID only when a pending GTID was actually finalized: Less frequent than "always return", but still updates all tables.SetReplicationOffset. Keep the pending GTID only in the connector's state for crash recovery.Reproduction
SHOW READYSET STATUSafter each insert[pending: ...]