FlotsamAssetCache.cs 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321
  1. /*
  2. * Copyright (c) Contributors, http://opensimulator.org/
  3. * See CONTRIBUTORS.TXT for a full list of copyright holders.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of the OpenSimulator Project nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. // Uncomment to make asset Get requests for existing
  28. // #define WAIT_ON_INPROGRESS_REQUESTS
  29. using System;
  30. using System.IO;
  31. using System.Collections.Generic;
  32. using System.Linq;
  33. using System.Reflection;
  34. using System.Runtime.Serialization;
  35. using System.Runtime.Serialization.Formatters.Binary;
  36. using System.Threading;
  37. using System.Timers;
  38. using log4net;
  39. using Nini.Config;
  40. using Mono.Addins;
  41. using OpenMetaverse;
  42. using OpenSim.Framework;
  43. using OpenSim.Framework.Console;
  44. using OpenSim.Framework.Monitoring;
  45. using OpenSim.Region.Framework.Interfaces;
  46. using OpenSim.Region.Framework.Scenes;
  47. using OpenSim.Services.Interfaces;
  48. //[assembly: Addin("FlotsamAssetCache", "1.1")]
  49. //[assembly: AddinDependency("OpenSim", "0.8.1")]
  50. namespace OpenSim.Region.CoreModules.Asset
  51. {
  52. [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "FlotsamAssetCache")]
  53. public class FlotsamAssetCache : ISharedRegionModule, IAssetCache, IAssetService
  54. {
  55. private static readonly ILog m_log =
  56. LogManager.GetLogger(
  57. MethodBase.GetCurrentMethod().DeclaringType);
  58. private bool m_Enabled;
  59. private bool m_timerRunning;
  60. private bool m_cleanupRunning;
  61. private const string m_ModuleName = "FlotsamAssetCache";
  62. private const string m_DefaultCacheDirectory = "./assetcache";
  63. private string m_CacheDirectory = m_DefaultCacheDirectory;
  64. private readonly List<char> m_InvalidChars = new List<char>();
  65. private int m_LogLevel = 0;
  66. private ulong m_HitRateDisplay = 100; // How often to display hit statistics, given in requests
  67. private static ulong m_Requests;
  68. private static ulong m_RequestsForInprogress;
  69. private static ulong m_DiskHits;
  70. private static ulong m_MemoryHits;
  71. private static ulong m_weakRefHits;
  72. #if WAIT_ON_INPROGRESS_REQUESTS
  73. private Dictionary<string, ManualResetEvent> m_CurrentlyWriting = new Dictionary<string, ManualResetEvent>();
  74. private int m_WaitOnInprogressTimeout = 3000;
  75. #else
  76. private HashSet<string> m_CurrentlyWriting = new HashSet<string>();
  77. #endif
  78. private bool m_FileCacheEnabled = true;
  79. private ExpiringCache<string, AssetBase> m_MemoryCache;
  80. private bool m_MemoryCacheEnabled = false;
  81. private ExpiringCache<string, object> m_negativeCache;
  82. private bool m_negativeCacheEnabled = true;
  83. private bool m_negativeCacheSliding = false;
  84. // Expiration is expressed in hours.
  85. private double m_MemoryExpiration = 0.016;
  86. private const double m_DefaultFileExpiration = 48;
  87. // Negative cache is in seconds
  88. private int m_negativeExpiration = 120;
  89. private TimeSpan m_FileExpiration = TimeSpan.FromHours(m_DefaultFileExpiration);
  90. private TimeSpan m_FileExpirationCleanupTimer = TimeSpan.FromHours(1.0);
  91. private static int m_CacheDirectoryTiers = 1;
  92. private static int m_CacheDirectoryTierLen = 3;
  93. private static int m_CacheWarnAt = 30000;
  94. private System.Timers.Timer m_CacheCleanTimer;
  95. private IAssetService m_AssetService;
  96. private List<Scene> m_Scenes = new List<Scene>();
  97. private object timerLock = new object();
  98. private Dictionary<string,WeakReference> weakAssetReferences = new Dictionary<string, WeakReference>();
  99. private object weakAssetReferencesLock = new object();
  100. private bool m_updateFileTimeOnCacheHit = false;
  101. public FlotsamAssetCache()
  102. {
  103. m_InvalidChars.AddRange(Path.GetInvalidPathChars());
  104. m_InvalidChars.AddRange(Path.GetInvalidFileNameChars());
  105. }
  106. public Type ReplaceableInterface
  107. {
  108. get { return null; }
  109. }
  110. public string Name
  111. {
  112. get { return m_ModuleName; }
  113. }
  114. public void Initialise(IConfigSource source)
  115. {
  116. IConfig moduleConfig = source.Configs["Modules"];
  117. if (moduleConfig != null)
  118. {
  119. string name = moduleConfig.GetString("AssetCaching", String.Empty);
  120. if (name == Name)
  121. {
  122. m_MemoryCache = new ExpiringCache<string, AssetBase>();
  123. m_negativeCache = new ExpiringCache<string, object>();
  124. m_Enabled = true;
  125. m_log.InfoFormat("[FLOTSAM ASSET CACHE]: {0} enabled", this.Name);
  126. IConfig assetConfig = source.Configs["AssetCache"];
  127. if (assetConfig == null)
  128. {
  129. m_log.Debug(
  130. "[FLOTSAM ASSET CACHE]: AssetCache section missing from config (not copied config-include/FlotsamCache.ini.example? Using defaults.");
  131. }
  132. else
  133. {
  134. m_FileCacheEnabled = assetConfig.GetBoolean("FileCacheEnabled", m_FileCacheEnabled);
  135. m_CacheDirectory = assetConfig.GetString("CacheDirectory", m_DefaultCacheDirectory);
  136. m_MemoryCacheEnabled = assetConfig.GetBoolean("MemoryCacheEnabled", m_MemoryCacheEnabled);
  137. m_MemoryExpiration = assetConfig.GetDouble("MemoryCacheTimeout", m_MemoryExpiration);
  138. m_MemoryExpiration *= 3600.0; // config in hours to seconds
  139. m_negativeCacheEnabled = assetConfig.GetBoolean("NegativeCacheEnabled", m_negativeCacheEnabled);
  140. m_negativeExpiration = assetConfig.GetInt("NegativeCacheTimeout", m_negativeExpiration);
  141. m_negativeCacheSliding = assetConfig.GetBoolean("NegativeCacheSliding", m_negativeCacheSliding);
  142. m_updateFileTimeOnCacheHit = assetConfig.GetBoolean("UpdateFileTimeOnCacheHit", m_updateFileTimeOnCacheHit);
  143. #if WAIT_ON_INPROGRESS_REQUESTS
  144. m_WaitOnInprogressTimeout = assetConfig.GetInt("WaitOnInprogressTimeout", 3000);
  145. #endif
  146. m_LogLevel = assetConfig.GetInt("LogLevel", m_LogLevel);
  147. m_HitRateDisplay = (ulong)assetConfig.GetLong("HitRateDisplay", (long)m_HitRateDisplay);
  148. m_FileExpiration = TimeSpan.FromHours(assetConfig.GetDouble("FileCacheTimeout", m_DefaultFileExpiration));
  149. m_FileExpirationCleanupTimer
  150. = TimeSpan.FromHours(
  151. assetConfig.GetDouble("FileCleanupTimer", m_FileExpirationCleanupTimer.TotalHours));
  152. m_CacheDirectoryTiers = assetConfig.GetInt("CacheDirectoryTiers", m_CacheDirectoryTiers);
  153. m_CacheDirectoryTierLen = assetConfig.GetInt("CacheDirectoryTierLength", m_CacheDirectoryTierLen);
  154. m_CacheWarnAt = assetConfig.GetInt("CacheWarnAt", m_CacheWarnAt);
  155. }
  156. m_log.InfoFormat("[FLOTSAM ASSET CACHE]: Cache Directory {0}", m_CacheDirectory);
  157. if (m_CacheDirectoryTiers < 1)
  158. {
  159. m_CacheDirectoryTiers = 1;
  160. }
  161. else if (m_CacheDirectoryTiers > 3)
  162. {
  163. m_CacheDirectoryTiers = 3;
  164. }
  165. if (m_CacheDirectoryTierLen < 1)
  166. {
  167. m_CacheDirectoryTierLen = 1;
  168. }
  169. else if (m_CacheDirectoryTierLen > 4)
  170. {
  171. m_CacheDirectoryTierLen = 4;
  172. }
  173. MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache status", "fcache status", "Display cache status", HandleConsoleCommand);
  174. MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache clear", "fcache clear [file] [memory]", "Remove all assets in the cache. If file or memory is specified then only this cache is cleared.", HandleConsoleCommand);
  175. MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache assets", "fcache assets", "Attempt a deep scan and cache of all assets in all scenes", HandleConsoleCommand);
  176. MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache expire", "fcache expire <datetime>", "Purge cached assets older then the specified date/time", HandleConsoleCommand);
  177. }
  178. }
  179. }
  180. public void PostInitialise()
  181. {
  182. }
  183. public void Close()
  184. {
  185. }
  186. public void AddRegion(Scene scene)
  187. {
  188. if (m_Enabled)
  189. {
  190. scene.RegisterModuleInterface<IAssetCache>(this);
  191. m_Scenes.Add(scene);
  192. }
  193. }
  194. public void RemoveRegion(Scene scene)
  195. {
  196. if (m_Enabled)
  197. {
  198. scene.UnregisterModuleInterface<IAssetCache>(this);
  199. m_Scenes.Remove(scene);
  200. lock(timerLock)
  201. {
  202. if(m_timerRunning && m_Scenes.Count <= 0)
  203. {
  204. m_timerRunning = false;
  205. m_CacheCleanTimer.Stop();
  206. m_CacheCleanTimer.Close();
  207. }
  208. }
  209. }
  210. }
  211. public void RegionLoaded(Scene scene)
  212. {
  213. if (m_Enabled)
  214. {
  215. if(m_AssetService == null)
  216. m_AssetService = scene.RequestModuleInterface<IAssetService>();
  217. lock(timerLock)
  218. {
  219. if(!m_timerRunning)
  220. {
  221. if (m_FileCacheEnabled && (m_FileExpiration > TimeSpan.Zero) && (m_FileExpirationCleanupTimer > TimeSpan.Zero))
  222. {
  223. m_CacheCleanTimer = new System.Timers.Timer(m_FileExpirationCleanupTimer.TotalMilliseconds);
  224. m_CacheCleanTimer.AutoReset = false;
  225. m_CacheCleanTimer.Elapsed += CleanupExpiredFiles;
  226. m_CacheCleanTimer.Start();
  227. m_timerRunning = true;
  228. }
  229. }
  230. }
  231. if (m_MemoryCacheEnabled)
  232. m_MemoryCache = new ExpiringCache<string, AssetBase>();
  233. lock(weakAssetReferencesLock)
  234. weakAssetReferences = new Dictionary<string, WeakReference>();
  235. }
  236. }
  237. ////////////////////////////////////////////////////////////
  238. // IAssetCache
  239. //
  240. private void UpdateWeakReference(string key, AssetBase asset)
  241. {
  242. WeakReference aref = new WeakReference(asset);
  243. lock(weakAssetReferencesLock)
  244. weakAssetReferences[key] = aref;
  245. }
  246. private void UpdateMemoryCache(string key, AssetBase asset)
  247. {
  248. // NOTE DO NOT USE SLIDEEXPIRE option on current libomv
  249. m_MemoryCache.AddOrUpdate(key, asset, m_MemoryExpiration);
  250. }
  251. private void UpdateFileCache(string key, AssetBase asset)
  252. {
  253. string filename = GetFileName(key);
  254. try
  255. {
  256. // If the file is already cached, don't cache it, just touch it so access time is updated
  257. if (File.Exists(filename))
  258. {
  259. UpdateFileLastAccessTime(filename);
  260. }
  261. else
  262. {
  263. // Once we start writing, make sure we flag that we're writing
  264. // that object to the cache so that we don't try to write the
  265. // same file multiple times.
  266. lock (m_CurrentlyWriting)
  267. {
  268. #if WAIT_ON_INPROGRESS_REQUESTS
  269. if (m_CurrentlyWriting.ContainsKey(filename))
  270. {
  271. return;
  272. }
  273. else
  274. {
  275. m_CurrentlyWriting.Add(filename, new ManualResetEvent(false));
  276. }
  277. #else
  278. if (m_CurrentlyWriting.Contains(filename))
  279. {
  280. return;
  281. }
  282. else
  283. {
  284. m_CurrentlyWriting.Add(filename);
  285. }
  286. #endif
  287. }
  288. // Util.FireAndForget(
  289. // delegate { WriteFileCache(filename, asset); }, null, "FlotsamAssetCache.UpdateFileCache");
  290. //this must be sync
  291. WriteFileCache(filename, asset);
  292. }
  293. }
  294. catch (Exception e)
  295. {
  296. m_log.ErrorFormat(
  297. "[FLOTSAM ASSET CACHE]: Failed to update cache for asset {0}. Exception {1} {2}",
  298. asset.ID, e.Message, e.StackTrace);
  299. }
  300. }
  301. public void Cache(AssetBase asset)
  302. {
  303. // TODO: Spawn this off to some seperate thread to do the actual writing
  304. if (asset != null)
  305. {
  306. //m_log.DebugFormat("[FLOTSAM ASSET CACHE]: Caching asset with id {0}", asset.ID);
  307. UpdateWeakReference(asset.ID, asset);
  308. if (m_MemoryCacheEnabled)
  309. UpdateMemoryCache(asset.ID, asset);
  310. if (m_FileCacheEnabled)
  311. UpdateFileCache(asset.ID, asset);
  312. }
  313. }
  314. public void CacheNegative(string id)
  315. {
  316. if (m_negativeCacheEnabled)
  317. {
  318. if (m_negativeCacheSliding)
  319. m_negativeCache.AddOrUpdate(id, null, TimeSpan.FromSeconds(m_negativeExpiration));
  320. else
  321. m_negativeCache.AddOrUpdate(id, null, m_negativeExpiration);
  322. }
  323. }
  324. /// <summary>
  325. /// Updates the cached file with the current time.
  326. /// </summary>
  327. /// <param name="filename">Filename.</param>
  328. /// <returns><c>true</c>, if the update was successful, false otherwise.</returns>
  329. private bool UpdateFileLastAccessTime(string filename)
  330. {
  331. try
  332. {
  333. File.SetLastAccessTime(filename, DateTime.Now);
  334. return true;
  335. }
  336. catch
  337. {
  338. return false;
  339. }
  340. }
  341. private AssetBase GetFromWeakReference(string id)
  342. {
  343. AssetBase asset = null;
  344. WeakReference aref;
  345. lock(weakAssetReferencesLock)
  346. {
  347. if (weakAssetReferences.TryGetValue(id, out aref))
  348. {
  349. asset = aref.Target as AssetBase;
  350. if(asset == null)
  351. weakAssetReferences.Remove(id);
  352. else
  353. m_weakRefHits++;
  354. }
  355. }
  356. return asset;
  357. }
  358. /// <summary>
  359. /// Try to get an asset from the in-memory cache.
  360. /// </summary>
  361. /// <param name="id"></param>
  362. /// <returns></returns>
  363. private AssetBase GetFromMemoryCache(string id)
  364. {
  365. AssetBase asset = null;
  366. if (m_MemoryCache.TryGetValue(id, out asset))
  367. m_MemoryHits++;
  368. return asset;
  369. }
  370. private bool CheckFromMemoryCache(string id)
  371. {
  372. return m_MemoryCache.Contains(id);
  373. }
  374. /// <summary>
  375. /// Try to get an asset from the file cache.
  376. /// </summary>
  377. /// <param name="id"></param>
  378. /// <returns>An asset retrieved from the file cache. null if there was a problem retrieving an asset.</returns>
  379. private AssetBase GetFromFileCache(string id)
  380. {
  381. string filename = GetFileName(id);
  382. #if WAIT_ON_INPROGRESS_REQUESTS
  383. // Check if we're already downloading this asset. If so, try to wait for it to
  384. // download.
  385. if (m_WaitOnInprogressTimeout > 0)
  386. {
  387. m_RequestsForInprogress++;
  388. ManualResetEvent waitEvent;
  389. if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
  390. {
  391. waitEvent.WaitOne(m_WaitOnInprogressTimeout);
  392. return Get(id);
  393. }
  394. }
  395. #else
  396. // Track how often we have the problem that an asset is requested while
  397. // it is still being downloaded by a previous request.
  398. if (m_CurrentlyWriting.Contains(filename))
  399. {
  400. m_RequestsForInprogress++;
  401. return null;
  402. }
  403. #endif
  404. AssetBase asset = null;
  405. if (File.Exists(filename))
  406. {
  407. try
  408. {
  409. using (FileStream stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
  410. {
  411. if (stream.Length == 0) // Empty file will trigger exception below
  412. return null;
  413. BinaryFormatter bformatter = new BinaryFormatter();
  414. asset = (AssetBase)bformatter.Deserialize(stream);
  415. m_DiskHits++;
  416. }
  417. }
  418. catch (System.Runtime.Serialization.SerializationException e)
  419. {
  420. m_log.WarnFormat(
  421. "[FLOTSAM ASSET CACHE]: Failed to get file {0} for asset {1}. Exception {2} {3}",
  422. filename, id, e.Message, e.StackTrace);
  423. // If there was a problem deserializing the asset, the asset may
  424. // either be corrupted OR was serialized under an old format
  425. // {different version of AssetBase} -- we should attempt to
  426. // delete it and re-cache
  427. File.Delete(filename);
  428. }
  429. catch (Exception e)
  430. {
  431. m_log.WarnFormat(
  432. "[FLOTSAM ASSET CACHE]: Failed to get file {0} for asset {1}. Exception {2} {3}",
  433. filename, id, e.Message, e.StackTrace);
  434. }
  435. }
  436. return asset;
  437. }
  438. private bool CheckFromFileCache(string id)
  439. {
  440. bool found = false;
  441. string filename = GetFileName(id);
  442. if (File.Exists(filename))
  443. {
  444. try
  445. {
  446. using (FileStream stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
  447. {
  448. if (stream != null)
  449. found = true;
  450. }
  451. }
  452. catch (Exception e)
  453. {
  454. m_log.ErrorFormat(
  455. "[FLOTSAM ASSET CACHE]: Failed to check file {0} for asset {1}. Exception {2} {3}",
  456. filename, id, e.Message, e.StackTrace);
  457. }
  458. }
  459. return found;
  460. }
  461. // For IAssetService
  462. public AssetBase Get(string id)
  463. {
  464. AssetBase asset;
  465. Get(id, out asset);
  466. return asset;
  467. }
  468. public bool Get(string id, out AssetBase asset)
  469. {
  470. asset = null;
  471. m_Requests++;
  472. object dummy;
  473. if (m_negativeCache.TryGetValue(id, out dummy))
  474. {
  475. return false;
  476. }
  477. asset = GetFromWeakReference(id);
  478. if (asset != null && m_updateFileTimeOnCacheHit)
  479. {
  480. string filename = GetFileName(id);
  481. UpdateFileLastAccessTime(filename);
  482. }
  483. if (m_MemoryCacheEnabled && asset == null)
  484. {
  485. asset = GetFromMemoryCache(id);
  486. if(asset != null)
  487. {
  488. UpdateWeakReference(id,asset);
  489. if (m_updateFileTimeOnCacheHit)
  490. {
  491. string filename = GetFileName(id);
  492. UpdateFileLastAccessTime(filename);
  493. }
  494. }
  495. }
  496. if (asset == null && m_FileCacheEnabled)
  497. {
  498. asset = GetFromFileCache(id);
  499. if(asset != null)
  500. UpdateWeakReference(id,asset);
  501. }
  502. if (m_MemoryCacheEnabled && asset != null)
  503. UpdateMemoryCache(id, asset);
  504. if (((m_LogLevel >= 1)) && (m_HitRateDisplay != 0) && (m_Requests % m_HitRateDisplay == 0))
  505. {
  506. m_log.InfoFormat("[FLOTSAM ASSET CACHE]: Cache Get :: {0} :: {1}", id, asset == null ? "Miss" : "Hit");
  507. GenerateCacheHitReport().ForEach(l => m_log.InfoFormat("[FLOTSAM ASSET CACHE]: {0}", l));
  508. }
  509. return true;
  510. }
  511. public bool Check(string id)
  512. {
  513. if (m_MemoryCacheEnabled && CheckFromMemoryCache(id))
  514. return true;
  515. if (m_FileCacheEnabled && CheckFromFileCache(id))
  516. return true;
  517. return false;
  518. }
  519. public AssetBase GetCached(string id)
  520. {
  521. AssetBase asset;
  522. Get(id, out asset);
  523. return asset;
  524. }
  525. public void Expire(string id)
  526. {
  527. if (m_LogLevel >= 2)
  528. m_log.DebugFormat("[FLOTSAM ASSET CACHE]: Expiring Asset {0}", id);
  529. try
  530. {
  531. if (m_FileCacheEnabled)
  532. {
  533. string filename = GetFileName(id);
  534. if (File.Exists(filename))
  535. {
  536. File.Delete(filename);
  537. }
  538. }
  539. if (m_MemoryCacheEnabled)
  540. m_MemoryCache.Remove(id);
  541. lock(weakAssetReferencesLock)
  542. weakAssetReferences.Remove(id);
  543. }
  544. catch (Exception e)
  545. {
  546. m_log.WarnFormat(
  547. "[FLOTSAM ASSET CACHE]: Failed to expire cached file {0}. Exception {1} {2}",
  548. id, e.Message, e.StackTrace);
  549. }
  550. }
  551. public void Clear()
  552. {
  553. if (m_LogLevel >= 2)
  554. m_log.Debug("[FLOTSAM ASSET CACHE]: Clearing caches.");
  555. if (m_FileCacheEnabled && Directory.Exists(m_CacheDirectory))
  556. {
  557. foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
  558. {
  559. Directory.Delete(dir);
  560. }
  561. }
  562. if (m_MemoryCacheEnabled)
  563. m_MemoryCache = new ExpiringCache<string, AssetBase>();
  564. if (m_negativeCacheEnabled)
  565. m_negativeCache = new ExpiringCache<string, object>();
  566. lock(weakAssetReferencesLock)
  567. weakAssetReferences = new Dictionary<string, WeakReference>();
  568. }
  569. private void CleanupExpiredFiles(object source, ElapsedEventArgs e)
  570. {
  571. if (m_LogLevel >= 2)
  572. m_log.DebugFormat("[FLOTSAM ASSET CACHE]: Checking for expired files older then {0}.", m_FileExpiration);
  573. lock(timerLock)
  574. {
  575. if(!m_timerRunning || m_cleanupRunning)
  576. return;
  577. m_cleanupRunning = true;
  578. }
  579. // Purge all files last accessed prior to this point
  580. DateTime purgeLine = DateTime.Now - m_FileExpiration;
  581. // An asset cache may contain local non-temporary assets that are not in the asset service. Therefore,
  582. // before cleaning up expired files we must scan the objects in the scene to make sure that we retain
  583. // such local assets if they have not been recently accessed.
  584. TouchAllSceneAssets(false);
  585. if(Directory.Exists(m_CacheDirectory))
  586. {
  587. foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
  588. CleanExpiredFiles(dir, purgeLine);
  589. }
  590. lock(timerLock)
  591. {
  592. if(m_timerRunning)
  593. m_CacheCleanTimer.Start();
  594. m_cleanupRunning = false;
  595. }
  596. }
  597. /// <summary>
  598. /// Recurses through specified directory checking for asset files last
  599. /// accessed prior to the specified purge line and deletes them. Also
  600. /// removes empty tier directories.
  601. /// </summary>
  602. /// <param name="dir"></param>
  603. /// <param name="purgeLine"></param>
  604. private void CleanExpiredFiles(string dir, DateTime purgeLine)
  605. {
  606. try
  607. {
  608. if(!Directory.Exists(dir))
  609. return;
  610. foreach (string file in Directory.GetFiles(dir))
  611. {
  612. if (File.GetLastAccessTime(file) < purgeLine)
  613. {
  614. File.Delete(file);
  615. }
  616. }
  617. // Recurse into lower tiers
  618. foreach (string subdir in Directory.GetDirectories(dir))
  619. {
  620. CleanExpiredFiles(subdir, purgeLine);
  621. }
  622. // Check if a tier directory is empty, if so, delete it
  623. int dirSize = Directory.GetFiles(dir).Length + Directory.GetDirectories(dir).Length;
  624. if (dirSize == 0)
  625. {
  626. Directory.Delete(dir);
  627. }
  628. else if (dirSize >= m_CacheWarnAt)
  629. {
  630. m_log.WarnFormat(
  631. "[FLOTSAM ASSET CACHE]: Cache folder exceeded CacheWarnAt limit {0} {1}. Suggest increasing tiers, tier length, or reducing cache expiration",
  632. dir, dirSize);
  633. }
  634. }
  635. catch (DirectoryNotFoundException)
  636. {
  637. // If we get here, another node on the same box has
  638. // already removed the directory. Continue with next.
  639. }
  640. catch (Exception e)
  641. {
  642. m_log.Warn(
  643. string.Format("[FLOTSAM ASSET CACHE]: Could not complete clean of expired files in {0}, exception ", dir), e);
  644. }
  645. }
  646. /// <summary>
  647. /// Determines the filename for an AssetID stored in the file cache
  648. /// </summary>
  649. /// <param name="id"></param>
  650. /// <returns></returns>
  651. private string GetFileName(string id)
  652. {
  653. // Would it be faster to just hash the darn thing?
  654. foreach (char c in m_InvalidChars)
  655. {
  656. id = id.Replace(c, '_');
  657. }
  658. string path = m_CacheDirectory;
  659. for (int p = 1; p <= m_CacheDirectoryTiers; p++)
  660. {
  661. string pathPart = id.Substring((p - 1) * m_CacheDirectoryTierLen, m_CacheDirectoryTierLen);
  662. path = Path.Combine(path, pathPart);
  663. }
  664. return Path.Combine(path, id);
  665. }
  666. /// <summary>
  667. /// Writes a file to the file cache, creating any nessesary
  668. /// tier directories along the way
  669. /// </summary>
  670. /// <param name="filename"></param>
  671. /// <param name="asset"></param>
  672. private void WriteFileCache(string filename, AssetBase asset)
  673. {
  674. Stream stream = null;
  675. // Make sure the target cache directory exists
  676. string directory = Path.GetDirectoryName(filename);
  677. // Write file first to a temp name, so that it doesn't look
  678. // like it's already cached while it's still writing.
  679. string tempname = Path.Combine(directory, Path.GetRandomFileName());
  680. try
  681. {
  682. try
  683. {
  684. if (!Directory.Exists(directory))
  685. {
  686. Directory.CreateDirectory(directory);
  687. }
  688. stream = File.Open(tempname, FileMode.Create);
  689. BinaryFormatter bformatter = new BinaryFormatter();
  690. bformatter.Serialize(stream, asset);
  691. stream.Flush();
  692. }
  693. catch (IOException e)
  694. {
  695. m_log.WarnFormat(
  696. "[FLOTSAM ASSET CACHE]: Failed to write asset {0} to temporary location {1} (final {2}) on cache in {3}. Exception {4} {5}.",
  697. asset.ID, tempname, filename, directory, e.Message, e.StackTrace);
  698. return;
  699. }
  700. catch (UnauthorizedAccessException)
  701. {
  702. }
  703. finally
  704. {
  705. if (stream != null)
  706. stream.Close();
  707. }
  708. try
  709. {
  710. // Now that it's written, rename it so that it can be found.
  711. //
  712. // File.Copy(tempname, filename, true);
  713. // File.Delete(tempname);
  714. //
  715. // For a brief period, this was done as a separate copy and then temporary file delete operation to
  716. // avoid an IOException caused by move if some competing thread had already written the file.
  717. // However, this causes exceptions on Windows when other threads attempt to read a file
  718. // which is still being copied. So instead, go back to moving the file and swallow any IOException.
  719. //
  720. // This situation occurs fairly rarely anyway. We assume in this that moves are atomic on the
  721. // filesystem.
  722. File.Move(tempname, filename);
  723. if (m_LogLevel >= 2)
  724. m_log.DebugFormat("[FLOTSAM ASSET CACHE]: Cache Stored :: {0}", asset.ID);
  725. }
  726. catch (IOException)
  727. {
  728. // If we see an IOException here it's likely that some other competing thread has written the
  729. // cache file first, so ignore. Other IOException errors (e.g. filesystem full) should be
  730. // signally by the earlier temporary file writing code.
  731. }
  732. }
  733. finally
  734. {
  735. // Even if the write fails with an exception, we need to make sure
  736. // that we release the lock on that file, otherwise it'll never get
  737. // cached
  738. lock (m_CurrentlyWriting)
  739. {
  740. #if WAIT_ON_INPROGRESS_REQUESTS
  741. ManualResetEvent waitEvent;
  742. if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
  743. {
  744. m_CurrentlyWriting.Remove(filename);
  745. waitEvent.Set();
  746. }
  747. #else
  748. m_CurrentlyWriting.Remove(filename);
  749. #endif
  750. }
  751. }
  752. }
  753. /// <summary>
  754. /// Scan through the file cache, and return number of assets currently cached.
  755. /// </summary>
  756. /// <param name="dir"></param>
  757. /// <returns></returns>
  758. private int GetFileCacheCount(string dir)
  759. {
  760. if(!Directory.Exists(dir))
  761. return 0;
  762. int count = Directory.GetFiles(dir).Length;
  763. foreach (string subdir in Directory.GetDirectories(dir))
  764. {
  765. count += GetFileCacheCount(subdir);
  766. }
  767. return count;
  768. }
  769. /// <summary>
  770. /// This notes the last time the Region had a deep asset scan performed on it.
  771. /// </summary>
  772. /// <param name="regionID"></param>
  773. private void StampRegionStatusFile(UUID regionID)
  774. {
  775. string RegionCacheStatusFile = Path.Combine(m_CacheDirectory, "RegionStatus_" + regionID.ToString() + ".fac");
  776. try
  777. {
  778. if (File.Exists(RegionCacheStatusFile))
  779. {
  780. File.SetLastWriteTime(RegionCacheStatusFile, DateTime.Now);
  781. }
  782. else
  783. {
  784. File.WriteAllText(
  785. RegionCacheStatusFile,
  786. "Please do not delete this file unless you are manually clearing your Flotsam Asset Cache.");
  787. }
  788. }
  789. catch (Exception e)
  790. {
  791. m_log.Warn(
  792. string.Format(
  793. "[FLOTSAM ASSET CACHE]: Could not stamp region status file for region {0}. Exception ",
  794. regionID),
  795. e);
  796. }
  797. }
  798. /// <summary>
  799. /// Iterates through all Scenes, doing a deep scan through assets
  800. /// to update the access time of all assets present in the scene or referenced by assets
  801. /// in the scene.
  802. /// </summary>
  803. /// <param name="storeUncached">
  804. /// If true, then assets scanned which are not found in cache are added to the cache.
  805. /// </param>
  806. /// <returns>Number of distinct asset references found in the scene.</returns>
  807. private int TouchAllSceneAssets(bool storeUncached)
  808. {
  809. UuidGatherer gatherer = new UuidGatherer(m_AssetService);
  810. Dictionary<UUID, bool> assetsFound = new Dictionary<UUID, bool>();
  811. foreach (Scene s in m_Scenes)
  812. {
  813. StampRegionStatusFile(s.RegionInfo.RegionID);
  814. s.ForEachSOG(delegate(SceneObjectGroup e)
  815. {
  816. if(!m_timerRunning && !storeUncached)
  817. return;
  818. gatherer.AddForInspection(e);
  819. gatherer.GatherAll();
  820. if(!m_timerRunning && !storeUncached)
  821. return;
  822. foreach (UUID assetID in gatherer.GatheredUuids.Keys)
  823. {
  824. if (!assetsFound.ContainsKey(assetID))
  825. {
  826. string filename = GetFileName(assetID.ToString());
  827. if (File.Exists(filename))
  828. {
  829. UpdateFileLastAccessTime(filename);
  830. assetsFound[assetID] = true;
  831. }
  832. else if (storeUncached)
  833. {
  834. AssetBase cachedAsset = m_AssetService.Get(assetID.ToString());
  835. if (cachedAsset == null && gatherer.GatheredUuids[assetID] != (sbyte)AssetType.Unknown)
  836. assetsFound[assetID] = false;
  837. else
  838. assetsFound[assetID] = true;
  839. }
  840. }
  841. else if (!assetsFound[assetID])
  842. {
  843. m_log.DebugFormat(
  844. "[FLOTSAM ASSET CACHE]: Could not find asset {0}, type {1} referenced by object {2} at {3} in scene {4} when pre-caching all scene assets",
  845. assetID, gatherer.GatheredUuids[assetID], e.Name, e.AbsolutePosition, s.Name);
  846. }
  847. }
  848. gatherer.GatheredUuids.Clear();
  849. if(!m_timerRunning && !storeUncached)
  850. return;
  851. if(!storeUncached)
  852. Thread.Sleep(50);
  853. });
  854. if(!m_timerRunning && !storeUncached)
  855. break;
  856. }
  857. return assetsFound.Count;
  858. }
  859. /// <summary>
  860. /// Deletes all cache contents
  861. /// </summary>
  862. private void ClearFileCache()
  863. {
  864. if(!Directory.Exists(m_CacheDirectory))
  865. return;
  866. foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
  867. {
  868. try
  869. {
  870. Directory.Delete(dir, true);
  871. }
  872. catch (Exception e)
  873. {
  874. m_log.WarnFormat(
  875. "[FLOTSAM ASSET CACHE]: Couldn't clear asset cache directory {0} from {1}. Exception {2} {3}",
  876. dir, m_CacheDirectory, e.Message, e.StackTrace);
  877. }
  878. }
  879. foreach (string file in Directory.GetFiles(m_CacheDirectory))
  880. {
  881. try
  882. {
  883. File.Delete(file);
  884. }
  885. catch (Exception e)
  886. {
  887. m_log.WarnFormat(
  888. "[FLOTSAM ASSET CACHE]: Couldn't clear asset cache file {0} from {1}. Exception {1} {2}",
  889. file, m_CacheDirectory, e.Message, e.StackTrace);
  890. }
  891. }
  892. }
  893. private List<string> GenerateCacheHitReport()
  894. {
  895. List<string> outputLines = new List<string>();
  896. double invReq = 100.0 / m_Requests;
  897. double weakHitRate = m_weakRefHits * invReq;
  898. int weakEntries = weakAssetReferences.Count;
  899. double fileHitRate = m_DiskHits * invReq;
  900. double TotalHitRate = weakHitRate + fileHitRate;
  901. outputLines.Add(
  902. string.Format("Total requests: {0}", m_Requests));
  903. outputLines.Add(
  904. string.Format("unCollected Hit Rate: {0}% ({1} entries)", weakHitRate.ToString("0.00"),weakEntries));
  905. outputLines.Add(
  906. string.Format("File Hit Rate: {0}%", fileHitRate.ToString("0.00")));
  907. if (m_MemoryCacheEnabled)
  908. {
  909. double HitRate = m_MemoryHits * invReq;
  910. outputLines.Add(
  911. string.Format("Memory Hit Rate: {0}%", HitRate.ToString("0.00")));
  912. TotalHitRate += HitRate;
  913. }
  914. outputLines.Add(
  915. string.Format("Total Hit Rate: {0}%", TotalHitRate.ToString("0.00")));
  916. outputLines.Add(
  917. string.Format(
  918. "Requests overlap during file writing: {0}", m_RequestsForInprogress));
  919. return outputLines;
  920. }
  921. #region Console Commands
  922. private void HandleConsoleCommand(string module, string[] cmdparams)
  923. {
  924. ICommandConsole con = MainConsole.Instance;
  925. if (cmdparams.Length >= 2)
  926. {
  927. string cmd = cmdparams[1];
  928. switch (cmd)
  929. {
  930. case "status":
  931. if (m_MemoryCacheEnabled)
  932. con.Output("Memory Cache: {0} assets", m_MemoryCache.Count);
  933. else
  934. con.Output("Memory cache disabled");
  935. if (m_FileCacheEnabled)
  936. {
  937. int fileCount = GetFileCacheCount(m_CacheDirectory);
  938. con.Output("File Cache: {0} assets", fileCount);
  939. }
  940. else
  941. {
  942. con.Output("File cache disabled");
  943. }
  944. GenerateCacheHitReport().ForEach(l => con.Output(l));
  945. if (m_FileCacheEnabled)
  946. {
  947. con.Output("Deep scans have previously been performed on the following regions:");
  948. foreach (string s in Directory.GetFiles(m_CacheDirectory, "*.fac"))
  949. {
  950. string RegionID = s.Remove(0,s.IndexOf("_")).Replace(".fac","");
  951. DateTime RegionDeepScanTMStamp = File.GetLastWriteTime(s);
  952. con.Output("Region: {0}, {1}", RegionID, RegionDeepScanTMStamp.ToString("MM/dd/yyyy hh:mm:ss"));
  953. }
  954. }
  955. break;
  956. case "clear":
  957. if (cmdparams.Length < 2)
  958. {
  959. con.Output("Usage is fcache clear [file] [memory]");
  960. break;
  961. }
  962. bool clearMemory = false, clearFile = false;
  963. if (cmdparams.Length == 2)
  964. {
  965. clearMemory = true;
  966. clearFile = true;
  967. }
  968. foreach (string s in cmdparams)
  969. {
  970. if (s.ToLower() == "memory")
  971. clearMemory = true;
  972. else if (s.ToLower() == "file")
  973. clearFile = true;
  974. }
  975. if (clearMemory)
  976. {
  977. if (m_MemoryCacheEnabled)
  978. {
  979. m_MemoryCache.Clear();
  980. con.Output("Memory cache cleared.");
  981. }
  982. else
  983. {
  984. con.Output("Memory cache not enabled.");
  985. }
  986. }
  987. if (clearFile)
  988. {
  989. if (m_FileCacheEnabled)
  990. {
  991. ClearFileCache();
  992. con.Output("File cache cleared.");
  993. }
  994. else
  995. {
  996. con.Output("File cache not enabled.");
  997. }
  998. }
  999. break;
  1000. case "assets":
  1001. lock(timerLock)
  1002. {
  1003. if(m_cleanupRunning)
  1004. {
  1005. con.Output("Flotsam assets check already running");
  1006. return;
  1007. }
  1008. m_cleanupRunning = true;
  1009. }
  1010. con.Output("Flotsam Ensuring assets are cached for all scenes.");
  1011. WorkManager.RunInThreadPool(delegate
  1012. {
  1013. bool wasRunning= false;
  1014. lock(timerLock)
  1015. {
  1016. if(m_timerRunning)
  1017. {
  1018. m_CacheCleanTimer.Stop();
  1019. m_timerRunning = false;
  1020. wasRunning = true;
  1021. Thread.Sleep(100);
  1022. }
  1023. }
  1024. int assetReferenceTotal = TouchAllSceneAssets(true);
  1025. GC.Collect();
  1026. lock(timerLock)
  1027. {
  1028. if(wasRunning)
  1029. {
  1030. m_CacheCleanTimer.Start();
  1031. m_timerRunning = true;
  1032. }
  1033. m_cleanupRunning = false;
  1034. }
  1035. con.Output("Completed check with {0} assets.", assetReferenceTotal);
  1036. }, null, "TouchAllSceneAssets", false);
  1037. break;
  1038. case "expire":
  1039. if (cmdparams.Length < 3)
  1040. {
  1041. con.Output("Invalid parameters for Expire, please specify a valid date & time");
  1042. break;
  1043. }
  1044. string s_expirationDate = "";
  1045. DateTime expirationDate;
  1046. if (cmdparams.Length > 3)
  1047. {
  1048. s_expirationDate = string.Join(" ", cmdparams, 2, cmdparams.Length - 2);
  1049. }
  1050. else
  1051. {
  1052. s_expirationDate = cmdparams[2];
  1053. }
  1054. if (!DateTime.TryParse(s_expirationDate, out expirationDate))
  1055. {
  1056. con.Output("{0} is not a valid date & time", cmd);
  1057. break;
  1058. }
  1059. if (m_FileCacheEnabled)
  1060. CleanExpiredFiles(m_CacheDirectory, expirationDate);
  1061. else
  1062. con.Output("File cache not active, not clearing.");
  1063. break;
  1064. default:
  1065. con.Output("Unknown command {0}", cmd);
  1066. break;
  1067. }
  1068. }
  1069. else if (cmdparams.Length == 1)
  1070. {
  1071. con.Output("fcache assets - Attempt a deep cache of all assets in all scenes");
  1072. con.Output("fcache expire <datetime> - Purge assets older then the specified date & time");
  1073. con.Output("fcache clear [file] [memory] - Remove cached assets");
  1074. con.Output("fcache status - Display cache status");
  1075. }
  1076. }
  1077. #endregion
  1078. #region IAssetService Members
  1079. public AssetMetadata GetMetadata(string id)
  1080. {
  1081. AssetBase asset;
  1082. Get(id, out asset);
  1083. if (asset == null)
  1084. return null;
  1085. return asset.Metadata;
  1086. }
  1087. public byte[] GetData(string id)
  1088. {
  1089. AssetBase asset;
  1090. Get(id, out asset);
  1091. if (asset == null)
  1092. return null;
  1093. return asset.Data;
  1094. }
  1095. public bool Get(string id, object sender, AssetRetrieved handler)
  1096. {
  1097. AssetBase asset;
  1098. if (!Get(id, out asset))
  1099. return false;
  1100. handler(id, sender, asset);
  1101. return true;
  1102. }
  1103. public bool[] AssetsExist(string[] ids)
  1104. {
  1105. bool[] exist = new bool[ids.Length];
  1106. for (int i = 0; i < ids.Length; i++)
  1107. {
  1108. exist[i] = Check(ids[i]);
  1109. }
  1110. return exist;
  1111. }
  1112. public string Store(AssetBase asset)
  1113. {
  1114. if (asset.FullID == UUID.Zero)
  1115. {
  1116. asset.FullID = UUID.Random();
  1117. }
  1118. Cache(asset);
  1119. return asset.ID;
  1120. }
  1121. public bool UpdateContent(string id, byte[] data)
  1122. {
  1123. AssetBase asset;
  1124. if (!Get(id, out asset))
  1125. return false;
  1126. asset.Data = data;
  1127. Cache(asset);
  1128. return true;
  1129. }
  1130. public bool Delete(string id)
  1131. {
  1132. Expire(id);
  1133. return true;
  1134. }
  1135. #endregion
  1136. }
  1137. }