Commit d8ada081 authored by Marko Mäkelä's avatar Marko Mäkelä

MDEV-14477 InnoDB update_time is wrongly updated after partial rollback or internal COMMIT

The non-persistent UPDATE_TIME for InnoDB tables was not being
updated consistently at transaction commit.

If a transaction is partly rolled back so that in the end it will
not modify a table that it intended to modify, the update_time will
be updated nevertheless. This will also happen when InnoDB fails
to write an undo log record for the intended modification.

If a transaction is committed internally in InnoDB, instead of
being committed from the SQL interface, then the trx_t::mod_tables
will not be applied to the update_time of the tables.

trx_t::mod_tables: Replace the std::set<dict_table_t*>
with std::map<dict_table_t*,undo_no_t>, so that the very first
modification within the transaction is identified.

trx_undo_report_row_operation(): Update mod_tables for every operation
after the undo log record was successfully written.

trx_rollback_to_savepoint_low(): After partial rollback, erase from
trx_t::mod_tables any tables for which all changes were rolled back.

trx_commit_in_memory(): Tighten some assertions and simplify conditions.
Invoke trx_update_mod_tables_timestamp() if persistent tables were
affected.

trx_commit_for_mysql(): Remove the call to
trx_update_mod_tables_timestamp(), as it is now invoked at the
lower level, in trx_commit_in_memory().

