Skip to content

Commit 5fd6b4b

Browse files
committed
Fix loadgen seqno issus
1 parent 3f7b6d0 commit 5fd6b4b

11 files changed

Lines changed: 745 additions & 41 deletions

src/herder/HerderImpl.cpp

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "herder/RustQuorumCheckerAdaptor.h"
1818
#include "herder/TxSetFrame.h"
1919
#include "herder/TxSetUtils.h"
20+
#include "ledger/ImmutableLedgerView.h"
2021
#include "ledger/LedgerManager.h"
2122
#include "ledger/LedgerTxnImpl.h"
2223
#include "ledger/P23HotArchiveBug.h"
@@ -352,7 +353,8 @@ HerderImpl::processExternalized(uint64 slotIndex, StellarValue const& value,
352353
}
353354
}
354355
#ifdef BUILD_TESTS
355-
mApp.getLoadGenerator().cleanupAccounts(txFramesList);
356+
mApp.getLoadGenerator().cleanupAccounts(
357+
static_cast<uint32_t>(slotIndex), txFramesList);
356358
#endif
357359
}
358360
mApp.getOverlayManager().notifyTxSetExternalized(value.txSetHash, txHashes);
@@ -1671,6 +1673,32 @@ HerderImpl::triggerNextLedger(uint32_t ledgerSeqToTrigger,
16711673
classicTxs.push_back(txFrame);
16721674
}
16731675
}
1676+
// The mempool is fee-ordered and sequence-number-oblivious, so it can
1677+
// hand us several transactions from one source account (e.g. a chained
1678+
// pair). A tx set may only contain one tx per source account, so keep
1679+
// the lowest sequence number per account and let the others wait for a
1680+
// later ledger.
1681+
auto onePerSourceAccount = [](TxFrameList& txs) {
1682+
std::unordered_map<AccountID, size_t> firstBySource;
1683+
TxFrameList kept;
1684+
for (auto const& tx : txs)
1685+
{
1686+
auto [it, inserted] =
1687+
firstBySource.emplace(tx->getSourceID(), kept.size());
1688+
if (inserted)
1689+
{
1690+
kept.push_back(tx);
1691+
}
1692+
else if (tx->getSeqNum() < kept[it->second]->getSeqNum())
1693+
{
1694+
kept[it->second] = tx;
1695+
}
1696+
}
1697+
txs = std::move(kept);
1698+
};
1699+
onePerSourceAccount(classicTxs);
1700+
onePerSourceAccount(sorobanTxs);
1701+
16741702
txPhases.emplace_back(std::move(classicTxs));
16751703
if (supportsSoroban)
16761704
{
@@ -1686,6 +1714,44 @@ HerderImpl::triggerNextLedger(uint32_t ledgerSeqToTrigger,
16861714
CLOG_INFO(Herder, "Proposed TX set has {} transactions",
16871715
proposedSet->sizeTxTotal());
16881716

1717+
// The mempool does no stateful validation, so it would keep handing us the
1718+
// transactions that just failed validation (stale sequence number, can't
1719+
// pay fee, expired, ...) on every nomination, crowding out valid ones.
1720+
// Drop them, except for transactions with a *future* sequence number:
1721+
// those are chained behind a pending transaction from the same account
1722+
// and become valid once it applies.
1723+
std::vector<Hash> invalidTxHashes;
1724+
if (!invalidTxPhases.empty())
1725+
{
1726+
CheckValidLedgerViewWrapper ledgerView(mApp);
1727+
for (auto const& phase : invalidTxPhases)
1728+
{
1729+
for (auto const& tx : phase)
1730+
{
1731+
auto acc = ledgerView.getAccount(tx->getSourceID());
1732+
if (acc &&
1733+
tx->getSeqNum() > acc.current().data.account().seqNum + 1)
1734+
{
1735+
continue;
1736+
}
1737+
CLOG_DEBUG(Herder,
1738+
"Dropping invalid tx {} from mempool: seq {} "
1739+
"(account seq {})",
1740+
hexAbbrev(tx->getFullHash()), tx->getSeqNum(),
1741+
acc ? acc.current().data.account().seqNum : -1);
1742+
invalidTxHashes.push_back(tx->getFullHash());
1743+
}
1744+
}
1745+
}
1746+
if (!invalidTxHashes.empty())
1747+
{
1748+
CLOG_DEBUG(Herder,
1749+
"Removing {} transactions that failed tx set validation "
1750+
"from the mempool",
1751+
invalidTxHashes.size());
1752+
overlayMgr.removeTransactions(invalidTxHashes);
1753+
}
1754+
16891755
if (!applicableProposedSet)
16901756
{
16911757
releaseAssert(!mApp.getConfig().FORCE_SCP);

src/herder/test/HerderTests.cpp

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3322,6 +3322,18 @@ TEST_CASE("SCP checkpoint", "[catchup][herder]")
33223322
simulation->getExpectedLedgerCloseTime(),
33233323
false);
33243324

3325+
// An out of sync node should buffer every ledger from the checkpoint up to
3326+
// the main node's latest. The main node applies ledgers asynchronously
3327+
// after externalizing them and the out of sync node hears about them
3328+
// asynchronously, so this only holds at moments where the two line up.
3329+
auto hasBufferedCheckpointToLcl = [&](LedgerApplyManagerImpl const& lam) {
3330+
auto const& buffered = lam.getBufferedLedgers();
3331+
return !buffered.empty() &&
3332+
buffered.begin()->first == firstCheckpoint &&
3333+
buffered.crbegin()->first ==
3334+
mainNode->getLedgerManager().getLastClosedLedgerNum();
3335+
};
3336+
33253337
SECTION("GC old checkpoints")
33263338
{
33273339
HerderImpl& herder = static_cast<HerderImpl&>(mainNode->getHerder());
@@ -3362,14 +3374,13 @@ TEST_CASE("SCP checkpoint", "[catchup][herder]")
33623374

33633375
// Crank until outOfSync node has received checkpoint ledger and started
33643376
// catchup
3365-
simulation->crankUntil([&]() { return lam.isCatchupInitialized(); },
3366-
2 * Herder::SEND_LATEST_CHECKPOINT_DELAY, false);
3367-
3368-
auto const& bufferedLedgers = lam.getBufferedLedgers();
3369-
REQUIRE(!bufferedLedgers.empty());
3370-
REQUIRE(bufferedLedgers.begin()->first == firstCheckpoint);
3371-
REQUIRE(bufferedLedgers.crbegin()->first ==
3372-
mainNode->getLedgerManager().getLastClosedLedgerNum());
3377+
simulation->crankUntil(
3378+
[&]() {
3379+
return lam.isCatchupInitialized() &&
3380+
hasBufferedCheckpointToLcl(lam);
3381+
},
3382+
2 * Herder::SEND_LATEST_CHECKPOINT_DELAY, false);
3383+
REQUIRE(hasBufferedCheckpointToLcl(lam));
33733384
}
33743385

33753386
SECTION("Two out of sync nodes receive checkpoint")
@@ -3391,20 +3402,14 @@ TEST_CASE("SCP checkpoint", "[catchup][herder]")
33913402
// catchup
33923403
simulation->crankUntil(
33933404
[&]() {
3394-
return cm1.isCatchupInitialized() && cm2.isCatchupInitialized();
3405+
return cm1.isCatchupInitialized() &&
3406+
cm2.isCatchupInitialized() &&
3407+
hasBufferedCheckpointToLcl(cm1) &&
3408+
hasBufferedCheckpointToLcl(cm2);
33953409
},
33963410
2 * Herder::SEND_LATEST_CHECKPOINT_DELAY, false);
3397-
3398-
auto const& bufferedLedgers1 = cm1.getBufferedLedgers();
3399-
REQUIRE(!bufferedLedgers1.empty());
3400-
REQUIRE(bufferedLedgers1.begin()->first == firstCheckpoint);
3401-
REQUIRE(bufferedLedgers1.crbegin()->first ==
3402-
mainNode->getLedgerManager().getLastClosedLedgerNum());
3403-
auto const& bufferedLedgers2 = cm2.getBufferedLedgers();
3404-
REQUIRE(!bufferedLedgers2.empty());
3405-
REQUIRE(bufferedLedgers2.begin()->first == firstCheckpoint);
3406-
REQUIRE(bufferedLedgers2.crbegin()->first ==
3407-
mainNode->getLedgerManager().getLastClosedLedgerNum());
3411+
REQUIRE(hasBufferedCheckpointToLcl(cm1));
3412+
REQUIRE(hasBufferedCheckpointToLcl(cm2));
34083413
}
34093414
}
34103415

