Skip to content
2 changes: 1 addition & 1 deletion indra/newview/llimprocessing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1534,7 +1534,7 @@ void LLIMProcessing::requestOfflineMessages()
&& isAgentAvatarValid()
&& gAgent.getRegion()
&& gAgent.getRegion()->capabilitiesReceived()
&& (LLMuteList::getInstance()->isLoaded() || LLMuteList::getInstance()->getLoadFailed()))
&& LLMuteList::getInstance()->updateLoadState())
{
std::string cap_url = gAgent.getRegionCapability("ReadOfflineMsgs");

Expand Down
141 changes: 109 additions & 32 deletions indra/newview/llmutelist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ class LLDispatchEmptyMuteList : public LLDispatchHandler
const LLUUID& invoice,
const sparam_t& strings)
{
LLMuteList::getInstance()->setLoaded();
LLMuteList* mute_list = LLMuteList::getInstance();
mute_list->clearCachedMutes();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comment about the purpose of the 'clear'. That we received list from server, might have something from cache, but server takes priority?

mute_list->setLoaded(LLMuteList::MLS_SERVER_EMPTY);
return true;
}
};
Expand Down Expand Up @@ -155,7 +157,9 @@ std::string LLMute::getDisplayType() const
//-----------------------------------------------------------------------------
LLMuteList::LLMuteList() :
mLoadState(ML_INITIAL),
mRequestStartTime(0.f)
mLoadSource(MLS_NONE),
mRequestStartTime(0.f),
mTriedCacheFallback(false)
{
gGenericDispatcher.addHandler("emptymutelist", &sDispatchEmptyMuteList);

Expand Down Expand Up @@ -210,9 +214,9 @@ bool LLMuteList::isLinden(const std::string& name)
return last_name == "linden";
}

bool LLMuteList::getLoadFailed() const
bool LLMuteList::updateLoadState()
{
if (mLoadState == ML_FAILED)
if (isLoaded() || isFailed())
{
return true;
}
Expand All @@ -221,9 +225,81 @@ bool LLMuteList::getLoadFailed() const
constexpr F64 WAIT_SECONDS = 30;
if (mRequestStartTime + WAIT_SECONDS < LLTimer::getTotalSeconds())
{
return true;
LL_WARNS() << "Mute list request timed out; trying cache fallback once" << LL_ENDL;
tryLoadCacheFallback(gAgent.getID(), "request timeout");
return isLoaded() || isFailed();
}
}
return false;
Comment on lines 231 to +247
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title/description mentions retrying mute list requests on region change with a short cooldown, but updateLoadState() currently only performs a one-time cache fallback after a fixed timeout and does not trigger any resend/retry behavior on region change. If retries on region transition are still intended, the retry hook/timer logic seems to be missing; otherwise consider updating the PR title/description to avoid implying behavior that isn't present.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restarting the state machine on region change is still on the table, if maintainers think this is worthwhile still I'd be happy to restore this to the concept. I omitted this from this reimplementation to lighten the load/diff, and leave the real fix to the server team as was noted in the existing comments in code.

I should've changed the PR title to reflect that, and will. I forget I can do that 😆

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a chance a rerequest on a region change will fix the issue? From my observation it seemed like if server did not respond, it will keep it that way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes there is actually a chance a server request retry on region change could yield a successful request response. I have observed this to actually work a while back, during the few times I was able to reproduce this issue for testing.

This would would add a form of coverage for the currently unaddressed case of a fresh login without a cache being unlucky and also not getting a successful mute list response. I do agree though, if the login simulator did not respond it is not likely to do so if retried. I think to be fruitful this would only be attempted on a region change.

I recognize that at this point it's not a lot of extra work to wire back up a basic callback on region change to send another request to the new region, but I don't see the point of doing it every time. Only one extra request should do because in the reports of this that I've seen, this issue is often most prominent when logging into a busy/crowded region.

If the login simulator request fails, AND the next region also fails, it's likely there's something else going on and we're not going to get anything out of repeated attempts past that.

}

