Commit 11c57540 authored by kostja@bodhi.(none)'s avatar kostja@bodhi.(none)
Browse files

A fix and a test case for Bug#24918 drop table and lock / inconsistent

between perm and temp tables. Review fixes.

The original bug report complains that if we locked a temporary table
with LOCK TABLES statement, we would not leave LOCK TABLES mode
when this temporary table is dropped.

Additionally, the bug was escalated when it was discovered than
when a temporary transactional table that was previously
locked with LOCK TABLES statement was dropped, futher actions with
this table, such as UNLOCK TABLES, would lead to a crash.

The problem originates from incomplete support of transactional temporary
tables. When we added calls to handler::store_lock()/handler::external_lock()
to operations that work with such tables, we only covered the normal
server code flow and did not cover LOCK TABLES mode. 
In LOCK TABLES mode, ::external_lock(LOCK) would sometimes be called without
matching ::external_lock(UNLOCK), e.g. when a transactional temporary table
was dropped. Additionally, this table would be left in the list of LOCKed 
TABLES.

The patch aims to address this inadequacy. Now, whenever an instance
of 'handler' is destroyed, we assert that it was priorly
external_lock(UNLOCK)-ed. All the places that violate this assert
were fixed.

This patch introduces no changes in behavior -- the discrepancy in
behavior will be fixed when we start calling ::store_lock()/::external_lock()
for all tables, regardless whether they are transactional or not, 
temporary or not.
parent 9f8593e8
Loading
Loading
Loading
Loading
+28 −0
Original line number Diff line number Diff line
@@ -739,4 +739,32 @@ drop table if exists t1;
create table t1 (a int) engine=innodb;
alter table t1 alter a set default 1;
drop table t1;

Bug#24918 drop table and lock / inconsistent between 
perm and temp tables

Check transactional tables under LOCK TABLES

drop table if exists t24918, t24918_tmp, t24918_trans, t24918_trans_tmp, 
t24918_access;
create table t24918_access (id int);
create table t24918 (id int) engine=myisam;
create temporary table t24918_tmp (id int) engine=myisam;
create table t24918_trans (id int) engine=innodb;
create temporary table t24918_trans_tmp (id int) engine=innodb;
lock table t24918 write, t24918_tmp write, t24918_trans write, t24918_trans_tmp write;
drop table t24918;
select * from t24918_access;
ERROR HY000: Table 't24918_access' was not locked with LOCK TABLES
drop table t24918_trans;
select * from t24918_access;
ERROR HY000: Table 't24918_access' was not locked with LOCK TABLES
drop table t24918_trans_tmp;
select * from t24918_access;
ERROR HY000: Table 't24918_access' was not locked with LOCK TABLES
drop table t24918_tmp;
select * from t24918_access;
ERROR HY000: Table 't24918_access' was not locked with LOCK TABLES
unlock tables;
drop table t24918_access;
End of 5.0 tests
+34 −0
Original line number Diff line number Diff line
@@ -754,4 +754,38 @@ create table t1 (a int) engine=innodb;
alter table t1 alter a set default 1;
drop table t1;


--echo
--echo Bug#24918 drop table and lock / inconsistent between 
--echo perm and temp tables
--echo
--echo Check transactional tables under LOCK TABLES
--echo
--disable_warnings
drop table if exists t24918, t24918_tmp, t24918_trans, t24918_trans_tmp, 
t24918_access;
--enable_warnings
create table t24918_access (id int);
create table t24918 (id int) engine=myisam;
create temporary table t24918_tmp (id int) engine=myisam;
create table t24918_trans (id int) engine=innodb;
create temporary table t24918_trans_tmp (id int) engine=innodb;

lock table t24918 write, t24918_tmp write, t24918_trans write, t24918_trans_tmp write;
drop table t24918;
--error ER_TABLE_NOT_LOCKED
select * from t24918_access;
drop table t24918_trans;
--error ER_TABLE_NOT_LOCKED
select * from t24918_access;
drop table t24918_trans_tmp;
--error ER_TABLE_NOT_LOCKED
select * from t24918_access;
drop table t24918_tmp;
--error ER_TABLE_NOT_LOCKED
select * from t24918_access;
unlock tables;

