Skip to content
130 changes: 99 additions & 31 deletions indra/newview/llmutelist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class LLDispatchEmptyMuteList : public LLDispatchHandler
const LLUUID& invoice,
const sparam_t& strings)
{
LLMuteList::getInstance()->setLoaded();
LLMuteList::getInstance()->setLoaded(LLMuteList::MLS_SERVER_EMPTY);
return true;
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The "emptymutelist" dispatch now only marks the list as loaded (source=server-empty) but does not clear any existing entries. If a cache fallback was loaded earlier (e.g., after a request timeout) and the server message arrives late, this will incorrectly keep stale cached mutes even though the server is explicitly saying the list is empty. Consider clearing mMutes/mLegacyMutes (and possibly writing an empty cache) before calling setLoaded(MLS_SERVER_EMPTY).

Suggested change
LLMuteList::getInstance()->setLoaded(LLMuteList::MLS_SERVER_EMPTY);
return true;
}
LLMuteList* mute_list = LLMuteList::getInstance();
if (mute_list)
{
// The server explicitly reports an empty mute list: clear any
// locally cached mutes (including legacy mutes) and ensure the
// cache reflects an empty list before marking as loaded.
mute_list->clear();
mute_list->setLoaded(LLMuteList::MLS_SERVER_EMPTY);
}
return true;
}

Copilot uses AI. Check for mistakes.
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.

Makes sense. There is a case where user already did some changes this session, but probably not worth accounting for.

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.

Agreed on not worth accounting for, I operate on the belief that add()/remove() calls made in a degraded state still get received and accounted for by the simulator in lieu of maintaining some kind of clean/dirty transactional mess like I had thought up prior.

};
Expand Down Expand Up @@ -155,7 +155,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,7 +212,7 @@ bool LLMuteList::isLinden(const std::string& name)
return last_name == "linden";
}

bool LLMuteList::getLoadFailed() const
bool LLMuteList::getLoadFailed()
{
if (mLoadState == ML_FAILED)
{
Expand All @@ -221,12 +223,78 @@ 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 mLoadState == ML_FAILED;
}
}
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.

}

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;
}

static LLVOAvatar* find_avatar(const LLUUID& id)
{
LLViewerObject *obj = gObjectList.findObject(id);
Expand Down Expand Up @@ -580,25 +648,29 @@ 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.
mMutes.clear();
mLegacyMutes.clear();

// *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 +699,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 +809,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 +826,17 @@ void LLMuteList::requestFromServer(const LLUUID& agent_id)
if (gDisconnected)
{
LL_WARNS() << "Trying to request mute list when disconnected!" << LL_ENDL;
mLoadState = ML_FAILED;
tryLoadCacheFallback(agent_id, "disconnected before request");
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 +848,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 +884,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 +903,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 +912,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 +975,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
21 changes: 18 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,7 @@ class LLMuteList : public LLSingleton<LLMuteList>
static bool isLinden(const std::string& name);

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

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

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

private:
bool loadFromFile(const std::string& filename);
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 +190,9 @@ class LLMuteList : public LLSingleton<LLMuteList>
observer_set_t mObservers;

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

friend class LLDispatchEmptyMuteList;
};
Expand Down
Loading