/** * @file llavatarnamecache.cpp * @brief Provides lookup of avatar SLIDs ("bobsmith123") and display names * ("James Cook") from avatar UUIDs. * * $LicenseInfo:firstyear=2010&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "linden_common.h" #include "llavatarnamecache.h" #include "llcachename.h" // we wrap this system #include "llframetimer.h" #include "llhttpclient.h" #include "llsd.h" #include "llsdserialize.h" #include <boost/tokenizer.hpp> #include <map> #include <set> namespace LLAvatarNameCache { use_display_name_signal_t mUseDisplayNamesSignal; // Manual override for display names - can disable even if the region // supports it. bool sUseDisplayNames = true; // Cache starts in a paused state until we can determine if the // current region supports display names. bool sRunning = false; // Base lookup URL for name service. // On simulator, loaded from indra.xml // On viewer, usually a simulator capability (at People API team's request) // Includes the trailing slash, like "http://pdp60.lindenlab.com:8000/agents/" std::string sNameLookupURL; // accumulated agent IDs for next query against service typedef std::set<LLUUID> ask_queue_t; ask_queue_t sAskQueue; // agent IDs that have been requested, but with no reply // maps agent ID to frame time request was made typedef std::map<LLUUID, F64> pending_queue_t; pending_queue_t sPendingQueue; // Callbacks to fire when we received a name. // May have multiple callbacks for a single ID, which are // represented as multiple slots bound to the signal. // Avoid copying signals via pointers. typedef std::map<LLUUID, callback_signal_t*> signal_map_t; signal_map_t sSignalMap; // names we know about typedef std::map<LLUUID, LLAvatarName> cache_t; cache_t sCache; // Send bulk lookup requests a few times a second at most // only need per-frame timing resolution LLFrameTimer sRequestTimer; /// Maximum time an unrefreshed cache entry is allowed const F64 MAX_UNREFRESHED_TIME = 20.0 * 60.0; /// Time when unrefreshed cached names were checked last static F64 sLastExpireCheck; //----------------------------------------------------------------------- // Internal methods //----------------------------------------------------------------------- // Handle name response off network. // Optionally skip adding to cache, used when this is a fallback to the // legacy name system. void processName(const LLUUID& agent_id, const LLAvatarName& av_name, bool add_to_cache); void requestNamesViaCapability(); // Legacy name system callback void legacyNameCallback(const LLUUID& agent_id, const std::string& full_name, bool is_group ); void requestNamesViaLegacy(); // Fill in an LLAvatarName with the legacy name data void buildLegacyName(const std::string& full_name, LLAvatarName* av_name); // Do a single callback to a given slot void fireSignal(const LLUUID& agent_id, const callback_slot_t& slot, const LLAvatarName& av_name); // Is a request in-flight over the network? bool isRequestPending(const LLUUID& agent_id); // Erase expired names from cache void eraseUnrefreshed(); bool expirationFromCacheControl(LLSD headers, F64 *expires); } /* Sample response: <?xml version="1.0"?> <llsd> <map> <key>agents</key> <array> <map> <key>display_name_next_update</key> <date>2010-04-16T21:34:02+00:00Z</date> <key>display_name_expires</key> <date>2010-04-16T21:32:26.142178+00:00Z</date> <key>display_name</key> <string>MickBot390 LLQABot</string> <key>sl_id</key> <string>mickbot390.llqabot</string> <key>id</key> <string>0012809d-7d2d-4c24-9609-af1230a37715</string> <key>is_display_name_default</key> <boolean>false</boolean> </map> <map> <key>display_name_next_update</key> <date>2010-04-16T21:34:02+00:00Z</date> <key>display_name_expires</key> <date>2010-04-16T21:32:26.142178+00:00Z</date> <key>display_name</key> <string>Bjork Gudmundsdottir</string> <key>sl_id</key> <string>sardonyx.linden</string> <key>id</key> <string>3941037e-78ab-45f0-b421-bd6e77c1804d</string> <key>is_display_name_default</key> <boolean>true</boolean> </map> </array> </map> </llsd> */ class LLAvatarNameResponder : public LLHTTPClient::Responder { private: // need to store agent ids that are part of this request in case of // an error, so we can flag them as unavailable std::vector<LLUUID> mAgentIDs; // Need the headers to look up Expires: and Retry-After: LLSD mHeaders; public: LLAvatarNameResponder(const std::vector<LLUUID>& agent_ids) : mAgentIDs(agent_ids), mHeaders() { } /*virtual*/ void completedHeader(U32 status, const std::string& reason, const LLSD& headers) { mHeaders = headers; } /*virtual*/ void result(const LLSD& content) { // Pull expiration out of headers if available F64 expires = LLAvatarNameCache::nameExpirationFromHeaders(mHeaders); F64 now = LLFrameTimer::getTotalSeconds(); LLSD agents = content["agents"]; LLSD::array_const_iterator it = agents.beginArray(); for ( ; it != agents.endArray(); ++it) { const LLSD& row = *it; LLUUID agent_id = row["id"].asUUID(); LLAvatarName av_name; av_name.fromLLSD(row); // Use expiration time from header av_name.mExpires = expires; // Some avatars don't have explicit display names set if (av_name.mDisplayName.empty()) { av_name.mDisplayName = av_name.mUsername; } LL_DEBUGS("AvNameCache") << "LLAvatarNameResponder::result for " << agent_id << " " << "user '" << av_name.mUsername << "' " << "display '" << av_name.mDisplayName << "' " << "expires in " << expires - now << " seconds" << LL_ENDL; // cache it and fire signals LLAvatarNameCache::processName(agent_id, av_name, true); } // Same logic as error response case LLSD unresolved_agents = content["bad_ids"]; S32 num_unresolved = unresolved_agents.size(); if (num_unresolved > 0) { LL_WARNS("AvNameCache") << "LLAvatarNameResponder::result " << num_unresolved << " unresolved ids; " << "expires in " << expires - now << " seconds" << LL_ENDL; it = unresolved_agents.beginArray(); for ( ; it != unresolved_agents.endArray(); ++it) { const LLUUID& agent_id = *it; LL_WARNS("AvNameCache") << "LLAvatarNameResponder::result " << "failed id " << agent_id << LL_ENDL; LLAvatarNameCache::handleAgentError(agent_id); } } LL_DEBUGS("AvNameCache") << "LLAvatarNameResponder::result " << LLAvatarNameCache::sCache.size() << " cached names" << LL_ENDL; } /*virtual*/ void error(U32 status, const std::string& reason) { // If there's an error, it might be caused by PeopleApi, // or when loading textures on startup and using a very slow // network, this query may time out. // What we should do depends on whether or not we have a cached name LL_WARNS("AvNameCache") << "LLAvatarNameResponder::error " << status << " " << reason << LL_ENDL; // Add dummy records for any agent IDs in this request that we do not have cached already std::vector<LLUUID>::const_iterator it = mAgentIDs.begin(); for ( ; it != mAgentIDs.end(); ++it) { const LLUUID& agent_id = *it; LLAvatarNameCache::handleAgentError(agent_id); } } }; // Provide some fallback for agents that return errors void LLAvatarNameCache::handleAgentError(const LLUUID& agent_id) { std::map<LLUUID,LLAvatarName>::iterator existing = sCache.find(agent_id); if (existing == sCache.end()) { // there is no existing cache entry, so make a temporary name from legacy LL_WARNS("AvNameCache") << "LLAvatarNameCache get legacy for agent " << agent_id << LL_ENDL; gCacheName->get(agent_id, false, // legacy compatibility boost::bind(&LLAvatarNameCache::legacyNameCallback, _1, _2, _3)); } else { // we have a chached (but probably expired) entry - since that would have // been returned by the get method, there is no need to signal anyone // Clear this agent from the pending list LLAvatarNameCache::sPendingQueue.erase(agent_id); const LLAvatarName& av_name = existing->second; LL_DEBUGS("AvNameCache") << "LLAvatarNameCache use cache for agent " << agent_id << "user '" << av_name.mUsername << "' " << "display '" << av_name.mDisplayName << "' " << "expires in " << av_name.mExpires - LLFrameTimer::getTotalSeconds() << " seconds" << LL_ENDL; } } void LLAvatarNameCache::processName(const LLUUID& agent_id, const LLAvatarName& av_name, bool add_to_cache) { if (add_to_cache) { sCache[agent_id] = av_name; } sPendingQueue.erase(agent_id); // signal everyone waiting on this name signal_map_t::iterator sig_it = sSignalMap.find(agent_id); if (sig_it != sSignalMap.end()) { callback_signal_t* signal = sig_it->second; (*signal)(agent_id, av_name); sSignalMap.erase(agent_id); delete signal; signal = NULL; } } void LLAvatarNameCache::requestNamesViaCapability() { F64 now = LLFrameTimer::getTotalSeconds(); // URL format is like: // http://pdp60.lindenlab.com:8000/agents/?ids=3941037e-78ab-45f0-b421-bd6e77c1804d&ids=0012809d-7d2d-4c24-9609-af1230a37715&ids=0019aaba-24af-4f0a-aa72-6457953cf7f0 // // Apache can handle URLs of 4096 chars, but let's be conservative const U32 NAME_URL_MAX = 4096; const U32 NAME_URL_SEND_THRESHOLD = 3000; std::string url; url.reserve(NAME_URL_MAX); std::vector<LLUUID> agent_ids; agent_ids.reserve(128); U32 ids = 0; ask_queue_t::const_iterator it = sAskQueue.begin(); for ( ; it != sAskQueue.end(); ++it) { const LLUUID& agent_id = *it; if (url.empty()) { // ...starting new request url += sNameLookupURL; url += "?ids="; ids = 1; } else { // ...continuing existing request url += "&ids="; ids++; } url += agent_id.asString(); agent_ids.push_back(agent_id); // mark request as pending sPendingQueue[agent_id] = now; if (url.size() > NAME_URL_SEND_THRESHOLD) { LL_DEBUGS("AvNameCache") << "LLAvatarNameCache::requestNamesViaCapability first " << ids << " ids" << LL_ENDL; LLHTTPClient::get(url, new LLAvatarNameResponder(agent_ids)); url.clear(); agent_ids.clear(); } } if (!url.empty()) { LL_DEBUGS("AvNameCache") << "LLAvatarNameCache::requestNamesViaCapability all " << ids << " ids" << LL_ENDL; LLHTTPClient::get(url, new LLAvatarNameResponder(agent_ids)); url.clear(); agent_ids.clear(); } // We've moved all asks to the pending request queue sAskQueue.clear(); } void LLAvatarNameCache::legacyNameCallback(const LLUUID& agent_id, const std::string& full_name, bool is_group) { // Construct a dummy record for this name. By convention, SLID is blank // Never expires, but not written to disk, so lasts until end of session. LLAvatarName av_name; LL_DEBUGS("AvNameCache") << "LLAvatarNameCache::legacyNameCallback " << "agent " << agent_id << " " << "full name '" << full_name << "'" << ( is_group ? " [group]" : "" ) << LL_ENDL; buildLegacyName(full_name, &av_name); // Don't add to cache, the data already exists in the legacy name system // cache and we don't want or need duplicate storage, because keeping the // two copies in sync is complex. processName(agent_id, av_name, false); } void LLAvatarNameCache::requestNamesViaLegacy() { F64 now = LLFrameTimer::getTotalSeconds(); std::string full_name; ask_queue_t::const_iterator it = sAskQueue.begin(); for (; it != sAskQueue.end(); ++it) { const LLUUID& agent_id = *it; // Mark as pending first, just in case the callback is immediately // invoked below. This should never happen in practice. sPendingQueue[agent_id] = now; LL_DEBUGS("AvNameCache") << "LLAvatarNameCache::requestNamesViaLegacy agent " << agent_id << LL_ENDL; gCacheName->get(agent_id, false, // legacy compatibility boost::bind(&LLAvatarNameCache::legacyNameCallback, _1, _2, _3)); } // We've either answered immediately or moved all asks to the // pending queue sAskQueue.clear(); } void LLAvatarNameCache::initClass(bool running) { sRunning = running; } void LLAvatarNameCache::cleanupClass() { } void LLAvatarNameCache::importFile(std::istream& istr) { LLSD data; S32 parse_count = LLSDSerialize::fromXMLDocument(data, istr); if (parse_count < 1) return; // by convention LLSD storage is a map // we only store one entry in the map LLSD agents = data["agents"]; LLUUID agent_id; LLAvatarName av_name; LLSD::map_const_iterator it = agents.beginMap(); for ( ; it != agents.endMap(); ++it) { agent_id.set(it->first); av_name.fromLLSD( it->second ); sCache[agent_id] = av_name; } LL_INFOS("AvNameCache") << "loaded " << sCache.size() << LL_ENDL; // Some entries may have expired since the cache was stored, // but they will be flushed in the first call to eraseUnrefreshed // from LLAvatarNameResponder::idle } void LLAvatarNameCache::exportFile(std::ostream& ostr) { LLSD agents; F64 max_unrefreshed = LLFrameTimer::getTotalSeconds() - MAX_UNREFRESHED_TIME; cache_t::const_iterator it = sCache.begin(); for ( ; it != sCache.end(); ++it) { const LLUUID& agent_id = it->first; const LLAvatarName& av_name = it->second; // Do not write temporary or expired entries to the stored cache if (!av_name.mIsTemporaryName && av_name.mExpires >= max_unrefreshed) { // key must be a string agents[agent_id.asString()] = av_name.asLLSD(); } } LLSD data; data["agents"] = agents; LLSDSerialize::toPrettyXML(data, ostr); } void LLAvatarNameCache::setNameLookupURL(const std::string& name_lookup_url) { sNameLookupURL = name_lookup_url; } bool LLAvatarNameCache::hasNameLookupURL() { return !sNameLookupURL.empty(); } void LLAvatarNameCache::idle() { // By convention, start running at first idle() call sRunning = true; // *TODO: Possibly re-enabled this based on People API load measurements // 100 ms is the threshold for "user speed" operations, so we can // stall for about that long to batch up requests. //const F32 SECS_BETWEEN_REQUESTS = 0.1f; //if (!sRequestTimer.checkExpirationAndReset(SECS_BETWEEN_REQUESTS)) //{ // return; //} if (!sAskQueue.empty()) { if (useDisplayNames()) { requestNamesViaCapability(); } else { // ...fall back to legacy name cache system requestNamesViaLegacy(); } } // erase anything that has not been refreshed for more than MAX_UNREFRESHED_TIME eraseUnrefreshed(); } bool LLAvatarNameCache::isRequestPending(const LLUUID& agent_id) { bool isPending = false; const F64 PENDING_TIMEOUT_SECS = 5.0 * 60.0; pending_queue_t::const_iterator it = sPendingQueue.find(agent_id); if (it != sPendingQueue.end()) { // in the list of requests in flight, retry if too old F64 expire_time = LLFrameTimer::getTotalSeconds() - PENDING_TIMEOUT_SECS; isPending = (it->second > expire_time); } return isPending; } void LLAvatarNameCache::eraseUnrefreshed() { F64 now = LLFrameTimer::getTotalSeconds(); F64 max_unrefreshed = now - MAX_UNREFRESHED_TIME; if (!sLastExpireCheck || sLastExpireCheck < max_unrefreshed) { sLastExpireCheck = now; for (cache_t::iterator it = sCache.begin(); it != sCache.end();) { const LLAvatarName& av_name = it->second; if (av_name.mExpires < max_unrefreshed) { const LLUUID& agent_id = it->first; LL_DEBUGS("AvNameCache") << agent_id << " user '" << av_name.mUsername << "' " << "expired " << now - av_name.mExpires << " secs ago" << LL_ENDL; sCache.erase(it++); } else { ++it; } } LL_INFOS("AvNameCache") << sCache.size() << " cached avatar names" << LL_ENDL; } } void LLAvatarNameCache::buildLegacyName(const std::string& full_name, LLAvatarName* av_name) { llassert(av_name); av_name->mUsername = ""; av_name->mDisplayName = full_name; av_name->mIsDisplayNameDefault = true; av_name->mIsTemporaryName = true; av_name->mExpires = F64_MAX; // not used because these are not cached LL_DEBUGS("AvNameCache") << "LLAvatarNameCache::buildLegacyName " << full_name << LL_ENDL; } // fills in av_name if it has it in the cache, even if expired (can check expiry time) // returns bool specifying if av_name was filled, false otherwise bool LLAvatarNameCache::get(const LLUUID& agent_id, LLAvatarName *av_name) { if (sRunning) { // ...only do immediate lookups when cache is running if (useDisplayNames()) { // ...use display names cache std::map<LLUUID,LLAvatarName>::iterator it = sCache.find(agent_id); if (it != sCache.end()) { *av_name = it->second; // re-request name if entry is expired if (av_name->mExpires < LLFrameTimer::getTotalSeconds()) { if (!isRequestPending(agent_id)) { LL_DEBUGS("AvNameCache") << "LLAvatarNameCache::get " << "refresh agent " << agent_id << LL_ENDL; sAskQueue.insert(agent_id); } } return true; } } else { // ...use legacy names cache std::string full_name; if (gCacheName->getFullName(agent_id, full_name)) { buildLegacyName(full_name, av_name); return true; } } } if (!isRequestPending(agent_id)) { LL_DEBUGS("AvNameCache") << "LLAvatarNameCache::get " << "queue request for agent " << agent_id << LL_ENDL; sAskQueue.insert(agent_id); } return false; } void LLAvatarNameCache::fireSignal(const LLUUID& agent_id, const callback_slot_t& slot, const LLAvatarName& av_name) { callback_signal_t signal; signal.connect(slot); signal(agent_id, av_name); } void LLAvatarNameCache::get(const LLUUID& agent_id, callback_slot_t slot) { if (sRunning) { // ...only do immediate lookups when cache is running if (useDisplayNames()) { // ...use new cache std::map<LLUUID,LLAvatarName>::iterator it = sCache.find(agent_id); if (it != sCache.end()) { const LLAvatarName& av_name = it->second; if (av_name.mExpires > LLFrameTimer::getTotalSeconds()) { // ...name already exists in cache, fire callback now fireSignal(agent_id, slot, av_name); return; } } } else { // ...use old name system std::string full_name; if (gCacheName->getFullName(agent_id, full_name)) { LLAvatarName av_name; buildLegacyName(full_name, &av_name); fireSignal(agent_id, slot, av_name); return; } } } // schedule a request if (!isRequestPending(agent_id)) { sAskQueue.insert(agent_id); } // always store additional callback, even if request is pending signal_map_t::iterator sig_it = sSignalMap.find(agent_id); if (sig_it == sSignalMap.end()) { // ...new callback for this id callback_signal_t* signal = new callback_signal_t(); signal->connect(slot); sSignalMap[agent_id] = signal; } else { // ...existing callback, bind additional slot callback_signal_t* signal = sig_it->second; signal->connect(slot); } } void LLAvatarNameCache::setUseDisplayNames(bool use) { if (use != sUseDisplayNames) { sUseDisplayNames = use; // flush our cache sCache.clear(); mUseDisplayNamesSignal(); } } bool LLAvatarNameCache::useDisplayNames() { // Must be both manually set on and able to look up names. return sUseDisplayNames && !sNameLookupURL.empty(); } void LLAvatarNameCache::erase(const LLUUID& agent_id) { sCache.erase(agent_id); } void LLAvatarNameCache::fetch(const LLUUID& agent_id) { // re-request, even if request is already pending sAskQueue.insert(agent_id); } void LLAvatarNameCache::insert(const LLUUID& agent_id, const LLAvatarName& av_name) { // *TODO: update timestamp if zero? sCache[agent_id] = av_name; } F64 LLAvatarNameCache::nameExpirationFromHeaders(LLSD headers) { F64 expires = 0.0; if (expirationFromCacheControl(headers, &expires)) { return expires; } else { // With no expiration info, default to an hour const F64 DEFAULT_EXPIRES = 60.0 * 60.0; F64 now = LLFrameTimer::getTotalSeconds(); return now + DEFAULT_EXPIRES; } } bool LLAvatarNameCache::expirationFromCacheControl(LLSD headers, F64 *expires) { bool fromCacheControl = false; F64 now = LLFrameTimer::getTotalSeconds(); // Allow the header to override the default LLSD cache_control_header = headers["cache-control"]; if (cache_control_header.isDefined()) { S32 max_age = 0; std::string cache_control = cache_control_header.asString(); if (max_age_from_cache_control(cache_control, &max_age)) { *expires = now + (F64)max_age; fromCacheControl = true; } } LL_DEBUGS("AvNameCache") << ( fromCacheControl ? "expires based on cache control " : "default expiration " ) << "in " << *expires - now << " seconds" << LL_ENDL; return fromCacheControl; } void LLAvatarNameCache::addUseDisplayNamesCallback(const use_display_name_signal_t::slot_type& cb) { mUseDisplayNamesSignal.connect(cb); } static const std::string MAX_AGE("max-age"); static const boost::char_separator<char> EQUALS_SEPARATOR("="); static const boost::char_separator<char> COMMA_SEPARATOR(","); bool max_age_from_cache_control(const std::string& cache_control, S32 *max_age) { // Split the string on "," to get a list of directives typedef boost::tokenizer<boost::char_separator<char> > tokenizer; tokenizer directives(cache_control, COMMA_SEPARATOR); tokenizer::iterator token_it = directives.begin(); for ( ; token_it != directives.end(); ++token_it) { // Tokens may have leading or trailing whitespace std::string token = *token_it; LLStringUtil::trim(token); if (token.compare(0, MAX_AGE.size(), MAX_AGE) == 0) { // ...this token starts with max-age, so let's chop it up by "=" tokenizer subtokens(token, EQUALS_SEPARATOR); tokenizer::iterator subtoken_it = subtokens.begin(); // Must have a token if (subtoken_it == subtokens.end()) return false; std::string subtoken = *subtoken_it; // Must exactly equal "max-age" LLStringUtil::trim(subtoken); if (subtoken != MAX_AGE) return false; // Must have another token ++subtoken_it; if (subtoken_it == subtokens.end()) return false; subtoken = *subtoken_it; // Must be a valid integer // *NOTE: atoi() returns 0 for invalid values, so we have to // check the string first. // *TODO: Do servers ever send "0000" for zero? We don't handle it LLStringUtil::trim(subtoken); if (subtoken == "0") { *max_age = 0; return true; } S32 val = atoi( subtoken.c_str() ); if (val > 0 && val < S32_MAX) { *max_age = val; return true; } return false; } } return false; }