Commit a8ee6e48 authored by Sergey Vojtovich's avatar Sergey Vojtovich

BUG#11763712 - 56458: KILLING A FLUSH TABLE FOR A MERGE/CHILD

                      CRASHES SERVER

Flushing of MERGE table or one of its child tables, which was
locked by flushing thread using LOCK TABLES, might have caused
crashes or assertion failures if the thread failed to reopen
child or parent table.
Particularly, this might have happened when another connection
killed this FLUSH TABLE statement/connection.
Also this problem might have occurred when we failed to reopen
MERGE table or one of its children when executing DDL statement
under LOCK TABLES.

The problem was caused by the fact that reopen_tables() might
have failed to reopen child table but still tried to reopen,
reattach children for and re-lock its parent. Vice versa it
might have failed to reopen parent but kept references from
children to parent around. Since reopen_tables() closes table
it has failed to reopen and therefore frees all associated
memory such dangling references led to crashes when followed.

This patch solves this problem by ensuring that we always close
parent table and all its children if we fail to reopen this
table or one of its children. Same happens if we fail to reattach
children to parent.

Affects 5.1 only.

mysql-test/r/merge.result:
  A test case for BUG#11763712.
mysql-test/t/merge.test:
  A test case for BUG#11763712.
sql/sql_base.cc:
  When flushing tables under LOCK TABLES, all locked
  and flushed tables are released and then reopened.
  It may happen that we failed to reopen some tables,
  in this case we reopen as much tables as possible.
  
  If it was not possible to reopen MERGE child, MERGE
  parent is unusable and must be removed from thread
  open tables list.
  
  If it was not possible to reopen MERGE parent, all
  MERGE child table objects are unusable as well, at
  least because their locks are handled by MERGE parent.
  They must also be removed from thread open tables
  list.
  
  In other words if it was impossible to reopen any
  object of a MERGE table or reattach child tables,
  all objects of this MERGE table must be considered
  unusable and closed.
