Compare commits

...

14 Commits

Author SHA1 Message Date
Nicholas Dudfield
d8096685ee test: drop ConcurrentLedgerSave_test from the dev backport
It depends on a jtx helper (saveValidatedLedger) that exists only on the
feature-export-rng lineage. The production fix it covers stays; the test
can return with the helper if that is ported.
2026-09-23 10:02:51 +07:00
Nicholas Dudfield
52f112d52b fix(overlay): bound protobuf parsing to the current frame 2026-09-23 09:46:23 +07:00
Nicholas Dudfield
cf2ed8c299 fix(ledger): count publication as progress while running 2026-09-23 09:46:22 +07:00
Nicholas Dudfield
15eed3a497 fix(app): persist genesis successor state tree 2026-09-23 09:46:22 +07:00
Nicholas Dudfield
3d7c3236fc fix(app): keep first-ledger publication state per application 2026-09-23 09:46:21 +07:00
Nicholas Dudfield
1da084b3b8 fix(txq): keep parent-hash ordering keys per candidate 2026-09-23 09:46:21 +07:00
Nicholas Dudfield
541edc48c9 fix(overlay): isolate relay duplicate tracking per Slots instance 2026-09-23 09:46:20 +07:00
Nicholas Dudfield
cd9fb29eec fix(consensus): isolate validation keep-range refresh deadlines 2026-09-23 09:46:20 +07:00
Nicholas Dudfield
fd85c5d122 fix(consensus): serialize consensus phase publication 2026-09-23 09:46:19 +07:00
Nicholas Dudfield
723f4f28b6 fix(tx): synchronize mutable transaction response state 2026-09-23 09:46:19 +07:00
Nicholas Dudfield
9e7805e5aa fix(ledger): synchronize acquire state access and received-data admission 2026-09-23 09:46:18 +07:00
Nicholas Dudfield
cff7ea906c fix(ledger): lock acquire completion state before reporting it 2026-09-23 09:46:18 +07:00
Nicholas Dudfield
15739bf13d fix(ledger): synchronize ledger-age log throttles 2026-09-23 09:46:18 +07:00
Nicholas Dudfield
e08de61273 fix(rdb): keep ledger-save SQL formatters local to each call 2026-09-23 09:46:17 +07:00
14 changed files with 243 additions and 68 deletions

View File

@@ -0,0 +1,113 @@
#include <test/jtx.h>
#include <xrpld/app/misc/Transaction.h>
#include <xrpld/rpc/CTID.h>
#include <future>
#include <thread>
namespace ripple {
namespace test {
class TransactionState_test : public beast::unit_test::suite
{
void
run() override
{
testcase(
"concurrent response readers observe coherent locator snapshots");
using namespace jtx;
Env env(*this, envconfig([](std::unique_ptr<Config> config) {
config->NETWORK_ID = 11;
return config;
}));
Account const alice("alice");
env.fund(XRP(1000), alice);
env.close();
env(noop(alice));
if (!BEAST_EXPECT(!env.tx()->isFieldPresent(sfNetworkID)))
return;
std::string reason;
Transaction transaction(env.tx(), reason, env.app());
BEAST_EXPECT(reason.empty());
auto const& reader = transaction;
auto const ctidA = RPC::encodeCTID(101, 7, 11);
auto const ctidB = RPC::encodeCTID(202, 9, 22);
if (!BEAST_EXPECT(ctidA && ctidB))
return;
auto setState = [&](bool second) {
std::uint32_t const ledger = second ? 202 : 101;
transaction.setStatus(
COMMITTED, ledger, second ? 9 : 7, second ? 22 : 11);
transaction.setCurrentLedgerState(
ledger, XRPAmount{ledger}, ledger + 1, ledger + 2);
transaction.setResult(second ? TER{terQUEUED} : TER{tesSUCCESS});
transaction.clearSubmitResult();
transaction.setApplied();
transaction.setQueued();
transaction.setBroadcast();
transaction.setKept();
};
setState(false);
std::promise<void> ready;
auto start = ready.get_future().share();
constexpr int iterations = 10000;
int snapshots = 0;
int inconsistent = 0;
std::thread writer([&] {
start.wait();
for (int i = 0; i < iterations; ++i)
setState(i % 2 != 0);
});
std::thread observer([&] {
start.wait();
for (int i = 0; i < iterations; ++i)
{
auto const json = reader.getJson(JsonOptions::none);
auto const ledger = json[jss::ledger_index].asUInt();
auto const ctid = json[jss::ctid].asString();
if (!((ledger == 101 && ctid == *ctidA) ||
(ledger == 202 && ctid == *ctidB)))
++inconsistent;
auto const state = reader.getCurrentLedgerState();
if (!state ||
state->minFeeRequired !=
XRPAmount{state->validatedLedger} ||
state->accountSeqNext != state->validatedLedger + 1 ||
state->accountSeqAvail != state->validatedLedger + 2)
++inconsistent;
auto const result = reader.getResult();
if (result != tesSUCCESS && result != terQUEUED)
++inconsistent;
if (!reader.isValidated() || reader.getStatus() != COMMITTED)
++inconsistent;
auto const flags = reader.getSubmitResult();
(void)flags;
++snapshots;
}
});
ready.set_value();
writer.join();
observer.join();
BEAST_EXPECT(snapshots == iterations);
BEAST_EXPECT(inconsistent == 0);
BEAST_EXPECT(reader.getLedger() == 202);
BEAST_EXPECT(reader.getResult() == terQUEUED);
auto flags = reader.getSubmitResult();
BEAST_EXPECT(
flags.applied && flags.queued && flags.broadcast && flags.kept);
flags.clear();
BEAST_EXPECT(reader.getSubmitResult().any());
transaction.clearSubmitResult();
BEAST_EXPECT(!reader.getSubmitResult().any());
}
};
BEAST_DEFINE_TESTSUITE(TransactionState, app, ripple);
} // namespace test
} // namespace ripple

