Skip to content

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

Merged
merged 1 commit into from
Apr 29, 2025

Conversation

svoj
Copy link
Contributor

@svoj svoj commented Apr 24, 2025

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]

@CLAassistant
Copy link

CLAassistant commented Apr 24, 2025

CLA assistant check
All committers have signed the CLA.

@svoj svoj added the MariaDB Foundation Pull requests created by MariaDB Foundation label Apr 24, 2025
@svoj svoj changed the title Added VALGRIND_YIELD to be able to abort from busy loops MDEV-36684 - Added VALGRIND_YIELD to be able to abort from busy loops Apr 24, 2025
Copy link
Contributor

@dr-m dr-m left a 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()).

@svoj svoj force-pushed the pr-10.11-MDEV-36684 branch from 2652520 to 7bbd8bc Compare April 24, 2025 12:11
@svoj
Copy link
Contributor Author

svoj commented Apr 24, 2025

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()).

Yes, I also suggested std::this_thread::yield() when reviewing it, same thinking.

@svoj svoj force-pushed the pr-10.11-MDEV-36684 branch from 7bbd8bc to 73081b7 Compare April 28, 2025 18:54
@svoj svoj changed the base branch from 10.11 to 10.6 April 28, 2025 18:55
@svoj svoj force-pushed the pr-10.11-MDEV-36684 branch from 73081b7 to 50b9ddb Compare April 28, 2025 19:11
@svoj svoj requested a review from dr-m April 28, 2025 20:00
@svoj
Copy link
Contributor Author

svoj commented Apr 28, 2025

@dr-m retargeted for 10.6, moved to std::this_thread::yield(). Please merge if you agree.

@svoj svoj changed the title MDEV-36684 - Added VALGRIND_YIELD to be able to abort from busy loops MDEV-36684 - main.mdl_sync fails under valgrind (test for Bug#42643) Apr 28, 2025
Copy link
Contributor

@dr-m dr-m left a 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.

Comment on lines 4525 to 4527
error= FALSE;
std::this_thread::yield();
goto restart;
Copy link
Contributor

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().

Copy link
Contributor Author

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.

Comment on lines 903 to 907
lf_hash_search_unpin(thd->tdc_hash_pins);
std::this_thread::yield();
goto retry;
}
lf_hash_search_unpin(thd->tdc_hash_pins);
Copy link
Contributor

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;
  }

Copy link
Contributor Author

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.

Copy link
Contributor

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;
  }

Copy link
Contributor Author

@svoj svoj Apr 29, 2025

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.

@@ -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();
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@svoj svoj force-pushed the pr-10.11-MDEV-36684 branch from 50b9ddb to 7a3a0d4 Compare April 29, 2025 09:15
@svoj
Copy link
Contributor Author

svoj commented Apr 29, 2025

@dr-m PR updated. I can see Monty pushed his aae9b50 to main, so you'd have to cleanup VALGRIND_YIELD once this one is merged up to main.

Copy link
Contributor

@dr-m dr-m left a 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.

@svoj svoj force-pushed the pr-10.11-MDEV-36684 branch from 7a3a0d4 to ae74121 Compare April 29, 2025 10:43
@svoj
Copy link
Contributor Author

svoj commented Apr 29, 2025

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().
@svoj svoj force-pushed the pr-10.11-MDEV-36684 branch from ae74121 to 55ddfe1 Compare April 29, 2025 11:05
@svoj
Copy link
Contributor Author

svoj commented Apr 29, 2025

Removed test case fix, it is in 10.6 now.

@dr-m dr-m merged commit 55ddfe1 into MariaDB:10.6 Apr 29, 2025
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
MariaDB Foundation Pull requests created by MariaDB Foundation
Development

Successfully merging this pull request may close these issues.

3 participants