Skip to content

Commit 0985f66

Browse files
jolavilletteclaude
andcommitted
gxs: batch message meta updates + bulk forum markRead (fix mark-all-as-read hang)
Marking a large forum (thousands of posts) "read" used to be catastrophic: the GUI issued one blocking markRead() per post, and the GXS backend wrote each status change to the SQLCipher DB in its own implicit transaction (one fsync per message). On an 8000-post forum this took over an hour and, if force-quit, left most posts still unread. Backend changes: - RsGeneralDataService: add a batch updateMessageMetaData(vector) overload. The default loops; RsDataService overrides it to persist the whole batch in a single transaction, held under one mDbMutex so nothing interleaves. - RsGenExchange::processMsgMetaChanges(): resolve all status masks first, then persist every queued change through that single batched transaction. - RsGxsForums / p3GxsForums: add a bulk markRead(forumId, msgIds, read) that queues all status changes at once and emits a single event, instead of one blocking request + one event per message. "Mark all as read" now persists in a single transaction (one fsync) whatever the number of posts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f655c6d commit 0985f66

7 files changed

Lines changed: 180 additions & 16 deletions

File tree

src/gxs/rsdataservice.cc

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,6 +1581,56 @@ int RsDataService::updateMessageMetaData(const MsgLocMetaData& metaData)
15811581
return 0;
15821582
}
15831583

1584+
int RsDataService::updateMessageMetaData(const std::vector<MsgLocMetaData>& metaList)
1585+
{
1586+
if(metaList.empty())
1587+
return 0;
1588+
1589+
RsStackMutex stack(mDbMutex);
1590+
1591+
// Persist the whole batch inside a single transaction. Without this, every
1592+
// row update is its own implicit transaction (one fsync per message), which
1593+
// is what made "mark all as read" take more than an hour on a large forum.
1594+
// We hold mDbMutex for the whole span so no other statement can slip into
1595+
// the transaction.
1596+
mDb->beginTransaction();
1597+
1598+
int count = 0;
1599+
1600+
for(const MsgLocMetaData& metaData : metaList)
1601+
{
1602+
const RsGxsGroupId& grpId = metaData.msgId.first;
1603+
const RsGxsMessageId& msgId = metaData.msgId.second;
1604+
1605+
if(mDb->sqlUpdate(MSG_TABLE_NAME, KEY_GRP_ID+ "='" + grpId.toStdString() + "' AND " + KEY_MSG_ID + "='" + msgId.toStdString() + "'", metaData.val) )
1606+
{
1607+
// If we use the cache, update the meta data immediately.
1608+
if(mUseCache)
1609+
{
1610+
RetroCursor* c = mDb->sqlQuery(MSG_TABLE_NAME, mMsgMetaColumns, KEY_GRP_ID+ "='" + grpId.toStdString() + "' AND " + KEY_MSG_ID + "='" + msgId.toStdString() + "'", "");
1611+
1612+
c->moveToFirst();
1613+
1614+
// temporarily disable the cache so that we get the value from the DB itself.
1615+
mUseCache=false;
1616+
auto meta = locked_getMsgMeta(*c, 0);
1617+
mUseCache=true;
1618+
1619+
if(meta)
1620+
mMsgMetaDataCache[grpId].updateMeta(msgId,meta);
1621+
1622+
delete c;
1623+
}
1624+
1625+
++count;
1626+
}
1627+
}
1628+
1629+
mDb->commitTransaction();
1630+
1631+
return count;
1632+
}
1633+
15841634
int RsDataService::removeMsgs(const GxsMsgReq& msgIds)
15851635
{
15861636
RsStackMutex stack(mDbMutex);

src/gxs/rsdataservice.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,13 @@ class RsDataService : public RsGeneralDataService
242242
*/
243243
int updateMessageMetaData(const MsgLocMetaData& metaData) override;
244244

245+
/*!
246+
* @brief Batch variant persisting all updates in a single DB transaction.
247+
* @param metaList The meta data items to update
248+
* @return the number of items successfully updated
249+
*/
250+
int updateMessageMetaData(const std::vector<MsgLocMetaData>& metaList) override;
251+
245252
/*!
246253
* @param metaData The meta data item to update
247254
* @return error code

src/gxs/rsgds.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <set>
2626
#include <map>
2727
#include <string>
28+
#include <vector>
2829

2930
#include "inttypes.h"
3031

@@ -248,6 +249,25 @@ class RsGeneralDataService
248249
*/
249250
virtual int updateMessageMetaData(const MsgLocMetaData& metaData) = 0;
250251

252+
/*!
253+
* @brief Update the meta data of several messages at once.
254+
*
255+
* The default implementation just updates them one by one. Stores backed by
256+
* a transactional database should override this to persist the whole batch
257+
* in a single transaction, which is dramatically faster when marking
258+
* thousands of messages (e.g. "mark all as read" on a large forum).
259+
*
260+
* @param metaList the meta data items to update
261+
* @return the number of items successfully updated
262+
*/
263+
virtual int updateMessageMetaData(const std::vector<MsgLocMetaData>& metaList)
264+
{
265+
int count = 0;
266+
for(const MsgLocMetaData& m : metaList)
267+
count += updateMessageMetaData(m);
268+
return count;
269+
}
270+
251271
/*!
252272
* @param metaData
253273
*/

src/gxs/rsgenexchange.cc

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2093,6 +2093,18 @@ void RsGenExchange::processMsgMetaChanges()
20932093

20942094
GxsMsgReq msgIds;
20952095

2096+
// First pass: resolve the status masks into absolute values (reads come from
2097+
// the in-memory meta cache, so this stays cheap even for thousands of
2098+
// messages) and collect every change so they can be persisted together in a
2099+
// single DB transaction below, instead of one fsync'd write per message.
2100+
std::vector<MsgLocMetaData> updates;
2101+
updates.reserve(metaMap.size());
2102+
2103+
std::vector<std::pair<uint32_t, RsGxsGrpMsgIdPair> > tokenIds;
2104+
tokenIds.reserve(metaMap.size());
2105+
2106+
std::set<uint32_t> failedTokens;
2107+
20962108
std::map<uint32_t, MsgLocMetaData>::iterator mit;
20972109
for (mit = metaMap.begin(); mit != metaMap.end(); ++mit)
20982110
{
@@ -2133,28 +2145,38 @@ void RsGenExchange::processMsgMetaChanges()
21332145
}
21342146
}
21352147

