| /* |
| * 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 |