blob: ecf3ef9f39fcfa7edb0b99a97f4319fe54595ebd [file] [log] [blame]
/*
* Copyright 2016 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: cheesy@google.com (Steve Hill)
*/
#include "pagespeed/controller/popularity_contest_schedule_rewrite_controller.h"
#include <algorithm>
#include <queue>
#include "base/logging.h"
#include "pagespeed/kernel/base/function.h"
#include "pagespeed/kernel/base/gmock.h"
#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/mock_timer.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/util/platform.h"
#include "pagespeed/kernel/util/simple_stats.h"
using testing::Eq;
using testing::Gt;
using testing::IsEmpty;
namespace net_instaweb {
namespace {
const int kMaxRewrites = 2;
const int kMaxQueueLength = 5;
class TrackCallsFunction : public Function {
public:
TrackCallsFunction() : run_called_(false), cancel_called_(false) {
set_delete_after_callback(false);
}
virtual ~TrackCallsFunction() { }
void Run() override { run_called_ = true; }
void Cancel() override { cancel_called_ = true; }
bool run_called_;
bool cancel_called_;
};
class RecordKeyFunction : public Function {
public:
RecordKeyFunction(const GoogleString& key, std::queue<GoogleString>* queue)
: key_(key), queue_(queue) {
}
void Run() override {
queue_->push(key_);
}
void Cancel() override {
CHECK(false) << "Cancel called for key '" << key_ << "'";
}
private:
const GoogleString key_;
std::queue<GoogleString>* queue_;
};
} // namespace
class PopularityContestScheduleRewriteControllerTest : public testing::Test {
public:
PopularityContestScheduleRewriteControllerTest()
: thread_system_(Platform::CreateThreadSystem()),
stats_(thread_system_.get()),
timer_(thread_system_->NewMutex(), MockTimer::kApr_5_2010_ms) {
PopularityContestScheduleRewriteController::InitStats(&stats_);
ResetController(kMaxRewrites, kMaxQueueLength);
}
protected:
void ResetController(int max_rewrites, int max_queue) {
controller_.reset(new PopularityContestScheduleRewriteController(
thread_system_.get(), &stats_, &timer_, max_rewrites, max_queue));
}
// Schedule a rewrite from the Run() method of a Function. Useful for testing
// re-entrancy safety. Whether the controller calls Run or Cancel for
// bootstrap_key, arranges to invoke ScheduleRewrite for
// (main_key,main_callback). If Run() is invoked for bootstrap_key, also runs
// NotifyRewriteComplete(bootstrap_key).
void ScheduleRewriteFromCallback(const GoogleString& bootstrap_key,
const GoogleString& main_key,
Function* main_callback) {
// Make a function that will attempt to schedule another rewrite when it is
// run. This will deadlock (ie: Timeout) if controller doesn't correctly
// support this.
Function* bootstrap_callback = MakeFunction(this,
&PopularityContestScheduleRewriteControllerTest::
ScheduleRewriteCallback,
&PopularityContestScheduleRewriteControllerTest::CancelRewriteCallback,
bootstrap_key, main_key, main_callback);
// Schedule the function.
controller_->ScheduleRewrite(bootstrap_key, bootstrap_callback);
}
// Explicitly not passing by const reference (here and below), because
// MakeFunction doesn't handle that properly.
void ScheduleRewriteCallback(GoogleString bootstrap_key,
GoogleString main_key,
Function* main_callback) {
controller_->ScheduleRewrite(main_key, main_callback);
controller_->NotifyRewriteComplete(bootstrap_key);
}
void CancelRewriteCallback(GoogleString bootstrap_key,
GoogleString main_key,
Function* main_callback) {
controller_->ScheduleRewrite(main_key, main_callback);
}
void ScheduleRewriteAndAdvanceClock(const GoogleString& key, Function* cb) {
controller_->ScheduleRewrite(key, cb);
timer_.AdvanceMs(1);
}
void NotifyCompleteAndAdvanceClock(const GoogleString& key) {
controller_->NotifyRewriteComplete(key);
timer_.AdvanceMs(1);
}
void NotifyFailedAndAdvanceClock(const GoogleString& key) {
controller_->NotifyRewriteFailed(key);
timer_.AdvanceMs(1);
}
// TODO(cheesy): This is a bridge to avoid changing all call sites for
// CheckStats. Consider removing it in a follow-up change.
void CheckStats(int expected_num_requests, int expected_num_success,
int expected_num_failed, int expected_rejected_queue_size,
int expected_rejected_in_progress, int expected_queue_size,
int expected_running) {
CheckStats(expected_num_requests, expected_num_success, expected_num_failed,
expected_rejected_queue_size, expected_rejected_in_progress,
expected_queue_size, expected_running,
-1 /* expected_waiting_retry */);
}
void CheckStats(int expected_num_requests, int expected_num_success,
int expected_num_failed, int expected_rejected_queue_size,
int expected_rejected_in_progress, int expected_queue_size,
int expected_running, int expected_waiting_retry) {
EXPECT_THAT(
TimedVariableTotal(
PopularityContestScheduleRewriteController::kNumRewritesRequested),
Eq(expected_num_requests));
EXPECT_THAT(
TimedVariableTotal(
PopularityContestScheduleRewriteController::kNumRewritesSucceeded),
Eq(expected_num_success));
EXPECT_THAT(
TimedVariableTotal(
PopularityContestScheduleRewriteController::kNumRewritesFailed),
Eq(expected_num_failed));
EXPECT_THAT(TimedVariableTotal(PopularityContestScheduleRewriteController::
kNumRewritesRejectedQueueSize),
Eq(expected_rejected_queue_size));
EXPECT_THAT(TimedVariableTotal(PopularityContestScheduleRewriteController::
kNumRewritesRejectedInProgress),
Eq(expected_rejected_in_progress));
EXPECT_THAT(
CounterValue(
PopularityContestScheduleRewriteController::kRewriteQueueSize),
Eq(expected_queue_size));
EXPECT_THAT(
CounterValue(
PopularityContestScheduleRewriteController::kNumRewritesRunning),
Eq(expected_running));
if (expected_waiting_retry >= 0) {
EXPECT_THAT(CounterValue(PopularityContestScheduleRewriteController::
kNumRewritesAwaitingRetry),
Eq(expected_waiting_retry));
}
}
int64 TimedVariableTotal(const GoogleString& name) {
return stats_.GetTimedVariable(name)->Get(TimedVariable::START);
}
int64 CounterValue(const GoogleString& name) {
return stats_.GetUpDownCounter(name)->Get();
}
// Only use this if you absolutely need to resize the controller while it
// contains items. Mostly you should be using ResetController instead.
void SetMaxQueueSize(int size) {
controller_->SetMaxQueueSizeForTesting(size);
}
scoped_ptr<ThreadSystem> thread_system_;
SimpleStats stats_;
MockTimer timer_;
scoped_ptr<PopularityContestScheduleRewriteController> controller_;
};
namespace {
// Verify that a request placed into an empty popularlity contest is run
// immediately.
TEST_F(PopularityContestScheduleRewriteControllerTest, EmptyRunsImmediately) {
CheckStats(0 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
TrackCallsFunction f;
EXPECT_THAT(f.run_called_, Eq(false));
EXPECT_THAT(f.cancel_called_, Eq(false));
controller_->ScheduleRewrite("key1", &f);
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
EXPECT_THAT(f.run_called_, Eq(true));
EXPECT_THAT(f.cancel_called_, Eq(false));
controller_->NotifyRewriteComplete("key1");
CheckStats(1 /* total */, 1 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Verify that its OK to call ScheduleRewrite in your callback.
TEST_F(PopularityContestScheduleRewriteControllerTest, ReentrantSchedule) {
TrackCallsFunction f;
ScheduleRewriteFromCallback("key1_bootstrap", "key1", &f);
EXPECT_THAT(f.run_called_, Eq(true));
CheckStats(2 /* total */, 1 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Drain queue.
controller_->NotifyRewriteComplete("key1");
CheckStats(2 /* total */, 2 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Verify that its OK to call ScheduleRewrite in a callback that runs in
// response to NotifyRewriteComplete. See also ReentrantScheduleAfterFailure.
TEST_F(PopularityContestScheduleRewriteControllerTest,
ReentrantScheduleAfterComplete) {
ResetController(1 /* max_rewrites */, kMaxQueueLength);
// Schedule a rewrite to "plug up" the queue.
TrackCallsFunction f;
controller_->ScheduleRewrite("key1", &f);
EXPECT_THAT(f.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Schedule a second rewrite that will call ScheduleRewrite when run.
TrackCallsFunction f3; // Associated with key3, not key2.
ScheduleRewriteFromCallback("key2", "key3", &f3);
EXPECT_THAT(f3.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(false));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Mark key1 as done. This should:
// - Trigger key2.
// - key2 will insert key3.
// - key2 will be mark itself done.
// - key3 will be triggered.
// It will instead deadlock/timeout if re-entrancy is wrong.
controller_->NotifyRewriteComplete("key1");
EXPECT_THAT(f3.run_called_, Eq(true));
CheckStats(3 /* total */, 2 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Drain queue.
controller_->NotifyRewriteComplete("key3");
CheckStats(3 /* total */, 3 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Verify that the callback for an older request is always Cancel()ed in favour
// of the newer one.
TEST_F(PopularityContestScheduleRewriteControllerTest, NewReplacesOld) {
ResetController(1 /* max_rewrites */, kMaxQueueLength);
// Schedule a rewrite so nothing else can be run.
TrackCallsFunction f;
controller_->ScheduleRewrite("key1", &f);
EXPECT_THAT(f.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Queue a call for "key2". It should not run.
TrackCallsFunction f2;
controller_->ScheduleRewrite("key2", &f2);
EXPECT_THAT(f2.run_called_, Eq(false));
EXPECT_THAT(f2.cancel_called_, Eq(false));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Queue another call for "key2". It should also not run, but the old request
// (f2) should be canceled.
TrackCallsFunction f3;
controller_->ScheduleRewrite("key2", &f3);
EXPECT_THAT(f3.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(false));
EXPECT_THAT(f2.cancel_called_, Eq(true));
CheckStats(3 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Drain queue.
controller_->NotifyRewriteComplete("key1");
CheckStats(3 /* total */, 1 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
controller_->NotifyRewriteComplete("key2");
CheckStats(3 /* total */, 2 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// As NewReplacesOld, but attempt to schedule something else when the old
// callback is canceled. This tests the locking/re-entrancy behaviour of that
// code path.
TEST_F(PopularityContestScheduleRewriteControllerTest,
NewReplacesOldReentrant) {
ResetController(1 /* max_rewrites */, kMaxQueueLength);
// Schedule a rewrite so nothing else can be run.
TrackCallsFunction f;
controller_->ScheduleRewrite("key1", &f);
EXPECT_THAT(f.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Queue a call for "key2" that will attempt to schedule "key3" when
// Run/Canceled. It should not run.
TrackCallsFunction f2;
ScheduleRewriteFromCallback("key2", "key3", &f2);
EXPECT_THAT(f2.run_called_, Eq(false));
EXPECT_THAT(f2.cancel_called_, Eq(false));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Queue another call for "key2". It should also not run, but the old request
// should be canceled. This should result in queueing "key3". If the
// re-entrancy is wrong, this will deadlock/timeout.
TrackCallsFunction f3;
controller_->ScheduleRewrite("key2", &f3);
EXPECT_THAT(f3.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(false));
EXPECT_THAT(f2.run_called_, Eq(false)); // These trigger when "key3" runs.
EXPECT_THAT(f2.cancel_called_, Eq(false));
CheckStats(4 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Drain queue.
controller_->NotifyRewriteComplete("key1");
EXPECT_THAT(f2.run_called_, Eq(false));
CheckStats(4 /* total */, 1 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
controller_->NotifyRewriteComplete("key2");
EXPECT_THAT(f2.run_called_, Eq(true));
CheckStats(4 /* total */, 2 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
controller_->NotifyRewriteComplete("key3");
CheckStats(4 /* total */, 3 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Verify that if a rewrite is requested for a key that is currently running,
// it will immediately be rejected.
TEST_F(PopularityContestScheduleRewriteControllerTest, DuplicatesRejected) {
// Start processing key1.
TrackCallsFunction f;
controller_->ScheduleRewrite("key1", &f);
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
EXPECT_THAT(f.run_called_, Eq(true));
// Now try to process key1 while it's already running. It should be rejected.
TrackCallsFunction f2;
controller_->ScheduleRewrite("key1", &f2);
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
1 /* already_running */, 1 /* queue_size */, 1 /* running */);
EXPECT_THAT(f2.cancel_called_, Eq(true));
// Finish up key1.
controller_->NotifyRewriteComplete("key1");
CheckStats(2 /* total */, 1 /* success */, 0 /* fail */, 0 /* queue_full */,
1 /* already_running */, 0 /* queue_size */, 0 /* running */);
// Now try key1 again and make sure it runs.
TrackCallsFunction f3;
controller_->ScheduleRewrite("key1", &f3);
CheckStats(3 /* total */, 1 /* success */, 0 /* fail */, 0 /* queue_full */,
1 /* already_running */, 1 /* queue_size */, 1 /* running */);
EXPECT_THAT(f3.run_called_, Eq(true));
// Now try the same thing again, but try to start a rewrite from a callback.
// Here we are trying to verify correct re-entrancy on this failure path.
TrackCallsFunction f4;
ScheduleRewriteFromCallback("key1", "key2", &f4);
EXPECT_THAT(f4.run_called_, Eq(true));
CheckStats(5 /* total */, 1 /* success */, 0 /* fail */, 0 /* queue_full */,
2 /* already_running */, 2 /* queue_size */, 2 /* running */);
// Dain queue.
controller_->NotifyRewriteComplete("key1");
CheckStats(5 /* total */, 2 /* success */, 0 /* fail */, 0 /* queue_full */,
2 /* already_running */, 1 /* queue_size */, 1 /* running */);
controller_->NotifyRewriteComplete("key2");
CheckStats(5 /* total */, 3 /* success */, 0 /* fail */, 0 /* queue_full */,
2 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Verify "NotifyRewriteFailed" path correctly reports statistics and
// runs subsequent jobs.
TEST_F(PopularityContestScheduleRewriteControllerTest, BasicFailure) {
ResetController(1 /* max_rewrites */, kMaxQueueLength);
// Start "key1" runnining immediately.
TrackCallsFunction f;
controller_->ScheduleRewrite("key1", &f);
EXPECT_THAT(f.run_called_, Eq(true));
EXPECT_THAT(f.cancel_called_, Eq(false));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Add "key2" to the queue, to be run later.
TrackCallsFunction f2;
controller_->ScheduleRewrite("key2", &f2);
EXPECT_THAT(f2.run_called_, Eq(false));
EXPECT_THAT(f2.cancel_called_, Eq(false));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Report failure of key1. Verify key2 is now run and the statistics are
// correct. Note that key1 remains in the queue because of the failure.
controller_->NotifyRewriteFailed("key1");
EXPECT_THAT(f2.run_called_, Eq(true));
CheckStats(2 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Drain queue.
controller_->NotifyRewriteComplete("key2");
CheckStats(2 /* total */, 1 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 0 /* running */);
}
// Verify that its OK to call ScheduleRewrite in a callback that runs in
// response to NotifyRewriteFailed. Counterpart to
// ReentrantScheduleAfterComplete.
TEST_F(PopularityContestScheduleRewriteControllerTest,
ReentrantScheduleAfterFailure) {
ResetController(1 /* max_rewrites */, kMaxQueueLength);
// Schedule a rewrite to "plug up" the queue.
TrackCallsFunction f;
controller_->ScheduleRewrite("key1", &f);
EXPECT_THAT(f.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Schedule a second rewrite that will call ScheduleRewrite when run.
TrackCallsFunction f3; // Associated with key3, not key2.
ScheduleRewriteFromCallback("key2", "key3", &f3);
EXPECT_THAT(f3.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(false));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Mark key1 as failed. This should:
// - Trigger key2.
// - key2 will insert key3.
// - key2 will be mark itself done.
// - key3 will be triggered.
// It will instead deadlock/timeout if re-entrancy is wrong.
// Note that "key1" remains in the popularity contest after this.
controller_->NotifyRewriteFailed("key1");
EXPECT_THAT(f3.run_called_, Eq(true));
CheckStats(3 /* total */, 1 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Drain queue.
controller_->NotifyRewriteComplete("key3");
CheckStats(3 /* total */, 2 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 0 /* running */);
}
// Verify keys are run in order of popularity.
TEST_F(PopularityContestScheduleRewriteControllerTest, BasicPopularity) {
ResetController(1 /* max_rewrites */, kMaxQueueLength);
TrackCallsFunction f1;
controller_->ScheduleRewrite("k1", &f1); // k1 => 1 (run).
EXPECT_THAT(f1.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
TrackCallsFunction f2;
controller_->ScheduleRewrite("k2", &f2); // k1 => 1 (run), k2 => 1.
EXPECT_THAT(f2.run_called_, Eq(false));
EXPECT_THAT(f2.cancel_called_, Eq(false));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
TrackCallsFunction f3;
controller_->ScheduleRewrite("k3", &f3); // k1 => 1 (run), k2 => 1, k3 => 1.
EXPECT_THAT(f3.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(false));
CheckStats(3 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 3 /* queue_size */, 1 /* running */);
// k2 should now have been raised above k3.
TrackCallsFunction f2a;
controller_->ScheduleRewrite("k2", &f2a); // k1 => 1 (run), k2 => 2, k3 => 1.
EXPECT_THAT(f2a.run_called_, Eq(false));
EXPECT_THAT(f2a.cancel_called_, Eq(false));
// The old one should have been canceled.
EXPECT_THAT(f2.cancel_called_, Eq(true));
CheckStats(4 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Mark k1 as complete and verify k2 runs, not k3 which was added first.
controller_->NotifyRewriteComplete("k1"); // k2 => 2 (run), k3 => 1.
EXPECT_THAT(f2a.run_called_, Eq(true));
CheckStats(4 /* total */, 1 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
controller_->NotifyRewriteComplete("k2"); // k3 => 1 (run).
EXPECT_THAT(f3.run_called_, Eq(true));
CheckStats(4 /* total */, 2 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
controller_->NotifyRewriteComplete("k3");
CheckStats(4 /* total */, 3 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Priorities should be preserved for keys that report failure
// (NotifyRewriteFailed). Verify that the priority of a key is actually
// preserved.
TEST_F(PopularityContestScheduleRewriteControllerTest,
FailurePreservesPriority) {
ResetController(1 /* max_rewrites */, kMaxQueueLength);
// Start running "k1".
TrackCallsFunction f1;
controller_->ScheduleRewrite("k1", &f1); // k1 => 1 (run).
EXPECT_THAT(f1.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Put another couple of keys into the popularity contest, each at priority 1.
TrackCallsFunction f2;
controller_->ScheduleRewrite("k2", &f2); // k1 => 1 (run), k2 => 1.
EXPECT_THAT(f2.run_called_, Eq(false));
EXPECT_THAT(f2.cancel_called_, Eq(false));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
TrackCallsFunction f3;
controller_->ScheduleRewrite("k3", &f3); // k1 => 1 (run), k2 => 1, k3 => 1.
EXPECT_THAT(f3.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(false));
CheckStats(3 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Mark k1 as having failed (NotifyRewriteFailed). It should remain in the
// queue with its previous priority (1), which is reflected in the queue size.
controller_->NotifyRewriteFailed("k1"); // k2 => 2 (run), k3 => 1, k1 => 1.
EXPECT_THAT(f2.run_called_, Eq(true));
CheckStats(3 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Put "k1" back in the queue. It should be inserted at its previous priority
// plus 1.
TrackCallsFunction f1a;
controller_->ScheduleRewrite("k1", &f1a); // k2 => 1 (run), k1 => 2, k3 => 1.
EXPECT_THAT(f1a.run_called_, Eq(false));
CheckStats(4 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Now verify that when k2 completes we run the callback for k1 and not k3.
// (Compare this to SuccessForgetsPriority).
controller_->NotifyRewriteComplete("k2"); // k3 => 1 (run), k1 => 1.
EXPECT_THAT(f1a.run_called_, Eq(true));
EXPECT_THAT(f3.run_called_, Eq(false));
CheckStats(4 /* total */, 1 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Drain out the queue.
controller_->NotifyRewriteComplete("k1");
EXPECT_THAT(f3.run_called_, Eq(true));
CheckStats(4 /* total */, 2 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
controller_->NotifyRewriteComplete("k3");
CheckStats(4 /* total */, 3 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Counterpart to FailurePreservesPriority; Verify that for a successful
// rewrite (NotifyRewriteComplete), priority is not remembered across runs.
TEST_F(PopularityContestScheduleRewriteControllerTest, SuccessForgetsPriority) {
ResetController(1 /* max_rewrites */, kMaxQueueLength);
// Start running "k1".
TrackCallsFunction f1;
controller_->ScheduleRewrite("k1", &f1); // k1 => 1 (run).
EXPECT_THAT(f1.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Put another couple of keys into the popularity contest, each at priority 1.
TrackCallsFunction f2;
controller_->ScheduleRewrite("k2", &f2); // k1 => 1 (run), k2 => 1.
EXPECT_THAT(f2.run_called_, Eq(false));
EXPECT_THAT(f2.cancel_called_, Eq(false));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
TrackCallsFunction f3;
controller_->ScheduleRewrite("k3", &f3); // k1 => 1 (run), k2 => 1, k3 => 1.
EXPECT_THAT(f3.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(false));
CheckStats(3 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Mark k1 as having succeeded (NotifyRewriteComplete). It should be
// completely removed from the queue.
controller_->NotifyRewriteComplete("k1"); // k2 => 2 (run), k3 => 1.
EXPECT_THAT(f2.run_called_, Eq(true));
CheckStats(3 /* total */, 1 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Put "k1" back in the queue. It should be inserted at priority 1 and not
// "inherit" its previous priority.
TrackCallsFunction f1a;
controller_->ScheduleRewrite("k1", &f1a); // k2 => 1 (run), k3 => 1, k1 => 1.
EXPECT_THAT(f1a.run_called_, Eq(false));
CheckStats(4 /* total */, 1 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Now verify that when k2 completes we run the callback for k3 and not k1.
// (Compare this to FailurePreservesPriority).
controller_->NotifyRewriteComplete("k2"); // k3 => 1 (run), k1 => 1.
EXPECT_THAT(f3.run_called_, Eq(true));
EXPECT_THAT(f1a.run_called_, Eq(false));
CheckStats(4 /* total */, 2 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Drain out the queue.
controller_->NotifyRewriteComplete("k3");
EXPECT_THAT(f1a.run_called_, Eq(true));
CheckStats(4 /* total */, 3 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
controller_->NotifyRewriteComplete("k1");
CheckStats(4 /* total */, 4 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Counterpart to FailurePreservesPriority; Verify that priorities for failing
// keys are preserved even if the queue drains completely. Also verifies that
// the popularity contest doesn't try to do anything if it only contains
// a "hold-over" priority.
TEST_F(PopularityContestScheduleRewriteControllerTest,
FailedPriorityIsRememberedAcrossEmptyQueue) {
ResetController(1 /* max_rewrites */, kMaxQueueLength);
// Start running "k1".
TrackCallsFunction f1;
controller_->ScheduleRewrite("k1", &f1); // k1 => 1 (run).
EXPECT_THAT(f1.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Mark k1 as having failed. The popularity contest should now contain only a
// single entry for k1, that isn't runnable.
controller_->NotifyRewriteFailed("k1"); // (k1 => 1).
CheckStats(1 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 0 /* running */);
// Start running "k4".
TrackCallsFunction f4;
controller_->ScheduleRewrite("k4", &f4); // k4 => 1 (run), (k1 => 1).
EXPECT_THAT(f4.run_called_, Eq(true));
CheckStats(2 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Mark "k4" as done. Verifies that the popularity contest will not try to run
// "k1".
controller_->NotifyRewriteComplete("k4"); // (k1 => 1).
CheckStats(2 /* total */, 1 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 0 /* running */);
// Start running "k2".
TrackCallsFunction f2;
controller_->ScheduleRewrite("k2", &f2); // k2 => 1 (run), (k1 => 1).
EXPECT_THAT(f2.run_called_, Eq(true));
CheckStats(3 /* total */, 1 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Put "k3" into the queue.
TrackCallsFunction f3;
controller_->ScheduleRewrite("k3", &f3); // k2 => 1 (run), k3 => 1, (k1 => 1)
EXPECT_THAT(f3.run_called_, Eq(false));
CheckStats(4 /* total */, 1 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Put "k1" back in the queue. It should be inserted at its previous priority
// plus 1.
TrackCallsFunction f1a;
controller_->ScheduleRewrite("k1", &f1a); // k2 => 1 (run), k1 => 2, k3 => 1.
EXPECT_THAT(f1a.run_called_, Eq(false));
CheckStats(5 /* total */, 1 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Now verify that when k2 completes we run the callback for k1 and not k3
// (as per SuccessForgetsPriority).
controller_->NotifyRewriteComplete("k2"); // k1 => 2 (run), k3 => 1.
EXPECT_THAT(f1a.run_called_, Eq(true));
EXPECT_THAT(f3.run_called_, Eq(false));
CheckStats(5 /* total */, 2 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Drain out the queue.
controller_->NotifyRewriteComplete("k1"); // k3 => 1 (run).
EXPECT_THAT(f3.run_called_, Eq(true));
CheckStats(5 /* total */, 3 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
controller_->NotifyRewriteComplete("k3");
CheckStats(5 /* total */, 4 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Combination of FailurePreservesPriority and DuplicatesRejected; If a key
// is requested during run and then subsequently fails, verify that the request
// was rejected but still caused the priority to increase, applying to a re-run.
TEST_F(PopularityContestScheduleRewriteControllerTest,
PriorityIsIncrementedDuringRun) {
ResetController(1 /* max_rewrites */, kMaxQueueLength);
// Start running "k1".
TrackCallsFunction f1;
controller_->ScheduleRewrite("k1", &f1); // k1 => 1 (run).
EXPECT_THAT(f1.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Attempt another run for "k1". It should be rejected, but the priority
// should still be increased.
TrackCallsFunction f1a;
controller_->ScheduleRewrite("k1", &f1a); // k1 => 2 (run).
EXPECT_THAT(f1a.cancel_called_, Eq(true));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
1 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Put another couple of keys on the queue, at priority 1.
TrackCallsFunction f2;
controller_->ScheduleRewrite("k2", &f2); // k1 => 2 (run), k2 => 1.
EXPECT_THAT(f2.run_called_, Eq(false));
EXPECT_THAT(f2.cancel_called_, Eq(false));
CheckStats(3 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
1 /* already_running */, 2 /* queue_size */, 1 /* running */);
TrackCallsFunction f3;
controller_->ScheduleRewrite("k3", &f3); // k1 => 2 (run), k2 => 1, k3 => 1.
EXPECT_THAT(f3.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(false));
CheckStats(4 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Mark k1 as having failed. It should remain in the queue with its
// incremented priority.
controller_->NotifyRewriteFailed("k1"); // k2 => 2 (run), k3 => 1, (k1 => 2).
EXPECT_THAT(f2.run_called_, Eq(true));
CheckStats(4 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Raise the priority of k3 to 2.
TrackCallsFunction f3a;
controller_->ScheduleRewrite("k3", &f3a); // k2 => 2(run), k3 => 2, (k1 => 2)
EXPECT_THAT(f3a.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(true));
CheckStats(5 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Put "k1" back in the queue. It should be inserted at its previous priority
// plus 1, which is 3 in this case.
TrackCallsFunction f1b;
controller_->ScheduleRewrite("k1", &f1b); // k2 => 1 (run), k1 => 3, k3 => 2.
EXPECT_THAT(f1b.run_called_, Eq(false));
CheckStats(6 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */);
// Now verify that when k2 completes we run the callback for k1 and not k3.
controller_->NotifyRewriteComplete("k2"); // k1 => 3 (run), k3 => 2.
EXPECT_THAT(f1b.run_called_, Eq(true));
EXPECT_THAT(f3a.run_called_, Eq(false));
CheckStats(6 /* total */, 1 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 2 /* queue_size */, 1 /* running */);
// Drain queue.
controller_->NotifyRewriteComplete("k1");
EXPECT_THAT(f3a.run_called_, Eq(true));
CheckStats(6 /* total */, 2 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 1 /* queue_size */, 1 /* running */);
controller_->NotifyRewriteComplete("k3");
CheckStats(6 /* total */, 3 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Verify that, if the queue is full, a queued retry will be dropped to make
// room for another rewrite.
TEST_F(PopularityContestScheduleRewriteControllerTest, OldRetryEviction) {
ResetController(1 /* max_rewrites */, 3 /* max_queue_length */);
// Start running "k1".
TrackCallsFunction f1;
ScheduleRewriteAndAdvanceClock("k1", &f1); // k1 => 1 (run).
EXPECT_THAT(f1.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
// Try and run k1 again, to increment the priority.
TrackCallsFunction f1a;
ScheduleRewriteAndAdvanceClock("k1", &f1a); // k1 => 2 (run).
EXPECT_THAT(f1a.cancel_called_, Eq(true));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
1 /* already_running */, 1 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
// Mark "k1" as failed. This should queue it for retry.
NotifyFailedAndAdvanceClock("k1"); // (k1 => 2).
CheckStats(2 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 1 /* queue_size */, 0 /* running */,
1 /* queued_retries */);
// Start running "k2".
TrackCallsFunction f2;
ScheduleRewriteAndAdvanceClock("k2", &f2); // k2 => 1 (run), (k1 => 2).
EXPECT_THAT(f2.run_called_, Eq(true));
CheckStats(3 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 2 /* queue_size */, 1 /* running */,
1 /* queued_retries */);
// Queue "k3".
TrackCallsFunction f3;
ScheduleRewriteAndAdvanceClock("k3", &f3); // k2 =>1(run), k3 => 1, (k1 => 2)
EXPECT_THAT(f3.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(false));
CheckStats(4 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */,
1 /* queued_retries */);
// Queue "k4". This should push out k1.
TrackCallsFunction f4;
ScheduleRewriteAndAdvanceClock("k4", &f4); // k2 => 1(run), k3 => 1, k4 => 1.
EXPECT_THAT(f4.run_called_, Eq(false));
EXPECT_THAT(f4.cancel_called_, Eq(false));
CheckStats(5 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
// Mark k2 as done.
NotifyCompleteAndAdvanceClock("k2"); // k3 => 1 (run), k4 => 1.
EXPECT_THAT(f3.run_called_, Eq(true));
CheckStats(5 /* total */, 1 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 2 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
// Re-add k1. It should be queued with a priority of 1. It would be 3 if it
// had not been flushed above.
TrackCallsFunction f1b;
ScheduleRewriteAndAdvanceClock("k1", &f1b); // k3 => 1(run), k4 => 1, k1 => 1
EXPECT_THAT(f1b.run_called_, Eq(false));
EXPECT_THAT(f1b.cancel_called_, Eq(false));
CheckStats(6 /* total */, 1 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
// Mark k3 as having succeeded. k4 (*not* k1) should run.
NotifyCompleteAndAdvanceClock("k3"); // k4 => 1 (run), k1 => 1
EXPECT_THAT(f4.run_called_, Eq(true));
EXPECT_THAT(f1b.run_called_, Eq(false));
CheckStats(6 /* total */, 2 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 2 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
// Drain queue.
NotifyCompleteAndAdvanceClock("k4");
EXPECT_THAT(f1b.run_called_, Eq(true));
CheckStats(6 /* total */, 3 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 1 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
NotifyCompleteAndAdvanceClock("k1");
CheckStats(6 /* total */, 4 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 0 /* queue_size */, 0 /* running */,
0 /* queued_retries */);
}
// Verify that when the queue fills, queued retries are dropped in order of
// oldest first, not order of priority.
TEST_F(PopularityContestScheduleRewriteControllerTest,
RetryEvictionAgeBeforePriority) {
ResetController(1 /* max_rewrites */, 3 /* max_queue_length */);
// Start running "k1".
TrackCallsFunction f1;
ScheduleRewriteAndAdvanceClock("k1", &f1); // k1 => 1 (run).
EXPECT_THAT(f1.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
// Try and run k1 again, to increment the priority.
TrackCallsFunction f1a;
ScheduleRewriteAndAdvanceClock("k1", &f1a); // k1 => 2 (run).
EXPECT_THAT(f1a.cancel_called_, Eq(true));
CheckStats(2 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
1 /* already_running */, 1 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
// Mark "k1" as failed. This should queue it for retry.
NotifyFailedAndAdvanceClock("k1"); // (k1 => 2).
CheckStats(2 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 1 /* queue_size */, 0 /* running */,
1 /* queued_retries */);
// Start running "k2".
TrackCallsFunction f2;
ScheduleRewriteAndAdvanceClock("k2", &f2); // k2 => 1, (k1 => 2).
EXPECT_THAT(f2.run_called_, Eq(true));
CheckStats(3 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
1 /* already_running */, 2 /* queue_size */, 1 /* running */,
1 /* queued_retries */);
// Mark "k2" as failed. This should queue it for retry, too.
NotifyFailedAndAdvanceClock("k2"); // (k1 => 2), (k2 => 1).
CheckStats(3 /* total */, 0 /* success */, 2 /* fail */, 0 /* queue_full */,
1 /* already_running */, 2 /* queue_size */, 0 /* running */,
2 /* queued_retries */);
// Start running k3. The queue is now full.
TrackCallsFunction f3;
ScheduleRewriteAndAdvanceClock("k3", &f3); // k3=>1 (run), (k1=>2), (k2=>1).
EXPECT_THAT(f3.run_called_, Eq(true));
CheckStats(4 /* total */, 0 /* success */, 2 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */,
2 /* queued_retries */);
// Queue k4. This should push out k1.
TrackCallsFunction f4;
ScheduleRewriteAndAdvanceClock("k4", &f4); // k3=>1(run), k4=1, (k2=>1).
EXPECT_THAT(f4.run_called_, Eq(false));
EXPECT_THAT(f4.cancel_called_, Eq(false));
CheckStats(5 /* total */, 0 /* success */, 2 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */,
1 /* queued_retries */);
// Mark k3 done. This should start k4.
NotifyCompleteAndAdvanceClock("k3"); // k4=1 (run), (k2=>1).
EXPECT_THAT(f4.run_called_, Eq(true));
CheckStats(5 /* total */, 1 /* success */, 2 /* fail */, 0 /* queue_full */,
1 /* already_running */, 2 /* queue_size */, 1 /* running */,
1 /* queued_retries */);
// Now we enqueue k1 and k2 again.
TrackCallsFunction f1b;
ScheduleRewriteAndAdvanceClock("k1", &f1b); // k4=1 (run), k1 => 1, (k2=>1).
EXPECT_THAT(f1b.run_called_, Eq(false));
EXPECT_THAT(f1b.cancel_called_, Eq(false));
CheckStats(6 /* total */, 1 /* success */, 2 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */,
1 /* queued_retries */);
TrackCallsFunction f2a;
ScheduleRewriteAndAdvanceClock("k2", &f2a); // k4=1 (run), k2 => 2, k1 => 1.
EXPECT_THAT(f2a.run_called_, Eq(false));
EXPECT_THAT(f2a.cancel_called_, Eq(false));
CheckStats(7 /* total */, 1 /* success */, 2 /* fail */, 0 /* queue_full */,
1 /* already_running */, 3 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
// Now, mark k4 as done. k2 should be executed because it had a saved value
// of 2. If k1 was not ejected, it would be run now instead.
NotifyCompleteAndAdvanceClock("k4"); // k2 => 2 (run), k1 => 1.
EXPECT_THAT(f1b.run_called_, Eq(false));
EXPECT_THAT(f2a.run_called_, Eq(true));
CheckStats(7 /* total */, 2 /* success */, 2 /* fail */, 0 /* queue_full */,
1 /* already_running */, 2 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
// Drain queue.
NotifyCompleteAndAdvanceClock("k2"); // k1 => 1 (run).
EXPECT_THAT(f1b.run_called_, Eq(true));
CheckStats(7 /* total */, 3 /* success */, 2 /* fail */, 0 /* queue_full */,
1 /* already_running */, 1 /* queue_size */, 1 /* running */,
0 /* queued_retries */);
NotifyCompleteAndAdvanceClock("k1");
CheckStats(7 /* total */, 4 /* success */, 2 /* fail */, 0 /* queue_full */,
1 /* already_running */, 0 /* queue_size */, 0 /* running */,
0 /* queued_retries */);
}
// Verify the two queue bounds (max running, max queued).
TEST_F(PopularityContestScheduleRewriteControllerTest, QueueFills) {
// The test is agnostic to the values of kMaxRewrites and kMaxQueueLength, but
// kMaxQueueLength must be > kMaxRewrites for a full test, so that's asserted.
ASSERT_THAT(kMaxQueueLength, Gt(kMaxRewrites));
// Since they are equally weighted, the keys come out of the popularity
// contest in undefined order. This is important later, because if you call
// NotifyRewriteComplete on a key that's not been triggered, the popularity
// contest will CHECK-fail. So, we use RecordKeyFunction to keep a track of
// which keys were triggered.
std::queue<GoogleString> pending_rewrites;
// Queue kMaxQueueLength functions.
for (int i = 1; i <= kMaxQueueLength; ++i) {
const GoogleString key = IntegerToString(i);
controller_->ScheduleRewrite(key,
new RecordKeyFunction(key, &pending_rewrites));
// After kMaxRewrites, the functions should stop running and start queueing.
int expected_running = std::min(i, kMaxRewrites);
CheckStats(i /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, i /* queue_size */,
expected_running /* running */);
EXPECT_THAT(pending_rewrites.size(), Eq(expected_running));
}
// Double-check that the number of "running" rewrites matches the constant.
EXPECT_THAT(pending_rewrites.size(), Eq(kMaxRewrites));
// Now check that we can't queue another rewrite.
TrackCallsFunction f;
controller_->ScheduleRewrite("unused", &f);
EXPECT_THAT(f.cancel_called_, Eq(true));
CheckStats(kMaxQueueLength + 1 /* total */, 0 /* success */, 0 /* fail */,
1 /* queue_full */, 0 /* already_running */,
kMaxQueueLength /* queue_size */, kMaxRewrites /* running */);
// Now run through all the operations and process them.
for (int i = 1; i <= kMaxQueueLength; ++i) {
// Mark the most recently initiated work as complete.
ASSERT_THAT(pending_rewrites.size(), Gt(0));
controller_->NotifyRewriteComplete(pending_rewrites.front());
pending_rewrites.pop();
int expected_running = std::min(kMaxQueueLength - i, kMaxRewrites);
EXPECT_THAT(pending_rewrites.size(), Eq(expected_running));
CheckStats(kMaxQueueLength + 1 /* total */, i /* success */, 0 /* fail */,
1 /* queue_full */, 0 /* already_running */,
kMaxQueueLength - i /* queue_size */,
expected_running /* running */);
}
// Double check that nothing is left "running".
EXPECT_THAT(pending_rewrites, IsEmpty());
// Double check that the contest reports empty.
CheckStats(kMaxQueueLength + 1 /* total */, kMaxQueueLength /* success */,
0 /* fail */, 1 /* queue_full */, 0 /* already_running */,
0 /* queue_size */, 0 /* running */);
}
TEST_F(PopularityContestScheduleRewriteControllerTest, QueueFullReentrancy) {
ResetController(1 /* max_rewrites */, 1 /* max_queue_length */);
// Schedule a rewrite to "plug up" the queue.
TrackCallsFunction f;
controller_->ScheduleRewrite("key1", &f);
EXPECT_THAT(f.run_called_, Eq(true));
CheckStats(1 /* total */, 0 /* success */, 0 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Attempt to schedule a second rewrite, which should fail because the queue
// is full. This should:
// - Immediately reject key2.
// - key2's Cancel will attempt to insert key3.
// - key3's Cancel will be run.
// It will instead deadlock/timeout if re-entrancy is wrong.
TrackCallsFunction f3; // Associated with key3, not key2.
ScheduleRewriteFromCallback("key2", "key3", &f3);
EXPECT_THAT(f3.run_called_, Eq(false));
EXPECT_THAT(f3.cancel_called_, Eq(true));
CheckStats(3 /* total */, 0 /* success */, 0 /* fail */, 2 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 1 /* running */);
// Drain queue.
controller_->NotifyRewriteComplete("key1");
CheckStats(3 /* total */, 1 /* success */, 0 /* fail */, 2 /* queue_full */,
0 /* already_running */, 0 /* queue_size */, 0 /* running */);
}
// Run a series of rewrites to verify that the secondary queue for expired
// rewrites functions as expected. This test puts a number of failures into
// the retry queue and then pushes them out one by one. It is an exercise of
// the retry queue but also verifies that the retry queue prioritises time
// enqueued over priority.
TEST_F(PopularityContestScheduleRewriteControllerTest, OldRetryTortureTest) {
const int kNumFailuresToTest = 100;
const int kQueueSize = kNumFailuresToTest + 1;
ResetController(1 /* max_rewrites */, kQueueSize);
// Preload a failure for "0" so it winds up with a priority of 2, not 1.
{
TrackCallsFunction f;
ScheduleRewriteAndAdvanceClock("0", &f);
EXPECT_THAT(f.run_called_, Eq(true));
NotifyFailedAndAdvanceClock("0");
}
CheckStats(1 /* total */, 0 /* success */, 1 /* fail */, 0 /* queue_full */,
0 /* already_running */, 1 /* queue_size */, 0 /* running */,
1 /* queued_retries */);
std::queue<GoogleString> active_rewrites;
int cumulative_successes = 0;
int cumulative_failures = 1;
for (int i = 0; i < kNumFailuresToTest; ++i) {
// We want to engineer it so that there is a queued rewrite for key "i"
// with has a higher priority than all the other keys, but is first to be
// removed from the queue due to age. The previous iteration (or bootstrap)
// incremented the priority of the queued retry key for "i", which will have
// brought it to the top. So, now we engineer a failure for all keys, in
// order. This makes sure that "i" was touched the longest ago, which should
// put it first in line to be dropped.
for (int j = i; j < kNumFailuresToTest; ++j) {
const GoogleString key = IntegerToString(j);
TrackCallsFunction f;
ScheduleRewriteAndAdvanceClock(key, &f);
NotifyFailedAndAdvanceClock(key);
++cumulative_failures;
}
CheckStats(
1 + i * (kNumFailuresToTest + 3) + kNumFailuresToTest /* total */,
cumulative_successes /* success */,
cumulative_failures /* fail */, 0 /* queue_full */,
0 /* already_running */, kNumFailuresToTest - i /* queue_size */,
0 /* running */, kNumFailuresToTest - i /* queued_retries */);
// Plug up the head of the queue.
const GoogleString queue_block_key("block");
TrackCallsFunction queue_block_callback;
ScheduleRewriteAndAdvanceClock(queue_block_key, &queue_block_callback);
// Schedule something that should already exist in the retry queue.
const GoogleString hi_priority_key(IntegerToString(i + 1));
TrackCallsFunction hi_priority_callback;
ScheduleRewriteAndAdvanceClock(hi_priority_key, &hi_priority_callback);
EXPECT_THAT(hi_priority_callback.run_called_, Eq(false));
EXPECT_THAT(hi_priority_callback.cancel_called_, Eq(false));
// We know that there are kNumFailuresToTest - i queued retries right now.
// We want to queue just enough rewrites to push out i, but not i + 1.
// This value is clamped, otherwise the terminal case will over-fill the
// queue.
int num_dummies_to_insert = std::min(i + 1, kNumFailuresToTest - 1);
for (int j = 0; j < num_dummies_to_insert; ++j) {
const GoogleString key("dummy-" + IntegerToString(j));
ScheduleRewriteAndAdvanceClock(
key, new RecordKeyFunction(key, &active_rewrites));
}
// Verify that the queue is actually full (queue_size == kQueueSize).
CheckStats(3 + i * (kNumFailuresToTest + 3) + kNumFailuresToTest +
num_dummies_to_insert /* total */,
cumulative_successes /* success */,
cumulative_failures /* fail */, 0 /* queue_full */,
0 /* already_running */, kQueueSize /* queue_size */,
1 /* running */,
kNumFailuresToTest - i - 2 /* queued_retries */);
// Now that we have pushed key "i" out from the retry queue, we need to
// re-insert it into the main queue. Unfortunately, we pushed "i" out by
// filling the queue, so we can no longer insert it. We also can't remove
// one of the dummy entries, because that is not supported by the popularity
// contest API. So, we cheat and temporarily raise the limit of the queue.
SetMaxQueueSize(kQueueSize + 1);
const GoogleString i_as_string(IntegerToString(i));
ScheduleRewriteAndAdvanceClock(
i_as_string, new RecordKeyFunction(i_as_string, &active_rewrites));
// Mark the head entry as complete. This will now run the highest priority
// entry. That should be the one for i + 1 (hi_priority) and not the one
// for i.
NotifyCompleteAndAdvanceClock(queue_block_key);
++cumulative_successes;
ASSERT_THAT(hi_priority_callback.run_called_, Eq(true));
EXPECT_THAT(active_rewrites, IsEmpty());
// Restore size back to normal.
SetMaxQueueSize(kQueueSize);
CheckStats(4 + i * (kNumFailuresToTest + 3) + kNumFailuresToTest +
num_dummies_to_insert /* total */,
cumulative_successes /* success */,
cumulative_failures /* fail */, 0 /* queue_full */,
0 /* already_running */, kQueueSize /* queue_size */,
1 /* running */,
kNumFailuresToTest - i - 2 /* queued_retries */);
// This results in a queued retry with an incremented priority.
NotifyFailedAndAdvanceClock(hi_priority_key);
++cumulative_failures;
// Run all the outstanding rewrites, making sure that we did actually
// run the one for "i".
bool saw_rewrite_for_i = false;
for (int j = 0; j < num_dummies_to_insert + 1; ++j) {
ASSERT_THAT(active_rewrites.size(), Gt(0));
const GoogleString& front = active_rewrites.front();
if (front == i_as_string) {
saw_rewrite_for_i = true;
}
NotifyCompleteAndAdvanceClock(front);
active_rewrites.pop();
++cumulative_successes;
}
EXPECT_THAT(saw_rewrite_for_i, Eq(true));
ASSERT_THAT(active_rewrites, IsEmpty());
// This never hits zero because we always fail the i'th value.
int expected_queued_items =
(i == kNumFailuresToTest - 1) ? 1 : kNumFailuresToTest - i - 1;
CheckStats(4 + i * (kNumFailuresToTest + 3) + kNumFailuresToTest +
num_dummies_to_insert /* total */,
cumulative_successes /* success */,
cumulative_failures /* fail */, 0 /* queue_full */,
0 /* already_running */,
expected_queued_items /* queue_size */, 0 /* running */,
expected_queued_items /* queued_retries */);
}
}
} // namespace
} // namespace net_instaweb