← Back to team overview

maria-developers team mailing list archive

Re: MDEV-3917 multiple use locks (GET_LOCK) in one connection

 

Hi, Holyfoot!

(Kostja - there're a couple of questions at the end that you might want
to reply to)

On Apr 02, holyfoot@xxxxxxxxxxxx wrote:
> At file:///home/hf/wmar/mdev-3917/
> 
> ------------------------------------------------------------
> revno: 3510
> revision-id: holyfoot@xxxxxxxxxxxx-20130402085219-1sxuzssq7segeig4
> parent: igor@xxxxxxxxxxxx-20130331221855-1n7hagklb3soplxq
> committer: Alexey Botchkov <holyfoot@xxxxxxxxxxxx>
> branch nick: mdev-3917
> timestamp: Tue 2013-04-02 13:52:19 +0500
> message:
>   MDEV-3917 multiple use locks (GET_LOCK) in one connection.
>           The patch contributed by Konstantin Osipov applied.
>           Native comments:
>             Implement multiple user-level locks per connection.
>   
>             GET_LOCK() function in MySQL allows a connection  to hold at most
>             one user level lock. Taking a new lock automatically releases the
>             old lock, if any.
>   
>             The limit of one lock per session existed since  early versions
>             of MySQL didn't have a deadlock detector for SQL locks.
>             MDL patches in MySQL 5.5 added a deadlock detector,
>             so starting from 5.5 it became possible to take multiple locks
>             in any order -- a deadlock, should it occur, would be detected
>             and an error returned to the client which closed the wait chain.
>   
>             This is exactly what is done in this patch: ULLs are moved
>             to use MDL subsystem.
>   
>           also:
>             MDL_key::USER renamed to MDL::USER_LOCK

What does that mean?

>             boundary checks for user lock name added.

