Commit fea4742d authored by unknown's avatar unknown
Browse files

fix for bug#16406 (Events: DROP DATABASE doesn't automatically drop events)

WL#1034
- This changeset also changes the executor so its quite more stable now.
  Stressing test case added that executes ~800 events per second and dropping
  hundreds of events at once using DROP DATABASE.
(with fixes after review of JimW)
(with fixes after review of Serg)


mysql-test/r/events.result:
  update results after TRIGGER_ACL was added
mysql-test/t/events.test:
  -redundant line
sql/event.cc:
  Implemented evex_db_drop_events() which drops all events
  from a specific database. Needed for SQLCOM_DROP_DATABASE
sql/event.h:
  - protect the event better (see the changes to event_executor.cc
    and event.cc). An event object could be used in a spawned thread
    before it's executed but till now the object is marked as being
    executed when the anonymous sp_head is executed. However, there are
    timeframes before and after that during which the event is not marked
    as executed and other thread may delete the object -> so we end with
    a nirvana pointer.
sql/event_executor.cc:
  - extract some of the code executed in the main thread to a function. Too long
    functions are bad for the overview.
  - prepend all information/error messages to the console with "SCHEDULER:" for
    better overview, and easied searching in the log tables.
sql/event_priv.h:
  - change the name, of evex_db_find_event_by_name() and don't
    used C++ features like function overloading
  - define consts for result returned from event_timed::spawn_now()
sql/event_timed.cc:
  - add few methods related to event execution.
    now the event spawns the worker thread and
    passes itself as parameter. This way it locks itself for exectution
    first and then spawning -> no race condition. When the worker thread
    has finished working with the reference it calls back
    event_timed::spawn_thread_finish() to unlock itself.
sql/sql_db.cc:
  - call evex_drop_db_events() on DROP DATABASE
parent 7088b39d
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -64,7 +64,7 @@ SHOW GRANTS;
Grants for ev_test@localhost
GRANT USAGE ON *.* TO 'ev_test'@'localhost'
GRANT ALL PRIVILEGES ON `events_test`.* TO 'ev_test'@'localhost'
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, REFERENCES, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE ON `events_test2`.* TO 'ev_test'@'localhost'
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, REFERENCES, INDEX, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, TRIGGER ON `events_test2`.* TO 'ev_test'@'localhost'
"Here comes an error:";
SHOW EVENTS;
ERROR 42000: Access denied for user 'ev_test'@'localhost' to database 'events_test2'
+46 −0
Original line number Diff line number Diff line
CREATE DATABASE IF NOT EXISTS events_test;
CREATE DATABASE events_test2;
USE events_test2;
CREATE EVENT ev_drop1 ON SCHEDULE EVERY 10 MINUTE DISABLE DO SELECT 1;
CREATE EVENT ev_drop2 ON SCHEDULE EVERY 10 MINUTE DISABLE DO SELECT 1;
CREATE EVENT ev_drop3 ON SCHEDULE EVERY 10 MINUTE DISABLE DO SELECT 1;
USE events_test;
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test2';
COUNT(*)
3
DROP DATABASE events_test2;
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test2';
COUNT(*)
0
"Now testing stability - dropping db -> events while they are running"
CREATE DATABASE events_test2;
USE events_test2;
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test2';
COUNT(*)
1000
SET GLOBAL event_scheduler=1;
DROP DATABASE events_test2;
SET GLOBAL event_scheduler=0;
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test2';
COUNT(*)
0
CREATE DATABASE events_test3;
USE events_test3;
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test3';
COUNT(*)
950
CREATE DATABASE events_test4;
USE events_test4;
CREATE DATABASE events_test2;
USE events_test2;
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test2';
COUNT(*)
1050
DROP DATABASE events_test2;
SET GLOBAL event_scheduler=0;
DROP DATABASE events_test3;
SET GLOBAL event_scheduler=1;
DROP DATABASE events_test4;
SET GLOBAL event_scheduler=1;
USE events_test;
DROP DATABASE events_test;
+0 −1
Original line number Diff line number Diff line
@@ -109,7 +109,6 @@ drop event one_event;




