feat: More efficient cache (#1997)

Fixes #1473.
This commit is contained in:
Sergey Kuznetsov
2025-04-17 16:44:53 +01:00
committed by GitHub
parent 39d1ceace4
commit 46514c8fe9
16 changed files with 1092 additions and 260 deletions

View File

@@ -140,6 +140,7 @@ target_sources(
util/async/AnyStrandTests.cpp
util/async/AsyncExecutionContextTests.cpp
util/BatchingTests.cpp
util/BlockingCacheTests.cpp
util/ConceptsTests.cpp
util/CoroutineGroupTests.cpp
util/LedgerUtilsTests.cpp

View File

@@ -645,7 +645,7 @@ struct LoadBalancerForwardToRippledErrorTestBundle {
std::string testName;
rpc::ClioError firstSourceError;
rpc::ClioError secondSourceError;
rpc::ClioError responseExpectedError;
rpc::CombinedError responseExpectedError;
};
struct LoadBalancerForwardToRippledErrorTests
@@ -776,7 +776,7 @@ TEST_F(LoadBalancerForwardToRippledTests, commandLineMissing)
runSpawn([&](boost::asio::yield_context yield) {
EXPECT_EQ(
loadBalancer->forwardToRippled(request, clientIP_, false, yield).error(),
rpc::ClioError::RpcCommandIsMissing
rpc::CombinedError{rpc::ClioError::RpcCommandIsMissing}
);
});
}

View File

@@ -672,7 +672,7 @@ struct LoadBalancerForwardToRippledErrorNgTestBundle {
std::string testName;
rpc::ClioError firstSourceError;
rpc::ClioError secondSourceError;
rpc::ClioError responseExpectedError;
rpc::CombinedError responseExpectedError;
};
struct LoadBalancerForwardToRippledErrorNgTests
@@ -803,7 +803,7 @@ TEST_F(LoadBalancerForwardToRippledNgTests, commandLineMissing)
runSpawn([&](boost::asio::yield_context yield) {
EXPECT_EQ(
loadBalancer->forwardToRippled(request, clientIP_, false, yield).error(),
rpc::ClioError::RpcCommandIsMissing
rpc::CombinedError{rpc::ClioError::RpcCommandIsMissing}
);
});
}

View File

@@ -262,7 +262,7 @@ TEST_P(RPCEngineFlowParameterTest, Test)
if (testBundle.response.has_value()) {
EXPECT_EQ(*response, testBundle.response.value());
} else {
EXPECT_TRUE(*status == testBundle.status.value());
EXPECT_EQ(*status, testBundle.status.value());
}
});
}
@@ -295,7 +295,7 @@ TEST_F(RPCEngineTest, ThrowDatabaseError)
auto const res = engine->buildResponse(ctx);
auto const status = std::get_if<rpc::Status>(&res.response);
ASSERT_TRUE(status != nullptr);
EXPECT_TRUE(*status == Status{RippledError::rpcTOO_BUSY});
EXPECT_EQ(*status, Status{RippledError::rpcTOO_BUSY});
});
}
@@ -327,7 +327,7 @@ TEST_F(RPCEngineTest, ThrowException)
auto const res = engine->buildResponse(ctx);
auto const status = std::get_if<rpc::Status>(&res.response);
ASSERT_TRUE(status != nullptr);
EXPECT_TRUE(*status == Status{RippledError::rpcINTERNAL});
EXPECT_EQ(*status, Status{RippledError::rpcINTERNAL});
});
}
@@ -453,7 +453,7 @@ TEST_P(RPCEngineCacheParameterTest, Test)
auto const res = engine->buildResponse(ctx);
auto const response = std::get_if<boost::json::object>(&res.response);
EXPECT_TRUE(*response == boost::json::parse(R"JSON({ "computed": "world_50"})JSON").as_object());
EXPECT_EQ(*response, boost::json::parse(R"JSON({ "computed": "world_50"})JSON").as_object());
});
}
}
@@ -498,7 +498,8 @@ TEST_F(RPCEngineTest, NotCacheIfErrorHappen)
auto const res = engine->buildResponse(ctx);
auto const error = std::get_if<rpc::Status>(&res.response);
EXPECT_TRUE(*error == rpc::Status{"Very custom error"});
ASSERT_NE(error, nullptr);
EXPECT_EQ(*error, rpc::Status{"Very custom error"});
});
}
}

