MySQL Lists are EOL. Please join:

List:Commits« Previous MessageNext Message »
From:Konstantin Osipov Date:December 8 2009 8:38am
Subject:bzr commit into mysql-5.6-next-mr branch (kostja:2999) Bug#45035
View as plain text  
#At file:///opt/local/work/next-4284/ based on revid:kostja@stripped

 2999 Konstantin Osipov	2009-12-08
      Backport of:
      ----------------------------------------------------------
      revno: 2617.69.2
      committer: Konstantin Osipov <kostja@stripped>
      branch nick: 5.4-azalea-bugfixing
      timestamp: Mon 2009-08-03 19:26:04 +0400
      message:
        A fix and a test case for Bug#45035 "Altering table under LOCK TABLES
        results in "Error 1213 Deadlock found...".
      
        If a user had a table locked with LOCK TABLES
        for READ and for WRITE in the same connection, ALTER TABLE
        could fail.
      
        Root cause analysis:
      
        If a connection issues
      
        LOCK TABLE t1 write, t1 a read, t1 b read;
      
        the new LOCK TABLES code in 6.0 (part of WL 3726) will create
        the following list of TABLE_LIST objects
        (thd->locked_tables_list->m_locked_tables):
      
        {"t1" "b" tl_read_no_insert}, {"t1" "a" tl_read_no_insert},
        {"t1" "t1" tl_write }
      
        Later on, when we try to ALTER table t1, mysql_alter_table()
        closes all TABLE instances and releases its thr_lock locks,
        keeping only an exclusive metadata lock on t1.
      
        But when ALTER is finished, Locked_table_list::reopen_tables()
        tries to restore the original list of open and locked tables.
      
        Before this patch, it used to do so one by one:
        Open t1 b, get TL_READ_NO_INSERT lock,
        Open t1 a, get TL_READ_NO_INSERT lock
        Open t1, try to get TL_WRITE lock, deadlock.
      
        The cause of the deadlock is that thr_lock.c doesn't
        resolve the situation when the read list only consists
        of locks taken by the same thread, followed by this very
        thread trying to take a WRITE lock. Indeed, since
        thr_lock_multi always gets a sorted list of locks,
        WRITE locks always precede READ locks in the list
        to lock.
      
        Don't try to fix thr_lock.c deficiency, keep this
        code simple.
        Instead, try to take all thr_lock locks at once
        in ::reopen_tables().
     @ mysql-test/r/lock.result
        Update results: test case for Bug#45035.
     @ mysql-test/t/lock.test
        Add a test case for Bug#45035.
     @ sql/sql_base.cc
        Take all thr_lock locks at once in Locked_tables_list::reopen_tables().
     @ sql/sql_class.h
        Add a helper array to store tables for mysql_lock_tables()
        in reopen_tables().
     @ sql/sql_table.cc
        Update unlink_all_closed_tables() to the new signature.

    modified:
      mysql-test/r/lock.result
      mysql-test/t/lock.test
      sql/sql_base.cc
      sql/sql_class.h
      sql/sql_table.cc
=== modified file 'mysql-test/r/lock.result'
--- a/mysql-test/r/lock.result	2009-12-04 23:02:48 +0000
+++ b/mysql-test/r/lock.result	2009-12-08 08:38:45 +0000
@@ -282,5 +282,42 @@ insert into t1 values (1);
 # Ensure that metadata locks held by the transaction are released.
 drop table t1;
 #
+# Bug#45035 " Altering table under LOCK TABLES results in 
+# "Error 1213 Deadlock found..."
+#
+# When reopening tables under LOCK TABLES after ALTER TABLE,
+# 6.0 used to be taking thr_lock locks one by one, and
+# that would lead to a lock conflict. 
+# Check that taking all locks at once works.
+#
+drop table if exists t1;
+create table t1 (i int);
+lock tables t1 write, t1 as a read, t1 as b read;
+alter table t1 add column j int;
+unlock tables;
+drop table t1;
+create temporary table t1 (i int);
+#
+# This is just for test coverage purposes, 
+# when this is allowed, remove the --error.
+#
+lock tables t1 write, t1 as a read, t1 as b read;
+ERROR HY000: Can't reopen table: 't1'
+alter table t1 add column j int;
+unlock tables;
+drop table t1;
+#
+# Separate case for partitioned tables is important
+# because each partition has an own thr_lock object.
+#
+create table t1 (i int) partition by list (i)
+(partition p0 values in (1),
+partition p1 values in (2,3),
+partition p2 values in (4,5));
+lock tables t1 write, t1 as a read, t1 as b read;
+alter table t1 add column j int;
+unlock tables;
+drop table t1;
+#
 # End of 6.0 tests.
 #

