Fix transaction enumeration in account_tx (RIPD-734):

In some corner cases, an incorrect resume marker could be
returned, preventing the complete enumeration of account
transactions.

* Robust markers via improved paging support
* New unit tests
* Cleanup
This commit is contained in:
Nik Bougalis
2015-01-09 00:52:31 +07:00
parent 64259c7bcb
commit b2a9c79de5
12 changed files with 661 additions and 265 deletions

View File

@@ -1726,6 +1726,14 @@
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\misc\IHashRouter.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\misc\impl\AccountTxPaging.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\misc\impl\AccountTxPaging.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\misc\NetworkOPs.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
@@ -1746,6 +1754,12 @@
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\misc\SHAMapStoreImp.h">
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\misc\tests\AccountTxPaging.test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\misc\tests\AmendmentTable.test.cpp">
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>

View File

@@ -280,6 +280,9 @@
<Filter Include="ripple\app\misc">
<UniqueIdentifier>{5A1509B2-871B-A7AC-1E60-544D3F398741}</UniqueIdentifier>
</Filter>
<Filter Include="ripple\app\misc\impl">
<UniqueIdentifier>{C4BDB9F8-7DB7-E304-D286-098085D5D16E}</UniqueIdentifier>
</Filter>
<Filter Include="ripple\app\misc\tests">
<UniqueIdentifier>{815DC1A2-E2EF-E6E3-D979-19AD1476A28B}</UniqueIdentifier>
</Filter>
@@ -2379,6 +2382,12 @@
<ClInclude Include="..\..\src\ripple\app\misc\IHashRouter.h">
<Filter>ripple\app\misc</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\misc\impl\AccountTxPaging.cpp">
<Filter>ripple\app\misc\impl</Filter>
</ClCompile>
<ClInclude Include="..\..\src\ripple\app\misc\impl\AccountTxPaging.h">
<Filter>ripple\app\misc\impl</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\misc\NetworkOPs.cpp">
<Filter>ripple\app\misc</Filter>
</ClCompile>
@@ -2397,6 +2406,9 @@
<ClInclude Include="..\..\src\ripple\app\misc\SHAMapStoreImp.h">
<Filter>ripple\app\misc</Filter>
</ClInclude>
<ClCompile Include="..\..\src\ripple\app\misc\tests\AccountTxPaging.test.cpp">
<Filter>ripple\app\misc\tests</Filter>
</ClCompile>
<ClCompile Include="..\..\src\ripple\app\misc\tests\AmendmentTable.test.cpp">
<Filter>ripple\app\misc\tests</Filter>
</ClCompile>

View File

@@ -22,6 +22,7 @@
#include <ripple/app/ledger/Ledger.h>
#include <ripple/basics/Time.h>
#include <ripple/basics/StringUtilities.h>
#include <ripple/protocol/JsonFields.h>
#include <ripple/protocol/STTx.h>
#include <ripple/rpc/Yield.h>

View File