View File

@@ -0,0 +1,252 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2025, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "util/AsioContextTestFixture.hpp"
#include "util/BlockingCache.hpp"
#include "util/NameGenerator.hpp"
#include <boost/asio/error.hpp>
#include <boost/asio/post.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
using testing::MockFunction;
using testing::Return;
using testing::StrictMock;
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <chrono>
#include <expected>
#include <string>
struct BlockingCacheTest : SyncAsioContextTest {
using ErrorType = std::string;
using ValueType = int;
using Cache = util::BlockingCache<ValueType, ErrorType>;
using MockUpdater = StrictMock<MockFunction<std::expected<ValueType, ErrorType>(boost::asio::yield_context)>>;
using MockVerifier = StrictMock<MockFunction<bool(ValueType const&)>>;
std::unique_ptr<Cache> cache = std::make_unique<Cache>();
MockUpdater mockUpdater;
MockVerifier mockVerifier;
int const value = 42;
std::string error = "some error";
};
TEST_F(BlockingCacheTest, asyncGet_NoValueCacheUpdateSuccess)
{
EXPECT_CALL(mockUpdater, Call).WillOnce(Return(value));
EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(true));
runSpawn([&](boost::asio::yield_context yield) {
auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), 42);
});
}
TEST_F(BlockingCacheTest, asyncGet_NoValueCacheUpdateFailure)
{
EXPECT_CALL(mockUpdater, Call).WillOnce(Return(std::unexpected{error}));
runSpawn([&](boost::asio::yield_context yield) {
auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), error);
});
}
TEST_F(BlockingCacheTest, asyncGet_NoValueCacheUpdateSuccessButVerifierRejects)
{
runSpawn([&](boost::asio::yield_context yield) {
std::expected<ValueType, ErrorType> result;
{
EXPECT_CALL(mockUpdater, Call).WillOnce(Return(value));
EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(false));
result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), value);
}
int const newValue = 24;
{
EXPECT_CALL(mockUpdater, Call).WillOnce(Return(newValue));
EXPECT_CALL(mockVerifier, Call(newValue)).WillOnce(Return(true));
result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), newValue);
}
result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), newValue);
});
}
TEST_F(BlockingCacheTest, asyncGet_HasValueCacheReturnsValue)
{
cache = std::make_unique<Cache>(value);
runSpawn([&](boost::asio::yield_context yield) {
auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), value);
});
}
struct BlockingCacheWaitTestBundle {
bool updateSuccessful;
bool verifierAccepts;
std::string testName;
};
struct BlockingCacheWaitTest : BlockingCacheTest, testing::WithParamInterface<BlockingCacheWaitTestBundle> {};
TEST_P(BlockingCacheWaitTest, WaitForUpdate)
{
bool waitingCoroutineFinished = false;
auto waitingCoroutine = [&](boost::asio::yield_context yield) {
auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
if (GetParam().updateSuccessful) {
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), value);
} else {
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), error);
}
waitingCoroutineFinished = true;
};
EXPECT_CALL(mockUpdater, Call)
.WillOnce([this, &waitingCoroutine](boost::asio::yield_context yield) -> std::expected<ValueType, ErrorType> {
boost::asio::spawn(yield, waitingCoroutine);
if (GetParam().updateSuccessful) {
return value;
}
return std::unexpected{error};
});
if (GetParam().updateSuccessful)
EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(GetParam().verifierAccepts));
runSpawnWithTimeout(std::chrono::seconds{1}, [&](boost::asio::yield_context yield) {
auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
if (GetParam().updateSuccessful) {
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), value);
} else {
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), error);
}
ASSERT_FALSE(waitingCoroutineFinished);
});
}
INSTANTIATE_TEST_SUITE_P(
BlockingCacheTest,
BlockingCacheWaitTest,
testing::Values(
BlockingCacheWaitTestBundle{
.updateSuccessful = true,
.verifierAccepts = true,
.testName = "UpdateSucceedsVerifierAccepts"
},
BlockingCacheWaitTestBundle{
.updateSuccessful = true,
.verifierAccepts = false,
.testName = "UpdateSucceedsVerifierRejects"
},
BlockingCacheWaitTestBundle{.updateSuccessful = false, .verifierAccepts = false, .testName = "UpdateFails"}
),
tests::util::kNAME_GENERATOR
);
TEST_F(BlockingCacheTest, InvalidateWhenStateIsNoValue)
{
ASSERT_EQ(cache->state(), Cache::State::NoValue);
cache->invalidate();
ASSERT_EQ(cache->state(), Cache::State::NoValue);
}
TEST_F(BlockingCacheTest, InvalidateWhenStateIsUpdating)
{
EXPECT_CALL(mockUpdater, Call).WillOnce([this](auto&&) {
EXPECT_EQ(cache->state(), Cache::State::Updating);
cache->invalidate();
EXPECT_EQ(cache->state(), Cache::State::Updating);
return value;
});
EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(true));
runSpawn([&](boost::asio::yield_context yield) {
auto result = cache->asyncGet(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
ASSERT_EQ(result.value(), value);
ASSERT_EQ(cache->state(), Cache::State::HasValue);
});
}
TEST_F(BlockingCacheTest, InvalidateWhenStateIsHasValue)
{
cache = std::make_unique<Cache>(value);
ASSERT_EQ(cache->state(), Cache::State::HasValue);
cache->invalidate();
EXPECT_EQ(cache->state(), Cache::State::NoValue);
}
TEST_F(BlockingCacheTest, UpdateFromTwoCoroutinesHappensOnlyOnes)
{
auto waitingCoroutine = [&](boost::asio::yield_context yield) {
auto result = cache->update(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
ASSERT_EQ(result.value(), value);
};
EXPECT_CALL(mockUpdater, Call)
.WillOnce([this, &waitingCoroutine](boost::asio::yield_context yield) -> std::expected<ValueType, ErrorType> {
boost::asio::spawn(yield, waitingCoroutine);
return value;
});
EXPECT_CALL(mockVerifier, Call(value)).WillOnce(Return(true));
auto updatingCoroutine = [&](boost::asio::yield_context yield) {
auto const result = cache->update(yield, mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
EXPECT_TRUE(result.has_value());
ASSERT_EQ(result.value(), value);
};
runSpawnWithTimeout(std::chrono::seconds{1}, [&](boost::asio::yield_context yield) {
boost::asio::spawn(yield, updatingCoroutine);
});
}

View File

@@ -17,53 +17,292 @@
*/
//==============================================================================
#include "rpc/Errors.hpp"
#include "util/AsioContextTestFixture.hpp"
#include "util/MockAssert.hpp"
#include "util/ResponseExpirationCache.hpp"
#include <boost/asio/spawn.hpp>
#include <boost/json/object.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <chrono>
#include <string>
#include <thread>
#include <unordered_set>
using namespace util;
using testing::MockFunction;
using testing::Return;
using testing::StrictMock;
struct ResponseExpirationCacheTests : public ::testing::Test {
protected:
ResponseExpirationCache cache_{std::chrono::seconds{100}, {"key"}};
boost::json::object object_{{"key", "value"}};
struct ResponseExpirationCacheTest : SyncAsioContextTest {
using MockUpdater = StrictMock<MockFunction<
std::expected<ResponseExpirationCache::EntryData, ResponseExpirationCache::Error>(boost::asio::yield_context)>>;
using MockVerifier = StrictMock<MockFunction<bool(ResponseExpirationCache::EntryData const&)>>;
std::string const cmd = "server_info";
boost::json::object const obj = {{"some key", "some value"}};
MockUpdater mockUpdater;
MockVerifier mockVerifier;
};
TEST_F(ResponseExpirationCacheTests, PutAndGetNotExpired)
TEST_F(ResponseExpirationCacheTest, ShouldCacheDeterminesIfCommandIsCacheable)
{
EXPECT_FALSE(cache_.get("key").has_value());
std::unordered_set<std::string> cmds = {cmd, "account_info"};
ResponseExpirationCache cache{std::chrono::seconds(10), cmds};
cache_.put("key", object_);
auto result = cache_.get("key");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(*result, object_);
result = cache_.get("key2");
ASSERT_FALSE(result.has_value());
for (auto const& c : cmds) {
EXPECT_TRUE(cache.shouldCache(c));
}
cache_.put("key2", object_);
result = cache_.get("key2");
ASSERT_FALSE(result.has_value());
EXPECT_FALSE(cache.shouldCache("account_tx"));
EXPECT_FALSE(cache.shouldCache("ledger"));
EXPECT_FALSE(cache.shouldCache("submit"));
EXPECT_FALSE(cache.shouldCache(""));
}
TEST_F(ResponseExpirationCacheTests, Invalidate)
TEST_F(ResponseExpirationCacheTest, ShouldCacheEmptySetMeansNothingCacheable)
{
cache_.put("key", object_);
cache_.invalidate();
EXPECT_FALSE(cache_.get("key").has_value());
std::unordered_set<std::string> const emptyCmds;
ResponseExpirationCache cache{std::chrono::seconds(10), emptyCmds};
EXPECT_FALSE(cache.shouldCache("server_info"));
EXPECT_FALSE(cache.shouldCache("account_info"));
EXPECT_FALSE(cache.shouldCache("any_command"));
EXPECT_FALSE(cache.shouldCache(""));
}
TEST_F(ResponseExpirationCacheTests, GetExpired)
TEST_F(ResponseExpirationCacheTest, ShouldCacheCaseMatchingIsRequired)
{
ResponseExpirationCache cache{std::chrono::milliseconds{1}, {"key"}};
auto const response = boost::json::object{{"key", "value"}};
std::unordered_set<std::string> const specificCmds = {cmd};
ResponseExpirationCache cache{std::chrono::seconds(10), specificCmds};
cache.put("key", response);
std::this_thread::sleep_for(std::chrono::milliseconds{2});
auto const result = cache.get("key");
EXPECT_FALSE(result);
EXPECT_TRUE(cache.shouldCache(cmd));
EXPECT_FALSE(cache.shouldCache("SERVER_INFO"));
EXPECT_FALSE(cache.shouldCache("Server_Info"));
}
TEST_F(ResponseExpirationCacheTest, GetOrUpdateNoValueInCacheCallsUpdaterAndVerifier)
{
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
runSpawn([&](boost::asio::yield_context yield) {
EXPECT_CALL(mockUpdater, Call)
.WillOnce(Return(ResponseExpirationCache::EntryData{
.lastUpdated = std::chrono::steady_clock::now(),
.response = obj,
}));
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
auto result =
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), obj);
});
}
TEST_F(ResponseExpirationCacheTest, GetOrUpdateExpiredValueInCacheCallsUpdaterAndVerifier)
{
ResponseExpirationCache cache{std::chrono::milliseconds(1), {cmd}};
runSpawn([&](boost::asio::yield_context yield) {
boost::json::object const expiredObject = {{"some key", "expired value"}};
EXPECT_CALL(mockUpdater, Call)
.WillOnce(Return(ResponseExpirationCache::EntryData{
.lastUpdated = std::chrono::steady_clock::now(),
.response = expiredObject,
}));
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
auto result =
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), expiredObject);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
EXPECT_CALL(mockUpdater, Call)
.WillOnce(Return(
ResponseExpirationCache::EntryData{.lastUpdated = std::chrono::steady_clock::now(), .response = obj}
));
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
result = cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), obj);
});
}
TEST_F(ResponseExpirationCacheTest, GetOrUpdateCachedValueNotExpiredDoesNotCallUpdaterOrVerifier)
{
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
runSpawn([&](boost::asio::yield_context yield) {
// First call to populate cache
EXPECT_CALL(mockUpdater, Call)
.WillOnce(Return(ResponseExpirationCache::EntryData{
.lastUpdated = std::chrono::steady_clock::now(),
.response = obj,
}));
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
auto result =
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), obj);
// Second call should use cached value and not call updater/verifier
result = cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), obj);
});
}
TEST_F(ResponseExpirationCacheTest, GetOrUpdateHandlesErrorFromUpdater)
{
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
ResponseExpirationCache::Error const error{
.status = rpc::Status{rpc::ClioError::EtlConnectionError}, .warnings = {}
};
runSpawn([&](boost::asio::yield_context yield) {
EXPECT_CALL(mockUpdater, Call).WillOnce(Return(std::unexpected(error)));
auto result =
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_FALSE(result.has_value());
EXPECT_EQ(result.error(), error);
});
}
TEST_F(ResponseExpirationCacheTest, GetOrUpdateVerifierRejection)
{
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
runSpawn([&](boost::asio::yield_context yield) {
EXPECT_CALL(mockUpdater, Call)
.WillOnce(Return(ResponseExpirationCache::EntryData{
.lastUpdated = std::chrono::steady_clock::now(),
.response = obj,
}));
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(false));
auto result =
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), obj);
boost::json::object const anotherObj = {{"some key", "another value"}};
EXPECT_CALL(mockUpdater, Call)
.WillOnce(Return(ResponseExpirationCache::EntryData{
.lastUpdated = std::chrono::steady_clock::now(),
.response = anotherObj,
}));
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
result = cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), anotherObj);
});
}
TEST_F(ResponseExpirationCacheTest, GetOrUpdateMultipleConcurrentUpdates)
{
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
bool waitingCoroutineFinished = false;
auto waitingCoroutine = [&](boost::asio::yield_context yield) {
auto result =
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), obj);
waitingCoroutineFinished = true;
};
EXPECT_CALL(mockUpdater, Call)
.WillOnce(
[this, &waitingCoroutine](boost::asio::yield_context yield
) -> std::expected<ResponseExpirationCache::EntryData, ResponseExpirationCache::Error> {
boost::asio::spawn(yield, waitingCoroutine);
return ResponseExpirationCache::EntryData{
.lastUpdated = std::chrono::steady_clock::now(),
.response = obj,
};
}
);
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
runSpawnWithTimeout(std::chrono::seconds{1}, [&](boost::asio::yield_context yield) {
auto result =
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), obj);
ASSERT_FALSE(waitingCoroutineFinished);
});
}
TEST_F(ResponseExpirationCacheTest, InvalidateForcesRefresh)
{
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
runSpawn([&](boost::asio::yield_context yield) {
boost::json::object oldObject = {{"some key", "old value"}};
EXPECT_CALL(mockUpdater, Call)
.WillOnce(Return(ResponseExpirationCache::EntryData{
.lastUpdated = std::chrono::steady_clock::now(),
.response = oldObject,
}));
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
auto result =
cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), oldObject);
cache.invalidate();
EXPECT_CALL(mockUpdater, Call)
.WillOnce(Return(ResponseExpirationCache::EntryData{
.lastUpdated = std::chrono::steady_clock::now(),
.response = obj,
}));
EXPECT_CALL(mockVerifier, Call).WillOnce(Return(true));
result = cache.getOrUpdate(yield, "server_info", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction());
ASSERT_TRUE(result.has_value());
EXPECT_EQ(result.value(), obj);
});
}
struct ResponseExpirationCacheAssertTest : common::util::WithMockAssert, ResponseExpirationCacheTest {};
TEST_F(ResponseExpirationCacheAssertTest, NonCacheableCommandThrowsAssertion)
{
ResponseExpirationCache cache{std::chrono::seconds(10), {cmd}};
ASSERT_FALSE(cache.shouldCache("non_cacheable_command"));
runSpawn([&](boost::asio::yield_context yield) {
EXPECT_CLIO_ASSERT_FAIL({
[[maybe_unused]]
auto const v = cache.getOrUpdate(
yield, "non_cacheable_command", mockUpdater.AsStdFunction(), mockVerifier.AsStdFunction()
);
});
});
}