=== modified file 'mysql-test/t/lock.test'
--- a/mysql-test/t/lock.test	2009-12-04 23:02:48 +0000
+++ b/mysql-test/t/lock.test	2009-12-08 08:38:45 +0000
@@ -346,5 +346,45 @@ drop table t1;
 
 
 --echo #
+--echo # Bug#45035 " Altering table under LOCK TABLES results in 
+--echo # "Error 1213 Deadlock found..."
+--echo #
+--echo # When reopening tables under LOCK TABLES after ALTER TABLE,
+--echo # 6.0 used to be taking thr_lock locks one by one, and
+--echo # that would lead to a lock conflict. 
+--echo # Check that taking all locks at once works.
+--echo #
+--disable_warnings
+drop table if exists t1;
+--enable_warnings
+create table t1 (i int);
+lock tables t1 write, t1 as a read, t1 as b read;
+alter table t1 add column j int;
+unlock tables;
+drop table t1;
+create temporary table t1 (i int);
+--echo #
+--echo # This is just for test coverage purposes, 
+--echo # when this is allowed, remove the --error.
+--echo #
+--error ER_CANT_REOPEN_TABLE
+lock tables t1 write, t1 as a read, t1 as b read;
+alter table t1 add column j int;
+unlock tables;
+drop table t1;
+--echo #
+--echo # Separate case for partitioned tables is important
+--echo # because each partition has an own thr_lock object.
+--echo #
+create table t1 (i int) partition by list (i)
+  (partition p0 values in (1),
+   partition p1 values in (2,3),
+   partition p2 values in (4,5));
+lock tables t1 write, t1 as a read, t1 as b read;
+alter table t1 add column j int;
+unlock tables;
+drop table t1;
+
+--echo #
 --echo # End of 6.0 tests.
 --echo #

