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