View File

@@ -64,6 +64,7 @@ public:
bool
isComplete() const
{
ScopedLockType const sl(mtx_);
return complete_;
}
@@ -71,18 +72,21 @@ public:
bool
isFailed() const
{
ScopedLockType const sl(mtx_);
return failed_;
}
std::shared_ptr<Ledger const>
getLedger() const
{
ScopedLockType const sl(mtx_);
return mLedger;
}
std::uint32_t
getSeq() const
{
ScopedLockType const sl(mtx_);
return mSeq;
}
@@ -109,12 +113,14 @@ public:
void
touch()
{
ScopedLockType const sl(mtx_);
mLastAction = m_clock.now();
}
clock_type::time_point
getLastAction() const
{
ScopedLockType const sl(mtx_);
return mLastAction;
}

View File

@@ -434,6 +434,8 @@ InboundLedger::pmDowncast()
void
InboundLedger::done()
{
ScopedLockType const sl(mtx_);
if (mSignaled)
return;
@@ -471,14 +473,14 @@ InboundLedger::done()
// We hold the PeerSet lock, so must dispatch
app_.getJobQueue().addJob(
jtLEDGER_DATA, "AcquisitionDone", [self = shared_from_this()]() {
if (self->complete_ && !self->failed_)
if (self->isComplete() && !self->isFailed())
{
self->app_.getLedgerMaster().checkAccept(self->getLedger());
self->app_.getLedgerMaster().tryAdvance();
}
else
self->app_.getInboundLedgers().logFailure(
self->hash_, self->mSeq);
self->hash_, self->getSeq());
});
}
@@ -1039,11 +1041,12 @@ InboundLedger::gotData(
std::weak_ptr<Peer> peer,
std::shared_ptr<protocol::TMLedgerData> const& data)
{
std::lock_guard sl(mReceivedDataLock);
ScopedLockType const stateLock(mtx_);
if (isDone())
return false;
std::lock_guard sl(mReceivedDataLock);
mReceivedData.emplace_back(peer, data);
if (mReceiveDispatched)

View File

@@ -181,12 +181,15 @@ LedgerMaster::getPublishedLedgerAge()
std::chrono::seconds ret = app_.timeKeeper().closeTime().time_since_epoch();
ret -= pubClose;
ret = (ret > 0s) ? ret : 0s;
static std::chrono::seconds lastRet = -1s;
static std::atomic<std::chrono::seconds::rep> lastRet{-1};
auto const retCount = ret.count();
auto observedLastRet = lastRet.load(std::memory_order_relaxed);
if (ret != lastRet)
if (retCount != observedLastRet &&
lastRet.compare_exchange_strong(
observedLastRet, retCount, std::memory_order_relaxed))
{
JLOG(m_journal.trace()) << "Published ledger age is " << ret.count();
lastRet = ret;
JLOG(m_journal.trace()) << "Published ledger age is " << retCount;
}
return ret;
}
@@ -206,12 +209,15 @@ LedgerMaster::getValidatedLedgerAge()
std::chrono::seconds ret = app_.timeKeeper().closeTime().time_since_epoch();
ret -= valClose;
ret = (ret > 0s) ? ret : 0s;
static std::chrono::seconds lastRet = -1s;
static std::atomic<std::chrono::seconds::rep> lastRet{-1};
auto const retCount = ret.count();
auto observedLastRet = lastRet.load(std::memory_order_relaxed);
if (ret != lastRet)
if (retCount != observedLastRet &&
lastRet.compare_exchange_strong(
observedLastRet, retCount, std::memory_order_relaxed))
{
JLOG(m_journal.trace()) << "Validated ledger age is " << ret.count();
lastRet = ret;
JLOG(m_journal.trace()) << "Validated ledger age is " << retCount;
}
return ret;
}
@@ -2271,7 +2277,11 @@ LedgerMaster::doAdvance(std::unique_lock<std::recursive_mutex>& sl)
}
app_.getOPs().clearNeedNetworkLedger();
progress = newPFWork("pf:newLedger", sl);
// Publishing is progress even without pathfinding clients. Keep
// the shutdown guard so this loop cannot re-enter history work
// after the Application starts stopping.
newPFWork("pf:newLedger", sl);
progress = !app_.isStopping();
}
if (progress)
mAdvanceWork = true;