parent 16f26d2a
...@@ -2341,4 +2341,33 @@ REPAIR TABLE m1; ...@@ -2341,4 +2341,33 @@ REPAIR TABLE m1;
Table Op Msg_type Msg_text Table Op Msg_type Msg_text
test.m1 repair note The storage engine for the table doesn't support repair test.m1 repair note The storage engine for the table doesn't support repair
DROP TABLE m1, t1; DROP TABLE m1, t1;
#
# BUG#11763712 - 56458: KILLING A FLUSH TABLE FOR A MERGE/CHILD
# CRASHES SERVER
#
CREATE TABLE t1(a INT);
CREATE TABLE t2(a INT);
CREATE TABLE t3(a INT, b INT);
CREATE TABLE m1(a INT) ENGINE=MERGE UNION=(t1, t2);
# Test reopen merge parent failure
LOCK TABLES m1 READ;
# Remove 'm1' table using file operations.
FLUSH TABLES;
ERROR 42S02: Table 'test.m1' doesn't exist
UNLOCK TABLES;
CREATE TABLE m1(a INT) ENGINE=MERGE UNION=(t1, t2);
# Test reopen merge child failure
LOCK TABLES m1 READ;
# Remove 't1' table using file operations.
FLUSH TABLES;
ERROR 42S02: Table 'test.t1' doesn't exist
UNLOCK TABLES;
CREATE TABLE t1(a INT);
# Test reattach merge failure
LOCK TABLES m1 READ;
# Replace 't1' with 't3' table using file operations.
FLUSH TABLES;
ERROR HY000: Can't reopen table: 'm1'
UNLOCK TABLES;
DROP TABLE t1, t2, t3, m1;
End of 5.1 tests End of 5.1 tests
...@@ -1783,4 +1783,49 @@ REPAIR TABLE m1; ...@@ -1783,4 +1783,49 @@ REPAIR TABLE m1;
# #
DROP TABLE m1, t1; DROP TABLE m1, t1;
--echo #
--echo # BUG#11763712 - 56458: KILLING A FLUSH TABLE FOR A MERGE/CHILD
--echo # CRASHES SERVER
--echo #
CREATE TABLE t1(a INT);
CREATE TABLE t2(a INT);
CREATE TABLE t3(a INT, b INT);
CREATE TABLE m1(a INT) ENGINE=MERGE UNION=(t1, t2);
--echo # Test reopen merge parent failure
LOCK TABLES m1 READ;
--echo # Remove 'm1' table using file operations.
remove_file $MYSQLD_DATADIR/test/m1.MRG;
remove_file $MYSQLD_DATADIR/test/m1.frm;
--error ER_NO_SUCH_TABLE
FLUSH TABLES;
UNLOCK TABLES;
CREATE TABLE m1(a INT) ENGINE=MERGE UNION=(t1, t2);
--echo # Test reopen merge child failure
LOCK TABLES m1 READ;
--echo # Remove 't1' table using file operations.
remove_file $MYSQLD_DATADIR/test/t1.frm;
remove_file $MYSQLD_DATADIR/test/t1.MYI;
remove_file $MYSQLD_DATADIR/test/t1.MYD;
--error ER_NO_SUCH_TABLE
FLUSH TABLES;
UNLOCK TABLES;
CREATE TABLE t1(a INT);
--echo # Test reattach merge failure
LOCK TABLES m1 READ;
--echo # Replace 't1' with 't3' table using file operations.
remove_file $MYSQLD_DATADIR/test/t1.frm;
remove_file $MYSQLD_DATADIR/test/t1.MYI;
remove_file $MYSQLD_DATADIR/test/t1.MYD;
copy_file $MYSQLD_DATADIR/test/t3.frm $MYSQLD_DATADIR/test/t1.frm;
copy_file $MYSQLD_DATADIR/test/t3.MYI $MYSQLD_DATADIR/test/t1.MYI;
copy_file $MYSQLD_DATADIR/test/t3.MYD $MYSQLD_DATADIR/test/t1.MYD;
--error ER_CANT_REOPEN_TABLE
FLUSH TABLES;
UNLOCK TABLES;
DROP TABLE t1, t2, t3, m1;
--echo End of 5.1 tests --echo End of 5.1 tests
...@@ -96,6 +96,13 @@ static TABLE_SHARE *oldest_unused_share, end_of_unused_share; ...@@ -96,6 +96,13 @@ static TABLE_SHARE *oldest_unused_share, end_of_unused_share;
static pthread_mutex_t LOCK_table_share; static pthread_mutex_t LOCK_table_share;
static bool table_def_inited= 0; static bool table_def_inited= 0;
/**
Dummy TABLE instance which is used in reopen_tables() and reattach_merge()
functions to mark MERGE tables and their children with which there is some
kind of problem and which therefore we need to close.
*/
static TABLE bad_merge_marker;
static int open_unireg_entry(THD *thd, TABLE *entry, TABLE_LIST *table_list, static int open_unireg_entry(THD *thd, TABLE *entry, TABLE_LIST *table_list,
const char *alias, const char *alias,
char *cache_key, uint cache_key_length, char *cache_key, uint cache_key_length,
...@@ -3214,47 +3221,66 @@ void close_data_files_and_morph_locks(THD *thd, const char *db, ...@@ -3214,47 +3221,66 @@ void close_data_files_and_morph_locks(THD *thd, const char *db,
} }
/**
@brief Mark merge parent and children with bad_merge_marker
@param[in,out] parent the TABLE object of the parent
*/
static void mark_merge_parent_and_children_as_bad(TABLE *parent)
{
TABLE_LIST *child_l;
DBUG_ENTER("mark_merge_parent_and_children_as_bad");
parent->parent= &bad_merge_marker;
for (child_l= parent->child_l; ; child_l= child_l->next_global)
{
child_l->table->parent= &bad_merge_marker;
child_l->table= NULL;
if (&child_l->next_global == parent->child_last_l)
break;
}
DBUG_VOID_RETURN;
}
/** /**
Reattach MERGE children after reopen. Reattach MERGE children after reopen.
@param[in] thd thread context @param[in] thd thread context
@param[in,out] err_tables_p pointer to pointer of tables in error
@note If reattach failed for certain MERGE table, the table (and all
it's children) are marked with bad_merge_marker.
@return status @return status
@retval FALSE OK, err_tables_p unchanged @retval FALSE OK
@retval TRUE Error, err_tables_p contains table(s) @retval TRUE Error
*/ */
static bool reattach_merge(THD *thd, TABLE **err_tables_p) static bool reattach_merge(THD *thd)
{ {
TABLE *table; TABLE *table;
TABLE *next;
TABLE **prv_p= &thd->open_tables;
bool error= FALSE; bool error= FALSE;
DBUG_ENTER("reattach_merge"); DBUG_ENTER("reattach_merge");
for (table= thd->open_tables; table; table= next) for (table= thd->open_tables; table; table= table->next)
{ {
next= table->next; DBUG_PRINT("tcache", ("check table: '%s'.'%s' 0x%lx",
DBUG_PRINT("tcache", ("check table: '%s'.'%s' 0x%lx next: 0x%lx",
table->s->db.str, table->s->table_name.str, table->s->db.str, table->s->table_name.str,
(long) table, (long) next)); (long) table));
/* Reattach children for MERGE tables with "closed data files" only. */ /*
if (table->child_l && !table->children_attached) Reattach children only for MERGE tables that had children or parent
with "closed data files" and were reopen. For extra safety skip MERGE
tables which we failed to reopen (should not happen with current code).
*/
if (table->child_l && table->parent != &bad_merge_marker &&
!table->children_attached)
{ {
DBUG_PRINT("tcache", ("MERGE parent, attach children")); DBUG_PRINT("tcache", ("MERGE parent, attach children"));
if(table->file->extra(HA_EXTRA_ATTACH_CHILDREN)) if (table->file->extra(HA_EXTRA_ATTACH_CHILDREN))
{ {
my_error(ER_CANT_REOPEN_TABLE, MYF(0), table->alias); my_error(ER_CANT_REOPEN_TABLE, MYF(0), table->alias);
error= TRUE; error= TRUE;
/* Remove table from open_tables. */ mark_merge_parent_and_children_as_bad(table);
*prv_p= next;
if (next)
prv_p= &next->next;
/* Stack table on error list. */
table->next= *err_tables_p;
*err_tables_p= table;
continue;
} }
else else
{ {
...@@ -3264,7 +3290,6 @@ static bool reattach_merge(THD *thd, TABLE **err_tables_p) ...@@ -3264,7 +3290,6 @@ static bool reattach_merge(THD *thd, TABLE **err_tables_p)
table->s->table_name.str, (long) table)); table->s->table_name.str, (long) table));
} }
} }
prv_p= &table->next;
} }
DBUG_RETURN(error); DBUG_RETURN(error);
} }
...@@ -3294,7 +3319,6 @@ bool reopen_tables(THD *thd, bool get_locks, bool mark_share_as_old) ...@@ -3294,7 +3319,6 @@ bool reopen_tables(THD *thd, bool get_locks, bool mark_share_as_old)
{ {
TABLE *table,*next,**prev; TABLE *table,*next,**prev;
TABLE **tables,**tables_ptr; // For locks TABLE **tables,**tables_ptr; // For locks
TABLE *err_tables= NULL;
bool error=0, not_used; bool error=0, not_used;
bool merge_table_found= FALSE; bool merge_table_found= FALSE;
const uint flags= MYSQL_LOCK_NOTIFY_IF_NEED_REOPEN | const uint flags= MYSQL_LOCK_NOTIFY_IF_NEED_REOPEN |
...@@ -3328,29 +3352,69 @@ bool reopen_tables(THD *thd, bool get_locks, bool mark_share_as_old) ...@@ -3328,29 +3352,69 @@ bool reopen_tables(THD *thd, bool get_locks, bool mark_share_as_old)
for (table=thd->open_tables; table ; table=next) for (table=thd->open_tables; table ; table=next)
{ {
uint db_stat=table->db_stat; uint db_stat=table->db_stat;
TABLE *parent= table->child_l ? table : table->parent;
next=table->next; next=table->next;
DBUG_PRINT("tcache", ("open table: '%s'.'%s' 0x%lx " DBUG_PRINT("tcache", ("open table: '%s'.'%s' 0x%lx "
"parent: 0x%lx db_stat: %u", "parent: 0x%lx db_stat: %u",
table->s->db.str, table->s->table_name.str, table->s->db.str, table->s->table_name.str,
(long) table, (long) table->parent, db_stat)); (long) table, (long) table->parent, db_stat));
if (table->child_l && !db_stat) /*
If we need to reopen child or parent table in a MERGE table, then
children in this MERGE table has to be already detached at this
point.
*/
DBUG_ASSERT(db_stat || !parent || !parent->children_attached);
/*
Thanks to the above assumption the below condition will guarantee that
merge_table_found is TRUE when we need to reopen child or parent table.
Note that it works even in situation when it is only a child and not a
parent that needs reopen (this can happen when get_locks == FALSE).
*/
if (table->child_l && !table->children_attached)
merge_table_found= TRUE; merge_table_found= TRUE;
if (!tables || (!db_stat && reopen_table(table)))
if (!tables)
{ {
my_error(ER_CANT_REOPEN_TABLE, MYF(0), table->alias);
/* /*
If we could not allocate 'tables', we may close open tables If we could not allocate 'tables' we close ALL open tables here.
here. If a MERGE table is affected, detach the children first. Before closing MERGE child or parent we need to detach children
It is not necessary to clear the child or parent table reference and/or clear references in/to them.
of this table because the TABLE is freed. But we need to clear
the child or parent references of the other belonging tables so
that they cannot be moved into the unused_tables chain with
these pointers set.
*/ */
if (table->child_l || table->parent) if (parent)
detach_merge_children(table, TRUE); detach_merge_children(table, TRUE);
VOID(hash_delete(&open_cache,(uchar*) table)); }
error=1; else if (table->parent == &bad_merge_marker)
{
/*
This is either a child or a parent of a MERGE table for which
we already decided that we are unable to reopen it. Close it.
Reset parent reference, it may be used while freeing the table.
*/
table->parent= NULL;
}
else if (!db_stat && reopen_table(table))
{
/*
If we fail to reopen a child or a parent in a MERGE table and the
MERGE table is affected for the first time, mark all relevant tables
invalid. Otherwise handle it as usual.
All in all we must end up with:
- child tables are detached from parent. This was done earlier,
but child<->parent references were kept valid for reopen.
- parent is not in the to-be-locked tables
- all child tables and parent are not in the THD::open_tables.
- all child tables and parent are not in the open_cache.
Please note that below we do additional pass through THD::open_tables
list to achieve the last three points.
*/
if (parent)
{
mark_merge_parent_and_children_as_bad(parent);
table->parent= NULL;
}
} }
else else
{ {
...@@ -3366,21 +3430,56 @@ bool reopen_tables(THD *thd, bool get_locks, bool mark_share_as_old) ...@@ -3366,21 +3430,56 @@ bool reopen_tables(THD *thd, bool get_locks, bool mark_share_as_old)
table->s->version=0; table->s->version=0;
table->open_placeholder= 0; table->open_placeholder= 0;
} }
continue;
} }
my_error(ER_CANT_REOPEN_TABLE, MYF(0), table->alias);
VOID(hash_delete(&open_cache, (uchar *) table));
error= 1;
} }
*prev=0; *prev=0;
/* /*
When all tables are open again, we can re-attach MERGE children to When all tables are open again, we can re-attach MERGE children to
their parents. All TABLE objects are still present. their parents.
If there was an error while reopening a child or a parent of a MERGE
table, or while reattaching child tables to their parents, some tables
may have been kept open but marked for close with bad_merge_marker.
Close these tables now.
*/ */
DBUG_PRINT("tcache", ("re-attaching MERGE tables: %d", merge_table_found)); if (tables && merge_table_found && (error|= reattach_merge(thd)))
if (!error && merge_table_found && reattach_merge(thd, &err_tables)) {
prev= &thd->open_tables;
for (table= thd->open_tables; table; table= next)
{
next= table->next;
if (table->parent == &bad_merge_marker)
{
/* Remove merge parent from to-be-locked tables array. */
if (get_locks && table->child_l)
{
TABLE **t;
for (t= tables; t < tables_ptr; t++)
{ {
while (err_tables) if (*t == table)
{ {
VOID(hash_delete(&open_cache, (uchar*) err_tables)); tables_ptr--;
err_tables= err_tables->next; memmove(t, t + 1, (tables_ptr - t) * sizeof(TABLE *));
break;
}
}
}
/* Reset parent reference, it may be used while freeing the table. */
table->parent= NULL;
/* Free table. */
VOID(hash_delete(&open_cache, (uchar *) table));
}
else
{
*prev= table;
prev= &table->next;
}
} }
*prev= 0;
} }
DBUG_PRINT("tcache", ("open tables to lock: %u", DBUG_PRINT("tcache", ("open tables to lock: %u",
(uint) (tables_ptr - tables))); (uint) (tables_ptr - tables)));
......
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