-
-
Notifications
You must be signed in to change notification settings - Fork 2k
MDEV-39500 STRICT execution mode slave silently overwrites record eve… #5066
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| include/master-slave.inc | ||
| [connection master] | ||
| # | ||
| # MDEV-39500: Replicated UPDATE silently succeeds despite row divergence on slave | ||
| # | ||
| connection master; | ||
| CREATE TABLE t1 (a INT PRIMARY KEY, c VARCHAR(255)) ENGINE=InnoDB; | ||
| INSERT INTO t1 VALUES (1, 'initial_state'); | ||
| connection slave; | ||
| connection slave; | ||
| set @save_slave_exec_mode = @@global.slave_exec_mode; | ||
| set @@global.slave_exec_mode = STRICT; | ||
| UPDATE t1 SET c = 'diverged_to_pass'; | ||
| SELECT * FROM t1; | ||
| a c | ||
| 1 diverged_to_pass | ||
| connection master; | ||
| UPDATE t1 SET c = 'master_update1'; | ||
| # | ||
| # Expect it to succeed (slave gets in sync to have applied it). | ||
| # Exposing MDEV-39500: The slave applier uses the PK to find the row but | ||
| # fails to reject the update despite the before-image mismatch, applying it | ||
| # silently without throwing HA_ERR_RECORD_CHANGED (or similar). | ||
| # | ||
| connection slave; | ||
| connection slave; | ||
| # The slave has silently synced and overwritten the local changes | ||
| SELECT * FROM t1; | ||
| a c | ||
| 1 master_update1 | ||
| include/diff_tables.inc [master:t1, slave:t1] | ||
| connection slave; | ||
| set @@global.slave_exec_mode = STRINGENT; | ||
| call mtr.add_suppression("Slave: Replicated 'Update_rows_v1' record.s before-image on table"); | ||
| UPDATE t1 SET c = 'diverged_to_fail'; | ||
| SELECT * FROM t1; | ||
| a c | ||
| 1 diverged_to_fail | ||
| connection master; | ||
| UPDATE t1 SET c = 'master_update2'; | ||
| connection slave; | ||
| # | ||
| # Expect it to fail. | ||
| # | ||
| include/wait_for_slave_sql_error.inc [errno=4264] | ||
| connection slave; | ||
| # the stopped slave deverges | ||
| SELECT * FROM t1; | ||
| a c | ||
| 1 diverged_to_fail | ||
| set @@global.slave_exec_mode = STRICT; | ||
| include/start_slave.inc | ||
| Not anymore in STRICT mode | ||
| SELECT * FROM t1; | ||
| a c | ||
| 1 master_update2 | ||
| connection master; | ||
| connection slave; | ||
| # | ||
| # Cleanup | ||
| # | ||
| connection master; | ||
| DROP TABLE t1; | ||
| connection slave; | ||
| set @@global.slave_exec_mode = @save_slave_exec_mode; | ||
| include/rpl_end.inc |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| --source include/have_binlog_format_row.inc | ||
| --source include/have_innodb.inc | ||
| --source include/master-slave.inc | ||
|
|
||
| --echo # | ||
| --echo # MDEV-39500: Replicated UPDATE silently succeeds despite row divergence on slave | ||
| --echo # | ||
|
|
||
| # Having two slave_exec_mode modes conduct the following. | ||
| # 0. initially populate a PK-defined table with a record | ||
| # 1. makes slave data inconsistent with master via UPDATEing locally a | ||
| # replicated record, and run | ||
| # 2. a replicated UPDATE from master and expect, it to succeed | ||
| # in the default STRICT mode, or | ||
| # fail in the new STRINGENT. | ||
|
|
||
|
|
||
| connection master; | ||
| CREATE TABLE t1 (a INT PRIMARY KEY, c VARCHAR(255)) ENGINE=InnoDB; | ||
| INSERT INTO t1 VALUES (1, 'initial_state'); | ||
| sync_slave_with_master; | ||
|
|
||
| connection slave; | ||
| set @save_slave_exec_mode = @@global.slave_exec_mode; | ||
| set @@global.slave_exec_mode = STRICT; | ||
| # Modify the non-PK columns to simulate divergence | ||
| UPDATE t1 SET c = 'diverged_to_pass'; | ||
| SELECT * FROM t1; | ||
|
|
||
| connection master; | ||
| UPDATE t1 SET c = 'master_update1'; | ||
|
|
||
| --echo # | ||
| --echo # Expect it to succeed (slave gets in sync to have applied it). | ||
| --echo # Exposing MDEV-39500: The slave applier uses the PK to find the row but | ||
| --echo # fails to reject the update despite the before-image mismatch, applying it | ||
| --echo # silently without throwing HA_ERR_RECORD_CHANGED (or similar). | ||
| --echo # | ||
| sync_slave_with_master; | ||
|
|
||
| connection slave; | ||
| --echo # The slave has silently synced and overwritten the local changes | ||
| SELECT * FROM t1; | ||
|
|
||
| --let $diff_tables= master:t1, slave:t1 | ||
| --source include/diff_tables.inc | ||
|
|
||
| # the new mode | ||
| connection slave; | ||
| set @@global.slave_exec_mode = STRINGENT; | ||
| call mtr.add_suppression("Slave: Replicated 'Update_rows_v1' record.s before-image on table"); | ||
|
|
||
| # Modify the non-PK columns to simulate divergence | ||
| UPDATE t1 SET c = 'diverged_to_fail'; | ||
| SELECT * FROM t1; | ||
|
|
||
| connection master; | ||
| UPDATE t1 SET c = 'master_update2'; | ||
|
|
||
| connection slave; | ||
| --echo # | ||
| --echo # Expect it to fail. | ||
| --echo # | ||
| # ER_INCONSISTENT_SLAVE_RECORD | ||
| --let $slave_sql_errno = 4264 | ||
| --source include/wait_for_slave_sql_error.inc | ||
|
|
||
| connection slave; | ||
| --echo # the stopped slave deverges | ||
| SELECT * FROM t1; | ||
|
|
||
| set @@global.slave_exec_mode = STRICT; | ||
| --source include/start_slave.inc | ||
| --echo Not anymore in STRICT mode | ||
| SELECT * FROM t1; | ||
|
|
||
| connection master; | ||
| sync_slave_with_master; | ||
|
|
||
| --echo # | ||
| --echo # Cleanup | ||
| --echo # | ||
| connection master; | ||
| DROP TABLE t1; | ||
| sync_slave_with_master; | ||
| set @@global.slave_exec_mode = @save_slave_exec_mode; | ||
| --source include/rpl_end.inc |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,7 +119,8 @@ enum enum_ha_read_modes { RFIRST, RNEXT, RPREV, RLAST, RKEY, RNEXT_SAME }; | |
| enum enum_duplicates { DUP_ERROR, DUP_REPLACE, DUP_UPDATE }; | ||
| enum enum_delay_key_write { DELAY_KEY_WRITE_NONE, DELAY_KEY_WRITE_ON, | ||
| DELAY_KEY_WRITE_ALL }; | ||
| enum enum_slave_exec_mode { SLAVE_EXEC_MODE_STRICT, | ||
| enum enum_slave_exec_mode { SLAVE_EXEC_MODE_STRINGENT, | ||
| SLAVE_EXEC_MODE_STRICT, | ||
| SLAVE_EXEC_MODE_IDEMPOTENT, | ||
| SLAVE_EXEC_MODE_LAST_BIT }; | ||
|
Comment on lines
+122
to
125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding SLAVE_EXEC_MODE_STRINGENT at the beginning of the enum_slave_exec_mode changes the numeric values of existing modes (STRICT becomes 1 instead of 0, IDEMPOTENT becomes 2 instead of 1). This is a breaking change for any external tools or scripts that rely on the numeric values of @@slave_exec_mode. Furthermore, if the default value is initialized to 0, the default behavior of the server will silently change from STRICT to STRINGENT. It is safer to add new execution modes at the end of the enum, before SLAVE_EXEC_MODE_LAST_BIT. enum enum_slave_exec_mode { SLAVE_EXEC_MODE_STRICT,
SLAVE_EXEC_MODE_IDEMPOTENT,
SLAVE_EXEC_MODE_STRINGENT,
SLAVE_EXEC_MODE_LAST_BIT }; |
||
| enum enum_slave_run_triggers_for_rbr { SLAVE_RUN_TRIGGERS_FOR_RBR_NO, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3684,7 +3684,8 @@ Sys_slave_compressed_protocol( | |
| DEFAULT(FALSE)); | ||
|
|
||
| #ifdef HAVE_REPLICATION | ||
| static const char *slave_exec_mode_names[]= {"STRICT", "IDEMPOTENT", 0}; | ||
| static const char *slave_exec_mode_names[]= | ||
| {"STRINGENT", "STRICT", "IDEMPOTENT", 0}; | ||
|
Comment on lines
+3687
to
+3688
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The order of names in slave_exec_mode_names must match the order of the constants in enum enum_slave_exec_mode. If you move STRINGENT to the end of the enum as suggested, you must also move it to the end of this array to maintain consistency and avoid breaking the default value mapping. static const char *slave_exec_mode_names[]=
{"STRICT", "IDEMPOTENT", "STRINGENT", 0}; |
||
| static Sys_var_on_access_global<Sys_var_enum, | ||
| PRIV_SET_SYSTEM_GLOBAL_VAR_SLAVE_EXEC_MODE> | ||
| Slave_exec_mode( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When STRINGENT mode is enabled, the code performs a record_compare at the end of find_row. However, record_compare expects the before-image to be in table->record[1] and the found row in table->record[0]. In the index lookup path (e.g., PK lookup), table->record[0] is overwritten by the storage engine during the read operation, and the original before-image is lost because it was never copied to table->record[1]. This results in record_compare comparing the found row against uninitialized or stale data in record[1]. You must copy the before-image to record[1] before the index lookup occurs.