void LLMuteList::clearCachedMutes()
{
mMutes.clear();
mLegacyMutes.clear();
}

const char* LLMuteList::sourceToString(EMuteListSource source)
{
switch (source)
{
case MLS_NONE:
return "none";
case MLS_SERVER:
return "server";
case MLS_SERVER_EMPTY:
return "server-empty";
case MLS_SERVER_CACHE:
return "server-cached";
case MLS_FALLBACK_CACHE:
return "fallback-cache";
default:
return "unknown";
}
}

std::string LLMuteList::getCacheFilename(const LLUUID& agent_id) const
{
std::string agent_id_string;
agent_id.toString(agent_id_string);
return gDirUtilp->getExpandedFilename(LL_PATH_CACHE, agent_id_string) + ".cached_mute";
}

void LLMuteList::setFailed(const std::string& reason)
{
mLoadState = ML_FAILED;
if (mLoadSource == MLS_NONE)
{
LL_WARNS() << "Mute list unavailable: " << reason << LL_ENDL;
}
else
{
LL_WARNS() << "Mute list unavailable: " << reason << " (last source=" << sourceToString(mLoadSource) << ")" << LL_ENDL;
}
}

bool LLMuteList::tryLoadCacheFallback(const LLUUID& agent_id, const std::string& reason)
{
if (mTriedCacheFallback)
{
if (!isLoaded())
{
setFailed("cache fallback already attempted before " + reason);
}
return isLoaded();
}

mTriedCacheFallback = true;
const std::string filename = getCacheFilename(agent_id);
LL_INFOS() << "Trying mute list cache fallback due to " << reason << ": " << filename << LL_ENDL;

if (loadFromFile(filename, MLS_FALLBACK_CACHE))
{
LL_WARNS() << "Loaded mute list from cache fallback due to " << reason << LL_ENDL;
return true;
}

setFailed("cache fallback failed after " + reason);
return false;
}

Expand Down Expand Up @@ -580,25 +656,28 @@ std::vector<LLMute> LLMuteList::getMutes() const
//-----------------------------------------------------------------------------
// loadFromFile()
//-----------------------------------------------------------------------------
bool LLMuteList::loadFromFile(const std::string& filename)
bool LLMuteList::loadFromFile(const std::string& filename, EMuteListSource source)
{
LL_PROFILE_ZONE_SCOPED;

if(!filename.size())
{
LL_WARNS() << "Mute List Filename is Empty!" << LL_ENDL;
mLoadState = ML_FAILED;
setFailed("empty filename");
return false;
}

LLFILE* fp = LLFile::fopen(filename, "rb"); /*Flawfinder: ignore*/
if (!fp)
{
LL_WARNS() << "Couldn't open mute list " << filename << LL_ENDL;
mLoadState = ML_FAILED;
setFailed("cannot open " + filename);
return false;
}

// Replace previous server-backed state so fallback can be superseded by authoritative data.
clearCachedMutes();

// *NOTE: Changing the size of these buffers will require changes
// in the scanf below.
char id_buffer[MAX_STRING]; /*Flawfinder: ignore*/
Expand Down Expand Up @@ -627,7 +706,7 @@ bool LLMuteList::loadFromFile(const std::string& filename)
}
}
fclose(fp);
setLoaded();
setLoaded(source);

