-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
MDEV-36684 - main.mdl_sync fails under valgrind (test for Bug#42643) #4009
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’d use std::this_thread::yield()
in C++ code. But I don’t think it will make much difference, other than removing a dependency on POSIX or our POSIX simulation layer (#define pthread_yield()
).
2652520
to
7bbd8bc
Compare
Yes, I also suggested |
7bbd8bc
to
73081b7
Compare
73081b7
to
50b9ddb
Compare
@dr-m retargeted for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that we need a better solution for purge_sys_t::close_and_reopen()
where the thread would yield while not holding any metadata locks or table handles. My other comments are suggestions of follow-up improvements, not directly related to improving the Valgrind experience.
error= FALSE; | ||
std::this_thread::yield(); | ||
goto restart; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is duplicating the section of code 63 lines earlier. In fact, a wider section of the code block starting with if (unlikely(error))
is being duplicated. I think that a follow-up fix would be useful where we’d do the following:
if (unlikely(error))
{
if (!(error= open_tables_handle_error(error, …)))
goto restart;
goto error;
}
and there would be a new function ATTRIBUTE_COLD static bool open_tables_error_handler(bool, …)
that would include the duplicated code, including the call to std::this_thread::yield()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. There was a change in 10.11 though, that makes it different by one line at least. I wouldn't dare doing it in GA versions though.
lf_hash_search_unpin(thd->tdc_hash_pins); | ||
std::this_thread::yield(); | ||
goto retry; | ||
} | ||
lf_hash_search_unpin(thd->tdc_hash_pins); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would the following be a correct optimization?
mysql_mutex_lock(&element->LOCK_table_share);
lf_hash_search_unpin(thd->tdc_hash_pins);
if (!(share= element->share))
{
mysql_mutex_unlock(&element->LOCK_table_share);
std::this_thread::yield();
goto retry;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, depending on what you're trying to achieve. LOCK_table_share
is a per-share mutex, it is not locked for table DML, but still can become contested for views. The less we have under its protection the better. So, this will make things slightly worse in this regard, and that was the main reason to keep it out of LOCK_table_share
.
Speaking of correctness: lf_hash_search_unpin()
marks element
unused. In current LF_HASH
implementation it means element
is available for reuse. Taking it into account as well as considering how table definition cache is using it, nothing bad is going to happen. But I'd say it is like relying on undefined behaviour. Once it is unpinned it becomes property of LF_HASH
. If it happens before element->share
check, in theory we can expect LF_HASH
to free/corrupt its memory. So it is not good from correctness point of view either.
What we get if we do lf_hash_search_unpin()
earlier? It means we mark element
for reuse earlier, that is another thread may reuse it rather than allocating it anew.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What we would gain is avoiding some code duplication and instruction cache bloat. Another variant would avoid the potential race condition:
mysql_mutex_lock(&element->LOCK_table_share);
share= element->share;
lf_hash_search_unpin(thd->tdc_hash_pins);
if (!share)
{
mysql_mutex_unlock(&element->LOCK_table_share);
std::this_thread::yield();
goto retry;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It won't work either: mysql_mutex_unlock(&element->LOCK_table_share)
would access potentially corrupt memory.
I can foresee your question "Why it is safe to unpin right after this block?" Presence of element->share
guarantees there is no concurrent delete ongoing. Or if there is, it is waiting on element->LOCK_table_share
mutex and thus it is safe to unpin it immediately after this block.
I understand your concern, I'd have implemented it as you suggested originally (back 10+ years ago), but I didn't see the way to arrange code better. Nor do I see it now.
storage/innobase/trx/trx0purge.cc
Outdated
@@ -1176,6 +1176,7 @@ dict_table_t *purge_sys_t::close_and_reopen(table_id_t id, THD *thd, | |||
MDL_context *mdl_context= static_cast<MDL_context*>(thd_mdl_context(thd)); | |||
ut_ad(mdl_context); | |||
retry: | |||
std::this_thread::yield(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are going to insert this yield unconditionally (outside Valgrind builds), I think that the correct place for it would be right after closing all tables:
m_active= false;
wait_FTS(false);
m_active= true;
In fact, maybe the following should be rewritten to be Valgrind friendly:
void purge_sys_t::wait_FTS(bool also_sys)
{
for (const uint32_t mask= also_sys ? ~0U : ~PAUSED_SYS; m_FTS_paused & mask;)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
Maybe we should just add a call to std::this_thread::yield()
at the beginning of this function? Because purge_sys.m_FTS_paused
is being modified by atomic operations, I think that we need an actual sleep in the loop; invoking only a yield would likely result in excessive CPU consumption.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. Under complex locking scenarios, like:
purge: locks t2
con1: holds MDL_EXCLUSIVE on t1, waiting for MDL_EXCLUSIVE on t2
purge: locks t1, deadlocks (t1 lock not acquired), yields
con1: can't proceed as t2 is still locked
purge: locks t1, deadlocks (t1 lock not acquired), yields
...
In this case yielding before trx_purge_close_tables()
won't fix the problem we're trying to solve.
I don't feel like wait_FTS()
is the right name for this method, but I agree that yield
should be moved there. Will get it fixed.
50b9ddb
to
7a3a0d4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will need to be rebased once more, because a failing builder was made mandatory for branch protection.
7a3a0d4
to
ae74121
Compare
There is no fix for this test case in 10.6, I added a separate commit that fixes it to this PR. |
Valgrind is single threaded and only changes threads as part of system calls or waits. Some busy loops were identified and fixed where the server assumes that some other thread will change the state, which will not happen with valgrind. Based on patch by Monty. Original patch introduced VALGRIND_YIELD, which emits pthread_yield() only in valgrind builds. However it was agreed that it is a good idea to emit yield() unconditionally, such that other affected schedulers (like SCHED_FIFO) benefit from this change. Also avoid pthread_yield() in favour of standard std::this_thread::yield().
ae74121
to
55ddfe1
Compare
Removed test case fix, it is in 10.6 now. |
Valgrind is single threaded and only changes threads as part of system calls or waits.
I found some busy loops where the server assumes that some other thread will change the state, which will not happen with valgrind.
Added VALGRIND_YIELD to the loops, which calls pthread_yield() if HAVE_VALGRIND is defined.
Added pthread_yield() to the loops in table_cache.
We should consider changing some of the VALGRIND_YIELD calls to call pthread_yield() as busy loop without any sleep() is usually a bad thing.
Reviewer: [email protected]