@@ -4023,7 +4028,16 @@ TEST_CASE("SCP message capture from previous ledger", "[herder]")
40234028
{
40244029
// Initialize simulation
40254030
auto networkID = sha256(getTestConfig().NETWORK_PASSPHRASE);
4026-
auto simulation = std::make_shared<Simulation>(networkID);
4031+
// A and B keep closing ledgers in real time while C is fed slot 2 by
4032+
// hand. Use a longer close time so that C's slot-2 EXTERNALIZE reaches
4033+
// A and B before they close ledger 3.
4034+
auto confGen = [](int i) {
4035+
auto cfg = getTestConfig(i);
4036+
cfg.ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING = true;
4037+
cfg.ARTIFICIALLY_SET_CLOSE_TIME_FOR_TESTING = 5;
4038+
return cfg;
4039+
};
4040+
auto simulation = std::make_shared<Simulation>(networkID, confGen);
40274041

40284042
// Create three validators: A, B, and C
40294043
auto validatorAKey = SecretKey::fromSeed(sha256("validator-A"));

src/ledger/test/LedgerCloseMetaStreamTests.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,8 @@ TEST_CASE("LedgerCloseMetaStream file descriptor - LIVE_NODE",
8282
Config cfg3 = getTestConfig(3);
8383
Config cfg4 = getTestConfig(4);
8484

85-
// Star topology around node1: addPendingConnection is a no-op with
86-
// the Rust overlay, so wire the peers via KNOWN_PEERS instead
87-
// (libp2p connections are bidirectional).
85+
// Star topology around node1, wired via KNOWN_PEERS (libp2p
86+
// connections are bidirectional).
8887
cfg1.KNOWN_PEERS = {
8988
fmt::format(FMT_STRING("127.0.0.1:{}"), cfg2.PEER_PORT),
9089
fmt::format(FMT_STRING("127.0.0.1:{}"), cfg3.PEER_PORT),

src/main/test/ApplicationUtilsTests.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,7 @@ TEST_CASE("verify checkpoints command - wait condition", "[applicationutils]")
7272
cfg2.FORCE_SCP = false;
7373
cfg2.NODE_IS_VALIDATOR = false;
7474
cfg2.MODE_DOES_CATCHUP = false;
75-
// addPendingConnection is a no-op with the Rust overlay: point the
76-
// watcher at the validator via KNOWN_PEERS instead.
75+
// Point the watcher at the validator via KNOWN_PEERS.
7776
cfg2.KNOWN_PEERS = {
7877
fmt::format(FMT_STRING("127.0.0.1:{}"), cfg1.PEER_PORT)};
7978

src/overlay/OverlayIPC.cpp

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
#include <filesystem>
1515
#include <signal.h>
1616
#include <sstream>
17+
#include <sys/syscall.h>
1718
#include <sys/wait.h>
19+
#include <thread>
1820
#include <unistd.h>
1921
#include <vector>
2022

@@ -221,9 +223,26 @@ OverlayIPC::shutdown()
221223
pid_t result = waitpid(mOverlayPid, &status, WNOHANG);
222224
if (result == 0)
223225
{
224-
// Still running, send SIGTERM
226+
// Still running, send SIGTERM and give it a bounded amount of time
227+
// to exit before falling back to SIGKILL, so that a wedged overlay
228+
// process cannot hang core's shutdown.
225229
kill(mOverlayPid, SIGTERM);
226-
waitpid(mOverlayPid, &status, 0);
230+
auto const deadline =
231+
std::chrono::steady_clock::now() + std::chrono::seconds(5);
232+
while ((result = waitpid(mOverlayPid, &status, WNOHANG)) == 0 &&
233+
std::chrono::steady_clock::now() < deadline)
234+
{
235+
std::this_thread::sleep_for(std::chrono::milliseconds(20));
236+
}
237+
if (result == 0)
238+
{
239+
CLOG_WARNING(Overlay,
240+
"Overlay process {} did not exit after SIGTERM, "
241+
"sending SIGKILL",
242+
mOverlayPid);
243+
kill(mOverlayPid, SIGKILL);
244+
waitpid(mOverlayPid, &status, 0);
245+
}
227246
}
228247
mOverlayPid = -1;
229248
}
@@ -239,6 +258,7 @@ OverlayIPC::spawnOverlay()
239258
return false;
240259
}
241260

261+
long const maxFd = sysconf(_SC_OPEN_MAX);
242262
pid_t pid = fork();
243263
if (pid < 0)
244264
{
@@ -248,7 +268,25 @@ OverlayIPC::spawnOverlay()
248268

249269
if (pid == 0)
250270
{
251-
// Child process - exec overlay binary
271+
// Child process. Close every descriptor inherited from core except
272+
// stdin/stdout/stderr: otherwise the overlay keeps core's listening
273+
// sockets bound after core exits and holds the read ends of sibling
274+
// overlays' IPC sockets, which can leave a shutting-down sibling
275+
// blocked forever on a write nobody will read. Only async-signal-safe
276+
// calls are allowed between fork() and exec().
277+
bool closed = false;
278+
#if defined(__linux__) && defined(SYS_close_range)
279+
closed = syscall(SYS_close_range, 3, ~0U, 0) == 0;
280+
#endif
281+
if (!closed)
282+
{
283+
for (long fd = 3; fd < maxFd; ++fd)
284+
{
285+
close(static_cast<int>(fd));
286+
}
287+
}
288+
289+
// Exec overlay binary
252290
// Arguments: <binary> --listen <socket-path> --peer-port <port>
253291
std::string portStr = std::to_string(mPeerPort);
254292
execl(overlayBinaryPath->c_str(), overlayBinaryPath->c_str(),
@@ -500,6 +538,16 @@ OverlayIPC::notifyTxSetExternalized(Hash const& txSetHash,
500538
mChannel->send(msg);
501539
}
502540

541+
void
542+
OverlayIPC::removeTransactions(std::vector<Hash> const& txHashes)
543+
{
544+
if (txHashes.empty())
545+
{
546+
return;
547+
}
548+
notifyTxSetExternalized(Hash{}, txHashes);
549+
}
550+
503551
std::vector<TransactionEnvelope>
504552
OverlayIPC::getTopTransactions(size_t count)
505553
{

src/overlay/OverlayIPC.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ class OverlayIPC
103103
void notifyTxSetExternalized(Hash const& txSetHash,
104104
std::vector<Hash> const& txHashes);
105105

106+
/**
107+
* Remove transactions from the Rust mempool by hash, e.g. transactions
108+
* that failed validation while building a tx set and would otherwise be
109+
* handed back to Core on every nomination. Uses the TX_SET_EXTERNALIZED
110+
* message with a zero tx set hash: the overlay only acts on the hashes.
111+
*/
112+
void removeTransactions(std::vector<Hash> const& txHashes);
113+
106114
/**
107115
* Request top N transactions by fee for nomination.
108116
*

src/overlay/RustOverlayManager.cpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,18 @@ RustOverlayManager::effectiveKnownPeers() const
106106
void
107107
RustOverlayManager::addKnownPeerForTesting(std::string const& addr)
108108
{
109-
releaseAssert(!mOverlayIPC || !mOverlayIPC->isConnected());
110109
releaseAssert(std::find(mExtraKnownPeers.begin(), mExtraKnownPeers.end(),
111110
addr) == mExtraKnownPeers.end());
112111
mExtraKnownPeers.push_back(addr);
112+
113+
// If the overlay is already running, push the updated peer list so the
114+
// Rust side dials the new peer.
115+
if (mOverlayIPC && mOverlayIPC->isConnected())
116+
{
117+
auto const& cfg = mApp.getConfig();
118+
mOverlayIPC->setPeerConfig(effectiveKnownPeers(), cfg.PREFERRED_PEERS,
119+
cfg.PEER_PORT);
120+
}
113121
}
114122
#endif
115123

@@ -193,6 +201,15 @@ RustOverlayManager::notifyTxSetExternalized(Hash const& txSetHash,
193201
}
194202
}
195203

204+
void
205+
RustOverlayManager::removeTransactions(std::vector<Hash> const& txHashes)
206+
{
207+
if (mOverlayIPC && !mShuttingDown)
208+
{
209+
mOverlayIPC->removeTransactions(txHashes);
210+
}
211+
}
212+
196213
void
197214
RustOverlayManager::requestTxSet(Hash const& txSetHash, uint32_t slotIndex)
198215
{

src/overlay/RustOverlayManager.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ class RustOverlayManager
4242
#ifdef BUILD_TESTS
4343
// Advertise an additional peer address ("host:port") to the Rust overlay
4444
// on top of the config's KNOWN_PEERS. Used by Simulation to wire test
45-
// topologies; must be called before start().
45+
// topologies. May be called before start() (the peer is included in the
46+
// initial peer config) or after it (the updated peer list is pushed to the
47+
// Rust overlay immediately).
4648
void addKnownPeerForTesting(std::string const& addr);
4749
#endif
4850

@@ -59,6 +61,10 @@ class RustOverlayManager
5961
void notifyTxSetExternalized(Hash const& txSetHash,
6062
std::vector<Hash> const& txHashes);
6163

64+
// Drop transactions from the Rust mempool (e.g. ones that failed
65+
// validation while building a tx set).
66+
void removeTransactions(std::vector<Hash> const& txHashes);
67+
6268
// Request TX set from peers (via Rust overlay, async). slotIndex is the
6369
// slot the set is for, used to stamp the Rust-side cache entry.
6470
void requestTxSet(Hash const& txSetHash, uint32_t slotIndex);

0 commit comments

Comments
 (0)