Bug#17302896 DOUBLE PURGE ON ROLLBACK OF UPDATING A DELETE-MARKED RECORD
There was a race condition in the rollback of TRX_UNDO_UPD_DEL_REC. Once row_undo_mod_clust() has rolled back the changes by the rolling-back transaction, it attempts to purge the delete-marked record, if possible, in a separate mini-transaction. However, row_undo_mod_remove_clust_low() fails to check if the DB_TRX_ID of the record that it found after repositioning the cursor, is still the same. If it is not, it means that the record was purged and another record was inserted in its place. So, the rollback would have performed an incorrect purge, breaking the locking rules and causing corruption. The problem was found by creating a table that contains a unique secondary index and a primary key, and two threads running REPLACE with only one value for the unique column, so that the uniqueness constraint would be violated all the time, leading to statement rollback. This bug exists in all InnoDB versions (I checked MySQL 3.23.53). It has become easier to repeat in 5.5 and 5.6 thanks to scalability improvements and a dedicated purge thread. rb#3085 approved by Jimmy Yang
This commit is contained in:
parent
84b2f38d01
commit
5163c4a143
@ -1,7 +1,7 @@
|
||||
/******************************************************
|
||||
Undo modify of a row
|
||||
|
||||
(c) 1997 Innobase Oy
|
||||
Copyright (c) 1997, 2013, Oracle and/or its affiliates. All Rights Reserved.
|
||||
|
||||
Created 2/27/1997 Heikki Tuuri
|
||||
*******************************************************/
|
||||
@ -94,7 +94,10 @@ row_undo_mod_clust_low(
|
||||
}
|
||||
|
||||
/***************************************************************
|
||||
Removes a clustered index record after undo if possible. */
|
||||
Purges a clustered index record after undo if possible.
|
||||
This is attempted when the record was inserted by updating a
|
||||
delete-marked record and there no longer exist transactions
|
||||
that would see the delete-marked record. */
|
||||
static
|
||||
ulint
|
||||
row_undo_mod_remove_clust_low(
|
||||
@ -103,13 +106,16 @@ row_undo_mod_remove_clust_low(
|
||||
we may run out of file space */
|
||||
undo_node_t* node, /* in: row undo node */
|
||||
que_thr_t* thr __attribute__((unused)), /* in: query thread */
|
||||
mtr_t* mtr, /* in: mtr */
|
||||
mtr_t* mtr, /* in/out: mini-transaction */
|
||||
ulint mode) /* in: BTR_MODIFY_LEAF or BTR_MODIFY_TREE */
|
||||
{
|
||||
btr_pcur_t* pcur;
|
||||
btr_cur_t* btr_cur;
|
||||
ulint err;
|
||||
ibool success;
|
||||
byte* db_trx_id;
|
||||
|
||||
ut_ad(node->rec_type == TRX_UNDO_UPD_DEL_REC);
|
||||
|
||||
pcur = &(node->pcur);
|
||||
btr_cur = btr_pcur_get_btr_cur(pcur);
|
||||
@ -123,11 +129,37 @@ row_undo_mod_remove_clust_low(
|
||||
|
||||
/* Find out if we can remove the whole clustered index record */
|
||||
|
||||
if (node->rec_type == TRX_UNDO_UPD_DEL_REC
|
||||
&& !row_vers_must_preserve_del_marked(node->new_trx_id, mtr)) {
|
||||
if (row_vers_must_preserve_del_marked(node->new_trx_id, mtr)) {
|
||||
return(DB_SUCCESS);
|
||||
}
|
||||
|
||||
/* Ok, we can remove */
|
||||
if (!btr_cur_get_index(btr_cur)->trx_id_offset) {
|
||||
mem_heap_t* heap = NULL;
|
||||
ulint trx_id_col;
|
||||
ulint* offsets;
|
||||
ulint len;
|
||||
|
||||
trx_id_col = dict_index_get_sys_col_pos(
|
||||
btr_cur_get_index(btr_cur), DATA_TRX_ID);
|
||||
ut_ad(trx_id_col > 0);
|
||||
ut_ad(trx_id_col != ULINT_UNDEFINED);
|
||||
|
||||
offsets = rec_get_offsets(
|
||||
btr_cur_get_rec(btr_cur), btr_cur_get_index(btr_cur),
|
||||
NULL, trx_id_col + 1, &heap);
|
||||
|
||||
db_trx_id = rec_get_nth_field(btr_cur_get_rec(btr_cur),
|
||||
offsets, trx_id_col, &len);
|
||||
ut_ad(len == DATA_TRX_ID_LEN);
|
||||
mem_heap_free(heap);
|
||||
} else {
|
||||
db_trx_id = btr_cur_get_rec(btr_cur)
|
||||
+ btr_cur_get_index(btr_cur)->trx_id_offset;
|
||||
}
|
||||
|
||||
if (ut_dulint_cmp(trx_read_trx_id(db_trx_id), node->new_trx_id)) {
|
||||
/* The record must have been purged and then replaced
|
||||
with a different one. */
|
||||
return(DB_SUCCESS);
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,9 @@
|
||||
2013-08-15 The InnoDB Team
|
||||
|
||||
* row/row0umod.c:
|
||||
Fix Bug#17302896 DOUBLE PURGE ON ROLLBACK OF UPDATING
|
||||
A DELETE-MARKED RECORD
|
||||
|
||||
2013-08-14 The InnoDB Team
|
||||
|
||||
* btr/btr0cur.c, row/row0uins.c:
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*****************************************************************************
|
||||
|
||||
Copyright (c) 1997, 2010, Innobase Oy. All Rights Reserved.
|
||||
Copyright (c) 1997, 2013, Oracle and/or its affiliates. All Rights Reserved.
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under
|
||||
the terms of the GNU General Public License as published by the Free Software
|
||||
@ -128,11 +128,10 @@ row_undo_mod_clust_low(
|
||||
}
|
||||
|
||||
/***********************************************************//**
|
||||
Removes a clustered index record after undo if possible.
|
||||
Purges a clustered index record after undo if possible.
|
||||
This is attempted when the record was inserted by updating a
|
||||
delete-marked record and there no longer exist transactions
|
||||
that would see the delete-marked record. In other words, we
|
||||
roll back the insert by purging the record.
|
||||
that would see the delete-marked record.
|
||||
@return DB_SUCCESS, DB_FAIL, or error code: we may run out of file space */
|
||||
static
|
||||
ulint
|
||||
@ -140,11 +139,12 @@ row_undo_mod_remove_clust_low(
|
||||
/*==========================*/
|
||||
undo_node_t* node, /*!< in: row undo node */
|
||||
que_thr_t* thr, /*!< in: query thread */
|
||||
mtr_t* mtr, /*!< in: mtr */
|
||||
mtr_t* mtr, /*!< in/out: mini-transaction */
|
||||
ulint mode) /*!< in: BTR_MODIFY_LEAF or BTR_MODIFY_TREE */
|
||||
{
|
||||
btr_cur_t* btr_cur;
|
||||
ulint err;
|
||||
ulint trx_id_offset;
|
||||
|
||||
ut_ad(node->rec_type == TRX_UNDO_UPD_DEL_REC);
|
||||
|
||||
@ -159,6 +159,43 @@ row_undo_mod_remove_clust_low(
|
||||
|
||||
btr_cur = btr_pcur_get_btr_cur(&node->pcur);
|
||||
|
||||
trx_id_offset = btr_cur_get_index(btr_cur)->trx_id_offset;
|
||||
|
||||
if (!trx_id_offset) {
|
||||
mem_heap_t* heap = NULL;
|
||||
ulint trx_id_col;
|
||||
ulint* offsets;
|
||||
ulint len;
|
||||
|
||||
trx_id_col = dict_index_get_sys_col_pos(
|
||||
btr_cur_get_index(btr_cur), DATA_TRX_ID);
|
||||
ut_ad(trx_id_col > 0);
|
||||
ut_ad(trx_id_col != ULINT_UNDEFINED);
|
||||
|
||||
offsets = rec_get_offsets(
|
||||
btr_cur_get_rec(btr_cur), btr_cur_get_index(btr_cur),
|
||||
NULL, trx_id_col + 1, &heap);
|
||||
|
||||
trx_id_offset = rec_get_nth_field_offs(
|
||||
offsets, trx_id_col, &len);
|
||||
ut_ad(len == DATA_TRX_ID_LEN);
|
||||
mem_heap_free(heap);
|
||||
}
|
||||
|
||||
if (ut_dulint_cmp(trx_read_trx_id(btr_cur_get_rec(btr_cur)
|
||||
+ trx_id_offset),
|
||||
node->new_trx_id)) {
|
||||
/* The record must have been purged and then replaced
|
||||
with a different one. */
|
||||
return(DB_SUCCESS);
|
||||
}
|
||||
|
||||
/* We are about to remove an old, delete-marked version of the
|
||||
record that may have been delete-marked by a different transaction
|
||||
than the rolling-back one. */
|
||||
ut_ad(rec_get_deleted_flag(btr_cur_get_rec(btr_cur),
|
||||
dict_table_is_comp(node->table)));
|
||||
|
||||
if (mode == BTR_MODIFY_LEAF) {
|
||||
err = btr_cur_optimistic_delete(btr_cur, mtr)
|
||||
? DB_SUCCESS
|
||||
|
Loading…
x
Reference in New Issue
Block a user