blob: 8382ecdd0060d0de707be979b56ff26a8333c375 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "util/bthread_shared_mutex.h"
#include <bthread/bthread.h>
#include <gtest/gtest.h>
#include <atomic>
#include <shared_mutex>
#include <thread>
#include <vector>
namespace doris {
// 1) Basic SharedMutex contract. try_lock/try_lock_shared are non-blocking, so
// this can be checked single-threaded.
TEST(BthreadSharedMutexTest, BasicContract) {
BthreadSharedMutex m;
// Multiple shared holders can coexist.
ASSERT_TRUE(m.try_lock_shared());
ASSERT_TRUE(m.try_lock_shared());
// A writer cannot acquire while any reader is held.
ASSERT_FALSE(m.try_lock());
m.unlock_shared();
ASSERT_FALSE(m.try_lock()); // still one reader
m.unlock_shared();
// Exclusive ownership.
ASSERT_TRUE(m.try_lock());
ASSERT_FALSE(m.try_lock()); // exclusive: a second writer fails
ASSERT_FALSE(m.try_lock_shared()); // a reader is blocked while the writer holds
m.unlock();
// Free again.
ASSERT_TRUE(m.try_lock_shared());
m.unlock_shared();
}
// 2) Writer-pending behavior of the two-gate algorithm: once a writer has set
// the write-entered bit and is waiting for existing readers to drain, new
// readers must block until the writer acquires and releases the lock.
namespace {
struct WriterPendingCtx {
BthreadSharedMutex* m;
std::atomic<bool> acquired {false};
std::atomic<bool> released {false};
};
void* writer_pending_fn(void* arg) {
auto* c = static_cast<WriterPendingCtx*>(arg);
c->m->lock(); // sets write-entered, then blocks until the reader drains
c->acquired.store(true);
bthread_usleep(20 * 1000);
c->m->unlock();
c->released.store(true);
return nullptr;
}
} // namespace
TEST(BthreadSharedMutexTest, WriterPendingBlocksNewReaders) {
bthread_setconcurrency(8);
BthreadSharedMutex m;
m.lock_shared(); // reader R1 holds a shared lock
WriterPendingCtx ctx;
ctx.m = &m;
bthread_t w;
ASSERT_EQ(0, bthread_start_background(&w, nullptr, writer_pending_fn, &ctx));
// Wait until the writer has reached lock() and set the write-entered bit,
// observed via try_lock_shared() beginning to fail (writer-preferring: a new
// reader must not barge ahead of a pending writer). Probe instead of a fixed
// sleep so a slow CI worker doesn't flake; bounded so a stuck writer fails
// the test rather than hanging. Successful probes are released immediately so
// they don't add a lingering reader.
bool writer_pending = false;
for (int i = 0; i < 1000; ++i) { // up to ~10s
if (!m.try_lock_shared()) {
writer_pending = true;
break;
}
m.unlock_shared();
bthread_usleep(10 * 1000);
}
ASSERT_TRUE(writer_pending); // writer set _write_entered, blocking new readers
ASSERT_FALSE(ctx.acquired.load()); // still waiting on R1 to drain
m.unlock_shared(); // drain R1 -> writer can now acquire
ASSERT_EQ(0, bthread_join(w, nullptr));
ASSERT_TRUE(ctx.acquired.load());
ASSERT_TRUE(ctx.released.load());
// The lock is usable afterwards.
ASSERT_TRUE(m.try_lock_shared());
m.unlock_shared();
}
// 3) Cross-thread unlock regression. Ownership is not tied to the OS thread that
// acquired the lock, so unlocking from a different thread is well defined
// (with std::shared_mutex / pthread_rwlock_t this is undefined behavior and
// can permanently wedge the lock).
TEST(BthreadSharedMutexTest, CrossThreadUnlockExclusive) {
BthreadSharedMutex m;
m.lock(); // acquired on this thread
std::thread t([&] { m.unlock(); }); // released on a different OS thread
t.join();
// Not wedged: the lock can be acquired again.
ASSERT_TRUE(m.try_lock());
m.unlock();
}
TEST(BthreadSharedMutexTest, CrossThreadUnlockShared) {
BthreadSharedMutex m;
m.lock_shared();
std::thread t([&] { m.unlock_shared(); });
t.join();
// The reader was fully released, so a writer can acquire.
ASSERT_TRUE(m.try_lock());
m.unlock();
}
// 3b) bthread migration variant: hold the lock across a suspension point so the
// bthread may migrate to another worker pthread, and unlock from the resumed
// context (potentially a different OS thread). Many bthreads run concurrently
// to force migration; the test passing means the lock never wedges.
namespace {
struct MigrateCtx {
BthreadSharedMutex* m;
std::atomic<int>* done;
int iters;
};
void* migrate_fn(void* arg) {
auto* c = static_cast<MigrateCtx*>(arg);
for (int i = 0; i < c->iters; ++i) {
c->m->lock();
bthread_usleep(100); // suspension point while holding the write lock
c->m->unlock(); // may run on a different worker than lock()
c->m->lock_shared();
bthread_usleep(100);
c->m->unlock_shared();
}
c->done->fetch_add(1);
return nullptr;
}
} // namespace
TEST(BthreadSharedMutexTest, BthreadMigrationNoWedge) {
bthread_setconcurrency(8);
BthreadSharedMutex m;
std::atomic<int> done {0};
MigrateCtx ctx {&m, &done, 100};
constexpr int kBthreads = 16;
std::vector<bthread_t> tids(kBthreads);
for (auto& t : tids) {
ASSERT_EQ(0, bthread_start_background(&t, nullptr, migrate_fn, &ctx));
}
for (auto t : tids) {
ASSERT_EQ(0, bthread_join(t, nullptr));
}
ASSERT_EQ(kBthreads, done.load()); // all finished -> no permanent wedge
ASSERT_TRUE(m.try_lock());
m.unlock();
}
// 4) Mixed reader/writer stress with invariant counters, also exercising the
// RAII wrappers std::shared_lock / std::unique_lock (point 5).
namespace {
struct StressCtx {
BthreadSharedMutex* m;
std::atomic<int>* active_readers;
std::atomic<int>* active_writers;
std::atomic<bool>* failed;
int iters;
int id;
};
void* stress_fn(void* arg) {
auto* c = static_cast<StressCtx*>(arg);
for (int i = 0; i < c->iters; ++i) {
if ((c->id + i) % 4 == 0) { // ~25% writers
std::unique_lock<BthreadSharedMutex> wlock(*c->m);
int aw = c->active_writers->fetch_add(1) + 1;
// Invariant: at most one writer, and no readers while writing.
if (aw != 1 || c->active_readers->load() != 0) {
c->failed->store(true);
}
bthread_usleep(10);
c->active_writers->fetch_sub(1);
} else {
std::shared_lock<BthreadSharedMutex> rlock(*c->m);
c->active_readers->fetch_add(1);
// Invariant: no writer while readers are active.
if (c->active_writers->load() != 0) {
c->failed->store(true);
}
bthread_usleep(10);
c->active_readers->fetch_sub(1);
}
}
return nullptr;
}
} // namespace
TEST(BthreadSharedMutexTest, MixedReaderWriterInvariants) {
bthread_setconcurrency(8);
BthreadSharedMutex m;
std::atomic<int> active_readers {0};
std::atomic<int> active_writers {0};
std::atomic<bool> failed {false};
constexpr int kBthreads = 16;
constexpr int kIters = 500;
std::vector<bthread_t> tids(kBthreads);
std::vector<StressCtx> ctxs(kBthreads);
for (int i = 0; i < kBthreads; ++i) {
ctxs[i] = {&m, &active_readers, &active_writers, &failed, kIters, i};
ASSERT_EQ(0, bthread_start_background(&tids[i], nullptr, stress_fn, &ctxs[i]));
}
for (auto t : tids) {
ASSERT_EQ(0, bthread_join(t, nullptr));
}
ASSERT_FALSE(failed.load());
ASSERT_EQ(0, active_readers.load());
ASSERT_EQ(0, active_writers.load());
}
// 5) RAII drop-in coverage: BthreadSharedMutex must work with std::shared_lock
// and std::unique_lock, which is what the call sites rely on.
TEST(BthreadSharedMutexTest, RaiiWrappers) {
BthreadSharedMutex m;
{
std::shared_lock<BthreadSharedMutex> rlock(m);
ASSERT_FALSE(m.try_lock()); // writer blocked while shared lock held
}
{
std::unique_lock<BthreadSharedMutex> wlock(m);
ASSERT_FALSE(m.try_lock_shared()); // reader blocked while exclusive lock held
}
// Adopt an already-held lock, mirroring the call-site pattern.
ASSERT_TRUE(m.try_lock());
{ std::unique_lock<BthreadSharedMutex> wlock(m, std::adopt_lock); }
ASSERT_TRUE(m.try_lock_shared()); // released by the adopting guard
m.unlock_shared();
}
} // namespace doris