mirror of
https://github.com/XRPLF/clio.git
synced 2025-11-19 19:25:53 +00:00
197
unittests/data/BackendFactoryTests.cpp
Normal file
197
unittests/data/BackendFactoryTests.cpp
Normal file
@@ -0,0 +1,197 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, 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 <data/BackendFactory.h>
|
||||
#include <util/Fixtures.h>
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace {
|
||||
constexpr static auto contactPoints = "127.0.0.1";
|
||||
constexpr static auto keyspace = "factory_test";
|
||||
} // namespace
|
||||
|
||||
class BackendCassandraFactoryTest : public SyncAsioContextTest
|
||||
{
|
||||
protected:
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
SyncAsioContextTest::SetUp();
|
||||
}
|
||||
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
SyncAsioContextTest::TearDown();
|
||||
}
|
||||
};
|
||||
|
||||
class BackendCassandraFactoryTestWithDB : public BackendCassandraFactoryTest
|
||||
{
|
||||
protected:
|
||||
void
|
||||
SetUp() override
|
||||
{
|
||||
BackendCassandraFactoryTest::SetUp();
|
||||
}
|
||||
|
||||
void
|
||||
TearDown() override
|
||||
{
|
||||
BackendCassandraFactoryTest::TearDown();
|
||||
// drop the keyspace for next test
|
||||
data::cassandra::Handle handle{contactPoints};
|
||||
EXPECT_TRUE(handle.connect());
|
||||
handle.execute("DROP KEYSPACE " + std::string{keyspace});
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(BackendCassandraFactoryTest, NoSuchBackend)
|
||||
{
|
||||
util::Config cfg{boost::json::parse(
|
||||
R"({
|
||||
"database":
|
||||
{
|
||||
"type":"unknown"
|
||||
}
|
||||
})")};
|
||||
EXPECT_THROW(make_Backend(ctx, cfg), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraFactoryTest, CreateCassandraBackendDBDisconnect)
|
||||
{
|
||||
util::Config cfg{boost::json::parse(fmt::format(
|
||||
R"({{
|
||||
"database":
|
||||
{{
|
||||
"type" : "cassandra",
|
||||
"cassandra" : {{
|
||||
"contact_points": "{}",
|
||||
"keyspace": "{}",
|
||||
"replication_factor": 1,
|
||||
"connect_timeout": 2
|
||||
}}
|
||||
}}
|
||||
}})",
|
||||
"127.0.0.2",
|
||||
keyspace))};
|
||||
EXPECT_THROW(make_Backend(ctx, cfg), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraFactoryTestWithDB, CreateCassandraBackend)
|
||||
{
|
||||
util::Config cfg{boost::json::parse(fmt::format(
|
||||
R"({{
|
||||
"database":
|
||||
{{
|
||||
"type": "cassandra",
|
||||
"cassandra": {{
|
||||
"contact_points": "{}",
|
||||
"keyspace": "{}",
|
||||
"replication_factor": 1
|
||||
}}
|
||||
}}
|
||||
}})",
|
||||
contactPoints,
|
||||
keyspace))};
|
||||
|
||||
{
|
||||
auto backend = make_Backend(ctx, cfg);
|
||||
EXPECT_TRUE(backend);
|
||||
|
||||
// empty db does not have ledger range
|
||||
EXPECT_FALSE(backend->fetchLedgerRange());
|
||||
|
||||
// insert range table
|
||||
data::cassandra::Handle handle{contactPoints};
|
||||
EXPECT_TRUE(handle.connect());
|
||||
handle.execute(fmt::format("INSERT INTO {}.ledger_range (is_latest, sequence) VALUES (False, 100)", keyspace));
|
||||
handle.execute(fmt::format("INSERT INTO {}.ledger_range (is_latest, sequence) VALUES (True, 500)", keyspace));
|
||||
}
|
||||
|
||||
{
|
||||
auto backend = make_Backend(ctx, cfg);
|
||||
EXPECT_TRUE(backend);
|
||||
|
||||
auto const range = backend->fetchLedgerRange();
|
||||
EXPECT_EQ(range->minSequence, 100);
|
||||
EXPECT_EQ(range->maxSequence, 500);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraFactoryTestWithDB, CreateCassandraBackendReadOnlyWithEmptyDB)
|
||||
{
|
||||
util::Config cfg{boost::json::parse(fmt::format(
|
||||
R"({{
|
||||
"read_only": true,
|
||||
"database":
|
||||
{{
|
||||
"type" : "cassandra",
|
||||
"cassandra" : {{
|
||||
"contact_points": "{}",
|
||||
"keyspace": "{}",
|
||||
"replication_factor": 1
|
||||
}}
|
||||
}}
|
||||
}})",
|
||||
contactPoints,
|
||||
keyspace))};
|
||||
EXPECT_THROW(make_Backend(ctx, cfg), std::runtime_error);
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraFactoryTestWithDB, CreateCassandraBackendReadOnlyWithDBReady)
|
||||
{
|
||||
util::Config cfgReadOnly{boost::json::parse(fmt::format(
|
||||
R"({{
|
||||
"read_only": true,
|
||||
"database":
|
||||
{{
|
||||
"type" : "cassandra",
|
||||
"cassandra" : {{
|
||||
"contact_points": "{}",
|
||||
"keyspace": "{}",
|
||||
"replication_factor": 1
|
||||
}}
|
||||
}}
|
||||
}})",
|
||||
contactPoints,
|
||||
keyspace))};
|
||||
|
||||
util::Config cfgWrite{boost::json::parse(fmt::format(
|
||||
R"({{
|
||||
"read_only": false,
|
||||
"database":
|
||||
{{
|
||||
"type" : "cassandra",
|
||||
"cassandra" : {{
|
||||
"contact_points": "{}",
|
||||
"keyspace": "{}",
|
||||
"replication_factor": 1
|
||||
}}
|
||||
}}
|
||||
}})",
|
||||
contactPoints,
|
||||
keyspace))};
|
||||
|
||||
EXPECT_TRUE(make_Backend(ctx, cfgWrite));
|
||||
EXPECT_TRUE(make_Backend(ctx, cfgReadOnly));
|
||||
}
|
||||
165
unittests/data/cassandra/AsyncExecutorTests.cpp
Normal file
165
unittests/data/cassandra/AsyncExecutorTests.cpp
Normal file
@@ -0,0 +1,165 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, 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 <data/cassandra/impl/FakesAndMocks.h>
|
||||
#include <util/Fixtures.h>
|
||||
|
||||
#include <data/cassandra/Error.h>
|
||||
#include <data/cassandra/impl/AsyncExecutor.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
using namespace data::cassandra;
|
||||
using namespace data::cassandra::detail;
|
||||
using namespace testing;
|
||||
|
||||
class BackendCassandraAsyncExecutorTest : public SyncAsioContextTest
|
||||
{
|
||||
};
|
||||
|
||||
TEST_F(BackendCassandraAsyncExecutorTest, CompletionCalledOnSuccess)
|
||||
{
|
||||
auto statement = FakeStatement{};
|
||||
auto handle = MockHandle{};
|
||||
|
||||
ON_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([this](auto const&, auto&& cb) {
|
||||
ctx.post([cb = std::move(cb)]() { cb({}); });
|
||||
return FakeFutureWithCallback{};
|
||||
});
|
||||
EXPECT_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(AtLeast(1));
|
||||
|
||||
auto called = std::atomic_bool{false};
|
||||
auto work = std::optional<boost::asio::io_context::work>{ctx};
|
||||
|
||||
AsyncExecutor<FakeStatement, MockHandle>::run(ctx, handle, std::move(statement), [&called, &work](auto&&) {
|
||||
called = true;
|
||||
work.reset();
|
||||
});
|
||||
|
||||
ctx.run();
|
||||
ASSERT_TRUE(called);
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraAsyncExecutorTest, ExecutedMultipleTimesByRetryPolicyOnMainThread)
|
||||
{
|
||||
auto callCount = std::atomic_int{0};
|
||||
auto statement = FakeStatement{};
|
||||
auto handle = MockHandle{};
|
||||
|
||||
// emulate successfull execution after some attempts
|
||||
ON_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([&callCount](auto const&, auto&& cb) {
|
||||
++callCount;
|
||||
if (callCount >= 3)
|
||||
cb({});
|
||||
else
|
||||
cb({CassandraError{"timeout", CASS_ERROR_LIB_REQUEST_TIMED_OUT}});
|
||||
|
||||
return FakeFutureWithCallback{};
|
||||
});
|
||||
EXPECT_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(3);
|
||||
|
||||
auto called = std::atomic_bool{false};
|
||||
auto work = std::optional<boost::asio::io_context::work>{ctx};
|
||||
|
||||
AsyncExecutor<FakeStatement, MockHandle>::run(ctx, handle, std::move(statement), [&called, &work](auto&&) {
|
||||
called = true;
|
||||
work.reset();
|
||||
});
|
||||
|
||||
ctx.run();
|
||||
ASSERT_TRUE(callCount >= 3);
|
||||
ASSERT_TRUE(called);
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraAsyncExecutorTest, ExecutedMultipleTimesByRetryPolicyOnOtherThread)
|
||||
{
|
||||
auto callCount = std::atomic_int{0};
|
||||
auto statement = FakeStatement{};
|
||||
auto handle = MockHandle{};
|
||||
|
||||
auto threadedCtx = boost::asio::io_context{};
|
||||
auto work = std::optional<boost::asio::io_context::work>{threadedCtx};
|
||||
auto thread = std::thread{[&threadedCtx] { threadedCtx.run(); }};
|
||||
|
||||
// emulate successfull execution after some attempts
|
||||
ON_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([&callCount](auto const&, auto&& cb) {
|
||||
++callCount;
|
||||
if (callCount >= 3)
|
||||
cb({});
|
||||
else
|
||||
cb({CassandraError{"timeout", CASS_ERROR_LIB_REQUEST_TIMED_OUT}});
|
||||
|
||||
return FakeFutureWithCallback{};
|
||||
});
|
||||
EXPECT_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(3);
|
||||
|
||||
auto called = std::atomic_bool{false};
|
||||
auto work2 = std::optional<boost::asio::io_context::work>{ctx};
|
||||
|
||||
AsyncExecutor<FakeStatement, MockHandle>::run(
|
||||
threadedCtx, handle, std::move(statement), [&called, &work, &work2](auto&&) {
|
||||
called = true;
|
||||
work.reset();
|
||||
work2.reset();
|
||||
});
|
||||
|
||||
ctx.run();
|
||||
ASSERT_TRUE(callCount >= 3);
|
||||
ASSERT_TRUE(called);
|
||||
threadedCtx.stop();
|
||||
thread.join();
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraAsyncExecutorTest, CompletionCalledOnFailureAfterRetryCountExceeded)
|
||||
{
|
||||
auto statement = FakeStatement{};
|
||||
auto handle = MockHandle{};
|
||||
|
||||
// FakeRetryPolicy returns false for shouldRetry in which case we should
|
||||
// still call onComplete giving it whatever error we have raised internally.
|
||||
ON_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([](auto const&, auto&& cb) {
|
||||
cb({CassandraError{"not a timeout", CASS_ERROR_LIB_INTERNAL_ERROR}});
|
||||
return FakeFutureWithCallback{};
|
||||
});
|
||||
EXPECT_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(1);
|
||||
|
||||
auto called = std::atomic_bool{false};
|
||||
auto work = std::optional<boost::asio::io_context::work>{ctx};
|
||||
|
||||
AsyncExecutor<FakeStatement, MockHandle, FakeRetryPolicy>::run(
|
||||
ctx, handle, std::move(statement), [&called, &work](auto&& res) {
|
||||
EXPECT_FALSE(res);
|
||||
EXPECT_EQ(res.error().code(), CASS_ERROR_LIB_INTERNAL_ERROR);
|
||||
EXPECT_EQ(res.error().message(), "not a timeout");
|
||||
|
||||
called = true;
|
||||
work.reset();
|
||||
});
|
||||
|
||||
ctx.run();
|
||||
ASSERT_TRUE(called);
|
||||
}
|
||||
1276
unittests/data/cassandra/BackendTests.cpp
Normal file
1276
unittests/data/cassandra/BackendTests.cpp
Normal file
File diff suppressed because it is too large
Load Diff
457
unittests/data/cassandra/BaseTests.cpp
Normal file
457
unittests/data/cassandra/BaseTests.cpp
Normal file
@@ -0,0 +1,457 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, 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/Fixtures.h>
|
||||
|
||||
#include <data/cassandra/Handle.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <semaphore>
|
||||
|
||||
using namespace std;
|
||||
|
||||
using namespace data::cassandra;
|
||||
|
||||
namespace json = boost::json;
|
||||
|
||||
class BackendCassandraBaseTest : public NoLoggerFixture
|
||||
{
|
||||
protected:
|
||||
Handle
|
||||
createHandle(std::string_view contactPoints, std::string_view keyspace)
|
||||
{
|
||||
Handle handle{contactPoints};
|
||||
EXPECT_TRUE(handle.connect());
|
||||
auto const query = fmt::format(
|
||||
R"(
|
||||
CREATE KEYSPACE IF NOT EXISTS {}
|
||||
WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': '1'}}
|
||||
AND durable_writes = true
|
||||
)",
|
||||
keyspace);
|
||||
EXPECT_TRUE(handle.execute(query));
|
||||
EXPECT_TRUE(handle.reconnect(keyspace));
|
||||
return handle;
|
||||
}
|
||||
|
||||
void
|
||||
dropKeyspace(Handle const& handle, std::string_view keyspace)
|
||||
{
|
||||
std::string query = "DROP KEYSPACE " + std::string{keyspace};
|
||||
ASSERT_TRUE(handle.execute(query));
|
||||
}
|
||||
|
||||
void
|
||||
prepStringsTable(Handle const& handle)
|
||||
{
|
||||
auto const entries = std::vector<std::string>{
|
||||
"first",
|
||||
"second",
|
||||
"third",
|
||||
"fourth",
|
||||
"fifth",
|
||||
};
|
||||
|
||||
auto const q1 = fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS strings (hash blob PRIMARY KEY, sequence bigint)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
to_string(5000));
|
||||
|
||||
auto const f1 = handle.asyncExecute(q1);
|
||||
auto const rc = f1.await();
|
||||
ASSERT_TRUE(rc) << rc.error();
|
||||
|
||||
std::string q2 = "INSERT INTO strings (hash, sequence) VALUES (?, ?)";
|
||||
auto const insert = handle.prepare(q2);
|
||||
|
||||
std::vector<Statement> statements;
|
||||
int64_t idx = 1000;
|
||||
|
||||
for (auto const& entry : entries)
|
||||
statements.push_back(insert.bind(entry, static_cast<int64_t>(idx++)));
|
||||
|
||||
EXPECT_EQ(statements.size(), entries.size());
|
||||
EXPECT_TRUE(handle.execute(statements));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, ConnectionSuccess)
|
||||
{
|
||||
Handle handle{"127.0.0.1"};
|
||||
auto const f = handle.asyncConnect();
|
||||
auto const res = f.await();
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, ConnectionFailFormat)
|
||||
{
|
||||
Handle handle{"127.0.0."};
|
||||
auto const f = handle.asyncConnect();
|
||||
auto const res = f.await();
|
||||
|
||||
ASSERT_FALSE(res);
|
||||
EXPECT_EQ(res.error(), "No hosts available: Unable to connect to any contact points");
|
||||
EXPECT_EQ(res.error().code(), CASS_ERROR_LIB_NO_HOSTS_AVAILABLE);
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, ConnectionFailTimeout)
|
||||
{
|
||||
Settings settings;
|
||||
settings.connectionTimeout = std::chrono::milliseconds{30};
|
||||
settings.connectionInfo = Settings::ContactPoints{"127.0.0.2"};
|
||||
|
||||
Handle handle{settings};
|
||||
auto const f = handle.asyncConnect();
|
||||
auto const res = f.await();
|
||||
|
||||
ASSERT_FALSE(res);
|
||||
|
||||
// scylla and cassandra produce different text
|
||||
EXPECT_TRUE(res.error().message().starts_with("No hosts available: Underlying connection error:"));
|
||||
EXPECT_EQ(res.error().code(), CASS_ERROR_LIB_NO_HOSTS_AVAILABLE);
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, FutureCallback)
|
||||
{
|
||||
Handle handle{"127.0.0.1"};
|
||||
ASSERT_TRUE(handle.connect());
|
||||
|
||||
auto const statement = handle.prepare("SELECT keyspace_name FROM system_schema.keyspaces").bind();
|
||||
|
||||
bool complete = false;
|
||||
auto const f = handle.asyncExecute(statement, [&complete](auto const res) {
|
||||
complete = true;
|
||||
EXPECT_TRUE(res.value().hasRows());
|
||||
|
||||
for (auto [ks] : extract<std::string>(res.value()))
|
||||
EXPECT_TRUE(not ks.empty()); // keyspace got some name
|
||||
});
|
||||
|
||||
auto const res = f.await(); // callback should still be called
|
||||
ASSERT_TRUE(res);
|
||||
ASSERT_TRUE(complete);
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, FutureCallbackSurviveMove)
|
||||
{
|
||||
Handle handle{"127.0.0.1"};
|
||||
ASSERT_TRUE(handle.connect());
|
||||
|
||||
auto const statement = handle.prepare("SELECT keyspace_name FROM system_schema.keyspaces").bind();
|
||||
|
||||
bool complete = false;
|
||||
std::vector<FutureWithCallback> futures;
|
||||
std::binary_semaphore sem{0};
|
||||
|
||||
futures.push_back(handle.asyncExecute(statement, [&complete, &sem](auto const res) {
|
||||
complete = true;
|
||||
EXPECT_TRUE(res.value().hasRows());
|
||||
|
||||
for (auto [ks] : extract<std::string>(res.value()))
|
||||
EXPECT_TRUE(not ks.empty()); // keyspace got some name
|
||||
|
||||
sem.release();
|
||||
}));
|
||||
|
||||
sem.acquire();
|
||||
for (auto const& f : futures)
|
||||
ASSERT_TRUE(f.await());
|
||||
ASSERT_TRUE(complete);
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, KeyspaceManipulation)
|
||||
{
|
||||
Handle handle{"127.0.0.1"};
|
||||
std::string keyspace = "test_keyspace_manipulation";
|
||||
|
||||
{
|
||||
auto const f = handle.asyncConnect(keyspace);
|
||||
auto const rc = f.await();
|
||||
ASSERT_FALSE(rc); // initially expecting the keyspace does not exist
|
||||
}
|
||||
{
|
||||
auto const f = handle.asyncConnect();
|
||||
auto const rc = f.await();
|
||||
ASSERT_TRUE(rc); // expect that we can still connect without keyspace
|
||||
}
|
||||
{
|
||||
const auto query = fmt::format(
|
||||
R"(
|
||||
CREATE KEYSPACE {}
|
||||
WITH replication = {{'class': 'SimpleStrategy', 'replication_factor': '1'}}
|
||||
AND durable_writes = true
|
||||
)",
|
||||
keyspace);
|
||||
auto const f = handle.asyncExecute(query);
|
||||
auto const rc = f.await();
|
||||
ASSERT_TRUE(rc); // keyspace created
|
||||
}
|
||||
{
|
||||
auto const rc = handle.reconnect(keyspace);
|
||||
ASSERT_TRUE(rc); // connect to the keyspace we created earlier
|
||||
}
|
||||
{
|
||||
auto const f = handle.asyncExecute("DROP KEYSPACE " + keyspace);
|
||||
auto const rc = f.await();
|
||||
ASSERT_TRUE(rc); // dropped the keyspace
|
||||
}
|
||||
{
|
||||
auto const f = handle.asyncExecute("DROP KEYSPACE " + keyspace);
|
||||
auto const rc = f.await();
|
||||
ASSERT_FALSE(rc); // keyspace already does not exist
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, CreateTableWithStrings)
|
||||
{
|
||||
auto const entries = std::vector<std::string>{
|
||||
"first",
|
||||
"second",
|
||||
"third",
|
||||
"fourth",
|
||||
"fifth",
|
||||
};
|
||||
|
||||
auto handle = createHandle("127.0.0.1", "test");
|
||||
auto q1 = fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS strings (hash blob PRIMARY KEY, sequence bigint)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
5000);
|
||||
|
||||
auto const f1 = handle.asyncExecute(q1);
|
||||
auto const rc = f1.await();
|
||||
ASSERT_TRUE(rc) << rc.error();
|
||||
|
||||
std::string q2 = "INSERT INTO strings (hash, sequence) VALUES (?, ?)";
|
||||
auto insert = handle.prepare(q2);
|
||||
|
||||
// write data
|
||||
{
|
||||
std::vector<Future> futures;
|
||||
int64_t idx = 1000;
|
||||
|
||||
for (auto const& entry : entries)
|
||||
futures.push_back(handle.asyncExecute(insert, entry, static_cast<int64_t>(idx++)));
|
||||
|
||||
ASSERT_EQ(futures.size(), entries.size());
|
||||
for (auto const& f : futures)
|
||||
{
|
||||
auto const rc = f.await();
|
||||
ASSERT_TRUE(rc) << rc.error();
|
||||
}
|
||||
}
|
||||
|
||||
// read data back
|
||||
{
|
||||
auto const res = handle.execute("SELECT hash, sequence FROM strings");
|
||||
ASSERT_TRUE(res) << res.error();
|
||||
|
||||
auto const& results = res.value();
|
||||
auto const totalRows = results.numRows();
|
||||
EXPECT_EQ(totalRows, entries.size());
|
||||
|
||||
for (auto [hash, seq] : extract<std::string, int64_t>(results))
|
||||
EXPECT_TRUE(std::find(std::begin(entries), std::end(entries), hash) != std::end(entries));
|
||||
}
|
||||
|
||||
// delete everything
|
||||
{
|
||||
auto const res = handle.execute("DROP TABLE strings");
|
||||
ASSERT_TRUE(res) << res.error();
|
||||
dropKeyspace(handle, "test");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, BatchInsert)
|
||||
{
|
||||
auto const entries = std::vector<std::string>{
|
||||
"first",
|
||||
"second",
|
||||
"third",
|
||||
"fourth",
|
||||
"fifth",
|
||||
};
|
||||
|
||||
auto handle = createHandle("127.0.0.1", "test");
|
||||
auto const q1 = fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS strings (hash blob PRIMARY KEY, sequence bigint)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
5000);
|
||||
auto const f1 = handle.asyncExecute(q1);
|
||||
auto const rc = f1.await();
|
||||
ASSERT_TRUE(rc) << rc.error();
|
||||
|
||||
std::string q2 = "INSERT INTO strings (hash, sequence) VALUES (?, ?)";
|
||||
auto const insert = handle.prepare(q2);
|
||||
|
||||
// write data in bulk
|
||||
{
|
||||
std::vector<Statement> statements;
|
||||
int64_t idx = 1000;
|
||||
|
||||
for (auto const& entry : entries)
|
||||
statements.push_back(insert.bind(entry, static_cast<int64_t>(idx++)));
|
||||
|
||||
ASSERT_EQ(statements.size(), entries.size());
|
||||
|
||||
auto const rc = handle.execute(statements);
|
||||
ASSERT_TRUE(rc) << rc.error();
|
||||
}
|
||||
|
||||
// read data back
|
||||
{
|
||||
auto const res = handle.execute("SELECT hash, sequence FROM strings");
|
||||
ASSERT_TRUE(res) << res.error();
|
||||
|
||||
auto const& results = res.value();
|
||||
auto const totalRows = results.numRows();
|
||||
EXPECT_EQ(totalRows, entries.size());
|
||||
|
||||
for (auto [hash, seq] : extract<std::string, int64_t>(results))
|
||||
EXPECT_TRUE(std::find(std::begin(entries), std::end(entries), hash) != std::end(entries));
|
||||
}
|
||||
|
||||
dropKeyspace(handle, "test");
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, BatchInsertAsync)
|
||||
{
|
||||
using std::to_string;
|
||||
auto const entries = std::vector<std::string>{
|
||||
"first",
|
||||
"second",
|
||||
"third",
|
||||
"fourth",
|
||||
"fifth",
|
||||
};
|
||||
|
||||
auto handle = createHandle("127.0.0.1", "test");
|
||||
auto const q1 = fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS strings (hash blob PRIMARY KEY, sequence bigint)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
5000);
|
||||
auto const f1 = handle.asyncExecute(q1);
|
||||
auto const rc = f1.await();
|
||||
ASSERT_TRUE(rc) << rc.error();
|
||||
|
||||
std::string q2 = "INSERT INTO strings (hash, sequence) VALUES (?, ?)";
|
||||
auto const insert = handle.prepare(q2);
|
||||
|
||||
// write data in bulk
|
||||
{
|
||||
bool complete = false;
|
||||
std::optional<data::cassandra::FutureWithCallback> fut;
|
||||
|
||||
{
|
||||
std::vector<Statement> statements;
|
||||
int64_t idx = 1000;
|
||||
|
||||
for (auto const& entry : entries)
|
||||
statements.push_back(insert.bind(entry, static_cast<int64_t>(idx++)));
|
||||
|
||||
ASSERT_EQ(statements.size(), entries.size());
|
||||
fut.emplace(handle.asyncExecute(statements, [&](auto const res) {
|
||||
complete = true;
|
||||
EXPECT_TRUE(res);
|
||||
}));
|
||||
// statements are destructed here, async execute needs to survive
|
||||
}
|
||||
|
||||
auto const res = fut.value().await(); // future should still signal it finished
|
||||
EXPECT_TRUE(res);
|
||||
ASSERT_TRUE(complete);
|
||||
}
|
||||
|
||||
dropKeyspace(handle, "test");
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, AlterTableAddColumn)
|
||||
{
|
||||
auto handle = createHandle("127.0.0.1", "test");
|
||||
auto const q1 = fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS strings (hash blob PRIMARY KEY, sequence bigint)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
5000);
|
||||
ASSERT_TRUE(handle.execute(q1));
|
||||
|
||||
std::string update = "ALTER TABLE strings ADD tmp blob";
|
||||
ASSERT_TRUE(handle.execute(update));
|
||||
|
||||
dropKeyspace(handle, "test");
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraBaseTest, AlterTableMoveToNewTable)
|
||||
{
|
||||
auto handle = createHandle("127.0.0.1", "test");
|
||||
prepStringsTable(handle);
|
||||
|
||||
auto const newTable = fmt::format(
|
||||
R"(
|
||||
CREATE TABLE IF NOT EXISTS strings_v2 (hash blob PRIMARY KEY, sequence bigint, tmp bigint)
|
||||
WITH default_time_to_live = {}
|
||||
)",
|
||||
5000);
|
||||
ASSERT_TRUE(handle.execute(newTable));
|
||||
|
||||
// now migrate data; tmp column will just get the sequence number + 1 stored
|
||||
std::vector<Statement> migrationStatements;
|
||||
auto const migrationInsert = handle.prepare("INSERT INTO strings_v2 (hash, sequence, tmp) VALUES (?, ?, ?)");
|
||||
|
||||
auto const res = handle.execute("SELECT hash, sequence FROM strings");
|
||||
ASSERT_TRUE(res);
|
||||
|
||||
auto const& results = res.value();
|
||||
for (auto [hash, seq] : extract<std::string, int64_t>(results))
|
||||
{
|
||||
static_assert(std::is_same_v<decltype(hash), std::string>);
|
||||
static_assert(std::is_same_v<decltype(seq), int64_t>);
|
||||
migrationStatements.push_back(
|
||||
migrationInsert.bind(hash, static_cast<int64_t>(seq), static_cast<int64_t>(seq + 1u)));
|
||||
}
|
||||
|
||||
EXPECT_TRUE(handle.execute(migrationStatements));
|
||||
|
||||
// now let's read back the v2 table and compare
|
||||
auto const resV2 = handle.execute("SELECT sequence, tmp FROM strings_v2");
|
||||
EXPECT_TRUE(resV2);
|
||||
auto const& resultsV2 = resV2.value();
|
||||
|
||||
EXPECT_EQ(results.numRows(), resultsV2.numRows());
|
||||
for (auto [seq, tmp] : extract<int64_t, int64_t>(resultsV2))
|
||||
{
|
||||
static_assert(std::is_same_v<decltype(seq), int64_t>);
|
||||
static_assert(std::is_same_v<decltype(tmp), int64_t>);
|
||||
EXPECT_EQ(seq + 1, tmp);
|
||||
}
|
||||
|
||||
dropKeyspace(handle, "test");
|
||||
}
|
||||
307
unittests/data/cassandra/ExecutionStrategyTests.cpp
Normal file
307
unittests/data/cassandra/ExecutionStrategyTests.cpp
Normal file
@@ -0,0 +1,307 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, 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 <data/cassandra/impl/FakesAndMocks.h>
|
||||
#include <util/Fixtures.h>
|
||||
|
||||
#include <data/cassandra/impl/ExecutionStrategy.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace data::cassandra;
|
||||
using namespace data::cassandra::detail;
|
||||
using namespace testing;
|
||||
|
||||
class BackendCassandraExecutionStrategyTest : public SyncAsioContextTest
|
||||
{
|
||||
};
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, ReadOneInCoroutineSuccessful)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
|
||||
ON_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([](auto const& statement, auto&& cb) {
|
||||
cb({}); // pretend we got data
|
||||
return FakeFutureWithCallback{};
|
||||
});
|
||||
EXPECT_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(1);
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statement = FakeStatement{};
|
||||
strat.read(yield, statement);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, ReadOneInCoroutineThrowsOnTimeoutFailure)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
|
||||
ON_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([](auto const&, auto&& cb) {
|
||||
auto res = FakeResultOrError{CassandraError{"timeout", CASS_ERROR_LIB_REQUEST_TIMED_OUT}};
|
||||
cb(res); // notify that item is ready
|
||||
return FakeFutureWithCallback{res};
|
||||
});
|
||||
EXPECT_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(1);
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statement = FakeStatement{};
|
||||
EXPECT_THROW(strat.read(yield, statement), DatabaseTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, ReadOneInCoroutineThrowsOnInvalidQueryFailure)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
|
||||
ON_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([](auto const&, auto&& cb) {
|
||||
auto res = FakeResultOrError{CassandraError{"invalid", CASS_ERROR_SERVER_INVALID_QUERY}};
|
||||
cb(res); // notify that item is ready
|
||||
return FakeFutureWithCallback{res};
|
||||
});
|
||||
EXPECT_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(1);
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statement = FakeStatement{};
|
||||
EXPECT_THROW(strat.read(yield, statement), std::runtime_error);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, ReadBatchInCoroutineSuccessful)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
|
||||
ON_CALL(
|
||||
handle, asyncExecute(An<std::vector<FakeStatement> const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([](auto const& statements, auto&& cb) {
|
||||
EXPECT_EQ(statements.size(), 3);
|
||||
cb({}); // pretend we got data
|
||||
return FakeFutureWithCallback{};
|
||||
});
|
||||
EXPECT_CALL(
|
||||
handle, asyncExecute(An<std::vector<FakeStatement> const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(1);
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statements = std::vector<FakeStatement>(3);
|
||||
strat.read(yield, statements);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, ReadBatchInCoroutineThrowsOnTimeoutFailure)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
|
||||
ON_CALL(
|
||||
handle, asyncExecute(An<std::vector<FakeStatement> const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([](auto const& statements, auto&& cb) {
|
||||
EXPECT_EQ(statements.size(), 3);
|
||||
auto res = FakeResultOrError{CassandraError{"timeout", CASS_ERROR_LIB_REQUEST_TIMED_OUT}};
|
||||
cb(res); // notify that item is ready
|
||||
return FakeFutureWithCallback{res};
|
||||
});
|
||||
EXPECT_CALL(
|
||||
handle, asyncExecute(An<std::vector<FakeStatement> const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(1);
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statements = std::vector<FakeStatement>(3);
|
||||
EXPECT_THROW(strat.read(yield, statements), DatabaseTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, ReadBatchInCoroutineThrowsOnInvalidQueryFailure)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
|
||||
ON_CALL(
|
||||
handle, asyncExecute(An<std::vector<FakeStatement> const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([](auto const& statements, auto&& cb) {
|
||||
EXPECT_EQ(statements.size(), 3);
|
||||
auto res = FakeResultOrError{CassandraError{"invalid", CASS_ERROR_SERVER_INVALID_QUERY}};
|
||||
cb(res); // notify that item is ready
|
||||
return FakeFutureWithCallback{res};
|
||||
});
|
||||
EXPECT_CALL(
|
||||
handle, asyncExecute(An<std::vector<FakeStatement> const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(1);
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statements = std::vector<FakeStatement>(3);
|
||||
EXPECT_THROW(strat.read(yield, statements), std::runtime_error);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, ReadBatchInCoroutineMarksBusyIfRequestsOutstandingExceeded)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto settings = Settings{};
|
||||
settings.maxReadRequestsOutstanding = 2;
|
||||
auto strat = DefaultExecutionStrategy{settings, handle};
|
||||
|
||||
ON_CALL(
|
||||
handle, asyncExecute(An<std::vector<FakeStatement> const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([&strat](auto const& statements, auto&& cb) {
|
||||
EXPECT_EQ(statements.size(), 3);
|
||||
EXPECT_TRUE(strat.isTooBusy()); // 2 was the limit, we sent 3
|
||||
|
||||
cb({}); // notify that item is ready
|
||||
return FakeFutureWithCallback{};
|
||||
});
|
||||
EXPECT_CALL(
|
||||
handle, asyncExecute(An<std::vector<FakeStatement> const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(1);
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
EXPECT_FALSE(strat.isTooBusy()); // 2 was the limit, 0 atm
|
||||
auto statements = std::vector<FakeStatement>(3);
|
||||
strat.read(yield, statements);
|
||||
EXPECT_FALSE(strat.isTooBusy()); // after read completes it's 0 again
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, ReadEachInCoroutineSuccessful)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
|
||||
ON_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([](auto const&, auto&& cb) {
|
||||
cb({}); // pretend we got data
|
||||
return FakeFutureWithCallback{};
|
||||
});
|
||||
EXPECT_CALL(
|
||||
handle,
|
||||
asyncExecute(
|
||||
An<FakeStatement const&>(),
|
||||
An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(3); // once per statement
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statements = std::vector<FakeStatement>(3);
|
||||
auto res = strat.readEach(yield, statements);
|
||||
EXPECT_EQ(res.size(), statements.size());
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, ReadEachInCoroutineThrowsOnFailure)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
auto callCount = std::atomic_int{0};
|
||||
|
||||
ON_CALL(handle, asyncExecute(An<FakeStatement const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([&callCount](auto const&, auto&& cb) {
|
||||
if (callCount == 1) // error happens on one of the entries
|
||||
cb({CassandraError{"invalid data", CASS_ERROR_LIB_INVALID_DATA}});
|
||||
else
|
||||
cb({}); // pretend we got data
|
||||
++callCount;
|
||||
return FakeFutureWithCallback{};
|
||||
});
|
||||
EXPECT_CALL(
|
||||
handle,
|
||||
asyncExecute(
|
||||
An<FakeStatement const&>(),
|
||||
An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(3); // once per statement
|
||||
|
||||
runSpawn([&strat](boost::asio::yield_context yield) {
|
||||
auto statements = std::vector<FakeStatement>(3);
|
||||
EXPECT_THROW(strat.readEach(yield, statements), DatabaseTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, WriteSyncFirstTrySuccessful)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
|
||||
ON_CALL(handle, execute(An<FakeStatement const&>())).WillByDefault([](auto const&) { return FakeResultOrError{}; });
|
||||
EXPECT_CALL(handle,
|
||||
execute(An<FakeStatement const&>())).Times(1); // first one will succeed
|
||||
|
||||
EXPECT_TRUE(strat.writeSync({}));
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, WriteSyncRetrySuccessful)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
auto callCount = 0;
|
||||
|
||||
ON_CALL(handle, execute(An<FakeStatement const&>())).WillByDefault([&callCount](auto const&) {
|
||||
if (callCount++ == 1)
|
||||
return FakeResultOrError{};
|
||||
return FakeResultOrError{CassandraError{"invalid data", CASS_ERROR_LIB_INVALID_DATA}};
|
||||
});
|
||||
EXPECT_CALL(handle,
|
||||
execute(An<FakeStatement const&>())).Times(2); // first one will fail, second will succeed
|
||||
|
||||
EXPECT_TRUE(strat.writeSync({}));
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraExecutionStrategyTest, WriteMultipleAndCallSyncSucceeds)
|
||||
{
|
||||
auto handle = MockHandle{};
|
||||
auto strat = DefaultExecutionStrategy{Settings{}, handle};
|
||||
auto totalRequests = 1024u;
|
||||
auto callCount = std::atomic_uint{0u};
|
||||
|
||||
auto work = std::optional<boost::asio::io_context::work>{ctx};
|
||||
auto thread = std::thread{[this]() { ctx.run(); }};
|
||||
|
||||
ON_CALL(
|
||||
handle, asyncExecute(An<std::vector<FakeStatement> const&>(), An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.WillByDefault([this, &callCount](auto const&, auto&& cb) {
|
||||
// run on thread to emulate concurrency model of real asyncExecute
|
||||
boost::asio::post(ctx, [&callCount, cb = std::move(cb)] {
|
||||
++callCount;
|
||||
cb({}); // pretend we got data
|
||||
});
|
||||
return FakeFutureWithCallback{};
|
||||
});
|
||||
EXPECT_CALL(
|
||||
handle,
|
||||
asyncExecute(
|
||||
An<std::vector<FakeStatement> const&>(),
|
||||
An<std::function<void(FakeResultOrError)>&&>()))
|
||||
.Times(totalRequests); // one per write call
|
||||
|
||||
auto makeStatements = [] { return std::vector<FakeStatement>(16); };
|
||||
for (auto i = 0u; i < totalRequests; ++i)
|
||||
strat.write(makeStatements());
|
||||
|
||||
strat.sync(); // make sure all above writes are finished
|
||||
EXPECT_EQ(callCount, totalRequests); // all requests should finish
|
||||
|
||||
work.reset();
|
||||
thread.join();
|
||||
}
|
||||
83
unittests/data/cassandra/RetryPolicyTests.cpp
Normal file
83
unittests/data/cassandra/RetryPolicyTests.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, 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/Fixtures.h>
|
||||
|
||||
#include <data/cassandra/Error.h>
|
||||
#include <data/cassandra/impl/RetryPolicy.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace data::cassandra;
|
||||
using namespace data::cassandra::detail;
|
||||
using namespace testing;
|
||||
|
||||
class BackendCassandraRetryPolicyTest : public SyncAsioContextTest
|
||||
{
|
||||
};
|
||||
|
||||
TEST_F(BackendCassandraRetryPolicyTest, ShouldRetryAlwaysTrue)
|
||||
{
|
||||
auto retryPolicy = ExponentialBackoffRetryPolicy{ctx};
|
||||
EXPECT_TRUE(retryPolicy.shouldRetry(CassandraError{"timeout", CASS_ERROR_LIB_REQUEST_TIMED_OUT}));
|
||||
EXPECT_TRUE(retryPolicy.shouldRetry(CassandraError{"invalid data", CASS_ERROR_LIB_INVALID_DATA}));
|
||||
EXPECT_TRUE(retryPolicy.shouldRetry(CassandraError{"invalid query", CASS_ERROR_SERVER_INVALID_QUERY}));
|
||||
|
||||
// this policy actually always returns true
|
||||
auto const err = CassandraError{"ok", CASS_OK};
|
||||
for (auto i = 0; i < 1024; ++i)
|
||||
{
|
||||
EXPECT_TRUE(retryPolicy.shouldRetry(err));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraRetryPolicyTest, CheckComputedBackoffDelayIsCorrect)
|
||||
{
|
||||
auto retryPolicy = ExponentialBackoffRetryPolicy{ctx};
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(0).count(), 1);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(1).count(), 2);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(2).count(), 4);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(3).count(), 8);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(4).count(), 16);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(5).count(), 32);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(6).count(), 64);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(7).count(), 128);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(8).count(), 256);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(9).count(), 512);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(10).count(), 1024);
|
||||
EXPECT_EQ(retryPolicy.calculateDelay(11).count(),
|
||||
1024); // 10 is max, same after that
|
||||
}
|
||||
|
||||
TEST_F(BackendCassandraRetryPolicyTest, RetryCorrectlyExecuted)
|
||||
{
|
||||
auto callCount = std::atomic_int{0};
|
||||
auto work = std::optional<boost::asio::io_context::work>{ctx};
|
||||
auto retryPolicy = ExponentialBackoffRetryPolicy{ctx};
|
||||
|
||||
retryPolicy.retry([&callCount]() { ++callCount; });
|
||||
retryPolicy.retry([&callCount]() { ++callCount; });
|
||||
retryPolicy.retry([&callCount, &work]() {
|
||||
++callCount;
|
||||
work.reset();
|
||||
});
|
||||
|
||||
ctx.run();
|
||||
ASSERT_EQ(callCount, 3);
|
||||
}
|
||||
195
unittests/data/cassandra/SettingsProviderTests.cpp
Normal file
195
unittests/data/cassandra/SettingsProviderTests.cpp
Normal file
@@ -0,0 +1,195 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, 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/Fixtures.h>
|
||||
#include <util/TmpFile.h>
|
||||
|
||||
#include <data/cassandra/SettingsProvider.h>
|
||||
#include <util/config/Config.h>
|
||||
|
||||
#include <boost/json/parse.hpp>
|
||||
#include <fmt/core.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <thread>
|
||||
#include <variant>
|
||||
|
||||
using namespace util;
|
||||
using namespace std;
|
||||
namespace json = boost::json;
|
||||
|
||||
using namespace data::cassandra;
|
||||
|
||||
class SettingsProviderTest : public NoLoggerFixture
|
||||
{
|
||||
};
|
||||
|
||||
TEST_F(SettingsProviderTest, Defaults)
|
||||
{
|
||||
Config cfg{json::parse(R"({"contact_points": "127.0.0.1"})")};
|
||||
SettingsProvider provider{cfg};
|
||||
|
||||
auto const settings = provider.getSettings();
|
||||
EXPECT_EQ(settings.threads, std::thread::hardware_concurrency());
|
||||
|
||||
EXPECT_EQ(settings.enableLog, false);
|
||||
EXPECT_EQ(settings.connectionTimeout, std::chrono::milliseconds{10000});
|
||||
EXPECT_EQ(settings.requestTimeout, std::chrono::milliseconds{0});
|
||||
EXPECT_EQ(settings.maxWriteRequestsOutstanding, 10'000);
|
||||
EXPECT_EQ(settings.maxReadRequestsOutstanding, 100'000);
|
||||
EXPECT_EQ(settings.maxConnectionsPerHost, 2);
|
||||
EXPECT_EQ(settings.coreConnectionsPerHost, 2);
|
||||
EXPECT_EQ(settings.maxConcurrentRequestsThreshold, (100'000 + 10'000) / 2);
|
||||
EXPECT_EQ(settings.certificate, std::nullopt);
|
||||
EXPECT_EQ(settings.username, std::nullopt);
|
||||
EXPECT_EQ(settings.password, std::nullopt);
|
||||
EXPECT_EQ(settings.queueSizeIO, std::nullopt);
|
||||
EXPECT_EQ(settings.queueSizeEvent, std::nullopt);
|
||||
EXPECT_EQ(settings.writeBytesHighWatermark, std::nullopt);
|
||||
EXPECT_EQ(settings.writeBytesLowWatermark, std::nullopt);
|
||||
EXPECT_EQ(settings.pendingRequestsHighWatermark, std::nullopt);
|
||||
EXPECT_EQ(settings.pendingRequestsLowWatermark, std::nullopt);
|
||||
EXPECT_EQ(settings.maxRequestsPerFlush, std::nullopt);
|
||||
EXPECT_EQ(settings.maxConcurrentCreation, std::nullopt);
|
||||
|
||||
auto const* cp = std::get_if<Settings::ContactPoints>(&settings.connectionInfo);
|
||||
ASSERT_TRUE(cp != nullptr);
|
||||
EXPECT_EQ(cp->contactPoints, "127.0.0.1");
|
||||
EXPECT_FALSE(cp->port);
|
||||
|
||||
EXPECT_EQ(provider.getKeyspace(), "clio");
|
||||
EXPECT_EQ(provider.getReplicationFactor(), 3);
|
||||
EXPECT_EQ(provider.getTablePrefix(), std::nullopt);
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, SimpleConfig)
|
||||
{
|
||||
Config cfg{json::parse(R"({
|
||||
"contact_points": "123.123.123.123",
|
||||
"port": 1234,
|
||||
"keyspace": "test",
|
||||
"replication_factor": 42,
|
||||
"table_prefix": "prefix",
|
||||
"threads": 24
|
||||
})")};
|
||||
SettingsProvider provider{cfg};
|
||||
|
||||
auto const settings = provider.getSettings();
|
||||
EXPECT_EQ(settings.threads, 24);
|
||||
|
||||
auto const* cp = std::get_if<Settings::ContactPoints>(&settings.connectionInfo);
|
||||
ASSERT_TRUE(cp != nullptr);
|
||||
EXPECT_EQ(cp->contactPoints, "123.123.123.123");
|
||||
EXPECT_EQ(cp->port, 1234);
|
||||
|
||||
EXPECT_EQ(provider.getKeyspace(), "test");
|
||||
EXPECT_EQ(provider.getReplicationFactor(), 42);
|
||||
EXPECT_EQ(provider.getTablePrefix(), "prefix");
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, DriverOptionCalculation)
|
||||
{
|
||||
Config cfg{json::parse(R"({
|
||||
"contact_points": "123.123.123.123",
|
||||
"max_write_requests_outstanding": 100,
|
||||
"max_read_requests_outstanding": 200
|
||||
})")};
|
||||
SettingsProvider provider{cfg};
|
||||
|
||||
auto const settings = provider.getSettings();
|
||||
EXPECT_EQ(settings.maxReadRequestsOutstanding, 200);
|
||||
EXPECT_EQ(settings.maxWriteRequestsOutstanding, 100);
|
||||
|
||||
EXPECT_EQ(settings.maxConnectionsPerHost, 2);
|
||||
EXPECT_EQ(settings.coreConnectionsPerHost, 2);
|
||||
EXPECT_EQ(settings.maxConcurrentRequestsThreshold, 150); // calculated from above
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, DriverOptionSecifiedMaxConcurrentRequestsThreshold)
|
||||
{
|
||||
Config cfg{json::parse(R"({
|
||||
"contact_points": "123.123.123.123",
|
||||
"max_write_requests_outstanding": 100,
|
||||
"max_read_requests_outstanding": 200,
|
||||
"max_connections_per_host": 5,
|
||||
"core_connections_per_host": 4,
|
||||
"max_concurrent_requests_threshold": 1234
|
||||
})")};
|
||||
SettingsProvider provider{cfg};
|
||||
|
||||
auto const settings = provider.getSettings();
|
||||
EXPECT_EQ(settings.maxReadRequestsOutstanding, 200);
|
||||
EXPECT_EQ(settings.maxWriteRequestsOutstanding, 100);
|
||||
|
||||
EXPECT_EQ(settings.maxConnectionsPerHost, 5);
|
||||
EXPECT_EQ(settings.coreConnectionsPerHost, 4);
|
||||
EXPECT_EQ(settings.maxConcurrentRequestsThreshold, 1234);
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, DriverOptionalOptionsSpecified)
|
||||
{
|
||||
Config cfg{json::parse(R"({
|
||||
"contact_points": "123.123.123.123",
|
||||
"queue_size_event": 1,
|
||||
"queue_size_io": 2,
|
||||
"write_bytes_high_water_mark": 3,
|
||||
"write_bytes_low_water_mark": 4,
|
||||
"pending_requests_high_water_mark": 5,
|
||||
"pending_requests_low_water_mark": 6,
|
||||
"max_requests_per_flush": 7,
|
||||
"max_concurrent_creation": 8
|
||||
})")};
|
||||
SettingsProvider provider{cfg};
|
||||
|
||||
auto const settings = provider.getSettings();
|
||||
EXPECT_EQ(settings.queueSizeEvent, 1);
|
||||
EXPECT_EQ(settings.queueSizeIO, 2);
|
||||
EXPECT_EQ(settings.writeBytesHighWatermark, 3);
|
||||
EXPECT_EQ(settings.writeBytesLowWatermark, 4);
|
||||
EXPECT_EQ(settings.pendingRequestsHighWatermark, 5);
|
||||
EXPECT_EQ(settings.pendingRequestsLowWatermark, 6);
|
||||
EXPECT_EQ(settings.maxRequestsPerFlush, 7);
|
||||
EXPECT_EQ(settings.maxConcurrentCreation, 8);
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, SecureBundleConfig)
|
||||
{
|
||||
Config cfg{json::parse(R"({"secure_connect_bundle": "bundleData"})")};
|
||||
SettingsProvider provider{cfg};
|
||||
|
||||
auto const settings = provider.getSettings();
|
||||
auto const* sb = std::get_if<Settings::SecureConnectionBundle>(&settings.connectionInfo);
|
||||
ASSERT_TRUE(sb != nullptr);
|
||||
EXPECT_EQ(sb->bundle, "bundleData");
|
||||
}
|
||||
|
||||
TEST_F(SettingsProviderTest, CertificateConfig)
|
||||
{
|
||||
TmpFile file{"certificateData"};
|
||||
Config cfg{json::parse(fmt::format(
|
||||
R"({{
|
||||
"contact_points": "127.0.0.1",
|
||||
"certfile": "{}"
|
||||
}})",
|
||||
file.path))};
|
||||
SettingsProvider provider{cfg};
|
||||
|
||||
auto const settings = provider.getSettings();
|
||||
EXPECT_EQ(settings.certificate, "certificateData");
|
||||
}
|
||||
135
unittests/data/cassandra/impl/FakesAndMocks.h
Normal file
135
unittests/data/cassandra/impl/FakesAndMocks.h
Normal file
@@ -0,0 +1,135 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of clio: https://github.com/XRPLF/clio
|
||||
Copyright (c) 2023, 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 <data/cassandra/Error.h>
|
||||
#include <data/cassandra/impl/AsyncExecutor.h>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
using namespace data::cassandra;
|
||||
using namespace data::cassandra::detail;
|
||||
|
||||
struct FakeResult
|
||||
{
|
||||
};
|
||||
|
||||
struct FakeResultOrError
|
||||
{
|
||||
CassandraError err{"<default>", CASS_OK};
|
||||
|
||||
operator bool() const
|
||||
{
|
||||
return err.code() == CASS_OK;
|
||||
}
|
||||
|
||||
CassandraError
|
||||
error() const
|
||||
{
|
||||
return err;
|
||||
}
|
||||
|
||||
FakeResult
|
||||
value() const
|
||||
{
|
||||
return FakeResult{};
|
||||
}
|
||||
};
|
||||
|
||||
struct FakeMaybeError
|
||||
{
|
||||
};
|
||||
|
||||
struct FakeStatement
|
||||
{
|
||||
};
|
||||
|
||||
struct FakePreparedStatement
|
||||
{
|
||||
};
|
||||
|
||||
struct FakeFuture
|
||||
{
|
||||
FakeResultOrError data;
|
||||
|
||||
FakeResultOrError
|
||||
get() const
|
||||
{
|
||||
return data;
|
||||
}
|
||||
|
||||
FakeMaybeError
|
||||
await() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
struct FakeFutureWithCallback : public FakeFuture
|
||||
{
|
||||
};
|
||||
|
||||
struct MockHandle
|
||||
{
|
||||
using ResultOrErrorType = FakeResultOrError;
|
||||
using MaybeErrorType = FakeMaybeError;
|
||||
using FutureWithCallbackType = FakeFutureWithCallback;
|
||||
using FutureType = FakeFuture;
|
||||
using StatementType = FakeStatement;
|
||||
using PreparedStatementType = FakePreparedStatement;
|
||||
using ResultType = FakeResult;
|
||||
|
||||
MOCK_METHOD(
|
||||
FutureWithCallbackType,
|
||||
asyncExecute,
|
||||
(StatementType const&, std::function<void(ResultOrErrorType)>&&),
|
||||
(const));
|
||||
|
||||
MOCK_METHOD(
|
||||
FutureWithCallbackType,
|
||||
asyncExecute,
|
||||
(std::vector<StatementType> const&, std::function<void(ResultOrErrorType)>&&),
|
||||
(const));
|
||||
|
||||
MOCK_METHOD(ResultOrErrorType, execute, (StatementType const&), (const));
|
||||
};
|
||||
|
||||
struct FakeRetryPolicy
|
||||
{
|
||||
FakeRetryPolicy(boost::asio::io_context&){}; // required by concept
|
||||
|
||||
std::chrono::milliseconds
|
||||
calculateDelay(uint32_t attempt)
|
||||
{
|
||||
return std::chrono::milliseconds{1};
|
||||
}
|
||||
|
||||
bool shouldRetry(CassandraError) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
void
|
||||
retry(Fn&& fn)
|
||||
{
|
||||
fn();
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user