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

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
parent 300ac936
/******************************************************
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);
}
if (!btr_cur_get_index(btr_cur)->trx_id_offset) {
mem_heap_t* heap = NULL;
ulint trx_id_col;
ulint* offsets;
ulint len;
/* Ok, we can remove */
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);
}
......
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:
......
/*****************************************************************************
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
......
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