trx_rollback_finish(): Clear mod_tables before invoking trx_commit(),
because the trx_commit_in_memory() would otherwise wrongly process
mod_tables after a full ROLLBACK.
parent fda4fabe
CREATE TABLE tab1(c1 int,c2 varchar(30), c3 BLOB) ENGINE=InnoDB;
CREATE TABLE tab1u LIKE tab1;
CREATE TABLE tab1d LIKE tab1;
CREATE TABLE tab1i LIKE tab1;
CREATE TABLE tab3(c1 int,c2 varchar(30)) ENGINE=InnoDB;
CREATE TABLE tab4(c1 int,c2 varchar(30)) ENGINE=InnoDB;
CREATE TABLE tab5(c1 int,c2 varchar(30)) ENGINE=InnoDB;
INSERT INTO tab1u VALUES(1,'Testing the wl6658','Testing the wl6658');
INSERT INTO tab1d VALUES(1,'Updated','Updated');
INSERT INTO tab4 VALUES(1,'Test for Update');
INSERT INTO tab5 VALUES(1,'Test for Delete');
CREATE TRIGGER test_trig BEFORE INSERT ON tab1
FOR EACH ROW BEGIN
INSERT INTO tab3 VALUES(1,'Inserted From Trigger');
UPDATE tab4 SET c2='Updated from Trigger' WHERE c1=1;
DELETE FROM tab5;
END |
CREATE TABLE tab2(
id INT NOT NULL,
store_name VARCHAR(30),
parts VARCHAR(30),
store_id INT
) ENGINE=InnoDB
PARTITION BY LIST(store_id) (
PARTITION pNorth VALUES IN (10,20,30),
PARTITION pEast VALUES IN (40,50,60),
PARTITION pWest VALUES IN (70,80,100)
);
SELECT update_time
FROM information_schema.tables WHERE table_name='tab2';
update_time
NULL
CREATE PROCEDURE proc_wl6658()
BEGIN
INSERT INTO tab2 VALUES(1,'ORACLE','NUTT',10);
INSERT INTO tab2 VALUES(2,'HUAWEI','BOLT',40);
COMMIT;
END |
CALL proc_wl6658;
SELECT * FROM tab2 ORDER BY id,store_id;
id store_name parts store_id
1 ORACLE NUTT 10
2 HUAWEI BOLT 40
SELECT COUNT(update_time)
FROM information_schema.tables WHERE table_name='tab2';
COUNT(update_time)
1
TRUNCATE TABLE tab2;
SELECT COUNT(update_time)
FROM information_schema.tables WHERE table_name='tab2';
COUNT(update_time)
1
CREATE TABLE tab7(c1 INT NOT NULL, PRIMARY KEY (c1)) ENGINE=INNODB;
CREATE TABLE tab8(c1 INT PRIMARY KEY,c2 INT,
FOREIGN KEY (c2) REFERENCES tab7(c1) ON DELETE CASCADE )
ENGINE=INNODB;
SELECT table_name,update_time
FROM information_schema.tables WHERE table_name IN ('tab7','tab8')
GROUP BY table_name ORDER BY table_name;
table_name update_time
tab7 NULL
tab8 NULL
INSERT INTO tab7 VALUES(1);
INSERT INTO tab8 VALUES(1,1);
SELECT table_name,COUNT(update_time)
FROM information_schema.tables WHERE table_name IN ('tab7','tab8')
GROUP BY table_name ORDER BY table_name;
table_name COUNT(update_time)
tab7 1
tab8 1
#restart the server
SELECT table_name,update_time
FROM information_schema.tables
WHERE table_name IN ('tab1','tab2','tab3','tab4','tab5','tab7','tab8')
ORDER BY table_name;
table_name update_time
tab1 NULL
tab2 NULL
tab3 NULL
tab4 NULL
tab5 NULL
tab7 NULL
tab8 NULL
#case1:
BEGIN WORK;
INSERT INTO tab1
VALUES(1,'Testing the wl6658', 'Testing the wl6658');
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1';
update_time
NULL
COMMIT;
SELECT * FROM tab1;
c1 c2 c3
1 Testing the wl6658 Testing the wl6658
SELECT * FROM tab3;
c1 c2
1 Inserted From Trigger
SELECT * FROM tab4;
c1 c2
1 Updated from Trigger
SELECT * FROM tab5;
c1 c2
SELECT table_name,COUNT(update_time)
FROM information_schema.tables
WHERE table_name IN ('tab1','tab3','tab4','tab5')
GROUP BY table_name ORDER BY table_name;
table_name COUNT(update_time)
tab1 1
tab3 1
tab4 1
tab5 1
Testcase with UPDATE stmt and transaction
SELECT * FROM tab1u;
c1 c2 c3
1 Testing the wl6658 Testing the wl6658
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1u';
update_time
NULL
#case2:
START TRANSACTION;
UPDATE tab1u SET c2='Updated',c3='Updated' WHERE c1=1;
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1u';
update_time
NULL
COMMIT;
SELECT * FROM tab1u;
c1 c2 c3
1 Updated Updated
SELECT COUNT(update_time)
FROM information_schema.tables WHERE table_name='tab1u';
COUNT(update_time)
1
SELECT * FROM tab1d;
c1 c2 c3
1 Updated Updated
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1d';
update_time
NULL
#case3:
START TRANSACTION;
DELETE FROM tab1d;
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1d';
update_time
NULL
COMMIT;
SELECT * FROM tab1d;
c1 c2 c3
SELECT COUNT(update_time)
FROM information_schema.tables WHERE table_name='tab1d';
COUNT(update_time)
1
SELECT * FROM tab1i;
c1 c2 c3
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1i';
update_time
NULL
#case4:
START TRANSACTION;
INSERT INTO tab1i
VALUES(1,'Testing the wl6658', 'Testing the wl6658');
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1i';
update_time
NULL
ROLLBACK;
SELECT * FROM tab1i;
c1 c2 c3
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1i';
update_time
NULL
BEGIN WORK;
DELETE FROM tab1i;
SAVEPOINT A;
INSERT INTO tab2 VALUES(1,'Oracle','NUTT',10);
INSERT INTO tab2 VALUES(2,'HUAWEI','BOLT',40);
SAVEPOINT B;
INSERT INTO tab2 VALUES(3,'IBM','NAIL',70);
SAVEPOINT C;
ROLLBACK to A;
SELECT * FROM tab2;
id store_name parts store_id
SELECT update_time
FROM information_schema.tables WHERE table_name='tab2';
update_time
NULL
#execute DDL instead of commit
create table tab6(c1 int);
SELECT update_time
FROM information_schema.tables WHERE table_name='tab2';
update_time
NULL
START TRANSACTION;
DELETE FROM tab7;
ROLLBACK;
SELECT * FROM tab7;
c1
1
SELECT * FROM tab8;
c1 c2
1 1
SELECT table_name,update_time
FROM information_schema.tables WHERE table_name IN ('tab7','tab8')
GROUP BY table_name ORDER BY table_name;
table_name update_time
tab7 NULL
tab8 NULL
DELETE FROM tab7;
SELECT * FROM tab7;
c1
SELECT * FROM tab8;
c1 c2
SELECT table_name,COUNT(update_time)
FROM information_schema.tables WHERE table_name IN ('tab7','tab8')
GROUP BY table_name ORDER BY table_name;
table_name COUNT(update_time)
tab7 1
tab8 1
#cleanup
DROP TRIGGER test_trig;
DROP TABLE tab1,tab1u,tab1d,tab1i,tab2,tab3,tab4,tab5,tab6,tab8,tab7;
DROP PROCEDURE proc_wl6658;
###################################################################
#Testing functionality of the WL6658
#case1: begin work with INSERT with Triggers
#case2: (tab1u) begin transaction with UPDATE
#case3: (tab1d) begin transaction with DELETE
#case4: (tab1i) Rollback & INSERT
#case5: (tab2) partitioned table and procedures
#case6: (tab2) SAVEPOINT
#case7: (tab7,tab8) pk-fk with ON DELETE CASCADE
###################################################################
--source include/no_valgrind_without_big.inc
--source include/have_innodb.inc
--source include/have_partition.inc
--source include/not_embedded.inc
CREATE TABLE tab1(c1 int,c2 varchar(30), c3 BLOB) ENGINE=InnoDB;
CREATE TABLE tab1u LIKE tab1;
CREATE TABLE tab1d LIKE tab1;
CREATE TABLE tab1i LIKE tab1;
CREATE TABLE tab3(c1 int,c2 varchar(30)) ENGINE=InnoDB;
CREATE TABLE tab4(c1 int,c2 varchar(30)) ENGINE=InnoDB;
CREATE TABLE tab5(c1 int,c2 varchar(30)) ENGINE=InnoDB;
INSERT INTO tab1u VALUES(1,'Testing the wl6658','Testing the wl6658');
INSERT INTO tab1d VALUES(1,'Updated','Updated');
INSERT INTO tab4 VALUES(1,'Test for Update');
INSERT INTO tab5 VALUES(1,'Test for Delete');
delimiter |;
CREATE TRIGGER test_trig BEFORE INSERT ON tab1
FOR EACH ROW BEGIN
INSERT INTO tab3 VALUES(1,'Inserted From Trigger');
UPDATE tab4 SET c2='Updated from Trigger' WHERE c1=1;
DELETE FROM tab5;
END |
delimiter ;|
CREATE TABLE tab2(
id INT NOT NULL,
store_name VARCHAR(30),
parts VARCHAR(30),
store_id INT
) ENGINE=InnoDB
PARTITION BY LIST(store_id) (
PARTITION pNorth VALUES IN (10,20,30),
PARTITION pEast VALUES IN (40,50,60),
PARTITION pWest VALUES IN (70,80,100)
);
SELECT update_time
FROM information_schema.tables WHERE table_name='tab2';
delimiter |;
CREATE PROCEDURE proc_wl6658()
BEGIN
INSERT INTO tab2 VALUES(1,'ORACLE','NUTT',10);
INSERT INTO tab2 VALUES(2,'HUAWEI','BOLT',40);
COMMIT;
END |
delimiter ;|
CALL proc_wl6658;
SELECT * FROM tab2 ORDER BY id,store_id;
SELECT COUNT(update_time)
FROM information_schema.tables WHERE table_name='tab2';
TRUNCATE TABLE tab2;
SELECT COUNT(update_time)
FROM information_schema.tables WHERE table_name='tab2';
CREATE TABLE tab7(c1 INT NOT NULL, PRIMARY KEY (c1)) ENGINE=INNODB;
CREATE TABLE tab8(c1 INT PRIMARY KEY,c2 INT,
FOREIGN KEY (c2) REFERENCES tab7(c1) ON DELETE CASCADE )
ENGINE=INNODB;
SELECT table_name,update_time
FROM information_schema.tables WHERE table_name IN ('tab7','tab8')
GROUP BY table_name ORDER BY table_name;
INSERT INTO tab7 VALUES(1);
INSERT INTO tab8 VALUES(1,1);
SELECT table_name,COUNT(update_time)
FROM information_schema.tables WHERE table_name IN ('tab7','tab8')
GROUP BY table_name ORDER BY table_name;
--echo #restart the server
--source include/restart_mysqld.inc
SELECT table_name,update_time
FROM information_schema.tables
WHERE table_name IN ('tab1','tab2','tab3','tab4','tab5','tab7','tab8')
ORDER BY table_name;
--echo #case1:
BEGIN WORK;
INSERT INTO tab1
VALUES(1,'Testing the wl6658', 'Testing the wl6658');
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1';
COMMIT;
SELECT * FROM tab1;
SELECT * FROM tab3;
SELECT * FROM tab4;
SELECT * FROM tab5;
SELECT table_name,COUNT(update_time)
FROM information_schema.tables
WHERE table_name IN ('tab1','tab3','tab4','tab5')
GROUP BY table_name ORDER BY table_name;
--echo Testcase with UPDATE stmt and transaction
SELECT * FROM tab1u;
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1u';
--echo #case2:
START TRANSACTION;
UPDATE tab1u SET c2='Updated',c3='Updated' WHERE c1=1;
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1u';
COMMIT;
SELECT * FROM tab1u;
SELECT COUNT(update_time)
FROM information_schema.tables WHERE table_name='tab1u';
SELECT * FROM tab1d;
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1d';
--echo #case3:
START TRANSACTION;
DELETE FROM tab1d;
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1d';
COMMIT;
SELECT * FROM tab1d;
SELECT COUNT(update_time)
FROM information_schema.tables WHERE table_name='tab1d';
SELECT * FROM tab1i;
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1i';
--echo #case4:
START TRANSACTION;
INSERT INTO tab1i
VALUES(1,'Testing the wl6658', 'Testing the wl6658');
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1i';
ROLLBACK;
SELECT * FROM tab1i;
SELECT update_time
FROM information_schema.tables WHERE table_name='tab1i';
BEGIN WORK;
DELETE FROM tab1i;
SAVEPOINT A;
INSERT INTO tab2 VALUES(1,'Oracle','NUTT',10);
INSERT INTO tab2 VALUES(2,'HUAWEI','BOLT',40);
SAVEPOINT B;
INSERT INTO tab2 VALUES(3,'IBM','NAIL',70);
SAVEPOINT C;
ROLLBACK to A;
SELECT * FROM tab2;
SELECT update_time
FROM information_schema.tables WHERE table_name='tab2';
--echo #execute DDL instead of commit
create table tab6(c1 int);
SELECT update_time
FROM information_schema.tables WHERE table_name='tab2';
START TRANSACTION;
DELETE FROM tab7;
ROLLBACK;
SELECT * FROM tab7;
SELECT * FROM tab8;
SELECT table_name,update_time
FROM information_schema.tables WHERE table_name IN ('tab7','tab8')
GROUP BY table_name ORDER BY table_name;
DELETE FROM tab7;
SELECT * FROM tab7;
SELECT * FROM tab8;
SELECT table_name,COUNT(update_time)
FROM information_schema.tables WHERE table_name IN ('tab7','tab8')
GROUP BY table_name ORDER BY table_name;
--echo #cleanup
DROP TRIGGER test_trig;
DROP TABLE tab1,tab1u,tab1d,tab1i,tab2,tab3,tab4,tab5,tab6,tab8,tab7;
DROP PROCEDURE proc_wl6658;
...@@ -13350,7 +13350,7 @@ ha_innobase::delete_table( ...@@ -13350,7 +13350,7 @@ ha_innobase::delete_table(
iter != parent_trx->mod_tables.end(); iter != parent_trx->mod_tables.end();
++iter) { ++iter) {
dict_table_t* table_to_drop = *iter; dict_table_t* table_to_drop = iter->first;
if (strcmp(norm_name, table_to_drop->name.m_name) == 0) { if (strcmp(norm_name, table_to_drop->name.m_name) == 0) {
parent_trx->mod_tables.erase(table_to_drop); parent_trx->mod_tables.erase(table_to_drop);
......
...@@ -779,12 +779,13 @@ struct trx_lock_t { ...@@ -779,12 +779,13 @@ struct trx_lock_t {
bool start_stmt; bool start_stmt;
}; };
/** Type used to store the list of tables that are modified by a given /** Collection of persistent tables and their first modification
transaction. We store pointers to the table objects in memory because in a transaction.
We store pointers to the table objects in memory because
we know that a table object will not be destroyed while a transaction we know that a table object will not be destroyed while a transaction
that modified it is running. */ that modified it is running. */
typedef std::set< typedef std::map<
dict_table_t*, dict_table_t*, undo_no_t,
std::less<dict_table_t*>, std::less<dict_table_t*>,
ut_allocator<dict_table_t*> > trx_mod_tables_t; ut_allocator<dict_table_t*> > trx_mod_tables_t;
......
...@@ -1927,16 +1927,6 @@ trx_undo_report_row_operation( ...@@ -1927,16 +1927,6 @@ trx_undo_report_row_operation(
} else { } else {
ut_ad(!trx->read_only); ut_ad(!trx->read_only);
ut_ad(trx->id); ut_ad(trx->id);
if (UNIV_LIKELY(!clust_entry || clust_entry->info_bits
!= REC_INFO_DEFAULT_ROW)) {
/* Keep INFORMATION_SCHEMA.TABLES.UPDATE_TIME
up-to-date for persistent tables outside
instant ADD COLUMN. */
trx->mod_tables.insert(index->table);
} else {
ut_ad(index->is_instant());
}
pundo = &trx->rsegs.m_redo.undo; pundo = &trx->rsegs.m_redo.undo;
rseg = trx->rsegs.m_redo.rseg; rseg = trx->rsegs.m_redo.rseg;
} }
...@@ -2022,6 +2012,13 @@ trx_undo_report_row_operation( ...@@ -2022,6 +2012,13 @@ trx_undo_report_row_operation(
mutex_exit(&trx->undo_mutex); mutex_exit(&trx->undo_mutex);
if (!is_temp) {
trx->mod_tables.insert(
trx_mod_tables_t::value_type(
index->table,
undo->top_undo_no));
}
*roll_ptr = trx_undo_build_roll_ptr( *roll_ptr = trx_undo_build_roll_ptr(
!rec, rseg->id, page_no, offset); !rec, rseg->id, page_no, offset);
return(DB_SUCCESS); return(DB_SUCCESS);
......
...@@ -126,6 +126,14 @@ trx_rollback_to_savepoint_low( ...@@ -126,6 +126,14 @@ trx_rollback_to_savepoint_low(
trx_rollback_finish(trx); trx_rollback_finish(trx);
MONITOR_INC(MONITOR_TRX_ROLLBACK); MONITOR_INC(MONITOR_TRX_ROLLBACK);
} else { } else {
const undo_no_t limit = savept->least_undo_no;
for (trx_mod_tables_t::iterator i = trx->mod_tables.begin();
i != trx->mod_tables.end(); ) {
trx_mod_tables_t::iterator j = i++;
if (j->second >= limit) {
trx->mod_tables.erase(j);
}
}
trx->lock.que_state = TRX_QUE_RUNNING; trx->lock.que_state = TRX_QUE_RUNNING;
MONITOR_INC(MONITOR_TRX_ROLLBACK_SAVEPOINT); MONITOR_INC(MONITOR_TRX_ROLLBACK_SAVEPOINT);
} }
...@@ -1154,10 +1162,8 @@ trx_rollback_finish( ...@@ -1154,10 +1162,8 @@ trx_rollback_finish(
/*================*/ /*================*/
trx_t* trx) /*!< in: transaction */ trx_t* trx) /*!< in: transaction */
{ {
trx_commit(trx);
trx->mod_tables.clear(); trx->mod_tables.clear();
trx_commit(trx);
trx->lock.que_state = TRX_QUE_RUNNING; trx->lock.que_state = TRX_QUE_RUNNING;
} }
......
...@@ -783,7 +783,9 @@ trx_resurrect_table_locks( ...@@ -783,7 +783,9 @@ trx_resurrect_table_locks(
} }
if (trx->state == TRX_STATE_PREPARED) { if (trx->state == TRX_STATE_PREPARED) {
trx->mod_tables.insert(table); trx->mod_tables.insert(
trx_mod_tables_t::value_type(table,
0));
} }
lock_table_ix_resurrect(table, trx); lock_table_ix_resurrect(table, trx);
...@@ -1619,7 +1621,7 @@ trx_update_mod_tables_timestamp( ...@@ -1619,7 +1621,7 @@ trx_update_mod_tables_timestamp(
"garbage" in table->update_time is justified because "garbage" in table->update_time is justified because
protecting it with a latch here would be too performance protecting it with a latch here would be too performance
intrusive. */ intrusive. */
(*it)->update_time = now; it->first->update_time = now;
} }
trx->mod_tables.clear(); trx->mod_tables.clear();
...@@ -1725,8 +1727,12 @@ trx_commit_in_memory( ...@@ -1725,8 +1727,12 @@ trx_commit_in_memory(
trx->state = TRX_STATE_NOT_STARTED; trx->state = TRX_STATE_NOT_STARTED;
} else { } else {
const bool rw = trx->rsegs.m_redo.rseg != NULL;
if (trx->id > 0) { ut_ad(!trx->read_only || !rw);
ut_ad(trx->id || !rw);
if (rw) {
/* For consistent snapshot, we need to remove current /* For consistent snapshot, we need to remove current
transaction from running transaction id list for mvcc transaction from running transaction id list for mvcc
before doing commit and releasing locks. */ before doing commit and releasing locks. */
...@@ -1741,16 +1747,14 @@ trx_commit_in_memory( ...@@ -1741,16 +1747,14 @@ trx_commit_in_memory(
ut_ad(trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY)); ut_ad(trx_state_eq(trx, TRX_STATE_COMMITTED_IN_MEMORY));
DEBUG_SYNC_C("after_trx_committed_in_memory"); DEBUG_SYNC_C("after_trx_committed_in_memory");
if (trx->read_only || trx->rsegs.m_redo.rseg == NULL) { if (!rw) {
MONITOR_INC(MONITOR_TRX_RO_COMMIT); MONITOR_INC(MONITOR_TRX_RO_COMMIT);
if (trx->read_view != NULL) { if (trx->read_view != NULL) {
trx_sys->mvcc->view_close( trx_sys->mvcc->view_close(
trx->read_view, false); trx->read_view, false);
} }
} else { } else {
ut_ad(trx->id > 0); trx_update_mod_tables_timestamp(trx);
MONITOR_INC(MONITOR_TRX_RW_COMMIT); MONITOR_INC(MONITOR_TRX_RW_COMMIT);
} }
} }
...@@ -2205,10 +2209,6 @@ trx_commit_for_mysql( ...@@ -2205,10 +2209,6 @@ trx_commit_for_mysql(
trx->op_info = "committing"; trx->op_info = "committing";
if (trx->id != 0) {
trx_update_mod_tables_timestamp(trx);
}
trx_commit(trx); trx_commit(trx);
MONITOR_DEC(MONITOR_TRX_ACTIVE); MONITOR_DEC(MONITOR_TRX_ACTIVE);
......
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