Skip to content

Commit 020546c

Browse files
authored
Merge pull request #2252 from hemanth132/fix/schema-restore-binlog-numeric-sort
Fix schema restore selecting stale snapshot due to lexicographic binlog file ordering
2 parents 0e44ee8 + 8b0fa2c commit 020546c

2 files changed

Lines changed: 108 additions & 23 deletions

File tree

src/main/java/com/zendesk/maxwell/schema/MysqlSavedSchema.java

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -592,29 +592,65 @@ private static Long findSchema(Connection connection, Position targetPosition, L
592592
} else {
593593
// Only consider binlog positions before the target position on the current server.
594594
// Within those, sort for the latest binlog file, then the latest binlog position.
595-
String sql = "SELECT id from `schemas` "
596-
+ "WHERE deleted = 0 "
597-
+ "AND last_heartbeat_read <= ? AND ("
598-
+ "(binlog_file < ?) OR "
599-
+ "(binlog_file = ? and binlog_position < ? and base_schema_id is not null) OR "
600-
+ "(binlog_file = ? and binlog_position <= ? and base_schema_id is null) "
601-
+ ") AND server_id = ? "
602-
+ "ORDER BY last_heartbeat_read DESC, binlog_file DESC, binlog_position DESC limit 1";
603-
try ( PreparedStatement s = connection.prepareStatement(sql) ) {
604-
605-
s.setLong(1, targetPosition.getLastHeartbeatRead());
606-
s.setString(2, targetBinlogPosition.getFile());
607-
s.setString(3, targetBinlogPosition.getFile());
608-
s.setLong(4, targetBinlogPosition.getOffset());
609-
s.setString(5, targetBinlogPosition.getFile());
610-
s.setLong(6, targetBinlogPosition.getOffset());
611-
s.setLong(7, serverID);
612-
613-
try ( ResultSet rs = s.executeQuery() ) {
614-
if (rs.next()) {
615-
return rs.getLong("id");
616-
} else
617-
return null;
595+
//
596+
// Binlog file names contain a numeric suffix (e.g. "mysql-bin-changelog.1215501").
597+
// When that suffix is parseable, we compare and sort numerically via CAST to avoid
598+
// lexicographic ordering bugs where a shorter number string like "122046" compares
599+
// greater than a longer one like "1215501". If the suffix cannot be parsed we fall
600+
// back to string comparison so that non-standard formats still work.
601+
Long targetFileNumber = targetBinlogPosition.getFileNumber();
602+
if ( targetFileNumber != null ) {
603+
String sql = "SELECT id from `schemas` "
604+
+ "WHERE deleted = 0 "
605+
+ "AND last_heartbeat_read <= ? AND ("
606+
+ "(CAST(SUBSTRING_INDEX(binlog_file, '.', -1) AS UNSIGNED) < ?) OR "
607+
+ "(binlog_file = ? and binlog_position < ? and base_schema_id is not null) OR "
608+
+ "(binlog_file = ? and binlog_position <= ? and base_schema_id is null) "
609+
+ ") AND server_id = ? "
610+
+ "ORDER BY last_heartbeat_read DESC, "
611+
+ "CAST(SUBSTRING_INDEX(binlog_file, '.', -1) AS UNSIGNED) DESC, "
612+
+ "binlog_position DESC limit 1";
613+
try ( PreparedStatement s = connection.prepareStatement(sql) ) {
614+
s.setLong(1, targetPosition.getLastHeartbeatRead());
615+
s.setLong(2, targetFileNumber);
616+
s.setString(3, targetBinlogPosition.getFile());
617+
s.setLong(4, targetBinlogPosition.getOffset());
618+
s.setString(5, targetBinlogPosition.getFile());
619+
s.setLong(6, targetBinlogPosition.getOffset());
620+
s.setLong(7, serverID);
621+
622+
try ( ResultSet rs = s.executeQuery() ) {
623+
if (rs.next()) {
624+
return rs.getLong("id");
625+
} else
626+
return null;
627+
}
628+
}
629+
} else {
630+
// Fallback: file name has no parseable numeric suffix; use string comparison.
631+
String sql = "SELECT id from `schemas` "
632+
+ "WHERE deleted = 0 "
633+
+ "AND last_heartbeat_read <= ? AND ("
634+
+ "(binlog_file < ?) OR "
635+
+ "(binlog_file = ? and binlog_position < ? and base_schema_id is not null) OR "
636+
+ "(binlog_file = ? and binlog_position <= ? and base_schema_id is null) "
637+
+ ") AND server_id = ? "
638+
+ "ORDER BY last_heartbeat_read DESC, binlog_file DESC, binlog_position DESC limit 1";
639+
try ( PreparedStatement s = connection.prepareStatement(sql) ) {
640+
s.setLong(1, targetPosition.getLastHeartbeatRead());
641+
s.setString(2, targetBinlogPosition.getFile());
642+
s.setString(3, targetBinlogPosition.getFile());
643+
s.setLong(4, targetBinlogPosition.getOffset());
644+
s.setString(5, targetBinlogPosition.getFile());
645+
s.setLong(6, targetBinlogPosition.getOffset());
646+
s.setLong(7, serverID);
647+
648+
try ( ResultSet rs = s.executeQuery() ) {
649+
if (rs.next()) {
650+
return rs.getLong("id");
651+
} else
652+
return null;
653+
}
618654
}
619655
}
620656
}

src/test/java/com/zendesk/maxwell/MysqlSavedSchemaTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,55 @@ public void testTableWithSameNameInTwoDBs() throws Exception {
220220
}
221221

222222

223+
/**
224+
* Regression test for https://github.com/zendesk/maxwell/issues/2251.
225+
*
226+
* Binlog file numbers are variable-length on Aurora/RDS. String (lexicographic)
227+
* comparison of file names produces wrong ordering when numbers cross a digit-count
228+
* boundary: e.g. "mysql-bin-changelog.122046" sorts AFTER "mysql-bin-changelog.1215501"
229+
* as a string even though 122046 < 1215501 numerically. This causes findSchema to pick
230+
* a stale snapshot, skipping all deltas recorded at those incorrectly-ordered files.
231+
*/
232+
@Test
233+
public void testFindSchemaUsesNumericFileOrdering() throws Exception {
234+
if (context.getConfig().gtidMode) {
235+
return;
236+
}
237+
238+
Connection c = context.getMaxwellConnection();
239+
long serverId = 200;
240+
long heartbeat = 0L;
241+
242+
// Target position: 7-digit file number
243+
Position targetPosition = makePosition(500L, "mysql-bin-changelog.1215501", heartbeat + 1);
244+
245+
// This schema is at a 6-digit file that sorts AFTER the target under string comparison
246+
// ("122046" > "1215501" lexicographically) but is numerically BEFORE it.
247+
// It must be excluded from the result.
248+
new MysqlSavedSchema(serverId, caseSensitivity, buildSchema(),
249+
makePosition(100L, "mysql-bin-changelog.122046", heartbeat)
250+
).saveSchema(c);
251+
252+
// This schema is at a 7-digit file numerically closest to the target and before it.
253+
// It should be selected.
254+
MysqlSavedSchema expectedSchema = new MysqlSavedSchema(serverId, caseSensitivity, buildSchema(),
255+
makePosition(200L, "mysql-bin-changelog.1203422", heartbeat)
256+
);
257+
expectedSchema.save(c);
258+
259+
// This schema is at a 6-digit file that is also numerically before the target but
260+
// older than expectedSchema — it must not be preferred over expectedSchema.
261+
new MysqlSavedSchema(serverId, caseSensitivity, buildSchema(),
262+
makePosition(300L, "mysql-bin-changelog.121345", heartbeat)
263+
).saveSchema(c);
264+
265+
MysqlSavedSchema foundSchema = MysqlSavedSchema.restore(
266+
context.getMaxwellConnectionPool(), serverId, caseSensitivity, targetPosition);
267+
268+
assertThat(foundSchema.getBinlogPosition(), equalTo(expectedSchema.getBinlogPosition()));
269+
assertThat(foundSchema.getSchemaID(), equalTo(expectedSchema.getSchemaID()));
270+
}
271+
223272
private Schema buildSchema() {
224273
String charset = Charset.defaultCharset().toString();
225274
List<Database> databases = new ArrayList<>();

0 commit comments

Comments
 (0)