-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
shared_mutex: Add RAII locks #2196
Conversation
include/seastar/core/shared_mutex.hh
Outdated
/// The class is NOT responsible for locking a \ref shared_mutex. | ||
/// \see get_unique_lock(shared_mutex&) | ||
/// \see shared_lock | ||
class unique_lock { |
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.
The counterpart of shared lock is typically an exclusive lock, not unique.
Let's stay consistent with:
/// Lock the \c shared_mutex for exclusive access
///
/// \return a future that becomes ready when no access, shared or exclusive
/// is granted to anyone.
future<> lock() noexcept {
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.
The C++ standard library has std::shared_lock and std::unique_lock, and I assume this is wheret his choice of names comes from. The questions I raised in my review was why can't we use std::shared_lock and std::unique_lock themselves, and not just their names.
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.
see comments inline
I think it's worth having SEASTAR_SEMAPHORE_DEBUG analogue for this holder from the very beginning |
Before I review the specific code, I want to ask whether we could use the standard C++ (C++14) classes |
I don't think the APIs of |
std::shared_lock and std::unique_lock should work on any SharedLockable (see https://en.cppreference.com/w/cpp/named_req/SharedLockable). Doesn't our seastar::shared_mutex fit that constraint? If not, why not? What happens when you try? |
I don't think it satisfies that constraint. I think it might be possible to somehow make it work, but I'm not knowledgeable enough to argue for or against the existence of a solution. Perhaps C++20's coroutines have somehow changed the situation? Nonetheless, I attempted to make it work, but even this test SEASTAR_THREAD_TEST_CASE(test_standard_unique_lock) {
shared_mutex sm{};
unsigned counter = 0;
parallel_for_each(boost::irange(0, 10), [&] (int idx) -> future<> {
return async([&] {
const auto lock = std::unique_lock(sm);
BOOST_REQUIRE_EQUAL(counter, 0u);
++counter;
sleep(1ms).get();
--counter;
BOOST_REQUIRE_EQUAL(counter, 0u);
});
}).get();
} fails with the following repeated error:
We could defer the locking when constructing the lock and only then attempt to do it, but |
@dawmd yes, surely
would not work, but does something like this work?
In this code we only use the std::unique_lock() to wrap an already taken lock - not to actually take it. If it doesn't work I'll give up, and review your code :-) |
Thank you. I didn't understand what you meant at first, but you're right. Version 2:
|
include/seastar/core/shared_mutex.hh
Outdated
co_await mutex.lock_shared(); | ||
|
||
try { | ||
co_return std::shared_lock<shared_mutex>{mutex, std::adopt_lock_t{}}; |
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.
std::shared_lock is copyable, calling the murex.lock_shared, but in seastar case, this returns a future. We should prevent this.
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 seems it's actually not. Fortunately the standard specifies that both the copy constructor and the copy assignment operator are both deleted (link).
I think constraining the API of the locks so that the user cannot call e.g. std::shared_lock<Mutex>::try_lock_for()
might be worth considering, though.
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.
yes, sorry, I confused it with another ctor.
Version 3:
|
include/seastar/core/shared_mutex.hh
Outdated
/// | ||
/// The caller is responsible for keeping the corresponding \ref shared_mutex object | ||
/// alive at least until the returned lock is destroyed. | ||
inline future<std::shared_lock<shared_mutex>> get_shared_lock(shared_mutex& mutex) { |
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.
We should consider making the mutex type a template parameter so it could be extended/overridden, even for testing.
Version 4:
|
Version 5:
|
@nyh, would you like to review? You said that you would like to do so. |
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.
Looks good, and almost ready for merge, but I do have a few comments and questions I'd like you to please consider.
include/seastar/core/shared_mutex.hh
Outdated
/// | ||
/// The caller is responsible for keeping the corresponding Mutex object | ||
/// alive at least until the returned lock is destroyed. | ||
template <typename Mutex> |
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.
nitpick: I'm not sure the name "Mutex" really represents the type of object this is. It needs to have exactly the right methods lock_shared(), unlock_shared(). Would have been nice to create a concept like https://en.cppreference.com/w/cpp/named_req/SharedLockable (just with a different return type for lock_shared()) and use that concept.
But it's a nitpick, not critical.
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 idea. I'll add the concepts
tests/unit/locking_test.cc
Outdated
})); | ||
|
||
BOOST_REQUIRE_EQUAL(counter, 0u); | ||
BOOST_REQUIRE_NE(max, 0u); |
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 don't think this last check checks anything - obviously max can't be 0 because it will always be at least 1 (you increment counter, so it's at least 1, and then set max to at least that).
I'm not sure you really needed to check all this parallelization stuff, I think test_shared_mutex_locks above already checks the same thing more simply (it tries to take the same lock 3 times, and it succeeds).
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.
Can you please answer this?
It's not a critical issue (who cares if a test checks a silly condition that can never be false), but it is a hint that maybe you missed the fact that the test isn't testing what you think it checks - so maybe you forgot to test what you really wanted to test, or something. So please just have a look.
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'm sorry for the late response. Most of the tests I added are analogues of the existing tests with using locks and coroutines being the only difference. I think you're right it's not necessary to check those things again. You're definitely right that comparing max
against 0 is redundant. It would make more sense if Seastar were preemptive. I'll remove the test.
tests/unit/locking_test.cc
Outdated
BOOST_REQUIRE_THROW(co_await std::invoke([&] () -> future<> { | ||
const auto slock = co_await get_shared_lock(sm); | ||
BOOST_REQUIRE(false); | ||
}), std::bad_alloc); |
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.
Can you please help me understand what we're trying to test here? That the coroutine functions need to allocate and if they can't, they fail before even starting? Isn't this obvious for all coroutine functions? Is this really what you hoped to test here?
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.
As in #2196 (comment), this test is an analogue of the existing one, adjusted to using locks and coroutinised. The way I understand the logic here is we want to check what happens in case of an allocation failure: we enter a coroutine and await a lock. However, the lock will never be returned because the allocation of the coroutine awaiting it will fail, throwing an std::bad_alloc
(alternatively, the underlying allocation of a promise performed by seastar::shared_mutex
will fail. Either way, there should be an allocation failure). However, now that I look at it, it seems overly complex and probably unnecessary; chances are I also don't understand it correctly. I can remove the test.
tests/unit/locking_test.cc
Outdated
seastar::memory::local_failure_injector().cancel(); | ||
#endif // SEASTAR_ENABLE_ALLOC_FAILURE_INJECTION | ||
|
||
// If the macro resolves to 0, we need this to prevent compilation errors |
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.
since you used ifdef and not if, I assume you mean if SEASTAR_ENABLE_ALLOC_FAILURE_INJECTION is not defined - not if it "resolves to 0".
tests/unit/locking_test.cc
Outdated
try { | ||
const auto slock = co_await get_shared_lock(sm); | ||
if (expected == 42) { | ||
throw expected_exception(expected); |
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.
In test_shared_mutex_failed_func_locks above you already tested that an exception correctly unlocks the mutex. This seems to test the same thing, but not as well (you didn't check that sm got unlocked!). So why do we need this test? Why does this test's name have the strings "return_nothrow_move" - what does this refer to?
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.
Good catch. I forgot the rename the test. It was originally doing something else (and as most of the other tests I added, it was based on the existing ones). But you're right that it's unnecessary. I'll remove it.
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.
The code looks good, and I resolved some of my old comments, but I still have some unanswered questions about the test, so can you please answer them? Thanks.
tests/unit/locking_test.cc
Outdated
})); | ||
|
||
BOOST_REQUIRE_EQUAL(counter, 0u); | ||
BOOST_REQUIRE_NE(max, 0u); |
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.
Can you please answer this?
It's not a critical issue (who cares if a test checks a silly condition that can never be false), but it is a hint that maybe you missed the fact that the test isn't testing what you think it checks - so maybe you forgot to test what you really wanted to test, or something. So please just have a look.
Most of the tests this commit adds are existing ones adjusted to using RAII locks obtained by calls to `seastar::get_shared_lock()` and `seastar::get_unique_lock()`
Version 2:
|
In this PR, we introduce RAII locks for
seastar::shared_mutex
, similar toseastar::semaphore_units
andseastar::gate::holder
.