Hi Brandon,

Kristian Nielsen via commits <comm...@lists.mariadb.org> writes:

> Ensure that a pending semi-sync transaction can only be signalled on its THD
> while it is waiting in commit_trx(); not if the wait in commit_trx() is
> skipped for some reason.

Can you review this patch for MDEV-36934 so it can get into the upcoming
release? You've already seen and agreed to the first version of the patch.

I've included your test case for the bug, thanks a lot for writing that! I
only modified it slightly, to increase the timeout for potentially slow
Buildbot hosts, and to avoid an unnecessary use of DBUG injection.

I also added your suggestions to (in addition to my original
defensive-coding patch to only signal the THD while it is marked as waiting)
also avoid adding a transaction to the list if it will never be ack'ed due
to no waiting slaves; and to unlink_thd_as_waiter() if we skip waiting for
ack in commit_trx().

 - Kristian.

>
> There was a bug that if no semi-sync slaves were connected and
> rpl_semi_sync_master_wait_no_slave was off, the THD pointer could be left in
> the list of pending transactions after THD was deleted, and an invalid
> THD/condition variable could be signalled, causing hang or other corruption.
>
> Testcase based on work by Brandon Nesterenko <brandon.nestere...@mariadb.com>.
>
> Signed-off-by: Kristian Nielsen <kniel...@knielsen-hq.org>
> ---
>  .../r/rpl_semi_sync_cond_var_per_thd.result   |  63 ++++++++++-
>  .../rpl/t/rpl_semi_sync_cond_var_per_thd.test | 104 +++++++++++++++++-
>  sql/semisync_master.cc                        |  50 +++++++--
>  sql/semisync_master.h                         |  10 +-
>  4 files changed, 210 insertions(+), 17 deletions(-)
>
> diff --git a/mysql-test/suite/rpl/r/rpl_semi_sync_cond_var_per_thd.result 
> b/mysql-test/suite/rpl/r/rpl_semi_sync_cond_var_per_thd.result
> index 18ad5d3d2cc..96e7aa43fbf 100644
> --- a/mysql-test/suite/rpl/r/rpl_semi_sync_cond_var_per_thd.result
> +++ b/mysql-test/suite/rpl/r/rpl_semi_sync_cond_var_per_thd.result
> @@ -7,6 +7,8 @@ call mtr.add_suppression("Could not read packet");
>  call mtr.add_suppression("Could not write packet");
>  set @save_bgc_count= @@global.binlog_commit_wait_count;
>  set @save_bgc_usec= @@global.binlog_commit_wait_usec;
> +set @old_master_wait_point= @@global.rpl_semi_sync_master_wait_point;
> +set @old_master_wait_no_slave= @@global.rpl_semi_sync_master_wait_no_slave;
>  set @save_debug_dbug= @@global.debug_dbug;
>  set @@global.binlog_commit_wait_count=3;
>  set @@global.binlog_commit_wait_usec=10000000;
> @@ -46,8 +48,6 @@ drop table t1, t2, t3;
>  # the binlogging to semi-sync, and starting the wait for ACK; and during
>  # this pause, semi-sync is manually switched off and on.
>  connection master;
> -set @old_master_wait_point= @@global.rpl_semi_sync_master_wait_point;
> -set @old_master_wait_no_slave= @@global.rpl_semi_sync_master_wait_no_slave;
>  set @@global.rpl_semi_sync_master_wait_point= AFTER_SYNC;
>  set @@global.rpl_semi_sync_master_wait_no_slave= 1;
>  set @@global.debug_dbug="+d,semisync_log_skip_trx_wait";
> @@ -100,7 +100,66 @@ commit;
>  # Cleanup
>  connection master;
>  drop table tn;
> +set @@global.debug_dbug=@save_debug_dbug;
> +#
> +# MDEV-36934
> +# The server could indefinitely hang due to a memory leak which tried to
> +# pthread signal on a destroyed condition variable. In effect, no
> +# connections could commit transactions because there would be a thread
> +# stuck on a never-returning call to pthread_cond_signal() while
> +# holding Repl_semi_sync_master::LOCK_log.
> +connection master;
> +set @@global.rpl_semi_sync_master_wait_point= AFTER_COMMIT;
> +set @@global.rpl_semi_sync_master_wait_no_slave= 0;
> +# Ensure servers are in proper state
> +connection master;
> +connection slave;
> +# Test case initial set-up
> +connection master;
> +create table t_36934 (a int) engine=innodb;
> +include/save_master_gtid.inc
> +connection slave;
> +include/sync_with_master_gtid.inc
> +# Pause the user transaction before inserting into Active_tranx
> +connect user_con,localhost,root,,;
> +SET debug_sync= 'semisync_at_write_tranx_in_binlog SIGNAL 
> at_write_tranx_in_binlog WAIT_FOR resume_write_tranx_in_binlog';
> +insert into t_36934 values (1);
> +connection server_1;
> +set debug_sync="now wait_for at_write_tranx_in_binlog";
> +# Disconnect the slave (note that the binlog dump thread won't yet be
> +# notified of a binlog update from the last transaction, so the slave
> +# should neither receiver nor ACK the transaction).
> +connection slave;
> +include/stop_slave.inc
> +# Waiting for master to realize the slave has disconnected..
> +connection server_1;
> +# ..done
> +# Resuming transaction (it will exit commit_trx early without waiting)
> +set debug_sync="now signal resume_write_tranx_in_binlog";
> +connection user_con;
> +disconnect user_con;
> +# Force delete the user thread (FLUSH THREADS ensures the thread won't
> +# stay in the thread cache)
> +connection master;
> +FLUSH THREADS;
> +# BUG: Re-connect slave. MDEV-36934 reports that the master would hang
> +# when the slave would re-connect and try to ACK the last transaction
> +# who's thread has been deleted
> +connection slave;
> +include/start_slave.inc
> +# Try to commit another transaction (prior to MDEV-36934 fixes, this
> +# would hang indefinitely)
> +connection master;
> +set debug_sync="RESET";
> +insert into t_36934 values (2);
> +connection server_1;
> +# Waiting 30s for last query to complete..
> +connection master;
> +# ..done
> +# Cleanup
> +connection master;
>  set @@global.rpl_semi_sync_master_wait_point= @old_master_wait_point;
>  set @@global.rpl_semi_sync_master_wait_no_slave= @old_master_wait_no_slave;
>  set @@global.debug_dbug=@save_debug_dbug;
> +drop table t_36934;
>  include/rpl_end.inc
> diff --git a/mysql-test/suite/rpl/t/rpl_semi_sync_cond_var_per_thd.test 
> b/mysql-test/suite/rpl/t/rpl_semi_sync_cond_var_per_thd.test
> index 537a4bf7fa8..5d6702f8c42 100644
> --- a/mysql-test/suite/rpl/t/rpl_semi_sync_cond_var_per_thd.test
> +++ b/mysql-test/suite/rpl/t/rpl_semi_sync_cond_var_per_thd.test
> @@ -34,6 +34,8 @@ call mtr.add_suppression("Could not read packet");
>  call mtr.add_suppression("Could not write packet");
>  set @save_bgc_count= @@global.binlog_commit_wait_count;
>  set @save_bgc_usec= @@global.binlog_commit_wait_usec;
> +set @old_master_wait_point= @@global.rpl_semi_sync_master_wait_point;
> +set @old_master_wait_no_slave= @@global.rpl_semi_sync_master_wait_no_slave;
>  set @save_debug_dbug= @@global.debug_dbug;
>  set @@global.binlog_commit_wait_count=3;
>  set @@global.binlog_commit_wait_usec=10000000;
> @@ -98,8 +100,6 @@ drop table t1, t2, t3;
>  
>  
>  --connection master
> -set @old_master_wait_point= @@global.rpl_semi_sync_master_wait_point;
> -set @old_master_wait_no_slave= @@global.rpl_semi_sync_master_wait_no_slave;
>  set @@global.rpl_semi_sync_master_wait_point= AFTER_SYNC;
>  set @@global.rpl_semi_sync_master_wait_no_slave= 1;
>  --eval set @@global.debug_dbug="+d,semisync_log_skip_trx_wait"
> @@ -190,8 +190,108 @@ commit;
>  --echo # Cleanup
>  --connection master
>  drop table tn;
> +set @@global.debug_dbug=@save_debug_dbug;
> +
> +
> +--echo #
> +--echo # MDEV-36934
> +--echo # The server could indefinitely hang due to a memory leak which tried 
> to
> +--echo # pthread signal on a destroyed condition variable. In effect, no
> +--echo # connections could commit transactions because there would be a 
> thread
> +--echo # stuck on a never-returning call to pthread_cond_signal() while
> +--echo # holding Repl_semi_sync_master::LOCK_log.
> +
> +--connection master
> +set @@global.rpl_semi_sync_master_wait_point= AFTER_COMMIT;
> +set @@global.rpl_semi_sync_master_wait_no_slave= 0;
> +
> +--echo # Ensure servers are in proper state
> +--connection master
> +let $status_var= rpl_semi_sync_master_status;
> +let $status_var_value= ON;
> +source include/wait_for_status_var.inc;
> +--connection slave
> +let $status_var= rpl_semi_sync_slave_status;
> +let $status_var_value= ON;
> +source include/wait_for_status_var.inc;
> +
> +--echo # Test case initial set-up
> +--connection master
> +create table t_36934 (a int) engine=innodb;
> +--source include/save_master_gtid.inc
> +--connection slave
> +--source include/sync_with_master_gtid.inc
> +
> +--echo # Pause the user transaction before inserting into Active_tranx
> +--connect(user_con,localhost,root,,)
> +SET debug_sync= 'semisync_at_write_tranx_in_binlog SIGNAL 
> at_write_tranx_in_binlog WAIT_FOR resume_write_tranx_in_binlog';
> +--send insert into t_36934 values (1)
> +
> +--connection server_1
> +set debug_sync="now wait_for at_write_tranx_in_binlog";
> +
> +--echo # Disconnect the slave (note that the binlog dump thread won't yet be
> +--echo # notified of a binlog update from the last transaction, so the slave
> +--echo # should neither receiver nor ACK the transaction).
> +--connection slave
> +--source include/stop_slave.inc
> +
> +--echo # Waiting for master to realize the slave has disconnected..
> +--connection server_1
> +let $status_var= rpl_semi_sync_master_clients;
> +let $status_var_value= 0;
> +source include/wait_for_status_var.inc;
> +--echo # ..done
> +
> +--echo # Resuming transaction (it will exit commit_trx early without waiting)
> +set debug_sync="now signal resume_write_tranx_in_binlog";
> +
> +--connection user_con
> +--reap
> +--let $user_con_tid= `SELECT connection_id()`
> +--disconnect user_con
> +--source include/wait_until_disconnected.inc
> +
> +--echo # Force delete the user thread (FLUSH THREADS ensures the thread won't
> +--echo # stay in the thread cache)
> +--connection master
> +FLUSH THREADS;
> +
> +--echo # BUG: Re-connect slave. MDEV-36934 reports that the master would hang
> +--echo # when the slave would re-connect and try to ACK the last transaction
> +--echo # who's thread has been deleted
> +--connection slave
> +--source include/start_slave.inc
> +
> +--echo # Try to commit another transaction (prior to MDEV-36934 fixes, this
> +--echo # would hang indefinitely)
> +--connection master
> +set debug_sync="RESET";
> +--send insert into t_36934 values (2)
> +
> +--connection server_1
> +--echo # Waiting 30s for last query to complete..
> +--let $wait_timeout= 30
> +--let $wait_condition= SELECT count(*)=0 FROM information_schema.processlist 
> WHERE info LIKE 'insert into t_36934%';
> +--source include/wait_condition.inc
> +
> +# Variable `success` is set by wait_condition.inc
> +if (!$success)
> +{
> +  --echo # ..error
> +  --die Query is hung
> +}
> +
> +--connection master
> +--reap
> +--echo # ..done
> +
> +
> +--echo # Cleanup
> +--connection master
>  set @@global.rpl_semi_sync_master_wait_point= @old_master_wait_point;
>  set @@global.rpl_semi_sync_master_wait_no_slave= @old_master_wait_no_slave;
>  set @@global.debug_dbug=@save_debug_dbug;
> +drop table t_36934;
>  
>  --source include/rpl_end.inc
> diff --git a/sql/semisync_master.cc b/sql/semisync_master.cc
> index 43afb8f996e..854b6a61eeb 100644
> --- a/sql/semisync_master.cc
> +++ b/sql/semisync_master.cc
> @@ -68,15 +68,20 @@ static ulonglong timespec_to_usec(const struct timespec 
> *ts)
>    return (ulonglong) ts->tv_sec * TIME_MILLION + ts->tv_nsec / TIME_THOUSAND;
>  }
>  
> -int signal_waiting_transaction(THD *waiting_thd, const char *binlog_file,
> -                                my_off_t binlog_pos)
> +static int
> +signal_waiting_transaction(THD *waiting_thd, bool thd_valid,
> +                           const char *binlog_file, my_off_t binlog_pos)
>  {
>    /*
>      It is possible that the connection thd waiting for an ACK was killed. In
>      such circumstance, the connection thread will nullify the thd member of 
> its
>      Active_tranx node. So before we try to signal, ensure the THD exists.
> +
> +    The thd_valid is only set while the THD is waiting in commit_trx(); this
> +    is defensive coding to not signal an invalid THD if we somewhere
> +    accidentally did not remove the transaction from the list.
>    */
> -  if (waiting_thd)
> +  if (waiting_thd && thd_valid)
>      mysql_cond_signal(&waiting_thd->COND_wakeup_ready);
>    return 0;
>  }
> @@ -182,6 +187,7 @@ int Active_tranx::insert_tranx_node(THD *thd_to_wait,
>    ins_node->log_name[FN_REFLEN-1] = 0; /* make sure it ends properly */
>    ins_node->log_pos = log_file_pos;
>    ins_node->thd= thd_to_wait;
> +  ins_node->thd_valid= false;
>  
>    if (!m_trx_front)
>    {
> @@ -263,7 +269,8 @@ void Active_tranx::clear_active_tranx_nodes(
>      if ((log_file_name != NULL) &&
>          compare(new_front, log_file_name, log_file_pos) > 0)
>        break;
> -    pre_delete_hook(new_front->thd, new_front->log_name, new_front->log_pos);
> +    pre_delete_hook(new_front->thd, new_front->thd_valid,
> +                    new_front->log_name, new_front->log_pos);
>      new_front = new_front->next;
>    }
>  
> @@ -355,13 +362,17 @@ void Active_tranx::unlink_thd_as_waiter(const char 
> *log_file_name,
>    }
>  
>    if (entry)
> +  {
>      entry->thd= NULL;
> +    entry->thd_valid= false;
> +  }
>  
>    DBUG_VOID_RETURN;
>  }
>  
> -bool Active_tranx::is_thd_waiter(THD *thd_to_check, const char 
> *log_file_name,
> -                                 my_off_t log_file_pos)
> +Tranx_node *
> +Active_tranx::is_thd_waiter(THD *thd_to_check, const char *log_file_name,
> +                            my_off_t log_file_pos)
>  {
>    DBUG_ENTER("Active_tranx::assert_thd_is_waiter");
>    mysql_mutex_assert_owner(m_lock);
> @@ -377,7 +388,7 @@ bool Active_tranx::is_thd_waiter(THD *thd_to_check, const 
> char *log_file_name,
>      entry = entry->hash_next;
>    }
>  
> -  DBUG_RETURN(static_cast<bool>(entry));
> +  DBUG_RETURN(entry);
>  }
>  
>  
> /*******************************************************************************
> @@ -863,6 +874,10 @@ int Repl_semi_sync_master::commit_trx(const char 
> *trx_wait_binlog_name,
>  
>    if (!rpl_semi_sync_master_clients && !rpl_semi_sync_master_wait_no_slave)
>    {
> +    lock();
> +    m_active_tranxs->unlink_thd_as_waiter(trx_wait_binlog_name,
> +                                          trx_wait_binlog_pos);
> +    unlock();
>      rpl_semi_sync_master_no_transactions++;
>      DBUG_RETURN(0);
>    }
> @@ -922,6 +937,9 @@ int Repl_semi_sync_master::commit_trx(const char 
> *trx_wait_binlog_name,
>          }
>        }
>  
> +      Tranx_node *tranx_entry=
> +        m_active_tranxs->is_thd_waiter(thd, trx_wait_binlog_name,
> +                                       trx_wait_binlog_pos);
>        /* In between the binlogging of this transaction and this wait, it is
>         * possible that our entry in Active_tranx was removed (i.e. if
>         * semi-sync was switched off and on). It is also possible that the
> @@ -932,8 +950,7 @@ int Repl_semi_sync_master::commit_trx(const char 
> *trx_wait_binlog_name,
>         * rpl_semi_sync_master_yes/no_tx consistent with it, we check for a
>         * semi-sync restart _after_ checking the reply state.
>         */
> -      if (unlikely(!m_active_tranxs->is_thd_waiter(thd, trx_wait_binlog_name,
> -                                                   trx_wait_binlog_pos)))
> +      if (unlikely(!tranx_entry))
>        {
>          DBUG_EXECUTE_IF(
>              "semisync_log_skip_trx_wait",
> @@ -952,6 +969,16 @@ int Repl_semi_sync_master::commit_trx(const char 
> *trx_wait_binlog_name,
>          break;
>        }
>  
> +      /*
> +        Mark that our THD is now valid for signalling to by the ack thread.
> +        It is important to ensure that we can never leave a no longer valid
> +        THD in the transaction list and signal it, eg. MDEV-36934. This way,
> +        we ensure the THD will only be signalled while this function is
> +        running, even in case of some incorrect error handling or similar
> +        that might leave a dangling THD in the list.
> +      */
> +      tranx_entry->thd_valid= true;
> +
>        /* Let us update the info about the minimum binlog position of waiting
>         * threads.
>         */
> @@ -1284,6 +1311,8 @@ int Repl_semi_sync_master::write_tranx_in_binlog(THD 
> *thd,
>  
>    DBUG_ENTER("Repl_semi_sync_master::write_tranx_in_binlog");
>  
> +  DEBUG_SYNC(current_thd, "semisync_at_write_tranx_in_binlog");
> +
>    lock();
>  
>    /* This is the real check inside the mutex. */
> @@ -1317,7 +1346,8 @@ int Repl_semi_sync_master::write_tranx_in_binlog(THD 
> *thd,
>      m_commit_file_name_inited = true;
>    }
>  
> -  if (is_on())
> +  if (is_on() &&
> +      (rpl_semi_sync_master_clients || rpl_semi_sync_master_wait_no_slave))
>    {
>      DBUG_ASSERT(m_active_tranxs != NULL);
>      if(m_active_tranxs->insert_tranx_node(thd, log_file_name, log_file_pos))
> diff --git a/sql/semisync_master.h b/sql/semisync_master.h
> index a1c57959165..28de3ecf480 100644
> --- a/sql/semisync_master.h
> +++ b/sql/semisync_master.h
> @@ -30,6 +30,7 @@ extern PSI_cond_key key_COND_binlog_send;
>  
>  struct Tranx_node {
>    char              log_name[FN_REFLEN];
> +  bool              thd_valid;             /* thd is valid for signalling */
>    my_off_t          log_pos;
>    THD               *thd;                   /* The thread awaiting an ACK */
>    struct Tranx_node *next;            /* the next node in the sorted list */
> @@ -126,7 +127,9 @@ class Tranx_node_allocator
>  
>      trx_node= &(current_block->nodes[++last_node]);
>      trx_node->log_name[0] = '\0';
> +    trx_node->thd_valid= false;
>      trx_node->log_pos= 0;
> +    trx_node->thd= nullptr;
>      trx_node->next= 0;
>      trx_node->hash_next= 0;
>      return trx_node;
> @@ -298,7 +301,8 @@ class Tranx_node_allocator
>    its invocation. See the context in which it is called to know.
>  */
>  
> -typedef int (*active_tranx_action)(THD *trx_thd, const char *log_file_name,
> +typedef int (*active_tranx_action)(THD *trx_thd, bool thd_valid,
> +                                   const char *log_file_name,
>                                     my_off_t trx_log_file_pos);
>  
>  /**
> @@ -381,8 +385,8 @@ class Active_tranx
>     * matches the thread of the respective Tranx_node::thd of the passed in
>     * log_file_name and log_file_pos.
>     */
> -  bool is_thd_waiter(THD *thd_to_check, const char *log_file_name,
> -                     my_off_t log_file_pos);
> +  Tranx_node * is_thd_waiter(THD *thd_to_check, const char *log_file_name,
> +                             my_off_t log_file_pos);
>  
>    /* Given a position, check to see whether the position is an active
>     * transaction's ending position by probing the hash table.
_______________________________________________
developers mailing list -- developers@lists.mariadb.org
To unsubscribe send an email to developers-le...@lists.mariadb.org

Reply via email to