Files
clio/reporting/CassandraBackend.h
2021-03-03 16:48:00 -05:00

1534 lines
45 KiB
C++

//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2020 Ripple Labs Inc.
Permission to use, copy, modify, and/or 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.
*/
//==============================================================================
#ifndef RIPPLE_APP_REPORTING_CASSANDRABACKEND_H_INCLUDED
#define RIPPLE_APP_REPORTING_CASSANDRABACKEND_H_INCLUDED
#include <ripple/basics/base_uint.h>
#include <boost/asio.hpp>
#include <boost/filesystem.hpp>
#include <boost/json.hpp>
#include <boost/log/trivial.hpp>
#include <atomic>
#include <cassandra.h>
#include <cstddef>
#include <iostream>
#include <memory>
#include <mutex>
#include <reporting/BackendInterface.h>
#include <reporting/DBHelpers.h>
namespace Backend {
void
flatMapWriteCallback(CassFuture* fut, void* cbData);
void
flatMapWriteKeyCallback(CassFuture* fut, void* cbData);
void
flatMapWriteTransactionCallback(CassFuture* fut, void* cbData);
void
flatMapWriteBookCallback(CassFuture* fut, void* cbData);
void
flatMapWriteAccountTxCallback(CassFuture* fut, void* cbData);
void
flatMapReadCallback(CassFuture* fut, void* cbData);
void
flatMapReadObjectCallback(CassFuture* fut, void* cbData);
void
flatMapGetCreatedCallback(CassFuture* fut, void* cbData);
void
flatMapWriteLedgerHeaderCallback(CassFuture* fut, void* cbData);
void
flatMapWriteLedgerHashCallback(CassFuture* fut, void* cbData);
class CassandraPreparedStatement
{
private:
CassPrepared const* prepared_ = nullptr;
public:
CassPrepared const*
get() const
{
return prepared_;
}
bool
prepareStatement(std::stringstream const& query, CassSession* session)
{
return prepareStatement(query.str().c_str(), session);
}
bool
prepareStatement(std::string const& query, CassSession* session)
{
return prepareStatement(query.c_str(), session);
}
bool
prepareStatement(char const* query, CassSession* session)
{
if (!query)
throw std::runtime_error("prepareStatement: null query");
if (!session)
throw std::runtime_error("prepareStatement: null sesssion");
CassFuture* prepareFuture = cass_session_prepare(session, query);
/* Wait for the statement to prepare and get the result */
CassError rc = cass_future_error_code(prepareFuture);
if (rc == CASS_OK)
{
prepared_ = cass_future_get_prepared(prepareFuture);
}
else
{
std::stringstream ss;
ss << "nodestore: Error preparing statement : " << rc << ", "
<< cass_error_desc(rc) << ". query : " << query;
BOOST_LOG_TRIVIAL(error) << ss.str();
}
cass_future_free(prepareFuture);
return rc == CASS_OK;
}
~CassandraPreparedStatement()
{
if (prepared_)
{
cass_prepared_free(prepared_);
prepared_ = nullptr;
}
}
};
class CassandraStatement
{
CassStatement* statement_ = nullptr;
size_t curBindingIndex_ = 0;
public:
CassandraStatement(CassandraPreparedStatement const& prepared)
{
statement_ = cass_prepared_bind(prepared.get());
cass_statement_set_consistency(statement_, CASS_CONSISTENCY_QUORUM);
}
CassStatement*
get() const
{
return statement_;
}
void
bindBoolean(bool val)
{
if (!statement_)
throw std::runtime_error(
"CassandraStatement::bindBoolean - statement_ is null");
CassError rc = cass_statement_bind_bool(
statement_, 1, static_cast<cass_bool_t>(val));
if (rc != CASS_OK)
{
std::stringstream ss;
ss << "Error binding boolean to statement: " << rc << ", "
<< cass_error_desc(rc);
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
throw std::runtime_error(ss.str());
}
curBindingIndex_++;
}
void
bindBytes(const char* data, uint32_t size)
{
bindBytes((unsigned char*)data, size);
}
void
bindBytes(ripple::uint256 const& data)
{
bindBytes(data.data(), data.size());
}
void
bindBytes(ripple::AccountID const& data)
{
bindBytes(data.data(), data.size());
}
void
bindBytes(std::string const& data)
{
bindBytes(data.data(), data.size());
}
void
bindBytes(void const* key, uint32_t size)
{
bindBytes(static_cast<const unsigned char*>(key), size);
}
void
bindBytes(const unsigned char* data, uint32_t size)
{
if (!statement_)
throw std::runtime_error(
"CassandraStatement::bindBytes - statement_ is null");
CassError rc = cass_statement_bind_bytes(
statement_,
curBindingIndex_,
static_cast<cass_byte_t const*>(data),
size);
if (rc != CASS_OK)
{
std::stringstream ss;
ss << "Error binding bytes to statement: " << rc << ", "
<< cass_error_desc(rc);
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
throw std::runtime_error(ss.str());
}
curBindingIndex_++;
}
void
bindInt(uint32_t value)
{
bindInt((int64_t)value);
}
void
bindInt(int64_t value)
{
if (!statement_)
throw std::runtime_error(
"CassandraStatement::bindInt - statement_ is null");
CassError rc =
cass_statement_bind_int64(statement_, curBindingIndex_, value);
if (rc != CASS_OK)
{
std::stringstream ss;
ss << "Error binding int to statement: " << rc << ", "
<< cass_error_desc(rc);
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
throw std::runtime_error(ss.str());
}
curBindingIndex_++;
}
void
bindIntTuple(uint32_t first, uint32_t second)
{
CassTuple* tuple = cass_tuple_new(2);
CassError rc = cass_tuple_set_int64(tuple, 0, first);
if (rc != CASS_OK)
{
std::stringstream ss;
ss << "Error binding int to tuple: " << rc << ", "
<< cass_error_desc(rc);
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
throw std::runtime_error(ss.str());
}
rc = cass_tuple_set_int64(tuple, 1, second);
if (rc != CASS_OK)
{
std::stringstream ss;
ss << "Error binding int to tuple: " << rc << ", "
<< cass_error_desc(rc);
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
throw std::runtime_error(ss.str());
}
rc = cass_statement_bind_tuple(statement_, curBindingIndex_, tuple);
if (rc != CASS_OK)
{
std::stringstream ss;
ss << "Error binding tuple to statement: " << rc << ", "
<< cass_error_desc(rc);
BOOST_LOG_TRIVIAL(error) << __func__ << " : " << ss.str();
throw std::runtime_error(ss.str());
}
curBindingIndex_++;
}
CassandraStatement()
{
if (statement_)
cass_statement_free(statement_);
}
};
class CassandraResult
{
CassResult const* result_ = nullptr;
CassRow const* row_ = nullptr;
CassIterator* iter_ = nullptr;
size_t curGetIndex_ = 0;
public:
CassandraResult()
{
}
CassandraResult(CassandraResult const& other) = delete;
CassandraResult(CassResult const* result) : result_(result)
{
if (!result_)
throw std::runtime_error("CassandraResult - result is null");
iter_ = cass_iterator_from_result(result_);
if (cass_iterator_next(iter_))
{
row_ = cass_iterator_get_row(iter_);
}
}
bool
hasResult()
{
return row_ != nullptr;
}
bool
operator!()
{
return !hasResult();
}
size_t
numRows()
{
return cass_result_row_count(result_);
}
bool
nextRow()
{
curGetIndex_ = 0;
if (cass_iterator_next(iter_))
{
row_ = cass_iterator_get_row(iter_);
return true;
}
row_ = nullptr;
return false;
}
std::vector<unsigned char>
getBytes()
{
if (!row_)
throw std::runtime_error("CassandraResult::getBytes - no result");
cass_byte_t const* buf;
std::size_t bufSize;
CassError rc = cass_value_get_bytes(
cass_row_get_column(row_, curGetIndex_), &buf, &bufSize);
if (rc != CASS_OK)
{
std::stringstream msg;
msg << "CassandraResult::getBytes - error getting value: " << rc
<< ", " << cass_error_desc(rc);
BOOST_LOG_TRIVIAL(error) << msg.str();
throw std::runtime_error(msg.str());
}
curGetIndex_++;
return {buf, buf + bufSize};
}
ripple::uint256
getUInt256()
{
if (!row_)
throw std::runtime_error("CassandraResult::getBytes - no result");
cass_byte_t const* buf;
std::size_t bufSize;
CassError rc = cass_value_get_bytes(
cass_row_get_column(row_, curGetIndex_), &buf, &bufSize);
if (rc != CASS_OK)
{
std::stringstream msg;
msg << "CassandraResult::getBytes - error getting value: " << rc
<< ", " << cass_error_desc(rc);
BOOST_LOG_TRIVIAL(error) << msg.str();
throw std::runtime_error(msg.str());
}
curGetIndex_++;
return ripple::uint256::fromVoid(buf);
}
int64_t
getInt64()
{
if (!row_)
throw std::runtime_error("CassandraResult::getInt64 - no result");
cass_int64_t val;
CassError rc =
cass_value_get_int64(cass_row_get_column(row_, curGetIndex_), &val);
if (rc != CASS_OK)
{
std::stringstream msg;
msg << "CassandraResult::getInt64 - error getting value: " << rc
<< ", " << cass_error_desc(rc);
BOOST_LOG_TRIVIAL(error) << msg.str();
throw std::runtime_error(msg.str());
}
++curGetIndex_;
return val;
}
uint32_t
getUInt32()
{
return (uint32_t)getInt64();
}
std::pair<int64_t, int64_t>
getInt64Tuple()
{
if (!row_)
throw std::runtime_error(
"CassandraResult::getInt64Tuple - no result");
CassValue const* tuple = cass_row_get_column(row_, curGetIndex_);
CassIterator* tupleIter = cass_iterator_from_tuple(tuple);
if (!cass_iterator_next(tupleIter))
throw std::runtime_error(
"CassandraResult::getInt64Tuple - failed to iterate tuple");
CassValue const* value = cass_iterator_get_value(tupleIter);
int64_t first;
cass_value_get_int64(value, &first);
if (!cass_iterator_next(tupleIter))
throw std::runtime_error(
"CassandraResult::getInt64Tuple - failed to iterate tuple");
value = cass_iterator_get_value(tupleIter);
int64_t second;
cass_value_get_int64(value, &second);
++curGetIndex_;
return {first, second};
}
~CassandraResult()
{
cass_result_free(result_);
}
};
template <class T, class F>
class CassandraAsyncResult
{
T& requestParams_;
CassandraResult result_;
public:
CassandraAsyncResult(T& requestParams, CassFuture* fut, F retry)
: requestParams_(requestParams)
{
CassError rc = cass_future_error_code(fut);
if (rc != CASS_OK)
{
retry(requestParams_);
}
else
{
result_ = CassandraResult(cass_future_get_result(fut));
}
}
~CassandraAsyncResult()
{
if (!!result_)
{
BOOST_LOG_TRIVIAL(trace) << "finished a request";
size_t batchSize = requestParams_.batchSize;
if (++(requestParams_.numFinished) == batchSize)
requestParams_.cv.notify_all();
}
}
CassandraResult&
getResult()
{
return result_;
}
};
class CassandraBackend : public BackendInterface
{
private:
// convenience function for one-off queries. For normal reads and writes,
// use the prepared statements insert_ and select_
CassStatement*
makeStatement(char const* query, std::size_t params)
{
CassStatement* ret = cass_statement_new(query, params);
CassError rc =
cass_statement_set_consistency(ret, CASS_CONSISTENCY_QUORUM);
if (rc != CASS_OK)
{
std::stringstream ss;
ss << "nodestore: Error setting query consistency: " << query
<< ", result: " << rc << ", " << cass_error_desc(rc);
throw std::runtime_error(ss.str());
}
return ret;
}
std::atomic<bool> open_{false};
// mutex used for open() and close()
std::mutex mutex_;
std::unique_ptr<CassSession, void (*)(CassSession*)> session_{
nullptr,
[](CassSession* session) {
// Try to disconnect gracefully.
CassFuture* fut = cass_session_close(session);
cass_future_wait(fut);
cass_future_free(fut);
cass_session_free(session);
}};
// Database statements cached server side. Using these is more efficient
// than making a new statement
CassandraPreparedStatement insertObject_;
CassandraPreparedStatement insertTransaction_;
CassandraPreparedStatement selectTransaction_;
CassandraPreparedStatement selectObject_;
CassandraPreparedStatement selectLedgerPageKeys_;
CassandraPreparedStatement selectLedgerPage_;
CassandraPreparedStatement upperBound2_;
CassandraPreparedStatement getToken_;
CassandraPreparedStatement insertKey_;
CassandraPreparedStatement getCreated_;
CassandraPreparedStatement getBook_;
CassandraPreparedStatement insertBook_;
CassandraPreparedStatement deleteBook_;
CassandraPreparedStatement insertAccountTx_;
CassandraPreparedStatement selectAccountTx_;
CassandraPreparedStatement insertLedgerHeader_;
CassandraPreparedStatement insertLedgerHash_;
CassandraPreparedStatement updateLedgerRange_;
CassandraPreparedStatement updateLedgerHeader_;
CassandraPreparedStatement selectLedgerBySeq_;
CassandraPreparedStatement selectLatestLedger_;
CassandraPreparedStatement selectLedgerRange_;
// io_context used for exponential backoff for write retries
mutable boost::asio::io_context ioContext_;
std::optional<boost::asio::io_context::work> work_;
std::thread ioThread_;
// maximum number of concurrent in flight requests. New requests will wait
// for earlier requests to finish if this limit is exceeded
uint32_t maxRequestsOutstanding = 10000000;
mutable std::atomic_uint32_t numRequestsOutstanding_ = 0;
// mutex and condition_variable to limit the number of concurrent in flight
// requests
mutable std::mutex throttleMutex_;
mutable std::condition_variable throttleCv_;
// writes are asynchronous. This mutex and condition_variable is used to
// wait for all writes to finish
mutable std::mutex syncMutex_;
mutable std::condition_variable syncCv_;
boost::json::object config_;
mutable uint32_t ledgerSequence_ = 0;
mutable bool isFirstLedger_ = false;
public:
CassandraBackend(boost::json::object const& config) : config_(config)
{
}
~CassandraBackend() override
{
if (open_)
close();
}
std::string
getName()
{
return "cassandra";
}
bool
isOpen()
{
return open_;
}
// Setup all of the necessary components for talking to the database.
// Create the table if it doesn't exist already
// @param createIfMissing ignored
void
open() override;
// Close the connection to the database
void
close() override
{
{
std::lock_guard<std::mutex> lock(mutex_);
work_.reset();
ioThread_.join();
}
open_ = false;
}
std::pair<
std::vector<TransactionAndMetadata>,
std::optional<AccountTransactionsCursor>>
fetchAccountTransactions(
ripple::AccountID const& account,
std::uint32_t limit,
std::optional<AccountTransactionsCursor> const& cursor) const override
{
BOOST_LOG_TRIVIAL(debug) << "Starting doAccountTx";
CassandraStatement statement{selectAccountTx_};
statement.bindBytes(account);
if (cursor)
statement.bindIntTuple(
cursor->ledgerSequence, cursor->transactionIndex);
else
statement.bindIntTuple(INT32_MAX, INT32_MAX);
statement.bindInt(limit);
CassandraResult result = executeSyncRead(statement);
if (!result.hasResult())
{
BOOST_LOG_TRIVIAL(debug) << __func__ << " - no rows returned";
return {{}, {}};
}
std::vector<ripple::uint256> hashes;
size_t numRows = result.numRows();
bool returnCursor = numRows == limit;
std::optional<AccountTransactionsCursor> retCursor;
do
{
hashes.push_back(result.getUInt256());
--numRows;
if (numRows == 0 && returnCursor)
{
auto [lgrSeq, txnIdx] = result.getInt64Tuple();
retCursor = {(uint32_t)lgrSeq, (uint32_t)txnIdx};
}
} while (result.nextRow());
BOOST_LOG_TRIVIAL(debug)
<< "doAccountTx - populated hashes. num hashes = " << hashes.size();
if (hashes.size())
{
return {fetchTransactions(hashes), retCursor};
}
return {{}, {}};
}
struct WriteLedgerHeaderCallbackData
{
CassandraBackend const* backend;
uint32_t sequence;
std::string header;
uint32_t currentRetries = 0;
std::atomic<int> refs = 1;
WriteLedgerHeaderCallbackData(
CassandraBackend const* f,
uint32_t sequence,
std::string&& header)
: backend(f), sequence(sequence), header(std::move(header))
{
}
};
struct WriteLedgerHashCallbackData
{
CassandraBackend const* backend;
ripple::uint256 hash;
uint32_t sequence;
uint32_t currentRetries = 0;
std::atomic<int> refs = 1;
WriteLedgerHashCallbackData(
CassandraBackend const* f,
ripple::uint256 hash,
uint32_t sequence)
: backend(f), hash(hash), sequence(sequence)
{
}
};
bool
finishWrites() const override
{
// wait for all other writes to finish
sync();
// write range
if (isFirstLedger_)
{
CassandraStatement statement{updateLedgerRange_};
statement.bindInt(ledgerSequence_);
statement.bindBoolean(false);
statement.bindInt(ledgerSequence_);
executeSyncWrite(statement);
}
CassandraStatement statement{updateLedgerRange_};
statement.bindInt(ledgerSequence_);
statement.bindBoolean(true);
statement.bindInt(ledgerSequence_);
return executeSyncUpdate(statement);
}
void
writeLedger(
ripple::LedgerInfo const& ledgerInfo,
std::string&& header,
bool isFirst = false) const override
{
WriteLedgerHeaderCallbackData* headerCb =
new WriteLedgerHeaderCallbackData(
this, ledgerInfo.seq, std::move(header));
WriteLedgerHashCallbackData* hashCb = new WriteLedgerHashCallbackData(
this, ledgerInfo.hash, ledgerInfo.seq);
++numRequestsOutstanding_;
++numRequestsOutstanding_;
writeLedgerHeader(*headerCb, false);
writeLedgerHash(*hashCb, false);
}
void
writeLedgerHash(WriteLedgerHashCallbackData& cb, bool isRetry) const
{
CassandraStatement statement{insertLedgerHash_};
statement.bindBytes(cb.hash);
statement.bindInt(cb.sequence);
executeAsyncWrite(
statement, flatMapWriteLedgerHashCallback, cb, isRetry);
}
void
writeLedgerHeader(WriteLedgerHeaderCallbackData& cb, bool isRetry) const
{
CassandraStatement statement{insertLedgerHeader_};
statement.bindInt(cb.sequence);
statement.bindBytes(cb.header);
executeAsyncWrite(
statement, flatMapWriteLedgerHeaderCallback, cb, isRetry);
}
std::optional<uint32_t>
fetchLatestLedgerSequence() const override
{
BOOST_LOG_TRIVIAL(trace) << __func__;
CassandraStatement statement{selectLatestLedger_};
CassandraResult result = executeSyncRead(statement);
if (!result.hasResult())
{
BOOST_LOG_TRIVIAL(error)
<< "CassandraBackend::fetchLatestLedgerSequence - no rows";
return {};
}
return result.getUInt32();
}
std::optional<ripple::LedgerInfo>
fetchLedgerBySequence(uint32_t sequence) const override
{
BOOST_LOG_TRIVIAL(trace) << __func__;
CassandraStatement statement{selectLedgerBySeq_};
statement.bindInt(sequence);
CassandraResult result = executeSyncRead(statement);
if (!result)
{
BOOST_LOG_TRIVIAL(error) << __func__ << " - no rows";
return {};
}
std::vector<unsigned char> header = result.getBytes();
return deserializeHeader(ripple::makeSlice(header));
}
std::optional<LedgerRange>
fetchLedgerRange() const override;
// Synchronously fetch the object with key key and store the result in
// pno
// @param key the key of the object
// @param pno object in which to store the result
// @return result status of query
std::optional<Blob>
fetchLedgerObject(ripple::uint256 const& key, uint32_t sequence)
const override
{
BOOST_LOG_TRIVIAL(trace) << "Fetching from cassandra";
CassandraStatement statement{selectObject_};
statement.bindBytes(key);
statement.bindInt(sequence);
CassandraResult result = executeSyncRead(statement);
if (!result)
{
BOOST_LOG_TRIVIAL(error) << __func__ << " - no rows";
return {};
}
return result.getBytes();
}
std::optional<int64_t>
getToken(void const* key) const
{
BOOST_LOG_TRIVIAL(trace) << "Fetching from cassandra";
CassandraStatement statement{getToken_};
statement.bindBytes(key, 32);
CassandraResult result = executeSyncRead(statement);
if (!result)
{
BOOST_LOG_TRIVIAL(error) << __func__ << " - no rows";
return {};
}
int64_t token = result.getInt64();
if (token == INT64_MAX)
return {};
else
return token + 1;
}
std::optional<TransactionAndMetadata>
fetchTransaction(ripple::uint256 const& hash) const override
{
BOOST_LOG_TRIVIAL(trace) << __func__;
CassandraStatement statement{selectTransaction_};
statement.bindBytes(hash);
CassandraResult result = executeSyncRead(statement);
if (!result)
{
BOOST_LOG_TRIVIAL(error) << __func__ << " - no rows";
return {};
}
return {{result.getBytes(), result.getBytes()}};
}
LedgerPage
fetchLedgerPage(
std::optional<ripple::uint256> const& cursor,
std::uint32_t ledgerSequence,
std::uint32_t limit) const override
{
BOOST_LOG_TRIVIAL(trace) << __func__;
CassandraStatement statement{selectLedgerPageKeys_};
int64_t intCursor = INT64_MIN;
if (cursor)
{
auto token = getToken(cursor->data());
if (token)
intCursor = *token;
}
statement.bindInt(intCursor);
statement.bindInt(ledgerSequence);
statement.bindInt(ledgerSequence);
statement.bindInt(limit);
CassandraResult result = executeSyncRead(statement);
BOOST_LOG_TRIVIAL(debug) << __func__ << " - got keys";
std::vector<ripple::uint256> keys;
do
{
keys.push_back(result.getUInt256());
} while (result.nextRow());
BOOST_LOG_TRIVIAL(debug)
<< __func__ << " - populated keys. num keys = " << keys.size();
if (keys.size())
{
std::vector<LedgerObject> results;
std::vector<Blob> objs = fetchLedgerObjects(keys, ledgerSequence);
for (size_t i = 0; i < objs.size(); ++i)
{
results.push_back({keys[i], objs[i]});
}
return {results, keys[keys.size() - 1]};
}
return {{}, {}};
}
LedgerPage
fetchLedgerPage2(
std::optional<ripple::uint256> cursor,
std::uint32_t ledgerSequence,
std::uint32_t limit) const
{
BOOST_LOG_TRIVIAL(trace) << __func__;
std::vector<LedgerObject> objects;
while (objects.size() < limit)
{
CassandraStatement statement{selectLedgerPage_};
int64_t intCursor = INT64_MIN;
if (cursor)
{
auto token = getToken(cursor->data());
if (token)
intCursor = *token;
}
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " - cursor = " << std::to_string(intCursor)
<< " , sequence = " << std::to_string(ledgerSequence)
<< ", - limit = " << std::to_string(limit);
statement.bindInt(intCursor);
statement.bindInt(ledgerSequence);
statement.bindInt(limit);
CassandraResult result = executeSyncRead(statement);
BOOST_LOG_TRIVIAL(debug) << __func__ << " - got keys";
if (!!result)
{
do
{
objects.push_back({result.getUInt256(), result.getBytes()});
} while (result.nextRow());
if (objects.size() < limit)
{
double sparsity = limit / objects.size();
limit = (limit - objects.size()) * sparsity;
BOOST_LOG_TRIVIAL(debug)
<< __func__
<< " - sparsity = " << std::to_string(sparsity)
<< " , limit = " << std::to_string(limit);
}
assert(objects.size());
cursor = objects[objects.size() - 1].key;
}
}
if (objects.size())
return {objects, cursor};
return {{}, {}};
}
std::pair<std::vector<LedgerObject>, std::optional<ripple::uint256>>
fetchBookOffers(
ripple::uint256 const& book,
uint32_t sequence,
std::uint32_t limit,
std::optional<ripple::uint256> const& cursor) const override
{
CassandraStatement statement{getBook_};
statement.bindBytes(book);
statement.bindInt(sequence);
statement.bindInt(sequence);
if (cursor)
statement.bindBytes(*cursor);
else
{
ripple::uint256 zero = {};
statement.bindBytes(zero);
}
statement.bindInt(limit);
CassandraResult result = executeSyncRead(statement);
BOOST_LOG_TRIVIAL(debug) << __func__ << " - got keys";
std::vector<ripple::uint256> keys;
do
{
keys.push_back(result.getUInt256());
} while (result.nextRow());
BOOST_LOG_TRIVIAL(debug)
<< __func__ << " - populated keys. num keys = " << keys.size();
if (keys.size())
{
std::vector<LedgerObject> results;
std::vector<Blob> objs = fetchLedgerObjects(keys, sequence);
for (size_t i = 0; i < objs.size(); ++i)
{
results.push_back({keys[i], objs[i]});
}
return {results, results[results.size() - 1].key};
}
return {{}, {}};
}
bool
canFetchBatch()
{
return true;
}
struct ReadCallbackData
{
CassandraBackend const& backend;
ripple::uint256 const& hash;
TransactionAndMetadata& result;
std::condition_variable& cv;
std::atomic_uint32_t& numFinished;
size_t batchSize;
ReadCallbackData(
CassandraBackend const& backend,
ripple::uint256 const& hash,
TransactionAndMetadata& result,
std::condition_variable& cv,
std::atomic_uint32_t& numFinished,
size_t batchSize)
: backend(backend)
, hash(hash)
, result(result)
, cv(cv)
, numFinished(numFinished)
, batchSize(batchSize)
{
}
ReadCallbackData(ReadCallbackData const& other) = default;
};
std::vector<TransactionAndMetadata>
fetchTransactions(std::vector<ripple::uint256> const& hashes) const override
{
std::size_t const numHashes = hashes.size();
BOOST_LOG_TRIVIAL(trace)
<< "Fetching " << numHashes << " records from Cassandra";
std::atomic_uint32_t numFinished = 0;
std::condition_variable cv;
std::mutex mtx;
std::vector<TransactionAndMetadata> results{numHashes};
std::vector<std::shared_ptr<ReadCallbackData>> cbs;
cbs.reserve(numHashes);
for (std::size_t i = 0; i < hashes.size(); ++i)
{
cbs.push_back(std::make_shared<ReadCallbackData>(
*this, hashes[i], results[i], cv, numFinished, numHashes));
read(*cbs[i]);
}
assert(results.size() == cbs.size());
std::unique_lock<std::mutex> lck(mtx);
cv.wait(lck, [&numFinished, &numHashes]() {
return numFinished == numHashes;
});
BOOST_LOG_TRIVIAL(trace)
<< "Fetched " << numHashes << " records from Cassandra";
return results;
}
void
read(ReadCallbackData& data) const
{
CassandraStatement statement{selectTransaction_};
statement.bindBytes(data.hash);
executeAsyncRead(statement, flatMapReadCallback, data);
}
struct ReadObjectCallbackData
{
CassandraBackend const& backend;
ripple::uint256 const& key;
uint32_t sequence;
Blob& result;
std::condition_variable& cv;
std::atomic_uint32_t& numFinished;
size_t batchSize;
ReadObjectCallbackData(
CassandraBackend const& backend,
ripple::uint256 const& key,
uint32_t sequence,
Blob& result,
std::condition_variable& cv,
std::atomic_uint32_t& numFinished,
size_t batchSize)
: backend(backend)
, key(key)
, sequence(sequence)
, result(result)
, cv(cv)
, numFinished(numFinished)
, batchSize(batchSize)
{
}
ReadObjectCallbackData(ReadObjectCallbackData const& other) = default;
};
std::vector<Blob>
fetchLedgerObjects(
std::vector<ripple::uint256> const& keys,
uint32_t sequence) const override
{
std::size_t const numKeys = keys.size();
BOOST_LOG_TRIVIAL(trace)
<< "Fetching " << numKeys << " records from Cassandra";
std::atomic_uint32_t numFinished = 0;
std::condition_variable cv;
std::mutex mtx;
std::vector<Blob> results{numKeys};
std::vector<std::shared_ptr<ReadObjectCallbackData>> cbs;
cbs.reserve(numKeys);
for (std::size_t i = 0; i < keys.size(); ++i)
{
cbs.push_back(std::make_shared<ReadObjectCallbackData>(
*this,
keys[i],
sequence,
results[i],
cv,
numFinished,
numKeys));
readObject(*cbs[i]);
}
assert(results.size() == cbs.size());
std::unique_lock<std::mutex> lck(mtx);
cv.wait(
lck, [&numFinished, &numKeys]() { return numFinished == numKeys; });
BOOST_LOG_TRIVIAL(trace)
<< "Fetched " << numKeys << " records from Cassandra";
return results;
}
void
readObject(ReadObjectCallbackData& data) const
{
CassandraStatement statement{selectObject_};
statement.bindBytes(data.key);
statement.bindInt(data.sequence);
executeAsyncRead(statement, flatMapReadObjectCallback, data);
}
struct WriteCallbackData
{
CassandraBackend const* backend;
std::string key;
uint32_t sequence;
uint32_t createdSequence = 0;
std::string blob;
bool isCreated;
bool isDeleted;
std::optional<ripple::uint256> book;
uint32_t currentRetries = 0;
std::atomic<int> refs = 1;
WriteCallbackData(
CassandraBackend const* f,
std::string&& key,
uint32_t sequence,
std::string&& blob,
bool isCreated,
bool isDeleted,
std::optional<ripple::uint256>&& book)
: backend(f)
, key(std::move(key))
, sequence(sequence)
, blob(std::move(blob))
, isCreated(isCreated)
, isDeleted(isDeleted)
, book(std::move(book))
{
if (isCreated or isDeleted)
++refs;
if (book)
++refs;
}
};
struct WriteAccountTxCallbackData
{
CassandraBackend const* backend;
AccountTransactionsData data;
uint32_t currentRetries = 0;
std::atomic<int> refs;
WriteAccountTxCallbackData(
CassandraBackend const* f,
AccountTransactionsData&& data)
: backend(f), data(std::move(data)), refs(data.accounts.size())
{
}
};
void
write(WriteCallbackData& data, bool isRetry) const
{
{
CassandraStatement statement{insertObject_};
statement.bindBytes(data.key);
statement.bindInt(data.sequence);
statement.bindBytes(data.blob);
executeAsyncWrite(statement, flatMapWriteCallback, data, isRetry);
}
}
void
writeDeletedKey(WriteCallbackData& data, bool isRetry) const
{
CassandraStatement statement{insertKey_};
statement.bindBytes(data.key);
statement.bindInt(data.createdSequence);
statement.bindInt(data.sequence);
executeAsyncWrite(statement, flatMapWriteKeyCallback, data, isRetry);
}
void
writeKey(WriteCallbackData& data, bool isRetry) const
{
if (data.isCreated)
{
CassandraStatement statement{insertKey_};
statement.bindBytes(data.key);
statement.bindInt(data.sequence);
statement.bindInt(INT64_MAX);
executeAsyncWrite(
statement, flatMapWriteKeyCallback, data, isRetry);
}
else if (data.isDeleted)
{
CassandraStatement statement{getCreated_};
executeAsyncWrite(
statement, flatMapGetCreatedCallback, data, isRetry);
}
}
void
writeBook(WriteCallbackData& data, bool isRetry) const
{
assert(data.isCreated or data.isDeleted);
assert(data.book);
CassandraStatement statement{
(data.isCreated ? insertBook_ : deleteBook_)};
statement.bindBytes(*data.book);
statement.bindBytes(data.key);
statement.bindInt(data.sequence);
if (data.isCreated)
statement.bindInt(INT64_MAX);
executeAsyncWrite(statement, flatMapWriteBookCallback, data, isRetry);
}
void
writeLedgerObject(
std::string&& key,
uint32_t seq,
std::string&& blob,
bool isCreated,
bool isDeleted,
std::optional<ripple::uint256>&& book) const override
{
BOOST_LOG_TRIVIAL(trace) << "Writing ledger object to cassandra";
WriteCallbackData* data = new WriteCallbackData(
this,
std::move(key),
seq,
std::move(blob),
isCreated,
isDeleted,
std::move(book));
++numRequestsOutstanding_;
if (isCreated || isDeleted)
++numRequestsOutstanding_;
if (book)
++numRequestsOutstanding_;
write(*data, false);
if (isCreated || isDeleted)
writeKey(*data, false);
if (book)
writeBook(*data, false);
// handle book
}
void
writeAccountTransactions(
std::vector<AccountTransactionsData>&& data) const override
{
for (auto& record : data)
{
numRequestsOutstanding_ += record.accounts.size();
WriteAccountTxCallbackData* cbData =
new WriteAccountTxCallbackData(this, std::move(record));
writeAccountTx(*cbData, false);
}
}
void
writeAccountTx(WriteAccountTxCallbackData& data, bool isRetry) const
{
for (auto const& account : data.data.accounts)
{
CassandraStatement statement(insertAccountTx_);
statement.bindBytes(account);
statement.bindIntTuple(
data.data.ledgerSequence, data.data.transactionIndex);
executeAsyncWrite(
statement, flatMapWriteAccountTxCallback, data, isRetry);
}
}
struct WriteTransactionCallbackData
{
CassandraBackend const* backend;
// The shared pointer to the node object must exist until it's
// confirmed persisted. Otherwise, it can become deleted
// prematurely if other copies are removed from caches.
std::string hash;
uint32_t sequence;
std::string transaction;
std::string metadata;
uint32_t currentRetries = 0;
std::atomic<int> refs = 1;
WriteTransactionCallbackData(
CassandraBackend const* f,
std::string&& hash,
uint32_t sequence,
std::string&& transaction,
std::string&& metadata)
: backend(f)
, hash(std::move(hash))
, sequence(sequence)
, transaction(std::move(transaction))
, metadata(std::move(metadata))
{
}
};
void
writeTransaction(WriteTransactionCallbackData& data, bool isRetry) const
{
CassandraStatement statement{insertTransaction_};
statement.bindBytes(data.hash);
statement.bindInt(data.sequence);
statement.bindBytes(data.transaction);
statement.bindBytes(data.metadata);
executeAsyncWrite(
statement, flatMapWriteTransactionCallback, data, isRetry);
}
void
writeTransaction(
std::string&& hash,
uint32_t seq,
std::string&& transaction,
std::string&& metadata) const override
{
BOOST_LOG_TRIVIAL(trace) << "Writing txn to cassandra";
WriteTransactionCallbackData* data = new WriteTransactionCallbackData(
this,
std::move(hash),
seq,
std::move(transaction),
std::move(metadata));
++numRequestsOutstanding_;
writeTransaction(*data, false);
}
void
startWrites() const override
{
}
void
sync() const
{
std::unique_lock<std::mutex> lck(syncMutex_);
syncCv_.wait(lck, [this]() { return numRequestsOutstanding_ == 0; });
}
void
finishAsyncWrite() const
{
--numRequestsOutstanding_;
throttleCv_.notify_all();
if (numRequestsOutstanding_ == 0)
syncCv_.notify_all();
}
boost::asio::io_context&
getIOContext() const
{
return ioContext_;
}
friend void
flatMapWriteCallback(CassFuture* fut, void* cbData);
friend void
flatMapWriteKeyCallback(CassFuture* fut, void* cbData);
friend void
flatMapWriteTransactionCallback(CassFuture* fut, void* cbData);
friend void
flatMapWriteBookCallback(CassFuture* fut, void* cbData);
friend void
flatMapWriteAccountTxCallback(CassFuture* fut, void* cbData);
friend void
flatMapWriteLedgerHeaderCallback(CassFuture* fut, void* cbData);
friend void
flatMapWriteLedgerHashCallback(CassFuture* fut, void* cbData);
friend void
flatMapReadCallback(CassFuture* fut, void* cbData);
friend void
flatMapReadObjectCallback(CassFuture* fut, void* cbData);
friend void
flatMapGetCreatedCallback(CassFuture* fut, void* cbData);
void
waitIfNeccessary(bool isRetry) const
{
{
std::unique_lock<std::mutex> lck(throttleMutex_);
if (!isRetry && numRequestsOutstanding_ > maxRequestsOutstanding)
{
BOOST_LOG_TRIVIAL(trace)
<< __func__ << " : "
<< "Max outstanding requests reached. "
<< "Waiting for other requests to finish";
throttleCv_.wait(lck, [this]() {
return numRequestsOutstanding_ < maxRequestsOutstanding;
});
}
}
}
template <class T, class S>
void
executeAsyncHelper(
CassandraStatement const& statement,
T callback,
S& callbackData) const
{
CassFuture* fut = cass_session_execute(session_.get(), statement.get());
cass_future_set_callback(
fut, callback, static_cast<void*>(&callbackData));
cass_future_free(fut);
}
template <class T, class S>
void
executeAsyncWrite(
CassandraStatement const& statement,
T callback,
S& callbackData,
bool isRetry) const
{
waitIfNeccessary(isRetry);
executeAsyncHelper(statement, callback, callbackData);
}
template <class T, class S>
void
executeAsyncRead(
CassandraStatement const& statement,
T callback,
S& callbackData) const
{
executeAsyncHelper(statement, callback, callbackData);
}
void
executeSyncWrite(CassandraStatement const& statement) const
{
CassFuture* fut;
CassError rc;
do
{
fut = cass_session_execute(session_.get(), statement.get());
rc = cass_future_error_code(fut);
if (rc != CASS_OK)
{
std::stringstream ss;
ss << "Cassandra sync write error";
ss << ", retrying";
ss << ": " << cass_error_desc(rc);
BOOST_LOG_TRIVIAL(warning) << ss.str();
}
} while (rc != CASS_OK);
cass_future_free(fut);
}
bool
executeSyncUpdate(CassandraStatement const& statement) const
{
CassFuture* fut;
CassError rc;
do
{
fut = cass_session_execute(session_.get(), statement.get());
rc = cass_future_error_code(fut);
if (rc != CASS_OK)
{
std::stringstream ss;
ss << "Cassandra sync write error";
ss << ", retrying";
ss << ": " << cass_error_desc(rc);
BOOST_LOG_TRIVIAL(warning) << ss.str();
}
} while (rc != CASS_OK);
CassResult const* res = cass_future_get_result(fut);
cass_future_free(fut);
CassRow const* row = cass_result_first_row(res);
if (!row)
{
BOOST_LOG_TRIVIAL(error) << "executeSyncUpdate - no rows";
cass_result_free(res);
return false;
}
cass_bool_t success;
rc = cass_value_get_bool(cass_row_get_column(row, 0), &success);
if (rc != CASS_OK)
{
cass_result_free(res);
BOOST_LOG_TRIVIAL(error)
<< "executeSyncUpdate - error getting result " << rc << ", "
<< cass_error_desc(rc);
return false;
}
cass_result_free(res);
return success == cass_true;
}
CassandraResult
executeSyncRead(CassandraStatement const& statement) const
{
CassFuture* fut;
CassError rc;
do
{
fut = cass_session_execute(session_.get(), statement.get());
rc = cass_future_error_code(fut);
if (rc != CASS_OK)
{
std::stringstream ss;
ss << "Cassandra executeSyncRead error";
ss << ", retrying";
ss << ": " << cass_error_desc(rc);
BOOST_LOG_TRIVIAL(warning) << ss.str();
}
} while (rc != CASS_OK);
CassResult const* res = cass_future_get_result(fut);
cass_future_free(fut);
return {res};
}
};
} // namespace Backend
#endif