2136-
ok &= mDataStore->updateMessageMetaData(m) == 1;
2137-
uint32_t token = mit->first;
2148+
// The actual DB write is deferred to the single batched transaction
2149+
// below. Collect the resolved change and remember its token.
2150+
updates.push_back(m);
2151+
tokenIds.push_back(std::make_pair(mit->first, m.msgId));
2152+
2153+
if(!ok)
2154+
failedTokens.insert(mit->first);
2155+
else if(changed)
2156+
msgIds[m.msgId.first].insert(m.msgId.second);
2157+
}
2158+
2159+
// Second pass: persist every collected change in a single transaction.
2160+
int updated = mDataStore->updateMessageMetaData(updates);
2161+
bool batchOk = (updated == static_cast<int>(updates.size()));
2162+
2163+
for(std::vector<std::pair<uint32_t, RsGxsGrpMsgIdPair> >::iterator it = tokenIds.begin(); it != tokenIds.end(); ++it)
2164+
{
2165+
bool ok = batchOk && (failedTokens.find(it->first) == failedTokens.end());
21382166

21392167
if(ok)
2140-
{
2141-
mDataAccess->updatePublicRequestStatus(token, RsTokenService::COMPLETE);
2142-
if (changed)
2143-
{
2144-
msgIds[m.msgId.first].insert(m.msgId.second);
2145-
}
2146-
}
2168+
mDataAccess->updatePublicRequestStatus(it->first, RsTokenService::COMPLETE);
21472169
else
2148-
{
2149-
mDataAccess->updatePublicRequestStatus(token, RsTokenService::FAILED);
2150-
}
2170+
mDataAccess->updatePublicRequestStatus(it->first, RsTokenService::FAILED);
21512171

2152-
{
2153-
RS_STACK_MUTEX(mGenMtx);
2154-
mMsgNotify.insert(std::make_pair(token, m.msgId));
2155-
}
2172+
RS_STACK_MUTEX(mGenMtx);
2173+
mMsgNotify.insert(std::make_pair(it->first, it->second));
21562174
}
21572175