=== modified file 'sql/sql_base.cc'
--- a/sql/sql_base.cc	2009-12-08 08:26:49 +0000
+++ b/sql/sql_base.cc	2009-12-08 08:38:45 +0000
@@ -2980,8 +2980,11 @@ Locked_tables_list::init_locked_tables(T
 {
   DBUG_ASSERT(thd->locked_tables_mode == LTM_NONE);
   DBUG_ASSERT(m_locked_tables == NULL);
+  DBUG_ASSERT(m_reopen_array == NULL);
+  DBUG_ASSERT(m_locked_tables_count == 0);
 
-  for (TABLE *table= thd->open_tables; table; table= table->next)
+  for (TABLE *table= thd->open_tables; table;
+       table= table->next, m_locked_tables_count++)
   {
     TABLE_LIST *src_table_list= table->pos_in_table_list;
     char *db, *table_name, *alias;
@@ -3021,7 +3024,24 @@ Locked_tables_list::init_locked_tables(T
     m_locked_tables_last= &dst_table_list->next_global;
     table->pos_in_locked_tables= dst_table_list;
   }
+  if (m_locked_tables_count)
+  {
+    /**
+      Allocate an auxiliary array to pass to mysql_lock_tables()
+      in reopen_tables(). reopen_tables() is a critical
+      path and we don't want to complicate it with extra allocations.
+    */
+    m_reopen_array= (TABLE**)alloc_root(&m_locked_tables_root,
+                                        sizeof(TABLE*) *
+                                        (m_locked_tables_count+1));
+    if (m_reopen_array == NULL)
+    {
+      unlock_locked_tables(0);
+      return TRUE;
+    }
+  }
   thd->locked_tables_mode= LTM_LOCK_TABLES;
+
   return FALSE;
 }
 
@@ -3070,6 +3090,8 @@ Locked_tables_list::unlock_locked_tables
   free_root(&m_locked_tables_root, MYF(0));
   m_locked_tables= NULL;
   m_locked_tables_last= &m_locked_tables;
+  m_reopen_array= NULL;
+  m_locked_tables_count= 0;
 }
 
 
@@ -3141,8 +3163,39 @@ void Locked_tables_list::unlink_from_lis
   @note This function is a no-op if we're not under LOCK TABLES.
 */
 
-void Locked_tables_list::unlink_all_closed_tables()
+void Locked_tables_list::
+unlink_all_closed_tables(THD *thd, MYSQL_LOCK *lock, size_t reopen_count)
 {
+  /* If we managed to take a lock, unlock tables and free the lock. */
+  if (lock)
+    mysql_unlock_tables(thd, lock);
+  /*
+    If a failure happened in reopen_tables(), we may have succeeded
+    reopening some tables, but not all.
+    This works when the connection was killed in mysql_lock_tables().
+  */
+  if (reopen_count)
+  {
+    pthread_mutex_lock(&LOCK_open);
+    while (reopen_count--)
+    {
+      /*
+        When closing the table, we must remove it
+        from thd->open_tables list.
+        We rely on the fact that open_table() that was used
+        in reopen_tables() always links the opened table
+        to the beginning of the open_tables list.
+      */
+      DBUG_ASSERT(thd->open_tables == m_reopen_array[reopen_count]);
+
+      thd->open_tables->pos_in_locked_tables->table= NULL;
+
+      close_thread_table(thd, &thd->open_tables);
+    }
+    broadcast_refresh();
+    pthread_mutex_unlock(&LOCK_open);
+  }
+  /* Exclude all closed tables from the LOCK TABLES list. */
   for (TABLE_LIST *table_list= m_locked_tables; table_list; table_list=
        table_list->next_global)
   {
@@ -3176,12 +3229,13 @@ Locked_tables_list::reopen_tables(THD *t
 {
   enum enum_open_table_action ot_action_unused;
   bool lt_refresh_unused;
+  size_t reopen_count= 0;
+  MYSQL_LOCK *lock;
+  MYSQL_LOCK *merged_lock;
 
   for (TABLE_LIST *table_list= m_locked_tables;
        table_list; table_list= table_list->next_global)
   {
-    MYSQL_LOCK *lock;
-
     if (table_list->table)                      /* The table was not closed */
       continue;
 
@@ -3189,33 +3243,42 @@ Locked_tables_list::reopen_tables(THD *t
     if (open_table(thd, table_list, thd->mem_root, &ot_action_unused,
                    MYSQL_OPEN_REOPEN))
     {
-      unlink_all_closed_tables();
+      unlink_all_closed_tables(thd, 0, reopen_count);
       return TRUE;
     }
     table_list->table->pos_in_locked_tables= table_list;
     /* See also the comment on lock type in init_locked_tables(). */
     table_list->table->reginfo.lock_type= table_list->lock_type;
+
+    DBUG_ASSERT(reopen_count < m_locked_tables_count);
+    m_reopen_array[reopen_count++]= table_list->table;
+  }
+  if (reopen_count)
+  {
     thd->in_lock_tables= 1;
-    lock= mysql_lock_tables(thd, &table_list->table, 1,
+    /*
+      We re-lock all tables with mysql_lock_tables() at once rather
+      than locking one table at a time because of the case
+      reported in Bug#45035: when the same table is present
+      in the list many times, thr_lock.c fails to grant READ lock
+      on a table that is already locked by WRITE lock, even if
+      WRITE lock is taken by the same thread. If READ and WRITE
+      lock are passed to thr_lock.c in the same list, everything
+      works fine. Patching legacy code of thr_lock.c is risking to
+      break something else.
+    */
+    lock= mysql_lock_tables(thd, m_reopen_array, reopen_count,
                             MYSQL_OPEN_REOPEN, &lt_refresh_unused);
     thd->in_lock_tables= 0;
-    if (lock)
-      lock= mysql_lock_merge(thd->lock, lock);
-    if (lock == NULL)
+    if (lock == NULL || (merged_lock=
+                         mysql_lock_merge(thd->lock, lock)) == NULL)
     {
-      /*
-        No one's seen this branch work. Recover and report an
-        error just in case.
-      */
-      pthread_mutex_lock(&LOCK_open);
-      close_thread_table(thd, &thd->open_tables);
-      pthread_mutex_unlock(&LOCK_open);
-      table_list->table= 0;
-      unlink_all_closed_tables();
-      my_error(ER_LOCK_DEADLOCK, MYF(0));
+      unlink_all_closed_tables(thd, lock, reopen_count);
+      if (! thd->killed)
+        my_error(ER_LOCK_DEADLOCK, MYF(0));
       return TRUE;
     }
-    thd->lock= lock;
+    thd->lock= merged_lock;
   }
   return FALSE;
 }

=== modified file 'sql/sql_class.h'
--- a/sql/sql_class.h	2009-12-03 23:52:05 +0000
+++ b/sql/sql_class.h	2009-12-08 08:38:45 +0000
@@ -1192,7 +1192,7 @@ private:
   Therefore, we can't allocate metadata locks on execution memory
   root -- as well as tables, the locks need to stay around till
   UNLOCK TABLES is called.
-  The locks are allocated in the memory root encapsulate in this
+  The locks are allocated in the memory root encapsulated in this
   class.
 
   Some SQL commands, like FLUSH TABLE or ALTER TABLE, demand that
@@ -1211,10 +1211,21 @@ private:
   MEM_ROOT m_locked_tables_root;
   TABLE_LIST *m_locked_tables;
   TABLE_LIST **m_locked_tables_last;
+  /** An auxiliary array used only in reopen_tables(). */
+  TABLE **m_reopen_array;
+  /**
+    Count the number of tables in m_locked_tables list. We can't
+    rely on thd->lock->table_count because it excludes
+    non-transactional temporary tables. We need to know
+    an exact number of TABLE objects.
+  */
+  size_t m_locked_tables_count;
 public:
   Locked_tables_list()
     :m_locked_tables(NULL),
-    m_locked_tables_last(&m_locked_tables)
+    m_locked_tables_last(&m_locked_tables),
+    m_reopen_array(NULL),
+    m_locked_tables_count(0)
   {
     init_sql_alloc(&m_locked_tables_root, MEM_ROOT_BLOCK_SIZE, 0);
   }
@@ -1228,7 +1239,9 @@ public:
   MEM_ROOT *locked_tables_root() { return &m_locked_tables_root; }
   void unlink_from_list(THD *thd, TABLE_LIST *table_list,
                         bool remove_from_locked_tables);
-  void unlink_all_closed_tables();
+  void unlink_all_closed_tables(THD *thd,
+                                MYSQL_LOCK *lock,
+                                size_t reopen_count);
   bool reopen_tables(THD *thd);
 };
 

=== modified file 'sql/sql_table.cc'
--- a/sql/sql_table.cc	2009-12-04 23:02:48 +0000
+++ b/sql/sql_table.cc	2009-12-08 08:38:45 +0000
@@ -4515,7 +4515,7 @@ static int prepare_for_repair(THD *thd, 
   }
 
 end:
-  thd->locked_tables_list.unlink_all_closed_tables();
+  thd->locked_tables_list.unlink_all_closed_tables(thd, NULL, 0);
   if (table == &tmp_table)
   {
     pthread_mutex_lock(&LOCK_open);
@@ -7597,7 +7597,7 @@ err_with_mdl:
     remove all references to the altered table from the list of locked
     tables and release the exclusive metadata lock.
   */
-  thd->locked_tables_list.unlink_all_closed_tables();
+  thd->locked_tables_list.unlink_all_closed_tables(thd, NULL, 0);
   if (target_mdl_request)
   {
     thd->mdl_context.release_lock(target_mdl_request->ticket);


Attachment: [text/bzr-bundle] bzr/kostja@sun.com-20091208083845-kzlpva8tonby909l.bundle
Thread
bzr commit into mysql-5.6-next-mr branch (kostja:2999) Bug#45035Konstantin Osipov8 Dec