// server does not maintain up-to date account names (not display names!)
// in this list, so it falls to viewer.
Expand Down Expand Up @@ -737,12 +816,11 @@ bool LLMuteList::isMuted(const std::string& username, U32 flags) const
//-----------------------------------------------------------------------------
void LLMuteList::requestFromServer(const LLUUID& agent_id)
{
std::string agent_id_string;
std::string filename;
agent_id.toString(agent_id_string);
filename = gDirUtilp->getExpandedFilename(LL_PATH_CACHE,agent_id_string) + ".cached_mute";
const std::string filename = getCacheFilename(agent_id);
LLCRC crc;
crc.update(filename);
mTriedCacheFallback = false;
mLoadSource = MLS_NONE;

LLMessageSystem* msg = gMessageSystem;
msg->newMessageFast(_PREHASH_MuteListRequest);
Expand All @@ -755,17 +833,19 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id)
if (gDisconnected)
{
LL_WARNS() << "Trying to request mute list when disconnected!" << LL_ENDL;
mLoadState = ML_FAILED;
// Guard against potentially writing back to disk since we're not recovering our connection
mLoadState = ML_LOADED;
mLoadSource = MLS_FALLBACK_CACHE;
Comment on lines +858 to +859
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the gDisconnected path, requestFromServer() marks the mute list as ML_LOADED with source=MLS_FALLBACK_CACHE, but it never actually loads the cached mute list. This can leave the session with an empty mute list even when a cache file exists, which contradicts the PR goal of avoiding an empty list. Consider calling tryLoadCacheFallback(agent_id, "disconnected") (or otherwise loading the cache) instead of forcing ML_LOADED here.

Suggested change
mLoadState = ML_LOADED;
mLoadSource = MLS_FALLBACK_CACHE;
tryLoadCacheFallback(agent_id, "disconnected");

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By setting the load state as loaded and source as cache, we're putting the state of the list in one that will not change again because it is loaded, and that won't be written to disk from cache(). I believe that calling tryLoadCacheFallback() in this code path unnecessary since we will no longer be handling messages from a server so the block list is no longer important to have in a reliable state.

Yes this is don't and say we did situation, but it's a safe lie in this case.

return;
}
if (!gAgent.getRegion())
{
LL_WARNS() << "No region for agent yet, skipping mute list request!" << LL_ENDL;
mLoadState = ML_FAILED;
tryLoadCacheFallback(agent_id, "no region for request");
return;
}
mLoadState = ML_REQUESTED;
mRequestStartTime = LLTimer::getElapsedSeconds();
mRequestStartTime = LLTimer::getTotalSeconds();
// Double amount of retries due to this request happening during busy stage
// Ideally this should be turned into a capability
gMessageSystem->sendReliable(gAgent.getRegionHost(), LL_DEFAULT_RELIABLE_RETRIES * 2, true, LL_PING_BASED_TIMEOUT_DUMMY, NULL, NULL);
Expand All @@ -777,15 +857,16 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id)

void LLMuteList::cache(const LLUUID& agent_id)
{
// Write to disk even if empty.
if(isLoaded())
// Write to disk even if empty, but never from degraded fallback state.
if (isLoaded() && mLoadSource != MLS_FALLBACK_CACHE)
{
std::string agent_id_string;
std::string filename;
agent_id.toString(agent_id_string);
filename = gDirUtilp->getExpandedFilename(LL_PATH_CACHE,agent_id_string) + ".cached_mute";
const std::string filename = getCacheFilename(agent_id);
saveToFile(filename);
}
else if (isLoaded())
{
LL_WARNS() << "Skipping mute list cache write from fallback-only state" << LL_ENDL;
}
}

//-----------------------------------------------------------------------------
Expand All @@ -812,7 +893,7 @@ void LLMuteList::processMuteListUpdate(LLMessageSystem* msg, void**)

LLMuteList* mute_list = getInstance();
mute_list->mLoadState = ML_REQUESTED;
mute_list->mRequestStartTime = LLTimer::getElapsedSeconds();
mute_list->mRequestStartTime = LLTimer::getTotalSeconds();

// Todo: Based of logs and testing, there is no callback
// from server if file doesn't exist server side.
Expand All @@ -831,12 +912,7 @@ void LLMuteList::processMuteListUpdate(LLMessageSystem* msg, void**)
void LLMuteList::processUseCachedMuteList(LLMessageSystem* msg, void**)
{
LL_INFOS() << "LLMuteList::processUseCachedMuteList()" << LL_ENDL;

std::string agent_id_string;
gAgent.getID().toString(agent_id_string);
std::string filename;
filename = gDirUtilp->getExpandedFilename(LL_PATH_CACHE,agent_id_string) + ".cached_mute";
LLMuteList::getInstance()->loadFromFile(filename);
LLMuteList::getInstance()->loadFromFile(LLMuteList::getInstance()->getCacheFilename(gAgent.getID()), MLS_SERVER_CACHE);
}