2176+
// If the whole batch failed, don't emit change notifications for it.
2177+
if(!batchOk)
2178+
msgIds.clear();
2179+
21582180
if (!msgIds.empty())
21592181
{
21602182
RS_STACK_MUTEX(mGenMtx);

src/retroshare/rsgxsforums.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,23 @@ class RsGxsForums: public RsGxsIfaceHelper
407407
*/
408408
virtual bool markRead(const RsGxsGrpMsgIdPair& messageId, bool read) = 0;
409409

410+
/**
411+
* @brief Mark several messages of a forum read/unread in one batch. Blocking.
412+
*
413+
* Contrary to calling markRead() in a loop, this queues all the status
414+
* changes at once: they are persisted in a single database transaction and
415+
* only one event is emitted. It therefore stays cheap even for thousands of
416+
* posts (e.g. "mark all as read" on a large forum) and is meant to be called
417+
* from a background thread so the caller (typically the GUI) never blocks.
418+
* @param[in] forumId forum group identifier
419+
* @param[in] msgIds identifiers of the posts to update
420+
* @param[in] read true to mark as read, false to mark as unread
421+
* @return false on error, true otherwise
422+
*/
423+
virtual bool markRead( const RsGxsGroupId& forumId,
424+
const std::vector<RsGxsMessageId>& msgIds,
425+
bool read ) = 0;
426+
410427
/**
411428
* @brief Subscrbe to a forum. Blocking API
412429
* @jsonapi{development}

src/services/p3gxsforums.cc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,49 @@ bool p3GxsForums::markRead(const RsGxsGrpMsgIdPair& msgId, bool read)
895895
return true;
896896
}
897897

898+
bool p3GxsForums::markRead(const RsGxsGroupId& forumId, const std::vector<RsGxsMessageId>& msgIds, bool read)
899+
{
900+
if(msgIds.empty())
901+
return true;
902+
903+
uint32_t mask = GXS_SERV::GXS_MSG_STATUS_GUI_NEW | GXS_SERV::GXS_MSG_STATUS_GUI_UNREAD;
904+
uint32_t status = read ? 0 : GXS_SERV::GXS_MSG_STATUS_GUI_UNREAD;
905+
906+
// Queue every status change at once. They all land in mMsgLocMetaMap and are
907+
// drained together by a single processMsgMetaChanges() tick, which persists
908+
// them in one DB transaction. This is what keeps "mark all as read" cheap
909+
// instead of one blocking request (and one detached thread) per message.
910+
std::vector<uint32_t> tokens;
911+
tokens.reserve(msgIds.size());
912+
913+
for(const RsGxsMessageId& msgId : msgIds)
914+
{
915+
uint32_t token;
916+
setMsgStatusFlags(token, RsGxsGrpMsgIdPair(forumId, msgId), status, mask);
917+
tokens.push_back(token);
918+
}
919+
920+
// Wait for the whole batch to be processed. All tokens complete in the same
921+
// tick, so waiting on the last one is enough.
922+
waitToken(tokens.back(), std::chrono::milliseconds(30000));
923+
924+
RsGxsGrpMsgIdPair p;
925+
for(uint32_t token : tokens)
926+
acknowledgeMsg(token, p);
927+
928+
// A single event for the whole batch (the per-message event storm was part
929+
// of what froze the UI). Consumers only use the group id to refresh counts.
930+
if(rsEvents)
931+
{
932+
auto ev = std::make_shared<RsGxsForumEvent>();
933+
ev->mForumGroupId = forumId;
934+
ev->mForumEventCode = RsForumEventCode::READ_STATUS_CHANGED;
935+
rsEvents->postEvent(ev);
936+
}
937+
938+
return true;
939+
}
940+
898941
bool p3GxsForums::subscribeToForum(const RsGxsGroupId& groupId, bool subscribe )
899942
{
900943
uint32_t token;

src/services/p3gxsforums.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ class p3GxsForums: public RsGenExchange, public RsGxsForums, public p3Config,
124124
/// @see RsGxsForums::markRead
125125
virtual bool markRead(const RsGxsGrpMsgIdPair& messageId, bool read) override;
126126

127+
/// @see RsGxsForums::markRead (batch variant)
128+
virtual bool markRead( const RsGxsGroupId& forumId,
129+
const std::vector<RsGxsMessageId>& msgIds,
130+
bool read ) override;
131+
127132
/// @see RsGxsForums::updateReputationLevel
128133
virtual void updateReputationLevel(uint32_t forum_group_sign_flags,ForumPostEntry& e) const override;
129134

0 commit comments

Comments
 (0)