create event e_26 on schedule at '2017-01-01 00:00:00' disable do set @a = 5;
select db, name, body, definer, convert_tz(execute_at, 'UTC', 'SYSTEM'), on_completion from mysql.event;
drop event e_26;
+80 −0
Original line number Diff line number Diff line
CREATE DATABASE IF NOT EXISTS events_test;
#
# DROP DATABASE test start (bug #16406)
#
CREATE DATABASE events_test2;
USE events_test2;
CREATE EVENT ev_drop1 ON SCHEDULE EVERY 10 MINUTE DISABLE DO SELECT 1;
CREATE EVENT ev_drop2 ON SCHEDULE EVERY 10 MINUTE DISABLE DO SELECT 1;
CREATE EVENT ev_drop3 ON SCHEDULE EVERY 10 MINUTE DISABLE DO SELECT 1;
USE events_test;
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test2';
DROP DATABASE events_test2;
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test2';

--echo "Now testing stability - dropping db -> events while they are running"
CREATE DATABASE events_test2;
USE events_test2;
--disable_query_log
let $1= 1000;
while ($1)
{
  eval CREATE EVENT ev_drop$1 ON SCHEDULE EVERY 1 SECOND DO SELECT $1;
  dec $1;
}
--enable_query_log
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test2';
SET GLOBAL event_scheduler=1;
--sleep 4
DROP DATABASE events_test2;

SET GLOBAL event_scheduler=0;
--sleep 2
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test2';
CREATE DATABASE events_test3;
USE events_test3;
--disable_query_log
let $1= 950;
while ($1)
{
  eval CREATE EVENT ev_drop$1 ON SCHEDULE EVERY 1 SECOND DO SELECT $1;
  dec $1;
}
--enable_query_log
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test3';
--sleep 3
CREATE DATABASE events_test4;
USE events_test4;
--disable_query_log
let $1= 860;
while ($1)
{
  eval CREATE EVENT ev_drop$1 ON SCHEDULE EVERY 1 SECOND DO SELECT $1;
  dec $1;
}
--enable_query_log


CREATE DATABASE events_test2;
USE events_test2;
--disable_query_log
let $1= 1050;
while ($1)
{
  eval CREATE EVENT ev_drop$1 ON SCHEDULE EVERY 1 SECOND DO SELECT $1;
  dec $1;
}
--enable_query_log
SELECT COUNT(*) FROM INFORMATION_SCHEMA.EVENTS WHERE EVENT_SCHEMA='events_test2';
--sleep 6
DROP DATABASE events_test2;
SET GLOBAL event_scheduler=0;
DROP DATABASE events_test3;
SET GLOBAL event_scheduler=1;
DROP DATABASE events_test4;
SET GLOBAL event_scheduler=1;
USE events_test;
#
# DROP DATABASE test end (bug #16406)
#
DROP DATABASE events_test;
+218 −13
Original line number Diff line number Diff line
@@ -165,11 +165,34 @@ evex_open_event_table(THD *thd, enum thr_lock_type lock_type, TABLE **table)
}



/*
  Find row in open mysql.event table representing event

  SYNOPSIS
    evex_db_find_event_aux()
      thd    Thread context
      et     evet_timed object containing dbname, name & definer
      table  TABLE object for open mysql.event table.

  RETURN VALUE
    0                  - Routine found
    EVEX_KEY_NOT_FOUND - No routine with given name
*/

inline int
evex_db_find_event_aux(THD *thd, event_timed *et, TABLE *table)
{
  return evex_db_find_event_by_name(thd, et->dbname, et->name,
                                    et->definer, table);
}


/*
  Find row in open mysql.event table representing event

  SYNOPSIS
    evex_db_find_event_by_name()
      thd    Thread context
      dbname Name of event's database
      rname  Name of the event inside the db  
@@ -181,13 +204,13 @@ evex_open_event_table(THD *thd, enum thr_lock_type lock_type, TABLE **table)
*/