void LLMuteList::onFileMuteList(void** user_data, S32 error_code, LLExtStat ext_status)
Expand All @@ -845,13 +921,13 @@ void LLMuteList::onFileMuteList(void** user_data, S32 error_code, LLExtStat ext_
if(local_filename_and_path && !local_filename_and_path->empty() && (error_code == 0))
{
LL_INFOS() << "Received mute list from server" << LL_ENDL;
LLMuteList::getInstance()->loadFromFile(*local_filename_and_path);
LLMuteList::getInstance()->loadFromFile(*local_filename_and_path, MLS_SERVER);
LLFile::remove(*local_filename_and_path);
}
else
{
LL_INFOS() << "LLMuteList xfer failed with code " << error_code << LL_ENDL;
LLMuteList::getInstance()->mLoadState = ML_FAILED;
LLMuteList::getInstance()->tryLoadCacheFallback(gAgent.getID(), "xfer failure");
}
delete local_filename_and_path;
}
Expand Down Expand Up @@ -908,9 +984,10 @@ void LLMuteList::removeObserver(LLMuteListObserver* observer)
mObservers.erase(observer);
}

void LLMuteList::setLoaded()
void LLMuteList::setLoaded(EMuteListSource source)
{
mLoadState = ML_LOADED;
mLoadSource = source;
Copy link
Copy Markdown
Contributor

@akleshchev akleshchev Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a warns if we are setting ML_LOADED when it was already in the same state. It would be good for diagnosis to see potential 'overwrite' in logs.

notifyObservers();
}

Expand Down
26 changes: 23 additions & 3 deletions indra/newview/llmutelist.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ class LLMuteList : public LLSingleton<LLMuteList>
ML_LOADED,
ML_FAILED,
};

enum EMuteListSource
{
MLS_NONE,
MLS_SERVER,
MLS_SERVER_EMPTY,
MLS_SERVER_CACHE,
MLS_FALLBACK_CACHE,
};
public:
// reasons for auto-unmuting a resident
enum EAutoReason
Expand Down Expand Up @@ -116,7 +125,11 @@ class LLMuteList : public LLSingleton<LLMuteList>
static bool isLinden(const std::string& name);

bool isLoaded() const { return mLoadState == ML_LOADED; }
bool getLoadFailed() const;
bool isFailed() const { return mLoadState == ML_FAILED; }

// Advance the load state machine, trying cache fallback if necessary.
// Return value indicates mute list consumption readiness.
bool updateLoadState();

std::vector<LLMute> getMutes() const;

Expand All @@ -127,10 +140,15 @@ class LLMuteList : public LLSingleton<LLMuteList>
void cache(const LLUUID& agent_id);

private:
bool loadFromFile(const std::string& filename);
void clearCachedMutes();
bool loadFromFile(const std::string& filename, EMuteListSource source);
bool saveToFile(const std::string& filename);
bool tryLoadCacheFallback(const LLUUID& agent_id, const std::string& reason);
void setFailed(const std::string& reason);
static const char* sourceToString(EMuteListSource source);
std::string getCacheFilename(const LLUUID& agent_id) const;

void setLoaded();
void setLoaded(EMuteListSource source);
void notifyObservers();
void notifyObserversDetailed(const LLMute &mute);

Expand Down Expand Up @@ -177,7 +195,9 @@ class LLMuteList : public LLSingleton<LLMuteList>
observer_set_t mObservers;

EMuteListState mLoadState;
EMuteListSource mLoadSource;
F64 mRequestStartTime;
bool mTriedCacheFallback;

friend class LLDispatchEmptyMuteList;
};
Expand Down
Loading