View File

@@ -1741,6 +1741,10 @@ ApplicationImp::startGenesisLedger()
auto const next =
std::make_shared<Ledger>(*genesis, timeKeeper().closeTime());
next->updateSkipList();
// Consensus-built ledgers flush their state trees, but this genesis
// successor bypasses that path. Persist its state before it can be
// advertised as complete or loaded by a restarted node.
next->stateMap().flushDirty(hotACCOUNT_NODE);
XRPL_ASSERT(
next->read(keylet::fees()),
"ripple::ApplicationImp::startGenesisLedger : valid ledger fees");

View File

@@ -383,6 +383,11 @@ public:
reportFeeChange() override;
void
reportConsensusStateChange(ConsensusPhase phase);
void
reportConsensusStateChangeIfNeeded(
ConsensusPhase phase,
std::unique_ptr<std::stringstream> const& clog,
bool logPhase);
void
updateLocalTx(ReadView const& view) override;
@@ -674,12 +679,14 @@ private:
RCLConsensus mConsensus;
ConsensusPhase mLastConsensusPhase;
std::mutex lastConsensusPhaseMutex_;
ConsensusPhase mLastConsensusPhase{ConsensusPhase::open};
LedgerMaster& m_ledgerMaster;
SubInfoMapType mSubAccount;
SubInfoMapType mSubRTAccount;
bool firstLedgerPublished_{true}; // Guarded by mSubLock.
subRpcMapType mRpcSubMap;
@@ -1010,14 +1017,8 @@ NetworkOPsImp::processHeartbeatTimer()
mConsensus.timerEntry(app_.timeKeeper().closeTime(), clog.ss());
CLOG(clog.ss()) << "consensus phase " << to_string(mLastConsensusPhase);
const ConsensusPhase currPhase = mConsensus.phase();
if (mLastConsensusPhase != currPhase)
{
reportConsensusStateChange(currPhase);
mLastConsensusPhase = currPhase;
CLOG(clog.ss()) << " changed to " << to_string(mLastConsensusPhase);
}
reportConsensusStateChangeIfNeeded(currPhase, clog.ss(), true);
CLOG(clog.ss()) << ". ";
setHeartbeatTimer();
@@ -2094,11 +2095,7 @@ NetworkOPsImp::beginConsensus(
clog);
const ConsensusPhase currPhase = mConsensus.phase();
if (mLastConsensusPhase != currPhase)
{
reportConsensusStateChange(currPhase);
mLastConsensusPhase = currPhase;
}
reportConsensusStateChangeIfNeeded(currPhase, clog, false);
JLOG(m_journal.debug()) << "Initiating consensus engine";
return true;
@@ -3168,11 +3165,10 @@ NetworkOPsImp::pubLedger(std::shared_ptr<ReadView const> const& lpAccepted)
}
{
static bool firstTime = true;
if (firstTime)
if (firstLedgerPublished_)
{
// First validated ledger, start delayed SubAccountHistory
firstTime = false;
firstLedgerPublished_ = false;
for (auto& outer : mSubAccountHistory)
{
for (auto& inner : outer.second)
@@ -3225,6 +3221,24 @@ NetworkOPsImp::reportConsensusStateChange(ConsensusPhase phase)
[this, phase]() { pubConsensus(phase); });
}
void
NetworkOPsImp::reportConsensusStateChangeIfNeeded(
ConsensusPhase phase,
std::unique_ptr<std::stringstream> const& clog,
bool logPhase)
{
std::scoped_lock const lock(lastConsensusPhaseMutex_);
if (logPhase)
CLOG(clog) << "consensus phase " << to_string(mLastConsensusPhase);
if (mLastConsensusPhase != phase)
{
reportConsensusStateChange(phase);
mLastConsensusPhase = phase;
if (logPhase)
CLOG(clog) << " changed to " << to_string(mLastConsensusPhase);
}
}
inline void
NetworkOPsImp::updateLocalTx(ReadView const& view)
{

View File

@@ -29,6 +29,7 @@
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxMeta.h>
#include <mutex>
#include <optional>
#include <variant>
@@ -100,30 +101,35 @@ public:
LedgerIndex
getLedger() const
{
std::scoped_lock const lock(mutableStateMutex_);
return mLedgerIndex;
}
bool
isValidated() const
{
std::scoped_lock const lock(mutableStateMutex_);
return mLedgerIndex != 0;
}
TransStatus
getStatus() const
{
std::scoped_lock const lock(mutableStateMutex_);
return mStatus;
}
TER
getResult()
getResult() const
{
std::scoped_lock const lock(mutableStateMutex_);
return mResult;
}
void
setResult(TER terResult)
{
std::scoped_lock const lock(mutableStateMutex_);
mResult = terResult;
}
@@ -137,12 +143,14 @@ public:
void
setStatus(TransStatus status)
{
std::scoped_lock const lock(mutableStateMutex_);
mStatus = status;
}
void
setLedger(LedgerIndex ledger)
{
std::scoped_lock const lock(mutableStateMutex_);
mLedgerIndex = ledger;
}
@@ -212,6 +220,7 @@ public:
SubmitResult
getSubmitResult() const
{
std::scoped_lock const lock(mutableStateMutex_);
return submitResult_;
}
@@ -221,6 +230,7 @@ public:
void
clearSubmitResult()
{
std::scoped_lock const lock(mutableStateMutex_);
submitResult_.clear();
}
@@ -230,6 +240,7 @@ public:
void
setApplied()
{
std::scoped_lock const lock(mutableStateMutex_);
submitResult_.applied = true;
}
@@ -239,6 +250,7 @@ public:
void
setQueued()
{
std::scoped_lock const lock(mutableStateMutex_);
submitResult_.queued = true;
}
@@ -248,6 +260,7 @@ public:
void
setBroadcast()
{
std::scoped_lock const lock(mutableStateMutex_);
submitResult_.broadcast = true;
}
@@ -257,6 +270,7 @@ public:
void
setKept()
{
std::scoped_lock const lock(mutableStateMutex_);
submitResult_.kept = true;
}
@@ -289,6 +303,7 @@ public:
std::optional<CurrentLedgerState>
getCurrentLedgerState() const
{
std::scoped_lock const lock(mutableStateMutex_);
return currentLedgerState_;
}
@@ -306,6 +321,7 @@ public:
std::uint32_t accountSeq,
std::uint32_t availableSeq)
{
std::scoped_lock const lock(mutableStateMutex_);
currentLedgerState_.emplace(
validatedLedger, fee, accountSeq, availableSeq);
}
@@ -391,6 +407,9 @@ private:
uint256 mTransactionID;
// Response and locator state is read by RPC/relay while NetworkOPs
// updates it. Snapshot the related locator fields together for JSON.
mutable std::mutex mutableStateMutex_;
LedgerIndex mLedgerIndex = 0;
std::optional<uint32_t> mTxnSeq;
std::optional<uint16_t> mNetworkID;

View File

@@ -534,6 +534,8 @@ private:
FeeLevel64 const feeLevel;
/// Transaction ID.
TxID const txID;
// Updated only while absent from the intrusive byFee_ index.
uint256 parentHashSortKey;
/// Account submitting the transaction.
AccountID const account;
/// Expiration ledger for the transaction
@@ -589,16 +591,6 @@ private:
*/
static constexpr int retriesAllowed = 10;
/** The hash of the parent ledger.
This is used to pseudo-randomize the transaction order when
populating byFee_, by XORing it with the transaction hash (txID).
Using a single static and doing the XOR operation every time was
tested to be as fast or faster than storing the computed "sort key",
and obviously uses less memory.
*/
static LedgerHash parentHashComp;
public:
/// Constructor
MaybeTx(
@@ -608,6 +600,12 @@ private:
ApplyFlags const flags,
PreflightResult const& pfresult);
void
setParentHashSortKey(LedgerHash const& parentHash)
{
parentHashSortKey = txID ^ parentHash;
}
/// Attempt to apply the queued transaction to the open ledger.
ApplyResult
apply(Application& app, OpenView& view, beast::Journal j);
@@ -663,8 +661,7 @@ private:
operator()(const MaybeTx& lhs, const MaybeTx& rhs) const
{
if (lhs.feeLevel == rhs.feeLevel)
return (lhs.txID ^ MaybeTx::parentHashComp) <
(rhs.txID ^ MaybeTx::parentHashComp);
return lhs.parentHashSortKey < rhs.parentHashSortKey;
return lhs.feeLevel > rhs.feeLevel;
}
};
@@ -798,9 +795,7 @@ private:
*/
std::optional<size_t> maxSize_;
/**
parentHash_ used for logging only
*/
/// Parent hash used to salt newly queued candidates.
LedgerHash parentHash_{beast::zero};
/** Most queue operations are done under the master lock,

View File

@@ -64,6 +64,7 @@ Transaction::setStatus(
std::optional<std::uint32_t> tseq,
std::optional<std::uint16_t> netID)
{
std::scoped_lock const lock(mutableStateMutex_);
mStatus = ts;
mLedgerIndex = lseq;
if (tseq)
@@ -167,37 +168,47 @@ Transaction::getJson(JsonOptions options, bool binary) const
Json::Value ret(
mTransaction->getJson(options & ~JsonOptions::include_date, binary));
LedgerIndex ledgerIndex;
std::optional<std::uint32_t> transactionSeq;
std::optional<std::uint16_t> networkID;
{
std::scoped_lock const lock(mutableStateMutex_);
ledgerIndex = mLedgerIndex;
transactionSeq = mTxnSeq;
networkID = mNetworkID;
}
// NOTE Binary STTx::getJson output might not be a JSON object
if (ret.isObject() && mLedgerIndex)
if (ret.isObject() && ledgerIndex)
{
if (!(options & JsonOptions::disable_API_prior_V2))
{
// Behaviour before API version 2
ret[jss::inLedger] = mLedgerIndex;
ret[jss::inLedger] = ledgerIndex;
}
// TODO: disable_API_prior_V3 to disable output of both `date` and
// `ledger_index` elements (taking precedence over include_date)
ret[jss::ledger_index] = mLedgerIndex;
ret[jss::ledger_index] = ledgerIndex;
if (options & JsonOptions::include_date)
{
auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(mLedgerIndex);
auto ct = mApp.getLedgerMaster().getCloseTimeBySeq(ledgerIndex);
if (ct)
ret[jss::date] = ct->time_since_epoch().count();
}
// compute outgoing CTID
// override local network id if it's explicitly in the txn
std::optional netID = mNetworkID;
auto netID = networkID;
if (mTransaction->isFieldPresent(sfNetworkID))
netID = mTransaction->getFieldU32(sfNetworkID);
if (mTxnSeq && netID && *mTxnSeq <= 0xFFFFU && *netID < 0xFFFFU &&
mLedgerIndex < 0xFFFFFFFUL)
if (transactionSeq && netID && *transactionSeq <= 0xFFFFU &&
*netID < 0xFFFFU && ledgerIndex < 0xFFFFFFFUL)
{
std::optional<std::string> ctid =
RPC::encodeCTID(mLedgerIndex, *mTxnSeq, *netID);
RPC::encodeCTID(ledgerIndex, *transactionSeq, *netID);
if (ctid)
ret[jss::ctid] = *ctid;
}

View File

@@ -292,8 +292,6 @@ TxQ::FeeMetrics::escalatedSeriesFeeLevel(
return {totalFeeLevel.has_value(), *totalFeeLevel};
}
LedgerHash TxQ::MaybeTx::parentHashComp{};
TxQ::MaybeTx::MaybeTx(
std::shared_ptr<STTx const> const& txn_,
TxID const& txID_,
@@ -303,6 +301,7 @@ TxQ::MaybeTx::MaybeTx(
: txn(txn_)
, feeLevel(feeLevel_)
, txID(txID_)
, parentHashSortKey(txID_)
, account(txn_->getAccountID(sfAccount))
, firstValid(getFirstLedgerSequence(*txn_))
, lastValid(getLastLedgerSequence(*txn_))
@@ -1365,6 +1364,7 @@ TxQ::apply(
{tx, transactionID, feeLevelPaid, flags, pfresult});
// Then index it into the byFee lookup.
candidate.setParentHashSortKey(parentHash_);
byFee_.insert(candidate);
JLOG(j_.debug()) << "Added transaction " << candidate.txID
<< " with result " << transToken(pfresult.ter) << " from "
@@ -1850,12 +1850,11 @@ TxQ::accept(Application& app, OpenView& view)
// time, create a new list and merge the old list into it.
byFee_.clear();
MaybeTx::parentHashComp = parentHash;
for (auto& [_, account] : byAccount_)
{
for (auto& [_, candidate] : account.transactions)
{
candidate.setParentHashSortKey(parentHash);
byFee_.insert(candidate);
}
}

View File

@@ -297,13 +297,12 @@ saveValidatedLedger(
}
{
static boost::format deleteLedger(
"DELETE FROM Ledgers WHERE LedgerSeq = %u;");
static boost::format deleteTrans1(
boost::format deleteLedger("DELETE FROM Ledgers WHERE LedgerSeq = %u;");
boost::format deleteTrans1(
"DELETE FROM Transactions WHERE LedgerSeq = %u;");
static boost::format deleteTrans2(
boost::format deleteTrans2(
"DELETE FROM AccountTransactions WHERE LedgerSeq = %u;");
static boost::format deleteAcctTrans(
boost::format deleteAcctTrans(
"DELETE FROM AccountTransactions WHERE TransID = '%s';");
{

View File

@@ -310,6 +310,9 @@ class Validations
// Sequence of the largest validation received from each node
hash_map<NodeID, SeqEnforcer<Seq>> seqEnforcers_;
// Each validation store owns its keep-range refresh schedule.
std::chrono::steady_clock::time_point refreshTime_{};
//! Validations from listed nodes, indexed by ledger id (partial and full)
beast::aged_unordered_map<
ID,
@@ -735,13 +738,12 @@ public:
{
// We only need to refresh the keep range when it's just about
// to expire. Track the next time we need to refresh.
static std::chrono::steady_clock::time_point refreshTime;
if (auto const now = byLedger_.clock().now();
refreshTime <= now)
refreshTime_ <= now)
{
// The next refresh time is shortly before the expiration
// time from now.
refreshTime = now + parms_.validationSET_EXPIRES -
refreshTime_ = now + parms_.validationSET_EXPIRES -
parms_.validationFRESHNESS;
for (auto i = byLedger_.begin(); i != byLedger_.end(); ++i)

View File

@@ -658,8 +658,7 @@ private:
// to discard duplicate message from the same peer. A message
// is aged after IDLED seconds. A message received IDLED seconds
// after it was relayed is ignored by PeerImp.
inline static messages peersWithMessage_{
beast::get_abstract_clock<clock_type>()};
messages peersWithMessage_{beast::get_abstract_clock<clock_type>()};
};
template <typename clock_type>

View File

@@ -285,7 +285,8 @@ parseMessageContent(MessageHeader const& header, Buffers const& buffers)
if (payloadSize == 0 || !m->ParseFromArray(payload.data(), payloadSize))
return {};
}
else if (!m->ParseFromZeroCopyStream(&stream))
else if (!m->ParseFromBoundedZeroCopyStream(
&stream, static_cast<int>(header.payload_wire_size)))
return {};
return m;