Commit b5d95f02 authored by unknown's avatar unknown

BUG#26395: if crash during autocommit update to transactional table on master, slave fails

Now, every transaction (including autocommit transactions) start with
a BEGIN and end with a COMMIT/ROLLBACK in the binlog.
Added a test case, and updated lots of test case result files.


mysql-test/t/rpl_transaction-master.opt:
  BitKeeper file /home/sven/bk/b26395-autocommit-xa/5.0-rpl/mysql-test/t/rpl_transaction-master.opt
mysql-test/t/rpl_transaction-slave.opt:
  BitKeeper file /home/sven/bk/b26395-autocommit-xa/5.0-rpl/mysql-test/t/rpl_transaction-slave.opt
mysql-test/r/mix_innodb_myisam_binlog.result:
  Updated result file
mysql-test/r/multi_update.result:
  Updated result file
mysql-test/r/rpl_transaction.result:
  New result file for new test case.
mysql-test/r/sp_trans_log.result:
  Updated result file
mysql-test/r/variables-big.result:
  Updated result file
mysql-test/t/rpl_transaction.test:
  New test case.
sql/log.cc:
   - Always write BEGIN and COMMIT around statements, even in autocommit
     mode.
   - Added comments for binlog_commit and binlog_rollback.
sql/log_event.cc:
  Added debug trigger to avoid writing xid events to the binlog.