@@ -35,6 +35,7 @@
#include <ripple/app/misc/IHashRouter.h>
#include <ripple/app/misc/NetworkOPs.h>
#include <ripple/app/misc/Validations.h>
#include <ripple/app/misc/impl/AccountTxPaging.h>
#include <ripple/app/peers/ClusterNodeStatus.h>
#include <ripple/app/peers/UniqueNodeList.h>
#include <ripple/app/tx/TransactionMaster.h>
@@ -1916,7 +1917,6 @@ NetworkOPs::AccountTxs NetworkOPsImp::getAccountTxs (
auto txn = Transaction::transactionFromSQL (
ledgerSeq, status, rawTxn, Validate::NO);
if (txnMeta.empty ())
{ // Work around a bug that could leave the metadata missing
auto const seq = rangeCheckedCast<std::uint32_t>(
@@ -1942,7 +1942,7 @@ std::vector<NetworkOPsImp::txnMetaLedgerType> NetworkOPsImp::getAccountTxsB (
std::uint32_t offset, int limit, bool bAdmin)
{
// can be called with no locks
std::vector< txnMetaLedgerType> ret;
std::vector<txnMetaLedgerType> ret;
std::string sql = NetworkOPsImp::transactionsSQL (
"AccountTransactions.LedgerSeq,Status,RawTxn,TxnMeta", account,
@@ -1985,255 +1985,55 @@ std::vector<NetworkOPsImp::txnMetaLedgerType> NetworkOPsImp::getAccountTxsB (
return ret;
}
NetworkOPsImp::AccountTxs NetworkOPsImp::getTxsAccount (
NetworkOPsImp::AccountTxs
NetworkOPsImp::getTxsAccount (
RippleAddress const& account, std::int32_t minLedger,
std::int32_t maxLedger, bool forward, Json::Value& token,
int limit, bool bAdmin)
{
AccountTxs ret;
static std::uint32_t const page_length (200);
std::uint32_t NONBINARY_PAGE_LENGTH = 200;
std::uint32_t EXTRA_LENGTH = 100;
NetworkOPsImp::AccountTxs ret;
bool foundResume = token.isNull() || !token.isObject();
std::uint32_t numberOfResults, queryLimit;
if (limit <= 0)
numberOfResults = NONBINARY_PAGE_LENGTH;
else if (!bAdmin && (limit > NONBINARY_PAGE_LENGTH))
numberOfResults = NONBINARY_PAGE_LENGTH;
else
numberOfResults = limit;
queryLimit = numberOfResults + 1 + (foundResume ? 0 : EXTRA_LENGTH);
std::uint32_t findLedger = 0, findSeq = 0;
if (!foundResume)
auto bound = [&ret](
std::uint32_t ledger_index,
std::string const& status,
Blob const& rawTxn,
Blob const& rawMeta)
{
try
{
if (!token.isMember(jss::ledger) || !token.isMember(jss::seq))
return ret;
findLedger = token[jss::ledger].asInt();
findSeq = token[jss::seq].asInt();
}
catch (...)
{
return ret;
}
}
convertBlobsToTxResult (ret, ledger_index, status, rawTxn, rawMeta);
};
// ST NOTE We're using the token reference both for passing inputs and
// outputs, so we need to clear it in between.
token = Json::nullValue;
std::string sql = boost::str (boost::format
("SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,"
"Status,RawTxn,TxnMeta "
"FROM AccountTransactions INNER JOIN Transactions "
"ON Transactions.TransID = AccountTransactions.TransID "
"WHERE AccountTransactions.Account = '%s' "
"AND AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u' "
"ORDER BY AccountTransactions.LedgerSeq %s, "
"AccountTransactions.TxnSeq %s, AccountTransactions.TransID %s "
"LIMIT %u;")
% account.humanAccountID()
% ((forward && (findLedger != 0)) ? findLedger : minLedger)
% ((!forward && (findLedger != 0)) ? findLedger: maxLedger)
% (forward ? "ASC" : "DESC")
% (forward ? "ASC" : "DESC")
% (forward ? "ASC" : "DESC")
% queryLimit);
{
auto db = getApp().getTxnDB ().checkoutDb ();
boost::optional<std::uint64_t> ledgerSeq64;
boost::optional<std::int32_t> txnSeq;
boost::optional<std::string> status;
soci::blob sociTxnBlob (*db), sociTxnMetaBlob (*db);
soci::indicator rti, tmi;
Blob rawTxn, txnMeta;
soci::statement st =
(db->prepare << sql,
soci::into(ledgerSeq64),
soci::into(txnSeq),
soci::into(status),
soci::into(sociTxnBlob, rti),
soci::into(sociTxnMetaBlob, tmi));
st.execute ();
while (st.fetch ())
{
if (soci::i_ok == rti)
convert(sociTxnBlob, rawTxn);
else
rawTxn.clear ();
if (soci::i_ok == tmi)
convert (sociTxnMetaBlob, txnMeta);
else
txnMeta.clear ();
auto const ledgerSeq = rangeCheckedCast<std::uint32_t>(
ledgerSeq64.value_or (0));
if (!foundResume)
{
foundResume = (findLedger == ledgerSeq &&
findSeq == txnSeq.value_or (0));
}
else if (numberOfResults == 0)
{
token = Json::objectValue;
token[jss::ledger] = ledgerSeq;
token[jss::seq] = txnSeq.value_or (0);
break;
}
if (foundResume)
{
auto txn = Transaction::transactionFromSQL (
ledgerSeq64, status, rawTxn, Validate::NO);
if (txnMeta.empty ())
{
// Work around a bug that could leave the metadata missing
m_journal.warning << "Recovering ledger " << ledgerSeq
<< ", txn " << txn->getID();
Ledger::pointer ledger = getLedgerBySeq(ledgerSeq);
if (ledger)
ledger->pendSaveValidated(false, false);
}
--numberOfResults;
ret.emplace_back (std::move (txn),
std::make_shared<TransactionMetaSet> (
txn->getID (), txn->getLedger (), txnMeta));
}
}
}
accountTxPage(getApp().getTxnDB (), saveLedgerAsync, bound, account,
minLedger, maxLedger, forward, token, limit, bAdmin, page_length);
return ret;
}
NetworkOPsImp::MetaTxsList NetworkOPsImp::getTxsAccountB (
NetworkOPsImp::MetaTxsList
NetworkOPsImp::getTxsAccountB (
RippleAddress const& account, std::int32_t minLedger,
std::int32_t maxLedger, bool forward, Json::Value& token,
int limit, bool bAdmin)
{
static const std::uint32_t page_length (500);
MetaTxsList ret;
std::uint32_t BINARY_PAGE_LENGTH = 500;
std::uint32_t EXTRA_LENGTH = 100;
bool foundResume = token.isNull() || !token.isObject();
std::uint32_t numberOfResults, queryLimit;
if (limit <= 0)
numberOfResults = BINARY_PAGE_LENGTH;
else if (!bAdmin && (limit > BINARY_PAGE_LENGTH))
numberOfResults = BINARY_PAGE_LENGTH;
else
numberOfResults = limit;
queryLimit = numberOfResults + 1 + (foundResume ? 0 : EXTRA_LENGTH);
std::uint32_t findLedger = 0, findSeq = 0;
if (!foundResume)
auto bound = [&ret](
std::uint32_t ledgerIndex,
std::string const& status,
Blob const& rawTxn,
Blob const& rawMeta)
{
try
{
if (!token.isMember(jss::ledger) || !token.isMember(jss::seq))
return ret;
findLedger = token[jss::ledger].asInt();
findSeq = token[jss::seq].asInt();
}
catch (...)
{
return ret;
}
}
token = Json::nullValue;
std::string sql = boost::str (boost::format
("SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,"
"Status,RawTxn,TxnMeta "
"FROM AccountTransactions INNER JOIN Transactions "
"ON Transactions.TransID = AccountTransactions.TransID "
"WHERE AccountTransactions.Account = '%s' "
"AND AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u' "
"ORDER BY AccountTransactions.LedgerSeq %s, "
"AccountTransactions.TxnSeq %s, AccountTransactions.TransID %s "
"LIMIT %u;")
% account.humanAccountID()
% ((forward && (findLedger != 0)) ? findLedger : minLedger)
% ((!forward && (findLedger != 0)) ? findLedger: maxLedger)
% (forward ? "ASC" : "DESC")
% (forward ? "ASC" : "DESC")
% (forward ? "ASC" : "DESC")
% queryLimit);
{
auto db = getApp().getTxnDB ().checkoutDb ();
boost::optional<std::int64_t> ledgerSeq;
boost::optional<std::int32_t> txnSeq;
boost::optional<std::string> status;
soci::blob sociTxnBlob (*db);
soci::indicator rtI;
soci::blob sociTxnMetaBlob (*db);
soci::indicator tmI;
soci::statement st = (db->prepare << sql,
soci::into (ledgerSeq),
soci::into (txnSeq),
soci::into (status),
soci::into (sociTxnBlob, rtI),
soci::into (sociTxnMetaBlob, tmI));
st.execute ();
while (st.fetch ())
{
if (!foundResume)
{
if (findLedger == ledgerSeq.value_or (0) &&
findSeq == txnSeq.value_or (0))
{
foundResume = true;
}
}
else if (numberOfResults == 0)
{
token = Json::objectValue;
token[jss::ledger] = rangeCheckedCast<std::int32_t>(
ledgerSeq.value_or(0));
token[jss::seq] = txnSeq.value_or(0);
break;
}
if (foundResume)
{
Blob rawTxn;
if (soci::i_ok == rtI)
convert (sociTxnBlob, rawTxn);
Blob txnMeta;
if (soci::i_ok == tmI)
convert (sociTxnMetaBlob, txnMeta);
ret.emplace_back (
strHex (rawTxn.begin (), rawTxn.size ()),
strHex (txnMeta.begin (), txnMeta.size ()),
rangeCheckedCast<std::int32_t>(ledgerSeq.value_or (0)));
--numberOfResults;
}
}
}
ret.emplace_back (strHex(rawTxn), strHex (rawMeta), ledgerIndex);
};
accountTxPage(getApp().getTxnDB (), saveLedgerAsync, bound, account,
minLedger, maxLedger, forward, token, limit, bAdmin, page_length);
return ret;
}
std::vector<RippleAddress>
NetworkOPsImp::getLedgerAffectedAccounts (std::uint32_t ledgerSeq)
{
@@ -2256,7 +2056,7 @@ NetworkOPsImp::getLedgerAffectedAccounts (std::uint32_t ledgerSeq)
convert (accountBlob, accountStr);
else
accountStr.clear ();
if (acct.setAccountID (accountStr))
accounts.push_back (acct);
}

View File

@@ -321,6 +321,8 @@ public:
STTx::ref stTxn, TER terResult) = 0;
};
//------------------------------------------------------------------------------
std::unique_ptr<NetworkOPs>
make_NetworkOPs (NetworkOPs::clock_type& clock, bool standalone,
std::size_t network_quorum, JobQueue& job_queue, LedgerMaster& ledgerMaster,

View File

@@ -0,0 +1,261 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 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.
*/
//==============================================================================
#include <BeastConfig.h>
#include <ripple/app/ledger/LedgerToJson.h>
#include <ripple/app/main/Application.h>
#include <ripple/app/misc/impl/AccountTxPaging.h>
#include <ripple/app/tx/Transaction.h>
#include <ripple/protocol/Serializer.h>
#include <beast/cxx14/memory.h> // <memory>
#include <boost/format.hpp>
namespace ripple {
void
convertBlobsToTxResult (
NetworkOPs::AccountTxs& to,
std::uint32_t ledger_index,
std::string const& status,
Blob const& rawTxn,
Blob const& rawMeta)
{
SerialIter it (rawTxn);
STTx::pointer txn = std::make_shared<STTx> (it);
std::string reason;
auto tr = std::make_shared<Transaction> (txn, Validate::NO, reason);
tr->setStatus (Transaction::sqlTransactionStatus(status));
tr->setLedger (ledger_index);
to.emplace_back(std::make_pair(std::move(tr),
std::make_shared<TransactionMetaSet> (
tr->getID (), tr->getLedger (), rawMeta)));
};
void
saveLedgerAsync (std::uint32_t seq)
{
Ledger::pointer ledger = getApp().getOPs().getLedgerBySeq(seq);
if (ledger)
ledger->pendSaveValidated(false, false);
}
void
accountTxPage (
DatabaseCon& connection,
std::function<void (std::uint32_t)> const& onUnsavedLedger,
std::function<void (std::uint32_t,
std::string const&,
Blob const&,
Blob const&)> const& onTransaction,
RippleAddress const& account,
std::int32_t minLedger,
std::int32_t maxLedger,
bool forward,
Json::Value& token,
int limit,
bool bAdmin,
std::uint32_t page_length)
{
bool lookingForMarker = !token.isNull() && token.isObject();
std::uint32_t numberOfResults;
if (limit <= 0 || (limit > page_length && !bAdmin))
numberOfResults = page_length;
else
numberOfResults = limit;
// As an account can have many thousands of transactions, there is a limit
// placed on the amount of transactions returned. If the limit is reached
// before the result set has been exhausted (we always query for one more
// than the limit), then we return an opaque marker that can be supplied in
// a subsequent query.
std::uint32_t queryLimit = numberOfResults + 1;
std::uint32_t findLedger = 0, findSeq = 0;
if (lookingForMarker)
{
try
{
if (!token.isMember(jss::ledger) || !token.isMember(jss::seq))
return;
findLedger = token[jss::ledger].asInt();
findSeq = token[jss::seq].asInt();
}
catch (...)
{
return;
}
}
// We're using the token reference both for passing inputs and outputs, so
// we need to clear it in between.
token = Json::nullValue;
static std::string const prefix (
R"(SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,
Status,RawTxn,TxnMeta
FROM AccountTransactions INNER JOIN Transactions
ON Transactions.TransID = AccountTransactions.TransID
AND AccountTransactions.Account = '%s' WHERE
)");
std::string sql;
// SQL's BETWEEN uses a closed interval ([a,b])
if (forward && (findLedger == 0))
{
sql = boost::str (boost::format(
prefix +
(R"(AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u'
ORDER BY AccountTransactions.LedgerSeq ASC,
AccountTransactions.TxnSeq ASC
LIMIT %u;)"))
% account.humanAccountID()
% minLedger
% maxLedger
% queryLimit);
}
else if (forward && (findLedger != 0))
{
sql = boost::str (boost::format(
prefix +
(R"(
AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u' OR
( AccountTransactions.LedgerSeq = '%u' AND
AccountTransactions.TxnSeq >= '%u' )
ORDER BY AccountTransactions.LedgerSeq ASC,
AccountTransactions.TxnSeq ASC
LIMIT %u;
)"))
% account.humanAccountID()
% (findLedger + 1)
% maxLedger
% findLedger
% findSeq
% queryLimit);
}
else if (!forward && (findLedger == 0))
{
sql = boost::str (boost::format(
prefix +
(R"(AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u'
ORDER BY AccountTransactions.LedgerSeq DESC,
AccountTransactions.TxnSeq DESC
LIMIT %u;)"))
% account.humanAccountID()
% minLedger
% maxLedger
% queryLimit);
}
else if (!forward && (findLedger != 0))
{
sql = boost::str (boost::format(
prefix +
(R"(AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u' OR
(AccountTransactions.LedgerSeq = '%u' AND
AccountTransactions.TxnSeq <= '%u')
ORDER BY AccountTransactions.LedgerSeq DESC,
AccountTransactions.TxnSeq DESC
LIMIT %u;)"))
% account.humanAccountID()
% minLedger
% (findLedger - 1)
% findLedger
% findSeq
% queryLimit);
}
else
{
assert (false);
// sql is empty
return;
}
{
auto db (connection.checkoutDb());
Blob rawData;
Blob rawMeta;
boost::optional<std::uint64_t> ledgerSeq;
boost::optional<std::uint32_t> txnSeq;
boost::optional<std::string> status;
soci::blob txnData (*db);
soci::blob txnMeta (*db);
soci::indicator dataPresent, metaPresent;
soci::statement st = (db->prepare << sql,
soci::into (ledgerSeq),
soci::into (txnSeq),
soci::into (status),
soci::into (txnData, dataPresent),
soci::into (txnMeta, metaPresent));
st.execute ();
while (st.fetch ())
{
if (lookingForMarker)
{
if (findLedger == ledgerSeq.value_or (0) &&
findSeq == txnSeq.value_or (0))
{
lookingForMarker = false;
}
}
else if (numberOfResults == 0)
{
token = Json::objectValue;
token[jss::ledger] = rangeCheckedCast<std::uint32_t>(ledgerSeq.value_or (0));
token[jss::seq] = txnSeq.value_or (0);
break;
}
if (!lookingForMarker)
{
if (dataPresent == soci::i_ok)
convert (txnData, rawData);
else
rawData.clear ();
if (metaPresent == soci::i_ok)
convert (txnMeta, rawMeta);
else
rawMeta.clear ();
// Work around a bug that could leave the metadata missing
if (rawMeta.size() == 0)
onUnsavedLedger(ledgerSeq.value_or (0));
onTransaction(rangeCheckedCast<std::uint32_t>(ledgerSeq.value_or (0)),
*status, rawData, rawMeta);
--numberOfResults;
}
}
}
return;
}
}

View File

@@ -0,0 +1,64 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 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_MISC_IMPL_ACCOUNTTXPAGING_H_INCLUDED
#define RIPPLE_APP_MISC_IMPL_ACCOUNTTXPAGING_H_INCLUDED
#include <ripple/app/data/DatabaseCon.h>
#include <ripple/app/misc/NetworkOPs.h>
#include <cstdint>
#include <string>
#include <utility>
//------------------------------------------------------------------------------
namespace ripple {
void
convertBlobsToTxResult (
NetworkOPs::AccountTxs& to,
std::uint32_t ledger_index,
std::string const& status,
Blob const& rawTxn,
Blob const& rawMeta);
void
saveLedgerAsync (std::uint32_t seq);
void
accountTxPage (
DatabaseCon& database,
std::function<void (std::uint32_t)> const& onUnsavedLedger,
std::function<void (std::uint32_t,
std::string const&,
Blob const&,
Blob const&)> const&,
RippleAddress const& account,
std::int32_t minLedger,
std::int32_t maxLedger,
bool forward,
Json::Value& token,
int limit,
bool bAdmin,
std::uint32_t pageLength);
}
#endif

View File

@@ -0,0 +1,246 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2012, 2013 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.
*/
//==============================================================================
#include <ripple/app/data/DatabaseCon.h>
#include <ripple/app/misc/impl/AccountTxPaging.h>
#include <beast/cxx14/memory.h> // <memory>
#include <beast/unit_test/suite.h>
#include <cstdlib>
#include <vector>
namespace ripple {
struct AccountTxPaging_test : beast::unit_test::suite
{
std::unique_ptr<DatabaseCon> db_;
NetworkOPs::AccountTxs txs_;
RippleAddress account_;
void
run() override
{
std::string fixturesPath = std::getenv("TEST_FIXTURES");
if (fixturesPath.empty ())
{
fail("TEST_FIXTURES environment var not declared");
return;
}
DatabaseCon::Setup dbConf;
dbConf.dataDir = fixturesPath + "/";
db_ = std::make_unique <DatabaseCon> (
dbConf, "account-tx-transactions.db", nullptr, 0);
account_.setAccountID("rfu6L5p3azwPzQZsbTafuVk884N9YoKvVG");
testAccountTxPaging();
}
void
checkToken (Json::Value const& token, int ledger, int sequence)
{
expect (token.isMember ("ledger"));
expect (token["ledger"].asInt() == ledger);
expect (token.isMember ("seq"));
expect (token["seq"].asInt () == sequence);
}
void
checkTransaction (NetworkOPs::AccountTx const& tx, int ledger, int index)
{
expect (tx.second->getLgrSeq () == ledger);
expect (tx.second->getIndex () == index);
}
std::size_t
next (
int limit,
bool forward,
Json::Value& token,
std::int32_t minLedger,
std::int32_t maxLedger)
{
txs_.clear();
std::int32_t const page_length = 200;
bool const admin = true;
auto& txs = txs_;
auto bound = [&txs](
std::uint32_t ledger_index,
std::string const& status,
Blob const& rawTxn,
Blob const& rawMeta)
{
convertBlobsToTxResult (txs, ledger_index, status, rawTxn, rawMeta);
};
accountTxPage(*db_, [](std::uint32_t){}, bound, account_, minLedger,
maxLedger, forward, token, limit, admin, page_length);
return txs_.size();
}
void
testAccountTxPaging ()
{
using namespace std::placeholders;
bool const forward = true;
std::int32_t min_ledger;
std::int32_t max_ledger;
Json::Value token;
int limit;
// the supplied account-tx-transactions.db contains contains
// transactions with the following ledger/sequence pairs.
// 3|5
// 4|4
// 4|10
// 5|4
// 5|7
// 6|1
// 6|5
// 6|6
// 6|7
// 6|8
// 6|9
// 6|10
// 6|11
min_ledger = 2;
max_ledger = 5;
{
limit = 2;
expect (next(limit, forward, token, min_ledger, max_ledger) == 2);
checkTransaction (txs_[0], 3, 5);
checkTransaction (txs_[1], 4, 4);
checkToken (token, 4, 10);
expect (next(limit, forward, token, min_ledger, max_ledger) == 2);
checkTransaction (txs_[0], 4, 10);
checkTransaction (txs_[1], 5, 4);
checkToken (token, 5, 7);
expect (next(limit, forward, token, min_ledger, max_ledger) == 1);
checkTransaction (txs_[0], 5, 7);
expect(token["ledger"].isNull());
expect(token["seq"].isNull());
}
token = Json::nullValue;
min_ledger = 3;
max_ledger = 9;
{
limit = 1;
expect (next(limit, forward, token, min_ledger, max_ledger) == 1);
checkTransaction (txs_[0], 3, 5);
checkToken (token, 4, 4);
expect(next(limit, forward, token, min_ledger, max_ledger) == 1);
checkTransaction (txs_[0], 4, 4);
checkToken (token, 4, 10);
expect (next(limit, forward, token, min_ledger, max_ledger) == 1);
checkTransaction (txs_[0], 4, 10);
checkToken (token, 5, 4);
}
{
limit = 3;
expect (next(limit, forward, token, min_ledger, max_ledger) == 3);
checkTransaction (txs_[0], 5, 4);
checkTransaction (txs_[1], 5, 7);
checkTransaction (txs_[2], 6, 1);
checkToken (token, 6, 5);
expect (next(limit, forward, token, min_ledger, max_ledger) == 3);
checkTransaction (txs_[0], 6, 5);
checkTransaction (txs_[1], 6, 6);
checkTransaction (txs_[2], 6, 7);
checkToken (token, 6, 8);
expect (next(limit, forward, token, min_ledger, max_ledger) == 3);
checkTransaction (txs_[0], 6, 8);
checkTransaction (txs_[1], 6, 9);
checkTransaction (txs_[2], 6, 10);
checkToken (token, 6, 11);
expect(next(limit, forward, token, min_ledger, max_ledger) == 1);
checkTransaction (txs_[0], 6, 11);
expect(token["ledger"].isNull());
expect(token["seq"].isNull());
}
token = Json::nullValue;
{
limit = 2;
expect (next(limit, ! forward, token, min_ledger, max_ledger) == 2);
checkTransaction (txs_[0], 6, 11);
checkTransaction (txs_[1], 6, 10);
checkToken (token, 6, 9);
expect(next(limit, ! forward, token, min_ledger, max_ledger) == 2);
checkTransaction (txs_[0], 6, 9);
checkTransaction (txs_[1], 6, 8);
checkToken (token, 6, 7);
}
{
limit = 3;
expect (next(limit, ! forward, token, min_ledger, max_ledger) == 3);
checkTransaction (txs_[0], 6, 7);
checkTransaction (txs_[1], 6, 6);
checkTransaction (txs_[2], 6, 5);
checkToken (token, 6, 1);
expect (next(limit, ! forward, token, min_ledger, max_ledger) == 3);
checkTransaction (txs_[0], 6, 1);
checkTransaction (txs_[1], 5, 7);
checkTransaction (txs_[2], 5, 4);
checkToken (token, 4, 10);
expect (next(limit, ! forward, token, min_ledger, max_ledger) == 3);
checkTransaction (txs_[0], 4, 10);
checkTransaction (txs_[1], 4, 4);
checkTransaction (txs_[2], 3, 5);
}
expect (token["ledger"].isNull());
expect (token["seq"].isNull());
}
};
BEAST_DEFINE_TESTSUITE_MANUAL(AccountTxPaging,app,ripple);
}

View File

@@ -107,6 +107,24 @@ void Transaction::setStatus (TransStatus ts, std::uint32_t lseq)
mInLedger = lseq;
}
TransStatus Transaction::sqlTransactionStatus(
boost::optional<std::string> const& status)
{
char const c = (status) ? (*status)[0] : TXN_SQL_UNKNOWN;
switch (c)
{
case TXN_SQL_NEW: return NEW;
case TXN_SQL_CONFLICT: return CONFLICTED;
case TXN_SQL_HELD: return HELD;
case TXN_SQL_VALIDATED: return COMMITTED;
case TXN_SQL_INCLUDED: return INCLUDED;
}
assert (c == TXN_SQL_UNKNOWN);
return INVALID;
}
Transaction::pointer Transaction::transactionFromSQL (
boost::optional<std::uint64_t> const& ledgerSeq,
boost::optional<std::string> const& status,
@@ -121,40 +139,7 @@ Transaction::pointer Transaction::transactionFromSQL (
std::string reason;
auto tr = std::make_shared<Transaction> (txn, validate, reason);
TransStatus st (INVALID);
char const statusChar = status ? (*status)[0] : TXN_SQL_UNKNOWN;
switch (statusChar)
{
case TXN_SQL_NEW:
st = NEW;
break;
case TXN_SQL_CONFLICT:
st = CONFLICTED;
break;
case TXN_SQL_HELD:
st = HELD;
break;
case TXN_SQL_VALIDATED:
st = COMMITTED;
break;
case TXN_SQL_INCLUDED:
st = INCLUDED;
break;
case TXN_SQL_UNKNOWN:
break;
default:
assert (false);
}
tr->setStatus (st);
tr->setStatus (sqlTransactionStatus (status));
tr->setLedger (inLedger);
return tr;
}

View File

@@ -64,13 +64,22 @@ public:
public:
Transaction (STTx::ref, Validate, std::string&) noexcept;
static Transaction::pointer sharedTransaction (Blob const&, Validate);
static Transaction::pointer transactionFromSQL (
static
Transaction::pointer
sharedTransaction (Blob const&, Validate);
static
Transaction::pointer
transactionFromSQL (
boost::optional<std::uint64_t> const& ledgerSeq,
boost::optional<std::string> const& status,
Blob const& rawTxn,
Validate validate);
static
TransStatus
sqlTransactionStatus(boost::optional<std::string> const& status);
bool checkSign (std::string&) const;
STTx::ref getSTransaction ()

View File

@@ -25,3 +25,5 @@
#include <ripple/app/tx/LocalTxs.cpp>
#include <ripple/app/tx/InboundTransactions.cpp>
#include <ripple/app/misc/NetworkOPs.cpp>
#include <ripple/app/misc/impl/AccountTxPaging.cpp>
#include <ripple/app/misc/tests/AccountTxPaging.test.cpp>

BIN
test/fixtures/account-tx-transactions.db vendored Normal file

Binary file not shown.