drop table t24918_access;

--echo End of 5.0 tests
+37 −3
Original line number Diff line number Diff line
@@ -508,6 +508,29 @@ class handler :public Sql_alloc
  */
  virtual int rnd_init(bool scan) =0;
  virtual int rnd_end() { return 0; }
  /**
    Is not invoked for non-transactional temporary tables.

    Tells the storage engine that we intend to read or write data
    from the table. This call is prefixed with a call to handler::store_lock()
    and is invoked only for those handler instances that stored the lock.

    Calls to rnd_init/index_init are prefixed with this call. When table
    IO is complete, we call external_lock(F_UNLCK).
    A storage engine writer should expect that each call to
    ::external_lock(F_[RD|WR]LOCK is followed by a call to
    ::external_lock(F_UNLCK). If it is not, it is a bug in MySQL.

    The name and signature originate from the first implementation
    in MyISAM, which would call fcntl to set/clear an advisory
    lock on the data file in this method.

    @param   lock_type    F_RDLCK, F_WRLCK, F_UNLCK

    @return  non-0 in case of failure, 0 in case of success.
    When lock_type is F_UNLCK, the return value is ignored.
  */
  virtual int external_lock(THD *thd, int lock_type) { return 0; }

public:
  const handlerton *ht;                 /* storage engine of this handler */
@@ -548,6 +571,7 @@ class handler :public Sql_alloc
  uint raid_type,raid_chunks;
  FT_INFO *ft_handler;
  enum {NONE=0, INDEX, RND} inited;
  bool locked;
  bool  auto_increment_column_changed;
  bool implicit_emptied;                /* Can be !=0 only if HEAP */
  const COND *pushed_cond;
@@ -560,10 +584,11 @@ class handler :public Sql_alloc
    create_time(0), check_time(0), update_time(0),
    key_used_on_scan(MAX_KEY), active_index(MAX_KEY),
    ref_length(sizeof(my_off_t)), block_size(0),
    raid_type(0), ft_handler(0), inited(NONE), implicit_emptied(0),
    raid_type(0), ft_handler(0), inited(NONE),
    locked(FALSE), implicit_emptied(0),
    pushed_cond(NULL)
    {}
  virtual ~handler(void) { /* TODO: DBUG_ASSERT(inited == NONE); */ }
  virtual ~handler(void) { DBUG_ASSERT(locked == FALSE); /* TODO: DBUG_ASSERT(inited == NONE); */ }
  virtual handler *clone(MEM_ROOT *mem_root);
  int ha_open(const char *name, int mode, int test_if_locked);
  void adjust_next_insert_id_after_explicit_value(ulonglong nr);
@@ -597,6 +622,13 @@ class handler :public Sql_alloc

  virtual const char *index_type(uint key_number) { DBUG_ASSERT(0); return "";}

  int ha_external_lock(THD *thd, int lock_type)
  {
    int rc;
    DBUG_ENTER("ha_external_lock");
    locked= lock_type != F_UNLCK;
    DBUG_RETURN(external_lock(thd, lock_type));
  }
  int ha_index_init(uint idx)
  {
    DBUG_ENTER("ha_index_init");
@@ -689,7 +721,6 @@ class handler :public Sql_alloc
  virtual int extra_opt(enum ha_extra_function operation, ulong cache_size)
  { return extra(operation); }
  virtual int reset() { return extra(HA_EXTRA_RESET); }
  virtual int external_lock(THD *thd, int lock_type) { return 0; }
  virtual void unlock_row() {}
  virtual int start_stmt(THD *thd, thr_lock_type lock_type) {return 0;}
  /*
@@ -837,6 +868,9 @@ class handler :public Sql_alloc

  /* lock_count() can be more than one if the table is a MERGE */
  virtual uint lock_count(void) const { return 1; }
  /**
    Is not invoked for non-transactional temporary tables.
  */
  virtual THR_LOCK_DATA **store_lock(THD *thd,
				     THR_LOCK_DATA **to,
				     enum thr_lock_type lock_type)=0;
+29 −6
Original line number Diff line number Diff line
@@ -151,7 +151,8 @@ MYSQL_LOCK *mysql_lock_tables(THD *thd, TABLE **tables, uint count,
    }

    thd->proc_info="System lock";
    if (lock_external(thd, tables, count))
    if (sql_lock->table_count && lock_external(thd, sql_lock->table,
                                               sql_lock->table_count))
    {
      /* Clear the lock type of all lock data to avoid reusage. */
      reset_lock_data(sql_lock);
@@ -246,12 +247,12 @@ static int lock_external(THD *thd, TABLE **tables, uint count)
	 (*tables)->reginfo.lock_type <= TL_READ_NO_INSERT))
      lock_type=F_RDLCK;

    if ((error=(*tables)->file->external_lock(thd,lock_type)))
    if ((error= (*tables)->file->ha_external_lock(thd,lock_type)))
    {
      print_lock_error(error, (*tables)->file->table_type());
      for (; i-- ; tables--)
      {
	(*tables)->file->external_lock(thd, F_UNLCK);
	(*tables)->file->ha_external_lock(thd, F_UNLCK);
	(*tables)->current_lock=F_UNLCK;
      }
      DBUG_RETURN(error);
@@ -353,10 +354,28 @@ void mysql_unlock_read_tables(THD *thd, MYSQL_LOCK *sql_lock)
}