> === modified file 'sql/item_func.cc'
> --- a/sql/item_func.cc	2013-03-27 22:41:02 +0000
> +++ b/sql/item_func.cc	2013-04-02 08:52:19 +0000
> @@ -4010,7 +3896,121 @@ int Interruptible_wait::wait(mysql_cond_
>  
>  
>  /**
> -  Get a user level lock.  If the thread has an old lock this is first released.
> +  For locks with EXPLICIT duration, MDL returns a new ticket
> +  every time a lock is granted. This allows to implement recursive
> +  locks without extra allocation or additional data structures, such
> +  as below. However, if there are too many tickets in the same
> +  MDL_context, MDL_context::find_ticket() is getting too slow,
> +  since it's using a linear search.
> +  This is why a separate structure is allocated for a user
> +  level lock, and before requesting a new lock from MDL,
> +  GET_LOCK() checks thd->ull_hash if such lock is already granted,
> +  and if so, simply increments a reference counter.
> +*/
> +
> +class User_level_lock
> +{
> +public:
> +  MDL_ticket *lock;
> +  int refs;
> +};
> +
> +
> +/** Extract a hash key from User_level_lock. */
> +
> +uchar *ull_get_key(const uchar *ptr, size_t *length,
> +                   my_bool not_used __attribute__((unused)))
> +{
> +  User_level_lock *ull = (User_level_lock*) ptr;
> +  MDL_key *key = ull->lock->get_key();
> +  *length= key->length();
> +  return (uchar*) key->ptr();
> +}
> +
> +
> +/**
> +  Release all user level locks for this THD.
> +*/
> +
> +void mysql_ull_cleanup(THD *thd)
> +{
> +  User_level_lock *ull;
> +  DBUG_ENTER("mysql_ull_cleanup");
> +
> +  for (uint i= 0; i < thd->ull_hash.records; i++)
> +  {
> +    ull = (User_level_lock*) my_hash_element(&thd->ull_hash, i);
> +    thd->mdl_context.release_lock(ull->lock);
> +    free(ull);

my_free

> +  }
> +
> +  my_hash_free(&thd->ull_hash);
> +
> +  DBUG_VOID_RETURN;
> +}
> +
> +
> +/**
> +  Set explicit duration for metadata locks corresponding to
> +  user level locks to protect them from being released at the end
> +  of transaction.
> +*/
> +
> +void mysql_ull_set_explicit_lock_duration(THD *thd)
> +{
> +  User_level_lock *ull;
> +  DBUG_ENTER("mysql_ull_set_explicit_lock_duration");
> +
> +  for (uint i= 0; i < thd->ull_hash.records; i++)
> +  {
> +    ull= (User_level_lock*) my_hash_element(&thd->ull_hash, i);
> +    thd->mdl_context.set_lock_duration(ull->lock, MDL_EXPLICIT);
> +  }
> +  DBUG_VOID_RETURN;
> +}
> +
> +
> +/**
> +  When MDL detects a lock wait timeout, it pushes
> +  an error into the statement diagnostics area.
> +  For GET_LOCK(), lock wait timeout is not an error,
> +  but a special return value (0). NULL is returned in
> +  case of error.
> +  Capture and suppress lock wait timeout.
> +*/
> +
> +class Lock_wait_timeout_handler: public Internal_error_handler
> +{
> +public:
> +  Lock_wait_timeout_handler() :m_lock_wait_timeout(false) {}
> +
> +  bool m_lock_wait_timeout;
> +
> +  bool handle_condition(THD * /* thd */, uint sql_errno,
> +                        const char * /* sqlstate */,
> +                        MYSQL_ERROR::enum_warning_level /* level */,
> +                        const char *message,
> +                        MYSQL_ERROR ** /* cond_hdl */);
> +};
> +
> +bool
> +Lock_wait_timeout_handler::
> +handle_condition(THD * /* thd */, uint sql_errno,
> +                 const char * /* sqlstate */,
> +                 MYSQL_ERROR::enum_warning_level /* level */,
> +                 const char *message,
> +                 MYSQL_ERROR ** /* cond_hdl */)
> +{
> +  if (sql_errno == ER_LOCK_WAIT_TIMEOUT)
> +  {
> +    m_lock_wait_timeout= true;
> +    return true;                                /* condition handled */
> +  }
> +  return false;
> +}
> +
> +/**
> +  Get a user level lock.
>  
>    @retval
>      1    : Got lock
> @@ -4039,104 +4038,75 @@ longlong Item_func_get_lock::val_int()
>      it's not guaranteed to be same as on master.
>    */
>    if (thd->slave_thread)
> +  {
> +    null_value= 0;
>      DBUG_RETURN(1);
> +  }
>  
>    if (!res || !res->length())
> +    DBUG_RETURN(0);
> +
> +  if (res->length() > NAME_LEN)
>    {
> +    my_error(ER_TOO_LONG_IDENT, MYF(0), res->c_ptr_safe());
>      DBUG_RETURN(0);
>    }
> +
>    DBUG_PRINT("info", ("lock %.*s, thd=%ld", res->length(), res->ptr(),
>                        (long) thd->real_id));
> +  /* HASH entries are of type User_level_lock. */
> +  if (! my_hash_inited(&thd->ull_hash) &&
> +        my_hash_init(&thd->ull_hash, &my_charset_bin,
> +                     16 /* small hash */, 0, 0, ull_get_key, NULL, 0))
>    {
> +    DBUG_RETURN(0);
>    }
>  
> +  MDL_request ull_request;
> +  ull_request.init(MDL_key::USER_LOCK, res->c_ptr_safe(), "",
> +                   MDL_SHARED_NO_WRITE, MDL_EXPLICIT);
> +  MDL_key *ull_key = &ull_request.key;
>  
>  
> +  if ((ull= (User_level_lock*)
> +       my_hash_search(&thd->ull_hash, ull_key->ptr(), ull_key->length())))
> +  {
> +    /* Recursive lock */
> +    ull->refs++;
> +    null_value = 0;
> +    DBUG_RETURN(1);
> +  }
>  
> +  Lock_wait_timeout_handler lock_wait_timeout_handler;
> +  thd->push_internal_handler(&lock_wait_timeout_handler);
> +  bool error= thd->mdl_context.acquire_lock(&ull_request, timeout);
> +  (void) thd->pop_internal_handler();
> +  if (error)
>    {
> +    if (lock_wait_timeout_handler.m_lock_wait_timeout)
> +      null_value= 0;
> +    DBUG_RETURN(0);
>    }
>  
> +  ull= (User_level_lock*) malloc(sizeof(User_level_lock));

my_malloc() with MYF(MY_WME|MY_THREAD_SPECIFIC)

> +  if (ull == NULL)
>    {
> +    thd->mdl_context.release_lock(ull_request.ticket);
> +    DBUG_RETURN(0);
>    }
> +
> +  ull->lock= ull_request.ticket;
> +  ull->refs= 1;
> +
> +  if (my_hash_insert(&thd->ull_hash, (uchar*) ull))
>    {
> +    thd->mdl_context.release_lock(ull->lock);
> +    free(ull);

my_free

> +    DBUG_RETURN(0);
>    }
> +  null_value= 0;
>  
> +  DBUG_RETURN(1);
>  }
>  
>  
> @@ -4151,43 +4121,104 @@ longlong Item_func_get_lock::val_int()
>  longlong Item_func_release_lock::val_int()
>  {
>    DBUG_ASSERT(fixed == 1);
> +  String *res= args[0]->val_str(&value);
> +  THD *thd= current_thd;
>    DBUG_ENTER("Item_func_release_lock::val_int");
> +  null_value= 1;
> +
>    if (!res || !res->length())
> +    DBUG_RETURN(0);
> +
> +  if (res->length() > NAME_LEN)
>    {
> +    my_error(ER_TOO_LONG_IDENT, MYF(0), res->c_ptr_safe());
>      DBUG_RETURN(0);
>    }
> +
>    DBUG_PRINT("info", ("lock %.*s", res->length(), res->ptr()));
>  
> +  MDL_key ull_key;
> +  ull_key.mdl_key_init(MDL_key::USER_LOCK, res->c_ptr_safe(), "");
> +
> +  User_level_lock *ull;
> +
> +  if (!(ull=
> +        (User_level_lock*) my_hash_search(&thd->ull_hash,
> +                                          ull_key.ptr(), ull_key.length())))
>    {
> +    null_value= thd->mdl_context.get_lock_owner(&ull_key) == 0;
> +    DBUG_RETURN(0);
>    }
> +  null_value= 0;
> +  if (--ull->refs == 0)
>    {
> +    my_hash_delete(&thd->ull_hash, (uchar*) ull);
> +    thd->mdl_context.release_lock(ull->lock);
> +    free(ull);

my_free

>    }
> +  DBUG_RETURN(1);
> +}
> +
> +
> +/**
> +  Check a user level lock.
> +
> +  Sets null_value=TRUE on error.
> +
> +  @retval
> +    1           Available
> +  @retval
> +    0           Already taken, or error
> +*/
> +
> +longlong Item_func_is_free_lock::val_int()
> +{
> +  DBUG_ASSERT(fixed == 1);
> +  String *res= args[0]->val_str(&value);
> +  THD *thd= current_thd;
> +  null_value= 1;
> +

this block ----- from here
> +  if (!res || !res->length())
> +    return 0;
> +
> +  if (res->length() > NAME_LEN)
> +  {
> +    my_error(ER_TOO_LONG_IDENT, MYF(0), res->c_ptr_safe());
> +    return 0;
> +  }
and up ---- to here

is repeated four times. please move it to a separate function.
Like

   if (!ull_name_ok(res))
     return 0;

> +
> +  MDL_key ull_key;
> +  ull_key.mdl_key_init(MDL_key::USER_LOCK, res->c_ptr_safe(), "");
> +
> +  null_value= 0;
> +  return thd->mdl_context.get_lock_owner(&ull_key) == 0;
> +}
> +
> +
> +longlong Item_func_is_used_lock::val_int()
> +{
> +  DBUG_ASSERT(fixed == 1);
> +  String *res= args[0]->val_str(&value);
> +  THD *thd= current_thd;
> +  null_value= 1;
> +
> +  if (!res || !res->length())
> +    return 0;
> +
> +  if (res->length() > NAME_LEN)
> +  {
> +    my_error(ER_TOO_LONG_IDENT, MYF(0), res->c_ptr_safe());
> +    return 0;
> +  }
> +
> +  MDL_key ull_key;
> +  ull_key.mdl_key_init(MDL_key::USER_LOCK, res->c_ptr_safe(), "");
> +  ulong thread_id = thd->mdl_context.get_lock_owner(&ull_key);
> +  if (thread_id == 0)
> +    return 0;
> +
> +  null_value= 0;
> +  return thread_id;
>  }
>  
> === modified file 'sql/mdl.cc'
> --- a/sql/mdl.cc	2013-01-29 14:10:47 +0000
> +++ b/sql/mdl.cc	2013-04-02 08:52:19 +0000
> @@ -2094,8 +2151,6 @@ MDL_context::acquire_lock(MDL_request *m
>  
>    find_deadlock();
>  
> -  if (lock->needs_notification(ticket))
> -  {

okay, so before the patch only locks that needs_notification
were using the loop with short waits. Now all locks do.
Presumably to check whether thd_is_connected still.

Before the patch, how were client disconnects handled?
Like, one could have started to wait on the mdl lock, and then
disconnect. Was the wait aborted?

>      struct timespec abs_shortwait;
>      set_timespec(abs_shortwait, 1);
>      wait_status= MDL_wait::EMPTY;
> @@ -2106,17 +2163,25 @@ MDL_context::acquire_lock(MDL_request *m
>  
>        if (wait_status != MDL_wait::EMPTY)
>          break;
> +    /* Check if the client is gone while we were waiting. */
> +    if (! thd_is_connected(m_thd))
> +    {
> +      /*
> +       * The client is disconnected. Don't wait forever:
> +       * assume it's the same as a wait timeout, this
> +       * ensures all error handling is correct.
> +       */
> +      wait_status= MDL_wait::TIMEOUT;
> +      break;
> +    }
>  
>        mysql_prlock_wrlock(&lock->m_rwlock);
> +    if (lock->needs_notification(ticket))

why this if() is under the prlock, and not the other way around?

>        lock->notify_conflicting_locks(this);
>        mysql_prlock_unlock(&lock->m_rwlock);
>        set_timespec(abs_shortwait, 1);
>      }
>      if (wait_status == MDL_wait::EMPTY)
> -      wait_status= m_wait.timed_wait(m_thd, &abs_timeout, TRUE,
> -                                     mdl_request->key.get_wait_state_name());
> -  }
> -  else
>      wait_status= m_wait.timed_wait(m_thd, &abs_timeout, TRUE,
>                                     mdl_request->key.get_wait_state_name());

Regards,
Sergei