Bug #32070 Worker threads cannot be killed with awake(THD::KILL_CONNECTION)
Submitted: 3 Nov 2007 1:15 Modified: 27 Aug 2009 16:07
Reporter: Chuck Bell Email Updates:
Status: Won't fix Impact on me:
None 
Category:MySQL Server Severity:S3 (Non-critical)
Version:5.2 OS:Any
Assigned to: Chuck Bell CPU Architecture:Any

[3 Nov 2007 1:15] Chuck Bell
Description:
Background
----------
We are using a thread to open and lock tables. This lock thread (as I call it) owns its own THD object and has the necessary mutex and condition variable for use. The use of the locking thread allows us to have several drivers running at the same time who require the use of open_and_lock_tables(). But there is a problem -- we cannot kill the locking thread. :| 

Problem
-------
We need a way to ensure the thread can be killed if we desire -- either via the backup kernel or when the thread that is running the kernel is killed. As it is now, the thread will not die when awakened with a KILL option. But thankfully behaves well in a normal system shutdown.

How to repeat:
Brief
-----
There are two ways to reproduce this bug.

1) Create a worker thread and try to kill it with awake(THD::KILL_CONNECTION).
2) Attempt to kill the worker thread from the SQL KILL <thread_id> command.

Neither method kills the thread.

More Information
----------------
This problem was found while working on BUG#31383. The online backup kernel has a design feature that allows for cancelling the backup process. A worker thread is used in a portion of the online backup code to open and lock tables. 

The code to cancel the worker thread is:

   lock_thd->awake(THD::KILL_CONNECTION);

The thread is in a sleeping state then this occurs. However, the locking thread is never killed and still shows up in the processlist. 

I have included a sample of the locking thread code below.

/**
   @brief Lock tables in driver.

   This method creates a new THD for use in the new thread. It calls
   the method to open and lock the tables.
  */
pthread_handler_t backup_thread_for_locking(void *arg) {
  Backup_thread_driver *drv= static_cast<Backup_thread_driver *>(arg);
  pthread_mutex_t THR_LOCK_driver_thread= drv->THR_LOCK_driver_thread;
  pthread_cond_t COND_driver_thread_wait= drv->COND_driver_thread_wait;

  DBUG_PRINT("info", ("Default_backup - lock_tables_in_separate_thread"));

  /*
    Turn off condition variable check for lock.
  */
  pthread_mutex_lock(&THR_LOCK_driver_thread);
  drv->lock_called= FALSE;
  pthread_mutex_unlock(&THR_LOCK_driver_thread);

  my_thread_init();
  pthread_detach_this_thread();

  /*
    First, create a new THD object.
  */
  DBUG_PRINT("info",("Online backup creating THD struct for thread"));
  THD *thd= new THD;
  if (unlikely(!thd))
  {
    delete thd;
    return (0);
  }
  drv->lock_thd= thd;

  thd->thread_stack = (char*)&thd; // remember where our stack is
  pthread_mutex_lock(&LOCK_thread_count);
  thd->thread_id= thread_id++;
  pthread_mutex_unlock(&LOCK_thread_count);
  if (unlikely(thd->store_globals())) // for a proper MEM_ROOT
  {
    delete thd;
    return (0);
  }

  thd->init_for_queries(); // opening tables needs a proper LEX
  thd->command= COM_DAEMON;
  thd->system_thread= SYSTEM_THREAD_BACKUP;
  thd->version= refresh_version;
  thd->set_time();
  thd->main_security_ctx.host_or_ip= "";
  thd->client_capabilities= 0;
  my_net_init(&thd->net, 0);
  thd->main_security_ctx.master_access= ~0;
  thd->main_security_ctx.priv_user= 0;
  thd->real_id= pthread_self();
  /*
    Making this thread visible to SHOW PROCESSLIST is useful for
    troubleshooting a backup job (why does it stall etc).
  */
  pthread_mutex_lock(&LOCK_thread_count);
  threads.append(thd);
  pthread_mutex_unlock(&LOCK_thread_count);
  lex_start(thd);
  mysql_reset_thd_for_next_command(thd);

  if (thd->killed)
    goto end;

  /* 
    Now open and lock the tables.
  */
  DBUG_PRINT("info",("Online backup open tables in thread"));
  if (!drv->tables_in_backup)
  {
    DBUG_PRINT("info",("Online backup locking error no tables to lock"));
    return (0);
  }

  /*
    As locking tables can be a long operation, we need to support
    cancellability during that time. So we publish our THD to the thread which
    created us.
  */
  if (!thd->killed && open_and_lock_tables(thd, drv->tables_in_backup))
  {
    DBUG_PRINT("info",("Online backup locking thread dying"));
    close_thread_tables(thd);
    return (0);
  }

  if (thd->killed)
    goto end;

  /*
    Turn on condition variable check for lock.
  */
  pthread_mutex_lock(&THR_LOCK_driver_thread);
  drv->lock_called= TRUE;
  pthread_mutex_unlock(&THR_LOCK_driver_thread);

  /*
    Part of work is done. Rest until woken up.
  */
  pthread_mutex_lock(&THR_LOCK_driver_thread);
  thd->enter_cond(&COND_driver_thread_wait, &THR_LOCK_driver_thread,
                  "Online backup driver thread: holding table locks");
  while (!thd->killed)
    pthread_cond_wait(&COND_driver_thread_wait, &THR_LOCK_driver_thread);
  thd->exit_cond("Online backup driver thread: terminating");

  DBUG_PRINT("info",("Online backup driver thread locking thread terminating"));

end:
  /*
    Cleanup and return.
  */
  close_thread_tables(thd);
  net_end(&thd->net);
  my_thread_end();
  delete thd;
  pthread_exit(0);
  return (0);
}

Suggested fix:
Guilhem has traced the problem to the method THD::awake() which has the condition:

    if (!system_thread)		// Don't abort locks
      mysys_var->abort=1;

However, we set the locking thread to have system_thread!=0. This means that mysys_var->abort won't be set to 1 and the locking thread will not die. We have a condition wait loop that will eventually fire leaving the thread waiting perpetually for the condition broadcast (which will never come if the backup is cancelled). So if online backup driver or the kernel is killed or if the backup process is killed, the locking thread may not die.

Proposed Solution
-----------------
Guilhem has proposed changing the above condition to:

    if (system_thread != SYSTEM_THREAD_DELAYED_INSERT) // Don't abort locks
      mysys_var->abort=1;

Which should allow all but the delayed insert threads to abort.
[6 Nov 2007 20:30] Guilhem Bichot
less intrusive proposal:
--- a/sql/sql_class.cc  2007-09-13 10:56:30 +02:00
+++ b/sql/sql_class.cc  2007-11-06 20:56:00 +01:00
@@ -839,8 +839,9 @@ void THD::awake(THD::killed_state state_
   if (mysys_var)
   {
     pthread_mutex_lock(&mysys_var->mutex);
-    if (!system_thread)                // Don't abort locks
-      mysys_var->abort=1;
+    if (system_thread == NON_SYSTEM_THREAD ||
+        system_thread == SYSTEM_THREAD_BACKUP)
+      mysys_var->abort= 1; // abort locks
     /*
       This broadcast could be up in the air if the victim thread
       exits the cond in the time between read and broadcast, but that is
It is in the latest changeset for WL#866:
http://lists.mysql.com/commits/37216
[27 Aug 2009 16:07] Chuck Bell
This is no longer relevant. Backup can be canceled without drama or error as described. OBE