/**
  Try to find the table in the list of locked tables.
  In case of success, unlock the table and remove it from this list.

void mysql_lock_remove(THD *thd, MYSQL_LOCK *locked,TABLE *table)
  @note This function has a legacy side effect: the table is
  unlocked even if it is not found in the locked list.
  It's not clear if this side effect is intentional or still
  desirable. It might lead to unmatched calls to
  unlock_external(). Moreover, a discrepancy can be left
  unnoticed by the storage engine, because in
  unlock_external() we call handler::external_lock(F_UNLCK) only
  if table->current_lock is not F_UNLCK.

  @param  always_unlock   specify explicitly if the legacy side
                          effect is desired.
*/

void mysql_lock_remove(THD *thd, MYSQL_LOCK *locked,TABLE *table,
                       bool always_unlock)
{
  mysql_unlock_some_tables(thd, &table,1);
  if (always_unlock == TRUE)
    mysql_unlock_some_tables(thd, &table, /* table count */ 1);
  if (locked)
  {
    reg1 uint i;
@@ -370,6 +389,10 @@ void mysql_lock_remove(THD *thd, MYSQL_LOCK *locked,TABLE *table)

        DBUG_ASSERT(table->lock_position == i);

        /* Unlock if not yet unlocked */
        if (always_unlock == FALSE)
          mysql_unlock_some_tables(thd, &table, /* table count */ 1);

        /* Decrement table_count in advance, making below expressions easier */
        old_tables= --locked->table_count;

@@ -623,7 +646,7 @@ static int unlock_external(THD *thd, TABLE **table,uint count)
    if ((*table)->current_lock != F_UNLCK)
    {
      (*table)->current_lock = F_UNLCK;
      if ((error=(*table)->file->external_lock(thd, F_UNLCK)))
      if ((error= (*table)->file->ha_external_lock(thd, F_UNLCK)))
      {
	error_code=error;
	print_lock_error(error_code, (*table)->file->table_type());
+2 −1
Original line number Diff line number Diff line
@@ -1452,7 +1452,8 @@ MYSQL_LOCK *mysql_lock_tables(THD *thd, TABLE **table, uint count,
void mysql_unlock_tables(THD *thd, MYSQL_LOCK *sql_lock);
void mysql_unlock_read_tables(THD *thd, MYSQL_LOCK *sql_lock);
void mysql_unlock_some_tables(THD *thd, TABLE **table,uint count);
void mysql_lock_remove(THD *thd, MYSQL_LOCK *locked,TABLE *table);
void mysql_lock_remove(THD *thd, MYSQL_LOCK *locked,TABLE *table,
                       bool always_unlock);
void mysql_lock_abort(THD *thd, TABLE *table);
bool mysql_lock_abort_for_thread(THD *thd, TABLE *table);
MYSQL_LOCK *mysql_lock_merge(MYSQL_LOCK *a,MYSQL_LOCK *b);
Loading