blob: 1c186a800d187959772f3d82883bc97c39e81b52 [file] [edit]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/disk_cache/sql/exclusive_operation_coordinator.h"
#include <memory>
#include <string>
#include <vector>
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "net/disk_cache/sql/cache_entry_key.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::ElementsAre;
namespace disk_cache {
class ExclusiveOperationCoordinatorTest : public testing::Test {
protected:
using OperationHandle = ExclusiveOperationCoordinator::OperationHandle;
ExclusiveOperationCoordinator coordinator_;
std::vector<std::string> execution_log_;
};
TEST_F(ExclusiveOperationCoordinatorTest, ExclusiveOperationRunsWhenIdle) {
bool executed = false;
coordinator_.PostOrRunExclusiveOperation(base::BindLambdaForTesting(
[&](std::unique_ptr<OperationHandle> handle) { executed = true; }));
EXPECT_TRUE(executed);
}
TEST_F(ExclusiveOperationCoordinatorTest,
ExclusiveOperationDoesNotRunWhileAnotherIsRunning) {
std::unique_ptr<OperationHandle> exclusive_handle1;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("E1_start");
exclusive_handle1 = std::move(handle);
}));
ASSERT_TRUE(exclusive_handle1);
EXPECT_THAT(execution_log_, ElementsAre("E1_start"));
// Post another exclusive operation. This should be queued.
bool e2_executed = false;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("E2_start");
e2_executed = true;
}));
// E2 should not have run yet because E1 is in flight. The call to
// PostOrRunExclusiveOperation for E2 will call TryToRunNextOperation, which
// should return early because `exclusive_operation_running_` is true.
EXPECT_FALSE(e2_executed);
EXPECT_THAT(execution_log_, ElementsAre("E1_start"));
// Complete E1. Now E2 should run.
execution_log_.push_back("E1_end");
exclusive_handle1.reset();
EXPECT_TRUE(e2_executed);
EXPECT_THAT(execution_log_, ElementsAre("E1_start", "E1_end", "E2_start"));
}
TEST_F(ExclusiveOperationCoordinatorTest, NormalOperationsAreSerial) {
const CacheEntryKey kKey("my_key");
std::unique_ptr<OperationHandle> handle1;
coordinator_.PostOrRunNormalOperation(
kKey,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("N1_start");
handle1 = std::move(handle);
}));
ASSERT_TRUE(handle1);
EXPECT_THAT(execution_log_, ElementsAre("N1_start"));
std::unique_ptr<OperationHandle> handle2;
coordinator_.PostOrRunNormalOperation(
kKey,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("N2_start");
handle2 = std::move(handle);
}));
// N2 should not have run yet.
EXPECT_FALSE(handle2);
EXPECT_THAT(execution_log_, ElementsAre("N1_start"));
// Complete N1.
execution_log_.push_back("N1_end");
handle1.reset();
// Now N2 should run.
ASSERT_TRUE(handle2);
EXPECT_THAT(execution_log_, ElementsAre("N1_start", "N1_end", "N2_start"));
}
TEST_F(ExclusiveOperationCoordinatorTest,
NormalOperationsWithDifferentKeysRunConcurrently) {
const CacheEntryKey kKey1("key1");
const CacheEntryKey kKey2("key2");
std::unique_ptr<OperationHandle> handle1;
std::unique_ptr<OperationHandle> handle2;
coordinator_.PostOrRunNormalOperation(
kKey1,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
handle1 = std::move(handle);
}));
coordinator_.PostOrRunNormalOperation(
kKey2,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
handle2 = std::move(handle);
}));
// Both operations should have run and given us a handle.
EXPECT_TRUE(handle1);
EXPECT_TRUE(handle2);
}
TEST_F(ExclusiveOperationCoordinatorTest, KeyedNormalWaitsForExclusive) {
std::unique_ptr<OperationHandle> exclusive_handle;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
exclusive_handle = std::move(handle);
}));
ASSERT_TRUE(exclusive_handle);
bool normal_executed = false;
coordinator_.PostOrRunNormalOperation(
CacheEntryKey("some_key"),
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
normal_executed = true;
}));
// The normal operation should not have run yet.
EXPECT_FALSE(normal_executed);
// Now, complete the exclusive operation.
exclusive_handle.reset();
// The normal operation should run now.
EXPECT_TRUE(normal_executed);
}
TEST_F(ExclusiveOperationCoordinatorTest, ExclusiveOperationWaitsForNormal) {
const CacheEntryKey kKey("my_key");
std::unique_ptr<OperationHandle> normal_handle;
coordinator_.PostOrRunNormalOperation(
kKey,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("N1_start");
normal_handle = std::move(handle);
}));
ASSERT_TRUE(normal_handle);
EXPECT_THAT(execution_log_, ElementsAre("N1_start"));
// Post an exclusive operation. This should be queued because a normal
// operation is in flight.
bool exclusive_executed = false;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("E1_start");
exclusive_executed = true;
}));
// The exclusive operation should not have run yet. The call to
// PostOrRunExclusiveOperation will call TryToRunNextOperation, which should
// return early because active_normal_operations_ > 0.
EXPECT_FALSE(exclusive_executed);
EXPECT_THAT(execution_log_, ElementsAre("N1_start"));
// Complete the normal operation. Now the exclusive operation should run.
execution_log_.push_back("N1_end");
normal_handle.reset();
EXPECT_TRUE(exclusive_executed);
EXPECT_THAT(execution_log_, ElementsAre("N1_start", "N1_end", "E1_start"));
}
TEST_F(ExclusiveOperationCoordinatorTest,
QueuedKeyedNormalOperationsRunAfterExclusive) {
std::unique_ptr<OperationHandle> exclusive_handle;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("E_start");
exclusive_handle = std::move(handle);
}));
ASSERT_TRUE(exclusive_handle);
const CacheEntryKey kKey1("key1");
const CacheEntryKey kKey2("key2");
std::unique_ptr<OperationHandle> n1a_handle;
std::unique_ptr<OperationHandle> n2_handle;
coordinator_.PostOrRunNormalOperation(
kKey1,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("N1a_run");
n1a_handle = std::move(handle);
}));
coordinator_.PostOrRunNormalOperation(
kKey2,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("N2_run");
n2_handle = std::move(handle);
}));
// This operation uses the same key as the first normal operation.
coordinator_.PostOrRunNormalOperation(
kKey1,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("N1b_run");
}));
// Normal operations should be queued.
EXPECT_THAT(execution_log_, ElementsAre("E_start"));
// Complete the exclusive operation.
execution_log_.push_back("E_end");
exclusive_handle.reset();
// N1a and N2 should have run now. N1b is queued behind N1a.
EXPECT_TRUE(n1a_handle);
EXPECT_TRUE(n2_handle);
EXPECT_THAT(execution_log_,
ElementsAre("E_start", "E_end", "N1a_run", "N2_run"));
// Complete N1a.
execution_log_.push_back("N1a_end");
n1a_handle.reset();
// N1b should run now.
EXPECT_THAT(execution_log_, ElementsAre("E_start", "E_end", "N1a_run",
"N2_run", "N1a_end", "N1b_run"));
}
TEST_F(ExclusiveOperationCoordinatorTest,
OlderNormalOpRunsBeforeNewerExclusiveOp) {
// Start an exclusive operation (E1).
std::unique_ptr<OperationHandle> exclusive_handle1;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("E1_start");
exclusive_handle1 = std::move(handle);
}));
ASSERT_TRUE(exclusive_handle1);
EXPECT_THAT(execution_log_, ElementsAre("E1_start"));
// Queue a normal operation (N1).
const CacheEntryKey kKey("my_key");
std::unique_ptr<OperationHandle> n1_handle;
coordinator_.PostOrRunNormalOperation(
kKey,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("N1_start");
n1_handle = std::move(handle);
}));
// Queue another exclusive operation (E2).
std::unique_ptr<OperationHandle> exclusive_handle2;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("E2_start");
exclusive_handle2 = std::move(handle);
}));
// Neither N1 nor E2 should have run yet.
EXPECT_FALSE(n1_handle);
EXPECT_FALSE(exclusive_handle2);
EXPECT_THAT(execution_log_, ElementsAre("E1_start"));
// Complete E1. N1 should run next.
execution_log_.push_back("E1_end");
exclusive_handle1.reset();
ASSERT_TRUE(n1_handle);
EXPECT_FALSE(exclusive_handle2);
EXPECT_THAT(execution_log_, ElementsAre("E1_start", "E1_end", "N1_start"));
// Complete N1. E2 should run now.
execution_log_.push_back("N1_end");
n1_handle.reset();
ASSERT_TRUE(exclusive_handle2);
EXPECT_THAT(execution_log_, ElementsAre("E1_start", "E1_end", "N1_start",
"N1_end", "E2_start"));
}
TEST_F(ExclusiveOperationCoordinatorTest, ExclusiveOperationDoesNotStarve) {
// Start a normal operation N1a.
const CacheEntryKey kKey1("key1");
std::unique_ptr<OperationHandle> n1a_handle;
coordinator_.PostOrRunNormalOperation(
kKey1,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("N1a_start");
n1a_handle = std::move(handle);
}));
ASSERT_TRUE(n1a_handle);
EXPECT_THAT(execution_log_, ElementsAre("N1a_start"));
// While N1 is running, queue an exclusive operation E1.
std::unique_ptr<OperationHandle> e1_handle;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("E1_start");
e1_handle = std::move(handle);
}));
// E1 should not run yet.
EXPECT_FALSE(e1_handle);
// While N1a is still running, queue another normal operation N1a with the
// same key.
std::unique_ptr<OperationHandle> n1b_handle;
coordinator_.PostOrRunNormalOperation(
kKey1,
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> handle) {
execution_log_.push_back("N1b_start");
n1b_handle = std::move(handle);
}));
// N1b should not run yet.
EXPECT_FALSE(n1b_handle);
EXPECT_THAT(execution_log_, ElementsAre("N1a_start"));
// Complete N1.
execution_log_.push_back("N1a_end");
n1a_handle.reset();
// E1 should run next, not N1b. This shows E1 isn't starved.
ASSERT_TRUE(e1_handle);
EXPECT_FALSE(n1b_handle);
EXPECT_THAT(execution_log_, ElementsAre("N1a_start", "N1a_end", "E1_start"));
// Complete E1.
execution_log_.push_back("E1_end");
e1_handle.reset();
// N1b should run.
ASSERT_TRUE(n1b_handle);
EXPECT_THAT(execution_log_, ElementsAre("N1a_start", "N1a_end", "E1_start",
"E1_end", "N1b_start"));
}
TEST_F(ExclusiveOperationCoordinatorTest, HasPendingTaskFlagBehavior) {
auto& flag = coordinator_.GetHasPendingTaskFlag();
EXPECT_FALSE(flag->data.load());
// Operation 1
std::unique_ptr<OperationHandle> handle1;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> h) {
handle1 = std::move(h);
EXPECT_FALSE(flag->data.load());
}));
EXPECT_TRUE(handle1);
// No operations are waiting.
EXPECT_FALSE(flag->data.load());
// Operation 2
std::unique_ptr<OperationHandle> handle2;
coordinator_.PostOrRunNormalOperation(
CacheEntryKey("key1"),
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> h) {
handle2 = std::move(h);
EXPECT_FALSE(flag->data.load());
}));
EXPECT_FALSE(handle2);
// Operation 2 is waiting.
EXPECT_TRUE(flag->data.load());
handle1.reset();
EXPECT_TRUE(handle2);
// No operations are waiting.
EXPECT_FALSE(flag->data.load());
// Operation 3
std::unique_ptr<OperationHandle> handle3;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> h) {
handle3 = std::move(h);
EXPECT_TRUE(flag->data.load());
}));
EXPECT_FALSE(handle3);
// Operation 3 is waiting.
EXPECT_TRUE(flag->data.load());
// Operation 4
std::unique_ptr<OperationHandle> handle4;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting([&](std::unique_ptr<OperationHandle> h) {
handle4 = std::move(h);
EXPECT_FALSE(flag->data.load());
}));
EXPECT_FALSE(handle4);
// Operation 3 and 4 are waiting.
EXPECT_TRUE(flag->data.load());
handle2.reset();
EXPECT_TRUE(handle3);
EXPECT_FALSE(handle4);
// Operation 4 is waiting.
EXPECT_TRUE(flag->data.load());
handle3.reset();
EXPECT_TRUE(handle4);
// No operations are waiting.
EXPECT_FALSE(flag->data.load());
handle4.reset();
// No operations are waiting.
EXPECT_FALSE(flag->data.load());
}
TEST_F(ExclusiveOperationCoordinatorTest, LowPriorityNormalTaskBehavior) {
auto& flag = coordinator_.GetHasPendingTaskFlag();
EXPECT_FALSE(flag->data.load());
// Operation 1
std::unique_ptr<OperationHandle> handle1;
coordinator_.PostOrRunExclusiveOperation(base::BindLambdaForTesting(
[&](std::unique_ptr<OperationHandle> h) { handle1 = std::move(h); }));
EXPECT_TRUE(handle1);
EXPECT_FALSE(flag->data.load());
// Operation 2
std::unique_ptr<OperationHandle> handle2;
coordinator_.PostOrRunNormalOperation(
CacheEntryKey("key1"),
base::BindLambdaForTesting(
[&](std::unique_ptr<OperationHandle> h) { handle2 = std::move(h); }),
/*low_priority=*/true);
EXPECT_FALSE(handle2);
// Only a low priority operation 2 is waiting.
EXPECT_FALSE(flag->data.load());
handle1.reset();
EXPECT_TRUE(handle2);
EXPECT_FALSE(flag->data.load());
handle2.reset();
EXPECT_FALSE(flag->data.load());
}
TEST_F(ExclusiveOperationCoordinatorTest, LowPriorityExclusiveTaskBehavior) {
auto& flag = coordinator_.GetHasPendingTaskFlag();
EXPECT_FALSE(flag->data.load());
// Operation 1
std::unique_ptr<OperationHandle> handle1;
coordinator_.PostOrRunNormalOperation(
CacheEntryKey("key1"),
base::BindLambdaForTesting(
[&](std::unique_ptr<OperationHandle> h) { handle1 = std::move(h); }));
EXPECT_TRUE(handle1);
EXPECT_FALSE(flag->data.load());
// Operation 2
std::unique_ptr<OperationHandle> handle2;
coordinator_.PostOrRunExclusiveOperation(
base::BindLambdaForTesting(
[&](std::unique_ptr<OperationHandle> h) { handle2 = std::move(h); }),
/*low_priority=*/true);
EXPECT_FALSE(handle2);
// Only a low priority operation 2 is waiting.
EXPECT_FALSE(flag->data.load());
handle1.reset();
EXPECT_TRUE(handle2);
EXPECT_FALSE(flag->data.load());
handle2.reset();
EXPECT_FALSE(flag->data.load());
}
} // namespace disk_cache