blob: 695c035fc16e4eeaa5d4a7d1e073e662c202e384 [file] [log] [blame]
/*
* Copyright 2013 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)
#include "pagespeed/kernel/cache/purge_context.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mem_file_system.h"
#include "pagespeed/kernel/base/mock_message_handler.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/null_statistics.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/thread_system.h"
#include "pagespeed/kernel/thread/mock_scheduler.h"
#include "pagespeed/kernel/util/file_system_lock_manager.h"
#include "pagespeed/kernel/util/platform.h"
#include "pagespeed/kernel/util/simple_stats.h"
namespace {
const int kMaxBytes = 100;
const char kPurgeFile[] = "/cache/cache.flush";
const char kBasePath[] = "/cache";
} // namespace
namespace net_instaweb {
class PurgeContextTest : public ::testing::Test,
public ::testing::WithParamInterface<bool> {
public:
void CorruptWrittenFileHook(const GoogleString& filename) {
EXPECT_TRUE(file_system_.WriteFile(filename.c_str(), "bogus",
&message_handler_));
}
void CorruptFileAndAddNewUpdate(const GoogleString& filename) {
EXPECT_TRUE(file_system_.WriteFile(filename.c_str(), "bogus",
&message_handler_));
lock_->Unlock();
ASSERT_TRUE(lock_tester_.LockTimedWaitStealOld(0, 0, lock_.get()));
purge_context1_->AddPurgeUrl("a", 500000, ExpectSuccess());
}
bool PollAndTest(const GoogleString& url, int64 now_ms,
const CopyOnWrite<PurgeSet>& purge_set,
PurgeContext* purge_context) {
purge_context->PollFileSystem();
return purge_set->IsValid(url, now_ms);
}
bool PollAndTest1(const GoogleString& url, int64 now_ms) {
return PollAndTest(url, now_ms, purge_set1_, purge_context1_.get());
}
bool PollAndTest2(const GoogleString& url, int64 now_ms) {
return PollAndTest(url, now_ms, purge_set2_, purge_context2_.get());
}
protected:
PurgeContextTest()
: thread_system_(Platform::CreateThreadSystem()),
timer_(thread_system_->NewMutex(), MockTimer::kApr_5_2010_ms),
message_handler_(thread_system_->NewMutex()),
file_system_(thread_system_.get(), &timer_),
scheduler_(thread_system_.get(), &timer_),
lock_manager_(&file_system_, kBasePath, &scheduler_,
&message_handler_),
lock_tester_(thread_system_.get()) {
if (HasValidStats()) {
statistics_.reset(new SimpleStats(thread_system_.get()));
} else {
statistics_.reset(new NullStatistics);
}
PurgeContext::InitStats(statistics_.get());
purge_context1_.reset(MakePurgeContext());
purge_context2_.reset(MakePurgeContext());
purge_context1_->SetUpdateCallback(
NewPermanentCallback(this, &PurgeContextTest::UpdatePurgeSet1));
purge_context2_->SetUpdateCallback(
NewPermanentCallback(this, &PurgeContextTest::UpdatePurgeSet2));
message_handler_.AddPatternToSkipPrinting("*opening input file*");
}
bool HasValidStats() const { return GetParam(); }
PurgeContext* MakePurgeContext() {
return new PurgeContext(kPurgeFile, &file_system_, &timer_,
kMaxBytes, thread_system_.get(), &lock_manager_,
&scheduler_, statistics_.get(), &message_handler_);
}
GoogleString LockName() { return purge_context1_->LockName(); }
void ExpectSuccessHelper(bool x, StringPiece reason) {
EXPECT_TRUE(x);
}
PurgeContext::PurgeCallback* ExpectSuccess() {
return NewCallback(this, &PurgeContextTest::ExpectSuccessHelper);
}
void ExpectFailureHelper(bool x, StringPiece reason) {
EXPECT_FALSE(x);
}
PurgeContext::PurgeCallback* ExpectFailure() {
return NewCallback(this, &PurgeContextTest::ExpectFailureHelper);
}
int64 LockContentionStart(PurgeContext::PurgeCallback* callback) {
scheduler_.AdvanceTimeMs(10 * Timer::kSecondMs);
lock_.reset(lock_manager_.CreateNamedLock(LockName()));
EXPECT_TRUE(lock_tester_.LockTimedWaitStealOld(0, 0, lock_.get()));
EXPECT_TRUE(lock_->Held());
int64 now_ms = timer_.NowMs();
purge_context1_->SetCachePurgeGlobalTimestampMs(now_ms, callback);
// We don't check pending_purges_ in PollAndTestValid; the invalidation will
// only be visible to purge_context1 when it can acquire the lock and
// write its records.
EXPECT_TRUE(PollAndTest1("b", now_ms - 1));
EXPECT_TRUE(PollAndTest2("b", now_ms - 1));
// Advance time by a second; which is not enough to steal the lock,
// so we still consider 'b' to be valid in both contexts.
scheduler_.AdvanceTimeMs(1 * Timer::kSecondMs);
EXPECT_TRUE(PollAndTest1("b", now_ms - 1));
EXPECT_TRUE(PollAndTest2("b", now_ms - 1));
scheduler_.AdvanceTimeMs(1 * Timer::kSecondMs); // Not enough to steal it.
return now_ms;
}
int num_cancellations() {
return statistics_->GetVariable(PurgeContext::kCancellations)->Get();
}
int num_contentions() {
return statistics_->GetVariable(PurgeContext::kContentions)->Get();
}
int file_parse_failures() {
return statistics_->GetVariable(PurgeContext::kFileParseFailures)->Get();
}
int num_file_stats() {
return statistics_->GetVariable(PurgeContext::kFileStats)->Get();
}
int ExpectStat(int expected_value) const {
return HasValidStats() ? expected_value : 0;
}
int file_writes() {
return statistics_->GetVariable(PurgeContext::kFileWrites)->Get();
}
void UpdatePurgeSet1(const CopyOnWrite<PurgeSet>& purge_set) {
purge_set1_ = purge_set;
}
void UpdatePurgeSet2(const CopyOnWrite<PurgeSet>& purge_set) {
purge_set2_ = purge_set;
}
scoped_ptr<ThreadSystem> thread_system_;
MockTimer timer_;
MockMessageHandler message_handler_;
MemFileSystem file_system_;
MockScheduler scheduler_;
FileSystemLockManager lock_manager_;
scoped_ptr<Statistics> statistics_;
scoped_ptr<PurgeContext> purge_context1_;
scoped_ptr<PurgeContext> purge_context2_;
CopyOnWrite<PurgeSet> purge_set1_;
CopyOnWrite<PurgeSet> purge_set2_;
scoped_ptr<NamedLock> lock_;
NamedLockTester lock_tester_;
};
TEST_P(PurgeContextTest, Empty) {
EXPECT_TRUE(PollAndTest1("a", 500));
}
TEST_P(PurgeContextTest, InvalidationSharing) {
// Set up a write-delay on purge_context1_, but let purge_context2_ have
// immediate writes.
purge_context1_->set_request_batching_delay_ms(1000);
scheduler_.AdvanceTimeMs(1000);
purge_context1_->SetCachePurgeGlobalTimestampMs(400000, ExpectSuccess());
purge_context1_->AddPurgeUrl("a", 500000, ExpectSuccess());
EXPECT_EQ(0, file_writes());
EXPECT_EQ(0, num_file_stats());
// Prior to waiting for the new purge requests to be written, the purges
// will not take effect.
EXPECT_TRUE(PollAndTest1("a", 500000));
EXPECT_TRUE(PollAndTest1("b", 399999));
// Wait a second for the write-timer to fire, then both purges will be
// written together in one file-write.
scheduler_.AdvanceTimeMs(1000);
EXPECT_EQ(ExpectStat(1), file_writes());
EXPECT_EQ(ExpectStat(2), num_file_stats());
if (!HasValidStats()) {
scheduler_.AdvanceTimeMs(6000);
}
EXPECT_FALSE(PollAndTest1("a", 500000));
EXPECT_TRUE(PollAndTest1("a", 500001));
EXPECT_FALSE(PollAndTest1("b", 399999));
EXPECT_FALSE(PollAndTest1("b", 400000));
EXPECT_TRUE(PollAndTest1("b", 400001));
// These will get transmitted to purge_context2_, which has not
// yet read the cache invalidation file, but will pick up the
// changes from the file system.
EXPECT_FALSE(PollAndTest2("a", 500000));
EXPECT_TRUE(PollAndTest2("a", 500001));
EXPECT_FALSE(PollAndTest2("b", 399999));
EXPECT_FALSE(PollAndTest2("b", 400000));
EXPECT_TRUE(PollAndTest2("b", 400001));
EXPECT_EQ(ExpectStat(4), num_file_stats());
// Now push a time-based flush the other direction. Because
// we only poll the file system periodically we do have to advance
// time.
purge_context2_->SetCachePurgeGlobalTimestampMs(600000, ExpectSuccess());
if (!HasValidStats()) {
scheduler_.AdvanceTimeMs(6000);
}
// This will have immediate effect because purge_context2_ has no write-delay.
EXPECT_FALSE(PollAndTest2("a", 500001));
// There will also be no delay for purge_context1 because purge_context2_
// found a new version of the purge file, it updated shared stat "purge_index"
// which is cheaply checked in every context on every poll.
EXPECT_FALSE(PollAndTest1("a", 500001));
scheduler_.AdvanceTimeMs(10 * Timer::kSecondMs); // force poll
EXPECT_FALSE(PollAndTest1("a", 500001));
EXPECT_TRUE(PollAndTest1("b", 600001));
EXPECT_FALSE(PollAndTest2("a", 500001));
EXPECT_TRUE(PollAndTest2("b", 600001));
// Now invalidate 'b' till 700k.
purge_context2_->AddPurgeUrl("b", 700000, ExpectSuccess());
scheduler_.AdvanceTimeMs(HasValidStats() ? 1000 : 6000);
EXPECT_FALSE(PollAndTest2("b", 700000));
// Again, this new value is immediately reflected in purge_context1.
EXPECT_FALSE(PollAndTest1("b", 700000));
scheduler_.AdvanceTimeMs(10 * Timer::kSecondMs); // force poll
EXPECT_FALSE(PollAndTest1("b", 700000));
EXPECT_TRUE(PollAndTest1("b", 700001));
EXPECT_FALSE(PollAndTest2("b", 700000));
EXPECT_TRUE(PollAndTest2("b", 700001));
EXPECT_EQ(0, file_parse_failures());
}
TEST_P(PurgeContextTest, EmptyPurgeFile) {
// The currently documented mechanism to flush the entire cache is
// to simply touch CACHE_DIR/cache.flush. This mode of operation
// requires disabling purging in the context.
purge_context1_->set_enable_purge(false);
scheduler_.AdvanceTimeMs(10 * Timer::kSecondMs);
ASSERT_TRUE(file_system_.WriteFile(kPurgeFile, "", &message_handler_));
EXPECT_FALSE(PollAndTest1("b", timer_.NowMs() - 1));
EXPECT_TRUE(PollAndTest1("b", timer_.NowMs() + 1));
EXPECT_EQ(0, file_parse_failures());
}
TEST_P(PurgeContextTest, LockContentionFailure) {
int64 now_ms = LockContentionStart(ExpectFailure());
// Release & retake the lock making it harder to steal by refreshing it.
lock_->Unlock();
ASSERT_TRUE(lock_tester_.LockTimedWaitStealOld(0, 0, lock_.get()));
// Get our ExpectFailure callback called and confirm that the invalidation
// didn't have any effect.
scheduler_.AdvanceTimeMs(10 * Timer::kSecondMs);
EXPECT_TRUE(PollAndTest1("b", now_ms - 1));
EXPECT_TRUE(PollAndTest2("b", now_ms - 1));
EXPECT_EQ(ExpectStat(1), num_cancellations());
EXPECT_EQ(ExpectStat(0), num_contentions());
EXPECT_EQ(ExpectStat(0), file_parse_failures());
}
TEST_P(PurgeContextTest, LockContentionSuccess) {
int64 now_ms = LockContentionStart(ExpectSuccess());
// Now advance time by 10 seconds; this should ensure that we steal
// the lock and can write the invalidation records for all to see.
scheduler_.AdvanceTimeMs(10 * Timer::kSecondMs);
EXPECT_FALSE(PollAndTest1("b", now_ms - 1));
EXPECT_FALSE(PollAndTest2("b", now_ms - 1));
EXPECT_EQ(0, num_cancellations());
EXPECT_EQ(0, num_contentions());
EXPECT_EQ(0, file_parse_failures());
}
TEST_P(PurgeContextTest, FileWriteConflict) {
int64 now_ms = LockContentionStart(ExpectSuccess());
file_system_.set_write_callback(
NewCallback(this, &PurgeContextTest::CorruptWrittenFileHook));
// Now advance time by 10 seconds; this should ensure that we steal
// the lock and can write the invalidation records for all to see.
// Unfortunately the file-write will not be verified and will have
// to grab the lock and do it again.
scheduler_.AdvanceTimeMs(10 * Timer::kSecondMs);
EXPECT_FALSE(PollAndTest1("b", now_ms - 1));
EXPECT_FALSE(PollAndTest2("b", now_ms - 1));
EXPECT_EQ(0, num_cancellations());
EXPECT_EQ(ExpectStat(1), num_contentions());
EXPECT_EQ(ExpectStat(1), file_parse_failures());
}
TEST_P(PurgeContextTest, FileWriteConflictWithInterveningUpdate) {
int64 now_ms = LockContentionStart(ExpectSuccess());
file_system_.set_write_callback(
NewCallback(this, &PurgeContextTest::CorruptFileAndAddNewUpdate));
// Now advance time by 10 seconds; this should ensure that we steal
// the lock and can write the invalidation records for all to see.
// Unfortunately the file-write will not be verified and will have
// to grab the lock and do it again.
scheduler_.AdvanceTimeMs(10 * Timer::kSecondMs);
EXPECT_FALSE(PollAndTest1("b", now_ms - 1));
EXPECT_FALSE(PollAndTest2("b", now_ms - 1));
EXPECT_EQ(0, num_cancellations());
EXPECT_EQ(ExpectStat(1), num_contentions());
EXPECT_EQ(ExpectStat(1), file_parse_failures());
}
TEST_P(PurgeContextTest, InvalidTimestampInPurgeRecord) {
ASSERT_TRUE(file_system_.WriteFile(
kPurgeFile,
"-1\n" // Valid initial timestamp
"x\n" // not enough tokens
"2000000000000 y\n" // timestamp(ms) in far future
"-2 z\n" // timestamp(ms) in far past
"500 a\n", // valid record should be parsed.
&message_handler_));
EXPECT_FALSE(PollAndTest1("a", 500));
EXPECT_EQ(ExpectStat(3), file_parse_failures());
EXPECT_TRUE(PollAndTest1("a", 501));
EXPECT_EQ(ExpectStat(6), file_parse_failures());
}
// We test with use_null_statistics == GetParam() as both true and false.
INSTANTIATE_TEST_CASE_P(PurgeContextTestInstance, PurgeContextTest,
::testing::Bool());
} // namespace net_instaweb