FlotsamAssetCache.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. /*
  2. Copyright (c) Contributors, http://osflotsam.org/
  3. See CONTRIBUTORS.TXT for a full list of copyright holders.
  4. Redistribution and use in source and binary forms, with or without
  5. modification, are permitted provided that the following conditions are met:
  6. * Redistributions of source code must retain the above copyright
  7. notice, this list of conditions and the following disclaimer.
  8. * Redistributions in binary form must reproduce the above copyright
  9. notice, this list of conditions and the following disclaimer in the
  10. documentation and/or other materials provided with the distribution.
  11. * Neither the name of the Flotsam Project nor the
  12. names of its contributors may be used to endorse or promote products
  13. derived from this software without specific prior written permission.
  14. THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY EXPRESS OR
  15. IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  16. WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  17. DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
  18. DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  19. DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
  20. GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  21. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
  22. IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
  23. OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  24. ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
  25. // Uncomment to make asset Get requests for existing
  26. // #define WAIT_ON_INPROGRESS_REQUESTS
  27. using System;
  28. using System.IO;
  29. using System.Collections.Generic;
  30. using System.Reflection;
  31. using System.Runtime.Serialization;
  32. using System.Runtime.Serialization.Formatters.Binary;
  33. using System.Threading;
  34. using System.Timers;
  35. using log4net;
  36. using Nini.Config;
  37. using Mono.Addins;
  38. using OpenMetaverse;
  39. using OpenSim.Framework;
  40. using OpenSim.Region.Framework.Interfaces;
  41. using OpenSim.Region.Framework.Scenes;
  42. using OpenSim.Services.Interfaces;
  43. [assembly: Addin("FlotsamAssetCache", "1.1")]
  44. [assembly: AddinDependency("OpenSim", "0.5")]
  45. namespace Flotsam.RegionModules.AssetCache
  46. {
  47. /// <summary>
  48. /// OpenSim.ini Options:
  49. /// -------
  50. /// [Modules]
  51. /// AssetCaching = "FlotsamAssetCache"
  52. ///
  53. /// [AssetCache]
  54. /// ; cache directory can be shared by multiple instances
  55. /// CacheDirectory = /directory/writable/by/OpenSim/instance
  56. ///
  57. /// ; Log level
  58. /// ; 0 - (Error) Errors only
  59. /// ; 1 - (Info) Hit Rate Stats + Level 0
  60. /// ; 2 - (Debug) Cache Activity (Reads/Writes) + Level 1
  61. /// ;
  62. /// LogLevel = 1
  63. ///
  64. /// ; How often should hit rates be displayed (given in AssetRequests)
  65. /// ; 0 to disable
  66. /// HitRateDisplay = 100
  67. ///
  68. /// ; Set to false for disk cache only.
  69. /// MemoryCacheEnabled = true
  70. ///
  71. /// ; How long {in hours} to keep assets cached in memory, .5 == 30 minutes
  72. /// MemoryCacheTimeout = 2
  73. ///
  74. /// ; How long {in hours} to keep assets cached on disk, .5 == 30 minutes
  75. /// ; Specify 0 if you do not want your disk cache to expire
  76. /// FileCacheTimeout = 0
  77. ///
  78. /// ; How often {in hours} should the disk be checked for expired filed
  79. /// ; Specify 0 to disable expiration checking
  80. /// FileCleanupTimer = .166 ;roughly every 10 minutes
  81. ///
  82. /// ; If WAIT_ON_INPROGRESS_REQUESTS has been defined then this specifies how
  83. /// ; long (in miliseconds) to block a request thread while trying to complete
  84. /// ; writing to disk.
  85. /// WaitOnInprogressTimeout = 3000
  86. ///
  87. /// ; Number of tiers to use for cache directories (current valid range 1 to 3)
  88. /// CacheDirectoryTiers = 1
  89. ///
  90. /// ; Number of letters per path tier, 1 will create 16 directories per tier, 2 - 256, 3 - 4096 and 4 - 65K
  91. /// CacheDirectoryTierLength = 3
  92. ///
  93. /// ; Warning level for cache directory size
  94. /// CacheWarnAt = 30000
  95. /// -------
  96. /// </summary>
  97. [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule")]
  98. public class FlotsamAssetCache : ISharedRegionModule, IImprovedAssetCache
  99. {
  100. private static readonly ILog m_log =
  101. LogManager.GetLogger(
  102. MethodBase.GetCurrentMethod().DeclaringType);
  103. private bool m_Enabled = false;
  104. private const string m_ModuleName = "FlotsamAssetCache";
  105. private const string m_DefaultCacheDirectory = m_ModuleName;
  106. private string m_CacheDirectory = m_DefaultCacheDirectory;
  107. private List<char> m_InvalidChars = new List<char>();
  108. private int m_LogLevel = 1;
  109. private ulong m_HitRateDisplay = 1; // How often to display hit statistics, given in requests
  110. private static ulong m_Requests = 0;
  111. private static ulong m_RequestsForInprogress = 0;
  112. private static ulong m_DiskHits = 0;
  113. private static ulong m_MemoryHits = 0;
  114. private static double m_HitRateMemory = 0.0;
  115. private static double m_HitRateFile = 0.0;
  116. #if WAIT_ON_INPROGRESS_REQUESTS
  117. private Dictionary<string, ManualResetEvent> m_CurrentlyWriting = new Dictionary<string, ManualResetEvent>();
  118. private int m_WaitOnInprogressTimeout = 3000;
  119. #else
  120. private List<string> m_CurrentlyWriting = new List<string>();
  121. #endif
  122. private ExpiringCache<string, AssetBase> m_MemoryCache = new ExpiringCache<string, AssetBase>();
  123. private bool m_MemoryCacheEnabled = true;
  124. // Expiration is expressed in hours.
  125. private const double m_DefaultMemoryExpiration = 1.0;
  126. private const double m_DefaultFileExpiration = 48;
  127. private TimeSpan m_MemoryExpiration = TimeSpan.Zero;
  128. private TimeSpan m_FileExpiration = TimeSpan.Zero;
  129. private TimeSpan m_FileExpirationCleanupTimer = TimeSpan.Zero;
  130. private static int m_CacheDirectoryTiers = 1;
  131. private static int m_CacheDirectoryTierLen = 3;
  132. private static int m_CacheWarnAt = 30000;
  133. private System.Timers.Timer m_CachCleanTimer = new System.Timers.Timer();
  134. public FlotsamAssetCache()
  135. {
  136. m_InvalidChars.AddRange(Path.GetInvalidPathChars());
  137. m_InvalidChars.AddRange(Path.GetInvalidFileNameChars());
  138. }
  139. public Type ReplaceableInterface
  140. {
  141. get { return null; }
  142. }
  143. public string Name
  144. {
  145. get { return m_ModuleName; }
  146. }
  147. public void Initialise(IConfigSource source)
  148. {
  149. IConfig moduleConfig = source.Configs["Modules"];
  150. if (moduleConfig != null)
  151. {
  152. string name = moduleConfig.GetString("AssetCaching", "");
  153. if (name == Name)
  154. {
  155. m_Enabled = true;
  156. m_log.InfoFormat("[ASSET CACHE]: {0} enabled", this.Name);
  157. IConfig assetConfig = source.Configs["AssetCache"];
  158. if (assetConfig == null)
  159. {
  160. m_log.Warn("[ASSET CACHE]: AssetCache missing from OpenSim.ini, using defaults.");
  161. m_log.InfoFormat("[ASSET CACHE]: Cache Directory", m_DefaultCacheDirectory);
  162. return;
  163. }
  164. m_CacheDirectory = assetConfig.GetString("CacheDirectory", m_DefaultCacheDirectory);
  165. m_log.InfoFormat("[ASSET CACHE]: Cache Directory", m_DefaultCacheDirectory);
  166. m_MemoryCacheEnabled = assetConfig.GetBoolean("MemoryCacheEnabled", true);
  167. m_MemoryExpiration = TimeSpan.FromHours(assetConfig.GetDouble("MemoryCacheTimeout", m_DefaultMemoryExpiration));
  168. #if WAIT_ON_INPROGRESS_REQUESTS
  169. m_WaitOnInprogressTimeout = assetConfig.GetInt("WaitOnInprogressTimeout", 3000);
  170. #endif
  171. m_LogLevel = assetConfig.GetInt("LogLevel", 1);
  172. m_HitRateDisplay = (ulong)assetConfig.GetInt("HitRateDisplay", 1);
  173. m_FileExpiration = TimeSpan.FromHours(assetConfig.GetDouble("FileCacheTimeout", m_DefaultFileExpiration));
  174. m_FileExpirationCleanupTimer = TimeSpan.FromHours(assetConfig.GetDouble("FileCleanupTimer", m_DefaultFileExpiration));
  175. if ((m_FileExpiration > TimeSpan.Zero) && (m_FileExpirationCleanupTimer > TimeSpan.Zero))
  176. {
  177. m_CachCleanTimer.Interval = m_FileExpirationCleanupTimer.TotalMilliseconds;
  178. m_CachCleanTimer.AutoReset = true;
  179. m_CachCleanTimer.Elapsed += CleanupExpiredFiles;
  180. m_CachCleanTimer.Enabled = true;
  181. m_CachCleanTimer.Start();
  182. }
  183. else
  184. {
  185. m_CachCleanTimer.Enabled = false;
  186. }
  187. m_CacheDirectoryTiers = assetConfig.GetInt("CacheDirectoryTiers", 1);
  188. if (m_CacheDirectoryTiers < 1)
  189. {
  190. m_CacheDirectoryTiers = 1;
  191. }
  192. else if (m_CacheDirectoryTiers > 3)
  193. {
  194. m_CacheDirectoryTiers = 3;
  195. }
  196. m_CacheDirectoryTierLen = assetConfig.GetInt("CacheDirectoryTierLength", 3);
  197. if (m_CacheDirectoryTierLen < 1)
  198. {
  199. m_CacheDirectoryTierLen = 1;
  200. }
  201. else if (m_CacheDirectoryTierLen > 4)
  202. {
  203. m_CacheDirectoryTierLen = 4;
  204. }
  205. m_CacheWarnAt = assetConfig.GetInt("CacheWarnAt", 30000);
  206. }
  207. }
  208. }
  209. public void PostInitialise()
  210. {
  211. }
  212. public void Close()
  213. {
  214. }
  215. public void AddRegion(Scene scene)
  216. {
  217. if (m_Enabled)
  218. scene.RegisterModuleInterface<IImprovedAssetCache>(this);
  219. }
  220. public void RemoveRegion(Scene scene)
  221. {
  222. }
  223. public void RegionLoaded(Scene scene)
  224. {
  225. }
  226. ////////////////////////////////////////////////////////////
  227. // IImprovedAssetCache
  228. //
  229. private void UpdateMemoryCache(string key, AssetBase asset)
  230. {
  231. if (m_MemoryCacheEnabled)
  232. {
  233. if (m_MemoryExpiration > TimeSpan.Zero)
  234. {
  235. m_MemoryCache.AddOrUpdate(key, asset, m_MemoryExpiration);
  236. }
  237. else
  238. {
  239. m_MemoryCache.AddOrUpdate(key, asset, DateTime.MaxValue);
  240. }
  241. }
  242. }
  243. public void Cache(AssetBase asset)
  244. {
  245. // TODO: Spawn this off to some seperate thread to do the actual writing
  246. if (asset != null)
  247. {
  248. UpdateMemoryCache(asset.ID, asset);
  249. string filename = GetFileName(asset.ID);
  250. try
  251. {
  252. // If the file is already cached, don't cache it, just touch it so access time is updated
  253. if (File.Exists(filename))
  254. {
  255. File.SetLastAccessTime(filename, DateTime.Now);
  256. } else {
  257. // Once we start writing, make sure we flag that we're writing
  258. // that object to the cache so that we don't try to write the
  259. // same file multiple times.
  260. lock (m_CurrentlyWriting)
  261. {
  262. #if WAIT_ON_INPROGRESS_REQUESTS
  263. if (m_CurrentlyWriting.ContainsKey(filename))
  264. {
  265. return;
  266. }
  267. else
  268. {
  269. m_CurrentlyWriting.Add(filename, new ManualResetEvent(false));
  270. }
  271. #else
  272. if (m_CurrentlyWriting.Contains(filename))
  273. {
  274. return;
  275. }
  276. else
  277. {
  278. m_CurrentlyWriting.Add(filename);
  279. }
  280. #endif
  281. }
  282. ThreadPool.QueueUserWorkItem(
  283. delegate
  284. {
  285. WriteFileCache(filename, asset);
  286. }
  287. );
  288. }
  289. }
  290. catch (Exception e)
  291. {
  292. LogException(e);
  293. }
  294. }
  295. }
  296. public AssetBase Get(string id)
  297. {
  298. m_Requests++;
  299. AssetBase asset = null;
  300. if (m_MemoryCacheEnabled && m_MemoryCache.TryGetValue(id, out asset))
  301. {
  302. m_MemoryHits++;
  303. }
  304. else
  305. {
  306. string filename = GetFileName(id);
  307. if (File.Exists(filename))
  308. {
  309. try
  310. {
  311. FileStream stream = File.Open(filename, FileMode.Open);
  312. BinaryFormatter bformatter = new BinaryFormatter();
  313. asset = (AssetBase)bformatter.Deserialize(stream);
  314. stream.Close();
  315. UpdateMemoryCache(id, asset);
  316. m_DiskHits++;
  317. }
  318. catch (System.Runtime.Serialization.SerializationException e)
  319. {
  320. LogException(e);
  321. // If there was a problem deserializing the asset, the asset may
  322. // either be corrupted OR was serialized under an old format
  323. // {different version of AssetBase} -- we should attempt to
  324. // delete it and re-cache
  325. File.Delete(filename);
  326. }
  327. catch (Exception e)
  328. {
  329. LogException(e);
  330. }
  331. }
  332. #if WAIT_ON_INPROGRESS_REQUESTS
  333. // Check if we're already downloading this asset. If so, try to wait for it to
  334. // download.
  335. if (m_WaitOnInprogressTimeout > 0)
  336. {
  337. m_RequestsForInprogress++;
  338. ManualResetEvent waitEvent;
  339. if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
  340. {
  341. waitEvent.WaitOne(m_WaitOnInprogressTimeout);
  342. return Get(id);
  343. }
  344. }
  345. #else
  346. // Track how often we have the problem that an asset is requested while
  347. // it is still being downloaded by a previous request.
  348. if (m_CurrentlyWriting.Contains(filename))
  349. {
  350. m_RequestsForInprogress++;
  351. }
  352. #endif
  353. }
  354. if (((m_LogLevel >= 1)) && (m_HitRateDisplay != 0) && (m_Requests % m_HitRateDisplay == 0))
  355. {
  356. m_HitRateFile = (double)m_DiskHits / m_Requests * 100.0;
  357. m_log.InfoFormat("[ASSET CACHE]: Cache Get :: {0} :: {1}", id, asset == null ? "Miss" : "Hit");
  358. m_log.InfoFormat("[ASSET CACHE]: File Hit Rate {0}% for {1} requests", m_HitRateFile.ToString("0.00"), m_Requests);
  359. if (m_MemoryCacheEnabled)
  360. {
  361. m_HitRateMemory = (double)m_MemoryHits / m_Requests * 100.0;
  362. m_log.InfoFormat("[ASSET CACHE]: Memory Hit Rate {0}% for {1} requests", m_HitRateMemory.ToString("0.00"), m_Requests);
  363. }
  364. m_log.InfoFormat("[ASSET CACHE]: {0} unnessesary requests due to requests for assets that are currently downloading.", m_RequestsForInprogress);
  365. }
  366. return asset;
  367. }
  368. public void Expire(string id)
  369. {
  370. if (m_LogLevel >= 2)
  371. m_log.DebugFormat("[ASSET CACHE]: Expiring Asset {0}.", id);
  372. try
  373. {
  374. string filename = GetFileName(id);
  375. if (File.Exists(filename))
  376. {
  377. File.Delete(filename);
  378. }
  379. if (m_MemoryCacheEnabled)
  380. m_MemoryCache.Remove(id);
  381. }
  382. catch (Exception e)
  383. {
  384. LogException(e);
  385. }
  386. }
  387. public void Clear()
  388. {
  389. if (m_LogLevel >= 2)
  390. m_log.Debug("[ASSET CACHE]: Clearing Cache.");
  391. foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
  392. {
  393. Directory.Delete(dir);
  394. }
  395. if (m_MemoryCacheEnabled)
  396. m_MemoryCache.Clear();
  397. }
  398. private void CleanupExpiredFiles(object source, ElapsedEventArgs e)
  399. {
  400. if (m_LogLevel >= 2)
  401. m_log.DebugFormat("[ASSET CACHE]: Checking for expired files older then {0}.", m_FileExpiration.ToString());
  402. foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
  403. {
  404. foreach (string file in Directory.GetFiles(dir))
  405. {
  406. if (DateTime.Now - File.GetLastAccessTime(file) > m_FileExpiration)
  407. {
  408. File.Delete(file);
  409. }
  410. }
  411. int dirSize = Directory.GetFiles(dir).Length;
  412. if (dirSize == 0)
  413. {
  414. Directory.Delete(dir);
  415. }
  416. else if (dirSize >= m_CacheWarnAt)
  417. {
  418. m_log.WarnFormat("[ASSET CACHE]: Cache folder exceeded CacheWarnAt limit {0} {1}. Suggest increasing tiers, tier length, or reducing cache expiration", dir, dirSize);
  419. }
  420. }
  421. }
  422. private string GetFileName(string id)
  423. {
  424. // Would it be faster to just hash the darn thing?
  425. foreach (char c in m_InvalidChars)
  426. {
  427. id = id.Replace(c, '_');
  428. }
  429. string path = m_CacheDirectory;
  430. for (int p = 1; p <= m_CacheDirectoryTiers; p++)
  431. {
  432. string pathPart = id.Substring((p - 1) * m_CacheDirectoryTierLen, m_CacheDirectoryTierLen);
  433. path = Path.Combine(path, pathPart);
  434. }
  435. return Path.Combine(path, id);
  436. }
  437. private void WriteFileCache(string filename, AssetBase asset)
  438. {
  439. try
  440. {
  441. // Make sure the target cache directory exists
  442. string directory = Path.GetDirectoryName(filename);
  443. if (!Directory.Exists(directory))
  444. {
  445. Directory.CreateDirectory(directory);
  446. }
  447. // Write file first to a temp name, so that it doesn't look
  448. // like it's already cached while it's still writing.
  449. string tempname = Path.Combine(directory, Path.GetRandomFileName());
  450. Stream stream = File.Open(tempname, FileMode.Create);
  451. BinaryFormatter bformatter = new BinaryFormatter();
  452. bformatter.Serialize(stream, asset);
  453. stream.Close();
  454. // Now that it's written, rename it so that it can be found.
  455. File.Move(tempname, filename);
  456. if (m_LogLevel >= 2)
  457. m_log.DebugFormat("[ASSET CACHE]: Cache Stored :: {0}", asset.ID);
  458. }
  459. catch (Exception e)
  460. {
  461. LogException(e);
  462. }
  463. finally
  464. {
  465. // Even if the write fails with an exception, we need to make sure
  466. // that we release the lock on that file, otherwise it'll never get
  467. // cached
  468. lock (m_CurrentlyWriting)
  469. {
  470. #if WAIT_ON_INPROGRESS_REQUESTS
  471. ManualResetEvent waitEvent;
  472. if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
  473. {
  474. m_CurrentlyWriting.Remove(filename);
  475. waitEvent.Set();
  476. }
  477. #else
  478. if (m_CurrentlyWriting.Contains(filename))
  479. {
  480. m_CurrentlyWriting.Remove(filename);
  481. }
  482. #endif
  483. }
  484. }
  485. }
  486. private static void LogException(Exception e)
  487. {
  488. string[] text = e.ToString().Split(new char[] { '\n' });
  489. foreach (string t in text)
  490. {
  491. m_log.ErrorFormat("[ASSET CACHE]: {0} ", t);
  492. }
  493. }
  494. }
  495. }