int
evex_db_find_event_aux(THD *thd, const LEX_STRING dbname,
evex_db_find_event_by_name(THD *thd, const LEX_STRING dbname,
                          const LEX_STRING ev_name,
                          const LEX_STRING user_name,
                          TABLE *table)
{
  byte key[MAX_KEY_LENGTH];
  DBUG_ENTER("evex_db_find_event_aux");
  DBUG_ENTER("evex_db_find_event_by_name");
  DBUG_PRINT("enter", ("name: %.*s", ev_name.length, ev_name.str));

  /*
@@ -373,7 +396,7 @@ db_create_event(THD *thd, event_timed *et, my_bool create_if_not,
  }
  
  DBUG_PRINT("info", ("check existance of an event with the same name"));
  if (!evex_db_find_event_aux(thd, et->dbname, et->name, et->definer, table))
  if (!evex_db_find_event_aux(thd, et, table))
  {
    if (create_if_not)
    {
@@ -511,7 +534,7 @@ db_update_event(THD *thd, event_timed *et, sp_name *new_name)
      goto err;    
    }
  
    if (!evex_db_find_event_aux(thd, new_name->m_db, new_name->m_name,
    if (!evex_db_find_event_by_name(thd, new_name->m_db, new_name->m_name,
                                et->definer, table))
    {
      my_error(ER_EVENT_ALREADY_EXISTS, MYF(0), new_name->m_name.str);
@@ -524,8 +547,7 @@ db_update_event(THD *thd, event_timed *et, sp_name *new_name)
    overwrite the key and SE will tell us that it cannot find the already found
    row (copied into record[1] later
  */
  if (EVEX_KEY_NOT_FOUND == evex_db_find_event_aux(thd, et->dbname, et->name,
                                                   et->definer, table))
  if (EVEX_KEY_NOT_FOUND == evex_db_find_event_aux(thd, et, table))
  {
    my_error(ER_EVENT_DOES_NOT_EXIST, MYF(0), et->name.str);
    goto err;    
@@ -603,7 +625,7 @@ db_find_event(THD *thd, sp_name *name, LEX_STRING definer, event_timed **ett,
    goto done;
  }

  if ((ret= evex_db_find_event_aux(thd, name->m_db, name->m_name, definer,
  if ((ret= evex_db_find_event_by_name(thd, name->m_db, name->m_name, definer,
                                       table)))
  {
    my_error(ER_EVENT_DOES_NOT_EXIST, MYF(0), name->m_name.str);
@@ -727,7 +749,7 @@ evex_remove_from_cache(LEX_STRING *db, LEX_STRING *name, bool use_lock,
    if (!sortcmp_lex_string(*name, et->name, system_charset_info) &&
        !sortcmp_lex_string(*db, et->dbname, system_charset_info))
    {
      if (!et->is_running())
      if (et->can_spawn_now())
      {
        DBUG_PRINT("evex_remove_from_cache", ("not running - free and delete"));
        et->free_sp();
@@ -887,7 +909,7 @@ evex_drop_event(THD *thd, event_timed *et, bool drop_if_exists,
    goto done;
  }

  if (!(ret= evex_db_find_event_aux(thd, et->dbname,et->name,et->definer,table)))
  if (!(ret= evex_db_find_event_aux(thd, et, table)))
  {
    if ((ret= table->file->ha_delete_row(table->record[0])))
    { 	
@@ -923,3 +945,186 @@ evex_drop_event(THD *thd, event_timed *et, bool drop_if_exists,
  DBUG_RETURN(ret);
}


/*
  evex_drop_db_events - Drops all events in the selected database
  
  thd  - Thread
  db   - ASCIIZ the name of the database
  
  Returns:
    0  - OK
    1  - Failed to delete a specific row
    2  - Got NULL while reading db name from a row

  Note:
    The algo is the following
    1. Go through the in-memory cache, if the scheduler is working
       and for every event whose dbname matches the database we drop
       check whether is currently in execution:
       - event_timed::can_spawn() returns true -> the event is not
         being executed in a child thread. The reason not to use
         event_timed::is_running() is that the latter shows only if
         it is being executed, which is 99% of the time in the thread
         but there are some initiliazations before and after the
         anonymous SP is being called. So if we delete in this moment
         -=> *boom*, so we have to check whether the thread has been
         spawned and can_spawn() is the right method.
       - event_timed::can_spawn() returns false -> being runned ATM
         just set the flags so it should drop itself.

*/

int
evex_drop_db_events(THD *thd, char *db)
{
  TABLE *table;
  READ_RECORD read_record_info;
  MYSQL_LOCK *lock;
  int ret= 0;
  int i;
  LEX_STRING db_lex= {db, strlen(db)};
  
  DBUG_ENTER("evex_drop_db_events");  
  DBUG_PRINT("info",("dropping events from %s", db));


  VOID(pthread_mutex_lock(&LOCK_event_arrays));

  if ((ret= evex_open_event_table(thd, TL_WRITE, &table)))
  {
    sql_print_error("Table mysql.event is damaged.");
    VOID(pthread_mutex_unlock(&LOCK_event_arrays));
    DBUG_RETURN(SP_OPEN_TABLE_FAILED);
  }

  DBUG_PRINT("info",("%d elements in the queue",
             evex_queue_num_elements(EVEX_EQ_NAME)));
  VOID(pthread_mutex_lock(&LOCK_evex_running));
  if (!evex_is_running)
    goto skip_memory;

  for (i= 0; i < evex_queue_num_elements(EVEX_EQ_NAME); ++i)
  {
    event_timed *et= evex_queue_element(&EVEX_EQ_NAME, i, event_timed*);
    if (sortcmp_lex_string(et->dbname, db_lex, system_charset_info))
      continue;

    if (et->can_spawn_now_n_lock(thd))
    {
      DBUG_PRINT("info",("event %s not running - direct delete", et->name.str));
      if (!(ret= evex_db_find_event_aux(thd, et, table)))
      {
        DBUG_PRINT("info",("event %s found on disk", et->name.str));
        if ((ret= table->file->ha_delete_row(table->record[0])))
        {
          sql_print_error("Error while deleting a row - dropping "
                          "a database. Skipping the rest.");
          my_error(ER_EVENT_DROP_FAILED, MYF(0), et->name.str);
          goto end;
        }
        DBUG_PRINT("info",("deleted event [%s] num [%d]. Time to free mem",
                   et->name.str, i));
      }
      else if (ret == EVEX_KEY_NOT_FOUND)
      {
        sql_print_error("Expected to find event %s.%s of %s on disk-not there.",
                         et->dbname.str, et->name.str, et->definer.str);
      }
      et->free_sp();
      delete et;
      et= 0;
      /* no need to call et->spawn_unlock because we already cleaned et */
    }
    else
    {
      DBUG_PRINT("info",("event %s is running. setting exec_no_more and dropped",
                  et->name.str));
      et->flags|= EVENT_EXEC_NO_MORE;
      et->dropped= TRUE;
    }
    DBUG_PRINT("info",("%d elements in the queue",
               evex_queue_num_elements(EVEX_EQ_NAME)));
    evex_queue_delete_element(&EVEX_EQ_NAME, i);// 1 is top
    DBUG_PRINT("info",("%d elements in the queue",
               evex_queue_num_elements(EVEX_EQ_NAME)));
    /*
      decrease so we start at the same position, there will be
      less elements in the queue, it will still be ordered so on
      next iteration it will be again i the current element or if
      no more we finish.
    */
    --i;
  }

skip_memory:
  /*
   The reasoning behind having two loops is the following:
   If there was only one loop, the table-scan, then for every element which
   matches, the queue in memory has to be searched to remove the element.
   While if we go first over the queue and remove what's in there we have only
   one pass over it and after finishing it, moving to table-scan for the disabled
   events. This needs quite less time and means quite less locking on
   LOCK_event_arrays.
  */
  DBUG_PRINT("info",("Mem-cache checked, now going to db for disabled events"));
  /* only enabled events are in memory, so we go now and delete the rest */
  init_read_record(&read_record_info, thd, table ,NULL,1,0);
  while (!(read_record_info.read_record(&read_record_info)) && !ret)
  {
    char *et_db;

    if ((et_db= get_field(thd->mem_root, table->field[EVEX_FIELD_DB])) == NULL)
    {
      ret= 2;
      break;
    }
    
    LEX_STRING et_db_lex= {et_db, strlen(et_db)};
    if (!sortcmp_lex_string(et_db_lex, db_lex, system_charset_info))
    {
      event_timed ett;
      char *ptr;
      
      if ((ptr= get_field(thd->mem_root, table->field[EVEX_FIELD_STATUS]))
           == NullS)
      {
        sql_print_error("Error while loading from mysql.event. "
                        "Table probably corrupted");
        goto end;
      }
      /*
        When not running nothing is in memory so we have to clean
        everything.
        We don't delete EVENT_ENABLED events when the scheduler is running
        because maybe this is an event which we asked to drop itself when
        it is finished and it hasn't finished yet, so we don't touch it.
        It will drop itself. The not running ENABLED events has been already
        deleted from ha_delete_row() above in the loop over the QUEUE
        (in case the executor is running).
        'D' stands for DISABLED, 'E' for ENABLED - it's an enum
      */
      if ((evex_is_running && ptr[0] == 'D') || !evex_is_running)
      {
        DBUG_PRINT("info", ("Dropping %s.%s", et_db, ett.name.str));
        if ((ret= table->file->ha_delete_row(table->record[0])))
        {
          my_error(ER_EVENT_DROP_FAILED, MYF(0), ett.name.str);
          goto end;
        }
      }
    }
  }
  DBUG_PRINT("info",("Disk checked for disabled events. Finishing."));

end:
  VOID(pthread_mutex_unlock(&LOCK_evex_running));
  VOID(pthread_mutex_unlock(&LOCK_event_arrays));
  end_read_record(&read_record_info);

  thd->version--;  // Force close to free memory

  close_thread_tables(thd);

  DBUG_RETURN(ret);
}
Loading