blob: fde04a1439f20a6875725fddb0a95c17232cf0fe [file] [log] [blame]
/*
* Copyright 2015 Google Inc.
*
* Licensed 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.
*/
// Author: jmarantz@google.com (Joshua Marantz)
// Unit test the ThreadSafeLockManager.
//
// TODO(jmarantz): factor out common code between this and
// mem_lock_manager_test.cc.
#include "pagespeed/kernel/util/threadsafe_lock_manager.h"
#include "base/logging.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/google_message_handler.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_timer.h"
#include "pagespeed/kernel/base/named_lock_manager.h"
#include "pagespeed/kernel/base/named_lock_tester.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/thread/mock_scheduler.h"
#include "pagespeed/kernel/thread/scheduler.h"
#include "pagespeed/kernel/util/platform.h"
#include "pagespeed/kernel/util/lock_manager_spammer.h"
namespace net_instaweb {
namespace {
const char kLock1[] = "lock1";
const char kLock2[] = "lock2";
const int64 kStealMs = 50000;
const int64 kWaitMs = 10000;
class ThreadsafeLockManagerTest : public testing::Test {
public:
void DeleteManager() {
lock_manager_.reset(NULL);
}
protected:
ThreadsafeLockManagerTest()
: thread_system_(Platform::CreateThreadSystem()),
timer_(thread_system_->NewMutex(), 0),
scheduler_(new MockScheduler(thread_system_.get(), &timer_)),
lock_manager_(new ThreadSafeLockManager(scheduler_.get())),
tester_(thread_system_.get()) {
tester_.set_quiesce(MakeFunction(this,
&ThreadsafeLockManagerTest::Quiesce));
}
NamedLock* MakeLock(const StringPiece& name) {
return lock_manager_->CreateNamedLock(name);
}
void AllLocksFail(const scoped_ptr<NamedLock>& lock) {
scoped_ptr<NamedLock> lock2(MakeLock(lock->name()));
EXPECT_FALSE(TryLock(lock2));
EXPECT_FALSE(TryLockStealOld(kStealMs, lock2));
EXPECT_FALSE(LockTimedWaitStealOld(kWaitMs, kStealMs, lock2));
EXPECT_FALSE(LockTimedWait(kWaitMs, lock2));
}
MockTimer* timer() {
return &timer_;
}
bool TryLock(const scoped_ptr<NamedLock>& lock) {
return tester_.TryLock(lock.get());
}
bool TryLockStealOld(int64 steal_ms, const scoped_ptr<NamedLock>& lock) {
return tester_.LockTimedWaitStealOld(0 /* wait_ms */, steal_ms, lock.get());
}
bool LockTimedWaitStealOld(int64 wait_ms, int64 steal_ms,
const scoped_ptr<NamedLock>& lock) {
return tester_.LockTimedWaitStealOld(wait_ms, steal_ms, lock.get());
}
bool LockTimedWait(int64 wait_ms, const scoped_ptr<NamedLock>& lock) {
return tester_.LockTimedWait(wait_ms, lock.get());
}
void Quiesce() {
int64 wakeup_us;
bool ran_alarms;
ScopedMutex lock(scheduler_->mutex());
while ((wakeup_us = scheduler_->RunAlarms(&ran_alarms)) != 0) {
timer_.SetTimeUs(wakeup_us);
}
}
void LogGrant(const char* name) {
StrAppend(&log_, name, "(grant) ");
}
void LogDeny(const char* name) {
StrAppend(&log_, name, "(deny) ");
}
Function* LogFunction(const char* name) {
return MakeFunction(this,
&ThreadsafeLockManagerTest::LogGrant,
&ThreadsafeLockManagerTest::LogDeny,
name);
}
protected:
scoped_ptr<ThreadSystem> thread_system_;
MockTimer timer_;
scoped_ptr<Scheduler> scheduler_;
GoogleMessageHandler handler_;
scoped_ptr<ThreadSafeLockManager> lock_manager_;
NamedLockTester tester_;
GoogleString log_;
};
TEST_F(ThreadsafeLockManagerTest, LockUnlock) {
scoped_ptr<NamedLock> lock1(MakeLock(kLock1));
// Just do pairs of matched lock / unlock, making sure
// we can't lock while the lock is held.
EXPECT_TRUE(TryLock(lock1));
EXPECT_TRUE(lock1->Held());
AllLocksFail(lock1);
lock1->Unlock();
EXPECT_FALSE(lock1->Held());
EXPECT_TRUE(TryLock(lock1));
EXPECT_TRUE(lock1->Held());
AllLocksFail(lock1);
lock1->Unlock();
EXPECT_FALSE(lock1->Held());
EXPECT_TRUE(LockTimedWait(kWaitMs, lock1));
EXPECT_TRUE(lock1->Held());
AllLocksFail(lock1);
lock1->Unlock();
EXPECT_FALSE(lock1->Held());
EXPECT_TRUE(TryLockStealOld(kStealMs, lock1));
EXPECT_TRUE(lock1->Held());
AllLocksFail(lock1);
lock1->Unlock();
EXPECT_FALSE(lock1->Held());
EXPECT_TRUE(LockTimedWaitStealOld(kWaitMs, kStealMs, lock1));
EXPECT_TRUE(lock1->Held());
AllLocksFail(lock1);
lock1->Unlock();
EXPECT_FALSE(lock1->Held());
}
TEST_F(ThreadsafeLockManagerTest, DoubleLockUnlock) {
scoped_ptr<NamedLock> lock1(MakeLock(kLock1));
scoped_ptr<NamedLock> lock11(MakeLock(kLock1));
// Just do pairs of matched lock / unlock, but make sure
// we hold a separate lock object with the same lock name.
EXPECT_TRUE(TryLock(lock1));
EXPECT_TRUE(lock1->Held());
EXPECT_FALSE(lock11->Held());
AllLocksFail(lock11);
lock1->Unlock();
EXPECT_FALSE(lock1->Held());
EXPECT_FALSE(lock11->Held());
EXPECT_TRUE(TryLock(lock1));
AllLocksFail(lock11);
EXPECT_TRUE(lock1->Held());
EXPECT_FALSE(lock11->Held());
lock1->Unlock();
EXPECT_FALSE(lock1->Held());
EXPECT_FALSE(lock11->Held());
EXPECT_TRUE(LockTimedWait(kWaitMs, lock1));
EXPECT_TRUE(lock1->Held());
EXPECT_FALSE(lock11->Held());
AllLocksFail(lock11);
lock1->Unlock();
EXPECT_FALSE(lock1->Held());
EXPECT_FALSE(lock11->Held());
EXPECT_TRUE(TryLockStealOld(kStealMs, lock1));
EXPECT_TRUE(lock1->Held());
EXPECT_FALSE(lock11->Held());
AllLocksFail(lock11);
lock1->Unlock();
EXPECT_FALSE(lock1->Held());
EXPECT_FALSE(lock11->Held());
EXPECT_TRUE(LockTimedWaitStealOld(kWaitMs, kStealMs, lock1));
EXPECT_TRUE(lock1->Held());
EXPECT_FALSE(lock11->Held());
AllLocksFail(lock11);
lock1->Unlock();
EXPECT_FALSE(lock1->Held());
EXPECT_FALSE(lock11->Held());
}
// From this point, we assume all the locking routines hold
// the lock in equivalent ways. Now we just need to check that
// their timeout behaviors are correct.
TEST_F(ThreadsafeLockManagerTest, UnlockOnDestruct) {
scoped_ptr<NamedLock> lock1(MakeLock(kLock1));
{
scoped_ptr<NamedLock> lock11(MakeLock(kLock1));
EXPECT_TRUE(TryLock(lock11));
EXPECT_FALSE(TryLock(lock1));
// Should implicitly unlock on lock11 destructor call.
}
EXPECT_TRUE(TryLock(lock1));
}
TEST_F(ThreadsafeLockManagerTest, LockIndependence) {
// Differently-named locks are different.
scoped_ptr<NamedLock> lock1(MakeLock(kLock1));
scoped_ptr<NamedLock> lock1a(MakeLock(kLock1));
scoped_ptr<NamedLock> lock2(MakeLock(kLock2));
scoped_ptr<NamedLock> lock2a(MakeLock(kLock2));
EXPECT_TRUE(TryLock(lock1));
EXPECT_TRUE(TryLock(lock2));
EXPECT_FALSE(TryLock(lock1a));
EXPECT_FALSE(TryLock(lock2a));
lock2->Unlock();
EXPECT_FALSE(TryLock(lock1a));
EXPECT_TRUE(TryLock(lock2));
}
TEST_F(ThreadsafeLockManagerTest, TimeoutFail) {
scoped_ptr<NamedLock> lock1(MakeLock(kLock1));
EXPECT_TRUE(TryLock(lock1));
EXPECT_TRUE(lock1->Held());
int64 start_ms = timer()->NowMs();
scoped_ptr<NamedLock> lock1a(MakeLock(kLock1)); // Same name, new object.
EXPECT_FALSE(LockTimedWait(kWaitMs, lock1a));
EXPECT_TRUE(lock1->Held()); // was never unlocked...
int64 end_ms = timer()->NowMs();
EXPECT_LE(start_ms + kWaitMs, end_ms);
}
TEST_F(ThreadsafeLockManagerTest, StealOld) {
scoped_ptr<NamedLock> lock1(MakeLock(kLock1));
scoped_ptr<NamedLock> lock1a(MakeLock(kLock1));
scoped_ptr<NamedLock> lock1b(MakeLock(kLock1));
scoped_ptr<NamedLock> lock1c(MakeLock(kLock1));
EXPECT_TRUE(TryLock(lock1));
// Now we can't steal the lock until after >kStealMs has elapsed.
EXPECT_FALSE(TryLockStealOld(kStealMs, lock1a));
timer()->AdvanceMs(kStealMs);
EXPECT_FALSE(TryLockStealOld(kStealMs, lock1a));
// But 1ms longer than kStealMs and we can steal the lock.
EXPECT_TRUE(lock1->Held());
timer()->AdvanceMs(1);
EXPECT_TRUE(TryLockStealOld(kStealMs, lock1a));
EXPECT_FALSE(lock1->Held()); // Stealing turns releases the lock.
// After steal the timer should reset.
EXPECT_FALSE(TryLockStealOld(kStealMs, lock1b));
timer()->AdvanceMs(kStealMs);
EXPECT_FALSE(TryLockStealOld(kStealMs, lock1b));
EXPECT_FALSE(lock1->Held());
EXPECT_TRUE(lock1a->Held()); // 1a still has the lock
EXPECT_FALSE(lock1b->Held()); // 1b doesn't have it yet.
// But again expire after >kStealMs elapses.
timer()->AdvanceMs(1);
EXPECT_TRUE(TryLockStealOld(kStealMs, lock1c));
EXPECT_FALSE(lock1b->Held());
EXPECT_TRUE(lock1c->Held());
}
TEST_F(ThreadsafeLockManagerTest, BlockingStealOld) {
scoped_ptr<NamedLock> lock1(MakeLock(kLock1));
scoped_ptr<NamedLock> lock1a(MakeLock(kLock1));
scoped_ptr<NamedLock> lock1b(MakeLock(kLock1));
scoped_ptr<NamedLock> lock1c(MakeLock(kLock1));
EXPECT_TRUE(TryLock(lock1));
// Now a call to LockTimedWaitStealOld should block until kStealMs has
// elapsed.
int64 start_ms = timer()->NowMs();
EXPECT_TRUE(LockTimedWaitStealOld(kStealMs * 100, kStealMs, lock1a));
int64 end_ms = timer()->NowMs();
EXPECT_EQ(start_ms + kStealMs, end_ms);
EXPECT_GT(start_ms + kStealMs * 100, end_ms);
// Again the timer should reset after the lock is obtained.
EXPECT_FALSE(TryLockStealOld(kStealMs, lock1b));
timer()->AdvanceMs(kStealMs);
EXPECT_FALSE(TryLockStealOld(kStealMs, lock1b));
timer()->AdvanceMs(1);
EXPECT_TRUE(TryLockStealOld(kStealMs, lock1c));
}
TEST_F(ThreadsafeLockManagerTest, WaitStealOld) {
scoped_ptr<NamedLock> lock1(MakeLock(kLock1));
EXPECT_TRUE(TryLock(lock1));
int64 start_ms = timer()->NowMs();
// If we start now, we'll time out with time to spare.
scoped_ptr<NamedLock> lock1a(MakeLock(kLock1)); // Same name, new object.
EXPECT_FALSE(LockTimedWaitStealOld(kWaitMs, kStealMs, lock1a));
int64 end_ms = timer()->NowMs();
EXPECT_LE(start_ms + kWaitMs, end_ms);
EXPECT_GT(start_ms + kStealMs, end_ms);
// Advance time so that the lock timeout is within the wait time.
int64 time_ms = start_ms + kStealMs - kWaitMs / 2;
timer()->SetTimeUs(1000 * time_ms);
start_ms = timer()->NowMs();
EXPECT_TRUE(LockTimedWaitStealOld(kWaitMs, kStealMs, lock1a));
end_ms = timer()->NowMs();
EXPECT_GT(start_ms + kWaitMs, end_ms);
}
TEST_F(ThreadsafeLockManagerTest, MultipleLocksSameTimeouts) {
scoped_ptr<NamedLock> a(MakeLock(kLock1));
scoped_ptr<NamedLock> b(MakeLock(kLock1));
scoped_ptr<NamedLock> c(MakeLock(kLock1));
scoped_ptr<NamedLock> d(MakeLock(kLock2));
scoped_ptr<NamedLock> e(MakeLock(kLock2));
a->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("a"));
b->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("b"));
c->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("c"));
d->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("d"));
e->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("e"));
Quiesce();
// Because kStealMs (50k) is larger than kWaitMs (10k), both b and
// c will be cancelled before they steal the lock from a, ane e
// will be canceled before i steals the lock from d.
EXPECT_STREQ("a(grant) d(grant) b(deny) c(deny) e(deny) ", log_);
EXPECT_EQ(kWaitMs, timer()->NowMs());
log_.clear();
// However, if we add some more delay (to 45k ms) and try to lock b and c
// again, then the Steal should occur for b, but c will timeout.
timer()->SetTimeMs(kStealMs - (kWaitMs / 2) /* 45000 */);
b->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("b"));
c->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("c"));
Quiesce();
EXPECT_STREQ("b(grant) c(deny) ", log_);
}
TEST_F(ThreadsafeLockManagerTest, OverrideSteal) {
scoped_ptr<NamedLock> a(MakeLock(kLock1));
scoped_ptr<NamedLock> b(MakeLock(kLock1));
scoped_ptr<NamedLock> c(MakeLock(kLock1));
a->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("a"));
timer()->SetTimeMs(kStealMs - (kWaitMs / 2) /* 45000 */);
b->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("b"));
// Before 'b' gets a chance to steal a's lock, c comes in with
// a smaller steal delay (1 ms less), so it will get to steal the
// lock and not a.
c->LockTimedWaitStealOld(kWaitMs, kStealMs - 1, LogFunction("c"));
Quiesce();
EXPECT_STREQ("a(grant) c(grant) b(deny) ", log_);
}
TEST_F(ThreadsafeLockManagerTest, UnlockRevealingNewStealer) {
scoped_ptr<NamedLock> a(MakeLock(kLock1));
scoped_ptr<NamedLock> b(MakeLock(kLock1));
scoped_ptr<NamedLock> c(MakeLock(kLock1));
a->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("a"));
b->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("b"));
c->LockTimedWaitStealOld(2*kStealMs, kStealMs, LogFunction("c"));
timer()->SetTimeMs(1);
scheduler_->Wakeup();
EXPECT_TRUE(a->Held());
timer()->SetTimeMs(kStealMs - (kWaitMs / 2) /* 45000 */);
a->Unlock(); // grants B, C is stealer
Quiesce(); // C steals cause it has long wait
EXPECT_STREQ("a(grant) b(grant) c(grant) ", log_);
}
TEST_F(ThreadsafeLockManagerTest, CloseManagerWithActiveLocks) {
scoped_ptr<NamedLock> a(MakeLock(kLock1));
scoped_ptr<NamedLock> b(MakeLock(kLock1));
scoped_ptr<NamedLock> c(MakeLock(kLock1));
scoped_ptr<NamedLock> d(MakeLock(kLock1));
a->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("a"));
b->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("b"));
c->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("c"));
lock_manager_.reset(NULL); // Locks still active.
EXPECT_STREQ("a(grant) b(deny) c(deny) ", log_);
}
/*
* TODO(jmarantz): deleting the schedule with outstanding locks
* crashes the world. Find some way to fix this.
*
* TEST_F(ThreadsafeLockManagerTest, CloseSchedulerWithActiveLocks) {
* scoped_ptr<NamedLock> a(MakeLock(kLock1));
* scoped_ptr<NamedLock> b(MakeLock(kLock1));
* scoped_ptr<NamedLock> c(MakeLock(kLock1));
* scoped_ptr<NamedLock> d(MakeLock(kLock1));
* a->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("a"));
* b->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("b"));
* c->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("c"));
* scheduler_.reset(NULL); // Locks still active.
* EXPECT_STREQ("a(grant) b(deny) c(deny) ", log_);
* }
*/
TEST_F(ThreadsafeLockManagerTest, DeletePendingLock) {
scoped_ptr<NamedLock> a(MakeLock(kLock1));
scoped_ptr<NamedLock> b(MakeLock(kLock1));
scoped_ptr<NamedLock> c(MakeLock(kLock1));
a->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("a"));
b->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("b"));
c->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("c"));
timer()->SetTimeMs(1);
scheduler_->Wakeup();
EXPECT_TRUE(a->Held());
b.reset(NULL); // Denies B.
a->Unlock(); // Grants C.
Quiesce();
EXPECT_STREQ("a(grant) b(deny) c(grant) ", log_);
}
TEST_F(ThreadsafeLockManagerTest, TryLockWithDeletedManager) {
scoped_ptr<NamedLock> lock1(MakeLock(kLock1));
lock_manager_.reset(NULL);
EXPECT_FALSE(TryLock(lock1));
EXPECT_FALSE(TryLockStealOld(kStealMs, lock1));
}
TEST_F(ThreadsafeLockManagerTest, UnlockWithDeletedManager) {
scoped_ptr<NamedLock> a(MakeLock(kLock1));
scoped_ptr<NamedLock> b(MakeLock(kLock1));
a->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("a"));
b->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("b"));
lock_manager_.reset(NULL);
a->Unlock();
EXPECT_STREQ("a(grant) b(deny) ", log_);
}
TEST_F(ThreadsafeLockManagerTest, TryStealLockWithDeletedManager) {
scoped_ptr<NamedLock> a(MakeLock(kLock1));
scoped_ptr<NamedLock> b(MakeLock(kLock1));
a->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("a"));
timer()->AdvanceMs(kStealMs - 2);
ThreadsafeLockManagerTest* test = this;
scheduler_->AddAlarmAtUs(timer()->NowUs() + 100, MakeFunction(
test, &ThreadsafeLockManagerTest::DeleteManager));
b->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("b"));
Quiesce();
EXPECT_STREQ("a(grant) b(deny) ", log_);
}
TEST_F(ThreadsafeLockManagerTest, RunStolenLockWithDeletedManager) {
scoped_ptr<NamedLock> a(MakeLock(kLock1));
scoped_ptr<NamedLock> b(MakeLock(kLock1));
a->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("a"));
timer()->AdvanceMs(kStealMs - 2);
ThreadsafeLockManagerTest* test = this;
b->LockTimedWaitStealOld(kWaitMs, kStealMs, LogFunction("b"));
scheduler_->AddAlarmAtUs(timer()->NowUs() + 1, MakeFunction(
test, &ThreadsafeLockManagerTest::DeleteManager));
timer()->AdvanceMs(4); // Advances past steal-time, but not past wait-time.
Quiesce();
EXPECT_STREQ("a(grant) b(deny) ", log_);
}
// Use a separate class for the spammer test that uses a real-time timer
// and scheduler, rather than MockTimer and MockScheduler.
//
// TODO(jmarantz): move this class and tests to lock_manager_spammer.cc,
// and rename that to lock_manager_spammer_test.cc. I have it structured
// like this now for reviewability.
class ThreadsafeLockManagerSpammerTest : public testing::Test {
protected:
virtual void SetUp() {
timer_.reset(Platform::CreateTimer());
thread_system_.reset(Platform::CreateThreadSystem());
scheduler_.reset(new Scheduler(thread_system_.get(), timer_.get()));
lock_manager_.reset(new ThreadSafeLockManager(scheduler_.get()));
}
scoped_ptr<Timer> timer_;
scoped_ptr<ThreadSystem> thread_system_;
scoped_ptr<Scheduler> scheduler_;
scoped_ptr<ThreadSafeLockManager> lock_manager_;
};
TEST_F(ThreadsafeLockManagerSpammerTest, NoDelays) {
LockManagerSpammer::RunTests(10, // num threads
3, // num iters
100, // num names
false, // expecting_denials,
false, // delay_unlocks
lock_manager_.get(),
scheduler_.get());
}
TEST_F(ThreadsafeLockManagerSpammerTest, DelaysNoDenials) {
// By having only 2 threads contenting for locks, even though we are delaying
// the unlocks, the locks will be stolen before they expire.
LockManagerSpammer::RunTests(2, // num threads
1, // num iters
100, // num names
false, // expecting_denials,
true, // delay_unlocks
lock_manager_.get(),
scheduler_.get());
}
TEST_F(ThreadsafeLockManagerSpammerTest, DelaysAndDenials) {
// The cumulative delays on the lock-stealing will cause some of the later
// thread lock requests to be denied.
LockManagerSpammer::RunTests(10, // num threads
1, // num iters
100, // num names
true, // expecting_denials,
true, // delay_unlocks
lock_manager_.get(),
scheduler_.get());
}
} // namespace
} // namespace net_instaweb