From d728638e36db2326f54ad8fce094e8b33a02040c Mon Sep 17 00:00:00 2001 From: Rye Date: Sun, 1 Nov 2026 22:29:11 -0500 Subject: [PATCH] ALRegionRetirement leaves a retired region's circuit to its simulator Region retirement removes a far region some fifteen seconds after a teleport, rather than keeping it until its simulator sends DisableSimulator about a minute after it, and closed the region's circuit on the viewer alone. Before the circuit fixes in #617 that close did nothing; with them it does, and the simulator, not told, resent every reliable packet that went unacked and pinged until it timed the circuit out: some 35 seconds of packets dropped as off-circuit, each logged, in a session on a live grid. The circuit is now left for the simulator to close with DisableSimulator, as it would be without retirement, so that whatever it is still doing for the agent there ends on its own terms: until then the viewer acks what it sends and answers its pings. Its messages for the region that is gone are dropped as before. The object updates and layer data among them now warn once for each region or simulator rather than once each, and a DisableSimulator for a region already retired closes the circuit without warning that the region is missing. AlchemyRetireDepartedRegionsCloseCircuit, off by default, closes the circuit at once instead, through LLMessageSystem::closeCircuit, which sends the simulator CloseCircuit, once and unreliably, then disables the circuit. closeCircuit is tested. Retirement's choice between the two needs a world and is not, and how a simulator takes a viewer's CloseCircuit on a circuit the agent has left is not yet seen in world. Co-Authored-By: Claude Opus 5.5 --- indra/llmessage/message.cpp | 13 ++ indra/llmessage/message.h | 4 + indra/llmessage/tests/message_test.cpp | 143 ++++++++++++++++++ indra/newview/alregionretirement.cpp | 11 +- indra/newview/alregionretirement.h | 5 +- .../newview/app_settings/settings_alchemy.xml | 11 ++ indra/newview/llviewermessage.cpp | 3 +- indra/newview/llviewerobjectlist.cpp | 6 +- indra/newview/llworld.cpp | 6 +- 9 files changed, 193 insertions(+), 9 deletions(-) diff --git a/indra/llmessage/message.cpp b/indra/llmessage/message.cpp index e2cab409c5..4ea1654b7a 100644 --- a/indra/llmessage/message.cpp +++ b/indra/llmessage/message.cpp @@ -1969,6 +1969,19 @@ void LLMessageSystem::enableCircuit(const LLHost &host, bool trusted) cdp->setTrusted(trusted); } +void LLMessageSystem::closeCircuit(const LLHost &host) +{ + // Sent once, unreliably: the circuit is disabled at once, so nothing + // waits on an ack, and where the packet is lost the other end times the + // circuit out as it would have. + if (mCircuitInfo.findCircuit(host)) + { + newMessageFast(_PREHASH_CloseCircuit); + sendMessage(host); + } + disableCircuit(host); +} + void LLMessageSystem::disableCircuit(const LLHost &host) { LL_INFOS("Messaging") << "LLMessageSystem::disableCircuit for " << host << LL_ENDL; diff --git a/indra/llmessage/message.h b/indra/llmessage/message.h index 5a927d5c6a..2cb5f60e94 100644 --- a/indra/llmessage/message.h +++ b/indra/llmessage/message.h @@ -739,6 +739,10 @@ class LLMessageSystem : public LLMessageSenderInterface void enableCircuit(const LLHost &host, bool trusted); void disableCircuit(const LLHost &host); + // Tells the other end of the circuit to host that it is closing, with + // CloseCircuit, then disables it here. The other end stops sending on + // it, rather than resending what goes unacked until it times it out. + void closeCircuit(const LLHost &host); // Use this to establish trust on startup and in response to // DenyTrustedCircuit. diff --git a/indra/llmessage/tests/message_test.cpp b/indra/llmessage/tests/message_test.cpp index ac0f2dca20..8ab7421f61 100644 --- a/indra/llmessage/tests/message_test.cpp +++ b/indra/llmessage/tests/message_test.cpp @@ -148,6 +148,149 @@ namespace CHECK(response->mStatus == HTTP_NOT_FOUND); } + // CloseCircuit alone, as the template the viewer reads has it. + const char* const CLOSE_CIRCUIT_TEMPLATE = + "version 2.0\n" + "{\n" + " CloseCircuit Fixed 0xFFFFFFFD NotTrusted Unencoded\n" + "}\n"; + + // A message template file of the test's own, there while this lives. + class CloseCircuitTemplateFile + { + public: + CloseCircuitTemplateFile(const std::string& path) + : mPath(path) + { + llofstream file(mPath.c_str()); + file << CLOSE_CIRCUIT_TEMPLATE; + } + + ~CloseCircuitTemplateFile() + { + if (LLFile::remove(mPath) != 0) + { + LL_WARNS() << "Could not remove " << mPath << LL_ENDL; + } + } + + CloseCircuitTemplateFile(const CloseCircuitTemplateFile&) = delete; + CloseCircuitTemplateFile& operator=(const CloseCircuitTemplateFile&) = delete; + + private: + std::string mPath; + }; + + // gMessageSystem is the system under test while this lives, and what it + // was before once it ends: disableCircuit asks gMessageSystem for the + // circuit's code. + class CloseCircuitSystemScope + { + public: + explicit CloseCircuitSystemScope(LLMessageSystem& system) + : mPrior(gMessageSystem) + { + gMessageSystem = &system; + } + + ~CloseCircuitSystemScope() + { + gMessageSystem = mPrior; + } + + CloseCircuitSystemScope(const CloseCircuitSystemScope&) = delete; + CloseCircuitSystemScope& operator=(const CloseCircuitSystemScope&) = delete; + + private: + LLMessageSystem* mPrior; + }; + + // A socket of the test's own, standing for the other end of a circuit. + class PeerSocket + { + public: + PeerSocket() + { + start_net(mSocket, mPort); + } + + ~PeerSocket() + { + end_net(mSocket); + } + + PeerSocket(const PeerSocket&) = delete; + PeerSocket& operator=(const PeerSocket&) = delete; + + bool isOpen() const { return mSocket >= 0; } + LLHost host() const { return LLHost("127.0.0.1", mPort); } + + // The next packet to arrive within seconds, or nothing. + std::vector receive(F32 seconds) + { + char buffer[NET_BUFFER_SIZE]; + LLTimer waited; + while (waited.getElapsedTimeF32() < seconds) + { + const S32 size = receive_packet(mSocket, buffer); + if (size > 0) + { + return std::vector(buffer, buffer + size); + } + ms_sleep(5); + } + return {}; + } + + private: + S32 mSocket = -1; + int mPort = NET_USE_OS_ASSIGNED_PORT; + }; + + // The other end of a circuit closed here stops sending on it only where + // it is told so; otherwise it resends what goes unacked until it times + // the circuit out. + TEST_CASE_METHOD(LLMessageSystemTestData, "LLMessageSystem: closeCircuit tells the other end with CloseCircuit", "[LLMessageSystem]") + { + const std::string template_path = mTestConfigDir + mSep + "message_template.msg"; + const CloseCircuitTemplateFile message_template(template_path); + LLMessageSystem system(template_path, NET_USE_OS_ASSIGNED_PORT, 1, 0, 0, false, 5.f, 100.f); + REQUIRE(system.isOK()); + const CloseCircuitSystemScope scope(system); + + PeerSocket peer; + REQUIRE(peer.isOpen()); + system.enableCircuit(peer.host(), false); + REQUIRE(system.mCircuitInfo.findCircuit(peer.host()) != nullptr); + + system.closeCircuit(peer.host()); + + // The six-byte header, then CloseCircuit's number as a Fixed + // message's: all four bytes, high first. + const std::vector packet = peer.receive(1.f); + REQUIRE(packet.size() >= 10); + CHECK(packet[5] == 0); + CHECK(packet[6] == 0xFF); + CHECK(packet[7] == 0xFF); + CHECK(packet[8] == 0xFF); + CHECK(packet[9] == 0xFD); + } + + TEST_CASE_METHOD(LLMessageSystemTestData, "LLMessageSystem: closeCircuit sends nothing to a host it has no circuit with", "[LLMessageSystem]") + { + const std::string template_path = mTestConfigDir + mSep + "message_template.msg"; + const CloseCircuitTemplateFile message_template(template_path); + LLMessageSystem system(template_path, NET_USE_OS_ASSIGNED_PORT, 1, 0, 0, false, 5.f, 100.f); + REQUIRE(system.isOK()); + const CloseCircuitSystemScope scope(system); + + PeerSocket peer; + REQUIRE(peer.isOpen()); + system.closeCircuit(peer.host()); + CHECK(peer.receive(0.25f).empty()); + CHECK(system.mCircuitInfo.findCircuit(peer.host()) == nullptr); + } + TEST_CASE_METHOD(LLMessageSystemTestData, "LLMessageSystem: a system whose template failed to load still closes its socket", "[LLMessageSystem]") // a system whose template failed to load still closes its socket { diff --git a/indra/newview/alregionretirement.cpp b/indra/newview/alregionretirement.cpp index 0db6197eb7..511dc95b6f 100644 --- a/indra/newview/alregionretirement.cpp +++ b/indra/newview/alregionretirement.cpp @@ -270,9 +270,14 @@ void ALRegionRetirement::finish() regionp->saveObjectCache(); const F64 saved = LLTimer::getTotalSeconds(); LLWorld::getInstance()->removeRegion(host); - // As DisableSimulator does: what the simulator still sends is dropped by the message system, not handled for a - // region that is gone. - gMessageSystem->disableCircuit(host); + // The circuit is left for the simulator to close with DisableSimulator, as it would without retirement: until then + // what it sends is acked, its pings answered, and its messages for the region that is gone dropped. Where asked, + // it is closed here instead, the simulator told so with CloseCircuit. + static LLCachedControl close_circuit(gSavedSettings, "AlchemyRetireDepartedRegionsCloseCircuit", false); + if (close_circuit) + { + gMessageSystem->closeCircuit(host); + } const F64 removed = LLTimer::getTotalSeconds(); LL_INFOS("RegionRetirement") << "Retired " << name << ": " << mObjectCount << " objects retired over " << mFrames diff --git a/indra/newview/alregionretirement.h b/indra/newview/alregionretirement.h index a847876890..584ff180ef 100644 --- a/indra/newview/alregionretirement.h +++ b/indra/newview/alregionretirement.h @@ -38,8 +38,9 @@ class LLViewerRegion; // After a teleport the region left behind stays until its simulator sends DisableSimulator, about a minute later, // with its objects, avatars, terrain and probes. Once the arrival has settled, every region the agent is neither in // nor near is retired: marked so that its simulator creates no new objects, its objects killed a frame's budget at a -// time, then removed, and its circuit closed, as DisableSimulator removes one. One that comes near the view again -// meanwhile is kept, and what was killed of it is asked for again. +// time, then removed. Its circuit is left for its simulator to close with DisableSimulator, unless +// AlchemyRetireDepartedRegionsCloseCircuit asks for it to be closed at once, the simulator told so with CloseCircuit. +// One that comes near the view again meanwhile is kept, and what was killed of it is asked for again. class ALRegionRetirement { public: diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 08375c4e0e..5075ee26bd 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2894,6 +2894,17 @@ Value 1 + AlchemyRetireDepartedRegionsCloseCircuit + + Comment + Close a retired region's circuit at once, telling its simulator with CloseCircuit, instead of leaving it for the simulator to close when it disconnects the region. + Persist + 1 + Type + Boolean + Value + 0 + AlchemyRevokeObjectPerms Comment diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 5d513c2b5e..50a2024926 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -514,7 +514,8 @@ void process_layer_data(LLMessageSystem *mesgsys, void **user_data) if(!regionp) { - LL_WARNS() << "Invalid region for layer data." << LL_ENDL; + // Once for each simulator: a retired region's simulator sends on until it disables the circuit. + LL_WARNS_ONCE() << "Invalid region for layer data from " << mesgsys->getSender() << LL_ENDL; return; } S32 size; diff --git a/indra/newview/llviewerobjectlist.cpp b/indra/newview/llviewerobjectlist.cpp index 08ed753199..4c14d4d08c 100644 --- a/indra/newview/llviewerobjectlist.cpp +++ b/indra/newview/llviewerobjectlist.cpp @@ -459,7 +459,8 @@ void LLViewerObjectList::processObjectUpdate(LLMessageSystem *mesgsys, if (!regionp) { - LL_WARNS() << "Object update from unknown region! " << region_handle << LL_ENDL; + // Once for each region: a retired region's simulator sends on until it disables the circuit. + LL_WARNS_ONCE() << "Object update from unknown region! " << region_handle << LL_ENDL; return; } @@ -743,7 +744,8 @@ void LLViewerObjectList::processCachedObjectUpdate(LLMessageSystem *mesgsys, LLViewerRegion *regionp = LLWorld::getInstance()->getRegionFromHandle(region_handle); if (!regionp) { - LL_WARNS() << "Object update from unknown region! " << region_handle << LL_ENDL; + // Once for each region: a retired region's simulator sends on until it disables the circuit. + LL_WARNS_ONCE() << "Object update from unknown region! " << region_handle << LL_ENDL; return; } diff --git a/indra/newview/llworld.cpp b/indra/newview/llworld.cpp index c11fde4ced..1c87c7c0ff 100644 --- a/indra/newview/llworld.cpp +++ b/indra/newview/llworld.cpp @@ -1094,7 +1094,11 @@ void process_disable_simulator(LLMessageSystem *mesgsys, void **user_data) LLHost host = mesgsys->getSender(); //LL_INFOS() << "Disabling simulator with message from " << host << LL_ENDL; - LLWorld::getInstance()->removeRegion(host); + // A region retired before its simulator disabled it is gone already, and only its circuit is left. + if (LLWorld::getInstance()->getRegion(host)) + { + LLWorld::getInstance()->removeRegion(host); + } mesgsys->disableCircuit(host); }