parent 59035063
......@@ -100,9 +100,10 @@ insert into t1 values(9);
insert into t2 select * from t1;
show binlog events from 98;
Log_name Pos Event_type Server_id End_log_pos Info
master-bin.000001 98 Query 1 # use `test`; insert into t1 values(9)
master-bin.000001 185 Xid 1 # COMMIT /* XID */
master-bin.000001 212 Query 1 # use `test`; insert into t2 select * from t1
master-bin.000001 98 Query 1 # use `test`; BEGIN
master-bin.000001 166 Query 1 # use `test`; insert into t1 values(9)
master-bin.000001 253 Xid 1 # COMMIT /* XID */
master-bin.000001 280 Query 1 # use `test`; insert into t2 select * from t1
delete from t1;
delete from t2;
reset master;
......@@ -111,19 +112,21 @@ begin;
insert into t2 select * from t1;
show binlog events from 98;
Log_name Pos Event_type Server_id End_log_pos Info
master-bin.000001 98 Query 1 # use `test`; insert into t1 values(10)
master-bin.000001 186 Xid 1 # COMMIT /* XID */
master-bin.000001 213 Query 1 # use `test`; insert into t2 select * from t1
master-bin.000001 98 Query 1 # use `test`; BEGIN
master-bin.000001 166 Query 1 # use `test`; insert into t1 values(10)
master-bin.000001 254 Xid 1 # COMMIT /* XID */
master-bin.000001 281 Query 1 # use `test`; insert into t2 select * from t1
insert into t1 values(11);
commit;
show binlog events from 98;
Log_name Pos Event_type Server_id End_log_pos Info
master-bin.000001 98 Query 1 # use `test`; insert into t1 values(10)
master-bin.000001 186 Xid 1 # COMMIT /* XID */
master-bin.000001 213 Query 1 # use `test`; insert into t2 select * from t1
master-bin.000001 307 Query 1 # use `test`; BEGIN
master-bin.000001 375 Query 1 # use `test`; insert into t1 values(11)
master-bin.000001 463 Xid 1 # COMMIT /* XID */
master-bin.000001 98 Query 1 # use `test`; BEGIN
master-bin.000001 166 Query 1 # use `test`; insert into t1 values(10)
master-bin.000001 254 Xid 1 # COMMIT /* XID */
master-bin.000001 281 Query 1 # use `test`; insert into t2 select * from t1
master-bin.000001 375 Query 1 # use `test`; BEGIN
master-bin.000001 443 Query 1 # use `test`; insert into t1 values(11)
master-bin.000001 531 Xid 1 # COMMIT /* XID */
alter table t2 engine=INNODB;
delete from t1;
delete from t2;
......@@ -235,25 +238,29 @@ master-bin.000001 98 Query 1 # use `test`; BEGIN
master-bin.000001 166 Query 1 # use `test`; insert into t1 values(16)
master-bin.000001 254 Query 1 # use `test`; insert into t1 values(18)
master-bin.000001 342 Xid 1 # COMMIT /* XID */
master-bin.000001 369 Query 1 # use `test`; delete from t1
master-bin.000001 446 Xid 1 # COMMIT /* XID */
master-bin.000001 473 Query 1 # use `test`; delete from t2
master-bin.000001 550 Xid 1 # COMMIT /* XID */
master-bin.000001 577 Query 1 # use `test`; alter table t2 type=MyISAM
master-bin.000001 666 Query 1 # use `test`; insert into t1 values (1)
master-bin.000001 754 Xid 1 # COMMIT /* XID */
master-bin.000001 781 Query 1 # use `test`; insert into t2 values (20)
master-bin.000001 870 Query 1 # use `test`; drop table t1,t2
master-bin.000001 949 Query 1 # use `test`; create temporary table ti (a int) engine=innodb
master-bin.000001 1059 Query 1 # use `test`; insert into ti values(1)
master-bin.000001 1146 Xid 1 # COMMIT /* XID */
master-bin.000001 1173 Query 1 # use `test`; create temporary table t1 (a int) engine=myisam
master-bin.000001 1283 Query 1 # use `test`; insert t1 values (1)
master-bin.000001 1366 Query 1 # use `test`; create table t0 (n int)
master-bin.000001 1452 Query 1 # use `test`; insert t0 select * from t1
master-bin.000001 1541 Query 1 # use `test`; insert into t0 select GET_LOCK("lock1",null)
master-bin.000001 1648 Query 1 # use `test`; create table t2 (n int) engine=innodb
master-bin.000001 1748 Query 1 # use `test`; DROP /*!40005 TEMPORARY */ TABLE IF EXISTS `test`.`t1`,`test`.`ti`
master-bin.000001 369 Query 1 # use `test`; BEGIN
master-bin.000001 437 Query 1 # use `test`; delete from t1
master-bin.000001 514 Xid 1 # COMMIT /* XID */
master-bin.000001 541 Query 1 # use `test`; BEGIN
master-bin.000001 609 Query 1 # use `test`; delete from t2
master-bin.000001 686 Xid 1 # COMMIT /* XID */
master-bin.000001 713 Query 1 # use `test`; alter table t2 type=MyISAM
master-bin.000001 802 Query 1 # use `test`; BEGIN
master-bin.000001 870 Query 1 # use `test`; insert into t1 values (1)
master-bin.000001 958 Xid 1 # COMMIT /* XID */
master-bin.000001 985 Query 1 # use `test`; insert into t2 values (20)
master-bin.000001 1074 Query 1 # use `test`; drop table t1,t2
master-bin.000001 1153 Query 1 # use `test`; create temporary table ti (a int) engine=innodb
master-bin.000001 1263 Query 1 # use `test`; BEGIN
master-bin.000001 1331 Query 1 # use `test`; insert into ti values(1)
master-bin.000001 1418 Xid 1 # COMMIT /* XID */
master-bin.000001 1445 Query 1 # use `test`; create temporary table t1 (a int) engine=myisam
master-bin.000001 1555 Query 1 # use `test`; insert t1 values (1)
master-bin.000001 1638 Query 1 # use `test`; create table t0 (n int)
master-bin.000001 1724 Query 1 # use `test`; insert t0 select * from t1
master-bin.000001 1813 Query 1 # use `test`; insert into t0 select GET_LOCK("lock1",null)
master-bin.000001 1920 Query 1 # use `test`; create table t2 (n int) engine=innodb
master-bin.000001 2020 Query 1 # use `test`; DROP /*!40005 TEMPORARY */ TABLE IF EXISTS `test`.`t1`,`test`.`ti`
do release_lock("lock1");
drop table t0,t2;
reset master;
......@@ -402,7 +409,7 @@ insert into t2 values (bug27417(1));
ERROR 23000: Duplicate entry '1' for key 1
show master status /* the offset must denote there is the query */;
File Position Binlog_Do_DB Binlog_Ignore_DB
master-bin.000001 267
master-bin.000001 335
select count(*) from t1 /* must be 1 */;
count(*)
1
......@@ -414,7 +421,7 @@ insert into t2 select bug27417(1) union select bug27417(2);
ERROR 23000: Duplicate entry '2' for key 1
show master status /* the offset must denote there is the query */;
File Position Binlog_Do_DB Binlog_Ignore_DB
master-bin.000001 290
master-bin.000001 358
select count(*) from t1 /* must be 2 */;
count(*)
2
......@@ -438,7 +445,7 @@ UPDATE t4,t3 SET t4.a=t3.a + bug27417(1) /* top level non-ta table */;
ERROR 23000: Duplicate entry '2' for key 1
show master status /* the offset must denote there is the query */;
File Position Binlog_Do_DB Binlog_Ignore_DB
master-bin.000001 301
master-bin.000001 369
select count(*) from t1 /* must be 4 */;
count(*)
4
......@@ -466,7 +473,7 @@ delete from t2;
ERROR 23000: Duplicate entry '1' for key 1
show master status /* the offset must denote there is the query */;
File Position Binlog_Do_DB Binlog_Ignore_DB
master-bin.000001 246
master-bin.000001 314
select count(*) from t1 /* must be 1 */;
count(*)
1
......@@ -483,7 +490,7 @@ delete t2.* from t2,t5 where t2.a=t5.a + 1;
ERROR 23000: Duplicate entry '1' for key 1
show master status /* the offset must denote there is the query */;
File Position Binlog_Do_DB Binlog_Ignore_DB
master-bin.000001 274
master-bin.000001 342
select count(*) from t1 /* must be 1 */;
count(*)
1
......@@ -501,7 +508,7 @@ count(*)
2
show master status /* the offset must denote there is the query */;
File Position Binlog_Do_DB Binlog_Ignore_DB
master-bin.000001 376
master-bin.000001 444
drop trigger trg_del_t2;
drop table t1,t2,t3,t4,t5;
drop function bug27417;
......
......@@ -545,7 +545,7 @@ a b
4 4
show master status /* there must be the UPDATE query event */;
File Position Binlog_Do_DB Binlog_Ignore_DB
master-bin.000001 260
master-bin.000001 328
delete from t1;
delete from t2;
insert into t1 values (1,2),(3,4),(4,4);
......@@ -555,7 +555,7 @@ UPDATE t2,t1 SET t2.a=t2.b where t2.a=t1.a;
ERROR 23000: Duplicate entry '4' for key 1
show master status /* there must be the UPDATE query event */;
File Position Binlog_Do_DB Binlog_Ignore_DB
master-bin.000001 275
master-bin.000001 343
drop table t1, t2;
drop table if exists t1, t2, t3;
CREATE TABLE t1 (a int, PRIMARY KEY (a));
......
stop slave;
drop table if exists t1,t2,t3,t4,t5,t6,t7,t8,t9;
reset master;
reset slave;
drop table if exists t1,t2,t3,t4,t5,t6,t7,t8,t9;
start slave;
CREATE TABLE tmyisam (a int) ENGINE = MYISAM;
CREATE TABLE tinnodb (a int) ENGINE = INNODB;
SHOW CREATE TABLE tmyisam;
Table Create Table
tmyisam CREATE TABLE `tmyisam` (
`a` int(11) default NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1
SHOW CREATE TABLE tinnodb;
Table Create Table
tinnodb CREATE TABLE `tinnodb` (
`a` int(11) default NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1
==== Test 1: Non-XA Engines ====
--- on master ---
SET AUTOCOMMIT = 1;
INSERT INTO tmyisam VALUES (1);
BEGIN;
INSERT INTO tmyisam VALUES (2);
INSERT INTO tmyisam VALUES (3);
COMMIT;
BEGIN;
INSERT INTO tmyisam VALUES (5);
INSERT INTO tmyisam VALUES (6);
ROLLBACK;
Warnings:
Warning 1196 Some non-transactional changed tables couldn't be rolled back
SELECT * FROM tmyisam ORDER BY a;
a
1
2
3
5
6
--- on slave ---
SELECT * FROM tmyisam ORDER BY a;
a
1
2
3
5
6
==== Test 2: Master crash before writing XID event on XA engine ====
--- on master ---
INSERT INTO tinnodb VALUES (1);
SELECT * FROM tinnodb ORDER BY a;
a
1
--- on slave ---
STOP SLAVE;
SHOW SLAVE STATUS;
Slave_IO_State
Master_Host 127.0.0.1
Master_User root
Master_Port #
Connect_Retry 1
Master_Log_File master-bin.000001
Read_Master_Log_Pos #
Relay_Log_File #
Relay_Log_Pos #
Relay_Master_Log_File master-bin.000001
Slave_IO_Running No
Slave_SQL_Running No
Replicate_Do_DB
Replicate_Ignore_DB
Replicate_Do_Table
Replicate_Ignore_Table #
Replicate_Wild_Do_Table
Replicate_Wild_Ignore_Table
Last_Errno 0
Last_Error
Skip_Counter 0
Exec_Master_Log_Pos #
Relay_Log_Space #
Until_Condition None
Until_Log_File
Until_Log_Pos 0
Master_SSL_Allowed No
Master_SSL_CA_File
Master_SSL_CA_Path
Master_SSL_Cert
Master_SSL_Cipher
Master_SSL_Key
Seconds_Behind_Master #
SELECT * FROM tinnodb ORDER BY a;
a
DROP TABLE tmyisam;
DROP TABLE tinnodb;
DROP TABLE tmyisam;
DROP TABLE tinnodb;
......@@ -16,6 +16,7 @@ show binlog events from 98 /* with fixes for #23333 will show there are 2 querie
Log_name Pos Event_type Server_id End_log_pos Info
master-bin.000001 # Query 1 # #
master-bin.000001 # Query 1 # #
master-bin.000001 # Query 1 # #
select count(*),@a from t1 /* must be 1,1 */|
count(*) @a
1 1
......
set session transaction_prealloc_size=1024*1024*1024*1;
show processlist;
Id User Host db Command Time State Info
1 root localhost test Query 0 NULL show processlist
6 root localhost test Query 0 NULL show processlist
set session transaction_prealloc_size=1024*1024*1024*2;
show processlist;
Id User Host db Command Time State Info
1 root localhost test Query 2 NULL show processlist
6 root localhost test Query 1 NULL show processlist
set session transaction_prealloc_size=1024*1024*1024*3;
show processlist;
Id User Host db Command Time State Info
1 root localhost test Query 0 NULL show processlist
6 root localhost test Query 0 NULL show processlist
set session transaction_prealloc_size=1024*1024*1024*4;
Warnings:
Warning 1292 Truncated incorrect transaction_prealloc_size value: '4294967296'
show processlist;
Id User Host db Command Time State Info
1 root localhost test Query 0 NULL show processlist
6 root localhost test Query 0 NULL show processlist
set session transaction_prealloc_size=1024*1024*1024*5;
Warnings:
Warning 1292 Truncated incorrect transaction_prealloc_size value: '5368709120'
show processlist;
Id User Host db Command Time State Info
1 root localhost test Query 0 NULL show processlist
6 root localhost test Query 0 NULL show processlist
--innodb --debug=d,do_not_write_xid
# Tests that transactions are replicated correctly, with various
# combinations of non-transactional and transactional non-XA tables.
# Also tests that an XA transaction where the master crashes just
# before writing the XID log event is executed correctly. See below
# for implementation details.
# Note: this test should not exist in 5.1 or higher. It has been
# replaced by rpl_ndb_transaction.test, which tests a superset of what
# this test tests.
source include/have_innodb.inc;
source include/master-slave.inc;
CREATE TABLE tmyisam (a int) ENGINE = MYISAM;
CREATE TABLE tinnodb (a int) ENGINE = INNODB;
SHOW CREATE TABLE tmyisam;
SHOW CREATE TABLE tinnodb;
--echo ==== Test 1: Non-XA Engines ====
# Test that everything works fine with non-XA engines. We just try
# all ways to do transactions involving ndb and/or myisam, with
# rollback or commit.
--echo --- on master ---
SET AUTOCOMMIT = 1;
INSERT INTO tmyisam VALUES (1);
BEGIN;
INSERT INTO tmyisam VALUES (2);
INSERT INTO tmyisam VALUES (3);
COMMIT;
BEGIN;
INSERT INTO tmyisam VALUES (5);
INSERT INTO tmyisam VALUES (6);
--warning 1196
ROLLBACK;
SELECT * FROM tmyisam ORDER BY a;
--echo --- on slave ---
--sync_slave_with_master
SELECT * FROM tmyisam ORDER BY a;
--echo ==== Test 2: Master crash before writing XID event on XA engine ====
# We now want to test the following scenario, to verify that BUG#26395
# has been fixed:
# "master and slave have a transactional table that uses XA. Master
# has AUTOCOMMIT on and executes a statement (in this case an
# INSERT). Master crashes just before writing the XID event."
# In this scenario, master will roll back, so slave should not execute
# the statement, and slave should roll back later when master is
# restarted.
# However, we the master to be alive so that we are sure it replicates
# the statement to the slave. So in the test case, we must therefore
# not crash the master. Instead, we fake the crash by just not writing
# the XID event to the binlog. This is done by the
# --debug=d,do_not_write_xid flag in the .opt file.
# So, unlike if the master had crashed, the master *will* execute the
# statement. But the slave should not execute it. Hence, after the
# first test is executed, the expected result on master is a table
# with one row, and on slave a table with no rows.
# To simulate the slave correctly, we wait until everything up to the
# XID is replicated. We cannot sync_slave_with_master, because that
# would wait for the transaction to end. Instead, we wait for
# "sufficiently long time". Then we stop the slave.
# Note: since this puts the master binlog in an inconsistent state,
# this should be the last test of the file.
--echo --- on master ---
--connection master
INSERT INTO tinnodb VALUES (1);
SELECT * FROM tinnodb ORDER BY a;
--echo --- on slave ---
--connection slave
--sleep 3
STOP SLAVE;
source include/wait_for_slave_to_stop.inc;
--replace_column 4 # 7 # 8 # 9 # 16 # 22 # 23 # 33 #
query_vertical SHOW SLAVE STATUS;
# the following statement should show that nothing has been replicated
SELECT * FROM tinnodb ORDER BY a;
# clean up
connection master;
DROP TABLE tmyisam;
DROP TABLE tinnodb;
connection slave;
DROP TABLE tmyisam;
DROP TABLE tinnodb;
......@@ -122,6 +122,20 @@ static int binlog_prepare(THD *thd, bool all)
return 0;
}
/**
This function is called once after each statement.
It has the responsibility to flush the transaction cache to the
binlog file on commits.
@param thd The client thread that executes the transaction.
@param all true if this is the last statement before a COMMIT
statement; false if either this is a statement in a
transaction but not the last, or if this is a statement
not inside a BEGIN block and autocommit is on.
@see handlerton::commit
*/
static int binlog_commit(THD *thd, bool all)
{
IO_CACHE *trans_log= (IO_CACHE*)thd->ha_data[binlog_hton.slot];
......@@ -134,7 +148,15 @@ static int binlog_commit(THD *thd, bool all)
// we're here because trans_log was flushed in MYSQL_LOG::log_xid()
DBUG_RETURN(0);
}
if (all)
/*
Write commit event if at least one of the following holds:
- the user sends an explicit COMMIT; or
- the autocommit flag is on, and we are not inside a BEGIN.
However, if the user has not sent an explicit COMMIT, and we are
either inside a BEGIN or run with autocommit off, then this is not
the end of a transaction and we should not write a commit event.
*/
if (all || !(thd->options & (OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN)))
{
Query_log_event qev(thd, STRING_WITH_LEN("COMMIT"), TRUE, FALSE);
qev.error_code= 0; // see comment in MYSQL_LOG::write(THD, IO_CACHE)
......@@ -144,6 +166,22 @@ static int binlog_commit(THD *thd, bool all)
DBUG_RETURN(binlog_end_trans(thd, trans_log, &invisible_commit));
}
/**
This function is called when a transaction involving a transactional
table is rolled back.
It has the responsibility to flush the transaction cache to the
binlog file. However, if the transaction does not involve
non-transactional tables, nothing needs to be logged.
@param thd The client thread that executes the transaction.
@param all true if this is the last statement before a COMMIT
statement; false if either this is a statement in a
transaction but not the last, or if this is a statement
not inside a BEGIN block and autocommit is on.
@see handlerton::rollback
*/
static int binlog_rollback(THD *thd, bool all)
{
int error=0;
......@@ -1817,9 +1855,11 @@ uint MYSQL_LOG::next_file_id()
IMPLEMENTATION
- To support transaction over replication, we wrap the transaction
with BEGIN/COMMIT or BEGIN/ROLLBACK in the binary log.
We want to write a BEGIN/ROLLBACK block when a non-transactional table
was updated in a transaction which was rolled back. This is to ensure
that the same updates are run on the slave.
If a transaction that only involves transactional tables is
rolled back, we do not binlog it. However, we write a
BEGIN/ROLLBACK block when a non-transactional table was updated
in a transaction which was rolled back. This is to ensure that
the same updates are run on the slave.
*/
bool MYSQL_LOG::write(THD *thd, IO_CACHE *cache, Log_event *commit_event)
......@@ -1837,32 +1877,34 @@ bool MYSQL_LOG::write(THD *thd, IO_CACHE *cache, Log_event *commit_event)
byte header[LOG_EVENT_HEADER_LEN];
/*
Log "BEGIN" at the beginning of the transaction.
which may contain more than 1 SQL statement.
Log "BEGIN" at the beginning of every transaction. Here, a
transaction is either a BEGIN..COMMIT block or a single
statement in autocommit mode.
*/
if (thd->options & (OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN))
{
Query_log_event qinfo(thd, STRING_WITH_LEN("BEGIN"), TRUE, FALSE);
/*
Imagine this is rollback due to net timeout, after all statements of
the transaction succeeded. Then we want a zero-error code in BEGIN.
In other words, if there was a really serious error code it's already
in the statement's events, there is no need to put it also in this
internally generated event, and as this event is generated late it
would lead to false alarms.
This is safer than thd->clear_error() against kills at shutdown.
*/
qinfo.error_code= 0;
/*
Now this Query_log_event has artificial log_pos 0. It must be adjusted
to reflect the real position in the log. Not doing it would confuse the
slave: it would prevent this one from knowing where he is in the
master's binlog, which would result in wrong positions being shown to
the user, MASTER_POS_WAIT undue waiting etc.
*/
if (qinfo.write(&log_file))
goto err;
}
Query_log_event qinfo(thd, STRING_WITH_LEN("BEGIN"), TRUE, FALSE);
/*
Imagine this is rollback due to net timeout, after all
statements of the transaction succeeded. Then we want a
zero-error code in BEGIN. In other words, if there was a
really serious error code it's already in the statement's
events, there is no need to put it also in this internally
generated event, and as this event is generated late it would
lead to false alarms.
This is safer than thd->clear_error() against kills at shutdown.
*/
qinfo.error_code= 0;
/*
Now this Query_log_event has artificial log_pos 0. It must be
adjusted to reflect the real position in the log. Not doing it
would confuse the slave: it would prevent this one from
knowing where he is in the master's binlog, which would result
in wrong positions being shown to the user, MASTER_POS_WAIT
undue waiting etc.
*/
if (qinfo.write(&log_file))
goto err;
/* Read from the file used to cache the queries .*/
if (reinit_io_cache(cache, READ_CACHE, 0, 0, 0))
goto err;
......
......@@ -3793,6 +3793,7 @@ Xid_log_event(const char* buf,
#ifndef MYSQL_CLIENT
bool Xid_log_event::write(IO_CACHE* file)
{
DBUG_EXECUTE_IF("do_not_write_xid", return 0;);
return write_header(file, sizeof(xid)) ||
my_b_safe_write(file, (byte*) &xid, sizeof(xid));
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment