blob: e0ae56c68e3400a6156bd25c6c30d0c122c8bb3f [file] [log] [blame]
/*
* 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 <unistd.h>
#include <memory>
#include <vector>
#include "pagespeed/controller/context_registry.h"
#include "pagespeed/kernel/base/function.h"
#include "pagespeed/kernel/base/gmock.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/thread/worker_test_base.h"
#include "pagespeed/kernel/thread/queued_worker_pool.h"
#include "pagespeed/kernel/util/platform.h"
using testing::AtMost;
using testing::Eq;
using testing::Gt;
using testing::Invoke;
using testing::Lt;
namespace net_instaweb {
namespace {
// Trivial mock object that can be put into a ContextRegistry.
class MockContext {
public:
MockContext() {
EXPECT_CALL(*this, TryCancel()).Times(0);
}
MOCK_METHOD0(TryCancel, void());
};
class ContextRegistryTest : public testing::Test {
public:
ContextRegistryTest()
: thread_system_(Platform::CreateThreadSystem()),
worker_(1 /* max_workers */, "context_registry_test",
thread_system_.get()),
sequence_(worker_.NewSequence()),
registry_(thread_system_.get()) {}
~ContextRegistryTest() override { worker_.FreeSequence(sequence_); }
// MakeFunction doesn't know how to deal with protected, grumble.
void RemoveContexts(std::vector<MockContext>* contexts,
WorkerTestBase::SyncPoint* sync) {
sync->Notify();
for (MockContext& context : *contexts) {
registry_.RemoveContext(&context);
usleep(1); // yield.
}
}
protected:
typedef ContextRegistry<MockContext> MockRegistry;
std::unique_ptr<ThreadSystem> thread_system_;
QueuedWorkerPool worker_;
QueuedWorkerPool::Sequence* sequence_;
MockRegistry registry_;
};
TEST_F(ContextRegistryTest, CancelEmptyDoesntBlock) {
EXPECT_THAT(registry_.Empty(), Eq(true));
EXPECT_THAT(registry_.IsShutdown(), Eq(false));
registry_.CancelAllActiveAndWait();
// We're verifying that Cancel doesn't wait indefinitely when empty. If that
// happens the test will hang and require ^C or some other timeout.
EXPECT_THAT(registry_.Empty(), Eq(true));
EXPECT_THAT(registry_.IsShutdown(), Eq(true));
}
TEST_F(ContextRegistryTest, CantRegisterAfterShutdown) {
MockContext ctx;
registry_.CancelAllActive();
EXPECT_THAT(registry_.TryRegisterContext(&ctx), Eq(false));
EXPECT_THAT(registry_.Empty(), Eq(true));
}
TEST_F(ContextRegistryTest, DoesntCancelOldEntries) {
MockContext ctx;
EXPECT_CALL(ctx, TryCancel()).Times(0);
EXPECT_THAT(registry_.TryRegisterContext(&ctx), Eq(true));
EXPECT_THAT(registry_.Empty(), Eq(false));
registry_.RemoveContext(&ctx);
EXPECT_THAT(registry_.Empty(), Eq(true));
// Should do precisely nothing.
registry_.CancelAllActiveAndWait();
}
TEST_F(ContextRegistryTest, CancelsContainedItems) {
MockContext ctx;
// CancelAllActiveAndWait blocks until the registry is empty, so we need to
// call RemoveContext on another thread. Here we setup "ctx" so that it
// expects TryCancel to be called once. Once that happens, it will queue a
// task on sequence_ that removes ctx from the registry.
EXPECT_CALL(ctx, TryCancel()).WillOnce(Invoke([this, &ctx]() {
sequence_->Add(
MakeFunction(&registry_, &MockRegistry::RemoveContext, &ctx));
}));
EXPECT_THAT(registry_.TryRegisterContext(&ctx), Eq(true));
EXPECT_THAT(registry_.Empty(), Eq(false));
registry_.CancelAllActiveAndWait(); // Should block.
EXPECT_THAT(registry_.Empty(), Eq(true));
}
TEST_F(ContextRegistryTest, NoWaitDoesnWait) {
MockContext ctx;
EXPECT_CALL(ctx, TryCancel());
EXPECT_THAT(registry_.TryRegisterContext(&ctx), Eq(true));
EXPECT_THAT(registry_.Empty(), Eq(false));
registry_.CancelAllActive(); // Must not block, even though not empty.
EXPECT_THAT(registry_.Empty(), Eq(false));
registry_.RemoveContext(&ctx);
EXPECT_THAT(registry_.Empty(), Eq(true));
}
TEST_F(ContextRegistryTest, RemovalsWhileCanceling) {
// We want to verify that nothing bad happens if a context is removed by
// another thread while CancelAllActiveAndWait() is running. So, we put a
// bunch of things in the registry, start a thread that removes them and call
// CancelAllActiveAndWait() in parallel. It would be nice to force
// synchronization here, but that's tricky because we can't call back into the
// registry from TryCancel() because the lock is held.
static const int kNumContexts = 1000;
int num_canceled = 0;
std::vector<MockContext> contexts(kNumContexts);
for (MockContext& context : contexts) {
EXPECT_CALL(context, TryCancel())
.Times(AtMost(1))
// Calls to TryCancel are in series via this thread, so there's no
// concurrency issue on num_canceled.
.WillOnce(Invoke([&num_canceled]() { ++num_canceled; }));
ASSERT_THAT(registry_.TryRegisterContext(&context), Eq(true));
}
// Queue the task that unregisters everything from the registry.
WorkerTestBase::SyncPoint sync(thread_system_.get());
sequence_->Add(MakeFunction<ContextRegistryTest>(
this, &ContextRegistryTest::RemoveContexts, &contexts, &sync));
// Wait for the task to start, then start notifying everything in the
// registry.
sync.Wait();
registry_.CancelAllActiveAndWait();
EXPECT_THAT(registry_.Empty(), Eq(true));
// Verify that some contexts were canceled (> 0) and some were removed
// (< kNumContexts).
EXPECT_THAT(num_canceled, Gt(0));
EXPECT_THAT(num_canceled, Lt(kNumContexts));
}
} // namespace
} // namespace net_instaweb