FlotsamAssetCache.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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. /// </summary>
  88. [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule")]
  89. public class FlotsamAssetCache : ISharedRegionModule, IImprovedAssetCache
  90. {
  91. private static readonly ILog m_log =
  92. LogManager.GetLogger(
  93. MethodBase.GetCurrentMethod().DeclaringType);
  94. private bool m_Enabled = false;
  95. private const string m_ModuleName = "FlotsamAssetCache";
  96. private const string m_DefaultCacheDirectory = m_ModuleName;
  97. private string m_CacheDirectory = m_DefaultCacheDirectory;
  98. private List<char> m_InvalidChars = new List<char>();
  99. private int m_LogLevel = 1;
  100. private ulong m_HitRateDisplay = 1; // How often to display hit statistics, given in requests
  101. private static ulong m_Requests = 0;
  102. private static ulong m_RequestsForInprogress = 0;
  103. private static ulong m_DiskHits = 0;
  104. private static ulong m_MemoryHits = 0;
  105. private static double m_HitRateMemory = 0.0;
  106. private static double m_HitRateFile = 0.0;
  107. #if WAIT_ON_INPROGRESS_REQUESTS
  108. private Dictionary<string, ManualResetEvent> m_CurrentlyWriting = new Dictionary<string, ManualResetEvent>();
  109. private int m_WaitOnInprogressTimeout = 3000;
  110. #else
  111. private List<string> m_CurrentlyWriting = new List<string>();
  112. #endif
  113. private ExpiringCache<string, AssetBase> m_MemoryCache = new ExpiringCache<string, AssetBase>();
  114. private bool m_MemoryCacheEnabled = true;
  115. // Expiration is expressed in hours.
  116. private const double m_DefaultMemoryExpiration = 1.0;
  117. private const double m_DefaultFileExpiration = 48;
  118. private TimeSpan m_MemoryExpiration = TimeSpan.Zero;
  119. private TimeSpan m_FileExpiration = TimeSpan.Zero;
  120. private TimeSpan m_FileExpirationCleanupTimer = TimeSpan.Zero;
  121. private System.Timers.Timer m_CachCleanTimer = new System.Timers.Timer();
  122. public FlotsamAssetCache()
  123. {
  124. m_InvalidChars.AddRange(Path.GetInvalidPathChars());
  125. m_InvalidChars.AddRange(Path.GetInvalidFileNameChars());
  126. }
  127. public string Name
  128. {
  129. get { return m_ModuleName; }
  130. }
  131. public void Initialise(IConfigSource source)
  132. {
  133. IConfig moduleConfig = source.Configs["Modules"];
  134. if (moduleConfig != null)
  135. {
  136. string name = moduleConfig.GetString("AssetCaching", "");
  137. if (name == Name)
  138. {
  139. m_Enabled = true;
  140. m_log.InfoFormat("[ASSET CACHE]: {0} enabled", this.Name);
  141. IConfig assetConfig = source.Configs["AssetCache"];
  142. if (assetConfig == null)
  143. {
  144. m_log.Warn("[ASSET CACHE]: AssetCache missing from OpenSim.ini, using defaults.");
  145. m_log.InfoFormat("[ASSET CACHE]: Cache Directory", m_DefaultCacheDirectory);
  146. return;
  147. }
  148. m_CacheDirectory = assetConfig.GetString("CacheDirectory", m_DefaultCacheDirectory);
  149. m_log.InfoFormat("[ASSET CACHE]: Cache Directory", m_DefaultCacheDirectory);
  150. m_MemoryCacheEnabled = assetConfig.GetBoolean("MemoryCacheEnabled", true);
  151. m_MemoryExpiration = TimeSpan.FromHours(assetConfig.GetDouble("MemoryCacheTimeout", m_DefaultMemoryExpiration));
  152. #if WAIT_ON_INPROGRESS_REQUESTS
  153. m_WaitOnInprogressTimeout = assetConfig.GetInt("WaitOnInprogressTimeout", 3000);
  154. #endif
  155. m_LogLevel = assetConfig.GetInt("LogLevel", 1);
  156. m_HitRateDisplay = (ulong)assetConfig.GetInt("HitRateDisplay", 1);
  157. m_FileExpiration = TimeSpan.FromHours(assetConfig.GetDouble("FileCacheTimeout", m_DefaultFileExpiration));
  158. m_FileExpirationCleanupTimer = TimeSpan.FromHours(assetConfig.GetDouble("FileCleanupTimer", m_DefaultFileExpiration));
  159. if ((m_FileExpiration > TimeSpan.Zero) && (m_FileExpirationCleanupTimer > TimeSpan.Zero))
  160. {
  161. m_CachCleanTimer.Interval = m_FileExpirationCleanupTimer.TotalMilliseconds;
  162. m_CachCleanTimer.AutoReset = true;
  163. m_CachCleanTimer.Elapsed += CleanupExpiredFiles;
  164. m_CachCleanTimer.Enabled = true;
  165. m_CachCleanTimer.Start();
  166. }
  167. else
  168. {
  169. m_CachCleanTimer.Enabled = false;
  170. }
  171. }
  172. }
  173. }
  174. public void PostInitialise()
  175. {
  176. }
  177. public void Close()
  178. {
  179. }
  180. public void AddRegion(Scene scene)
  181. {
  182. if (m_Enabled)
  183. scene.RegisterModuleInterface<IImprovedAssetCache>(this);
  184. }
  185. public void RemoveRegion(Scene scene)
  186. {
  187. }
  188. public void RegionLoaded(Scene scene)
  189. {
  190. }
  191. ////////////////////////////////////////////////////////////
  192. // IImprovedAssetCache
  193. //
  194. private void UpdateMemoryCache(string key, AssetBase asset)
  195. {
  196. if (m_MemoryCacheEnabled)
  197. {
  198. if (m_MemoryExpiration > TimeSpan.Zero)
  199. {
  200. m_MemoryCache.AddOrUpdate(key, asset, m_MemoryExpiration);
  201. }
  202. else
  203. {
  204. m_MemoryCache.AddOrUpdate(key, asset, DateTime.MaxValue);
  205. }
  206. }
  207. }
  208. public void Cache(AssetBase asset)
  209. {
  210. // TODO: Spawn this off to some seperate thread to do the actual writing
  211. if (asset != null)
  212. {
  213. UpdateMemoryCache(asset.ID, asset);
  214. string filename = GetFileName(asset.ID);
  215. try
  216. {
  217. // If the file is already cached, don't cache it, just touch it so access time is updated
  218. if (File.Exists(filename))
  219. {
  220. File.SetLastAccessTime(filename, DateTime.Now);
  221. } else {
  222. // Once we start writing, make sure we flag that we're writing
  223. // that object to the cache so that we don't try to write the
  224. // same file multiple times.
  225. lock (m_CurrentlyWriting)
  226. {
  227. #if WAIT_ON_INPROGRESS_REQUESTS
  228. if (m_CurrentlyWriting.ContainsKey(filename))
  229. {
  230. return;
  231. }
  232. else
  233. {
  234. m_CurrentlyWriting.Add(filename, new ManualResetEvent(false));
  235. }
  236. #else
  237. if (m_CurrentlyWriting.Contains(filename))
  238. {
  239. return;
  240. }
  241. else
  242. {
  243. m_CurrentlyWriting.Add(filename);
  244. }
  245. #endif
  246. }
  247. ThreadPool.QueueUserWorkItem(
  248. delegate
  249. {
  250. WriteFileCache(filename, asset);
  251. }
  252. );
  253. }
  254. }
  255. catch (Exception e)
  256. {
  257. LogException(e);
  258. }
  259. }
  260. }
  261. public AssetBase Get(string id)
  262. {
  263. m_Requests++;
  264. AssetBase asset = null;
  265. if (m_MemoryCacheEnabled && m_MemoryCache.TryGetValue(id, out asset))
  266. {
  267. m_MemoryHits++;
  268. }
  269. else
  270. {
  271. string filename = GetFileName(id);
  272. if (File.Exists(filename))
  273. {
  274. try
  275. {
  276. FileStream stream = File.Open(filename, FileMode.Open);
  277. BinaryFormatter bformatter = new BinaryFormatter();
  278. asset = (AssetBase)bformatter.Deserialize(stream);
  279. stream.Close();
  280. UpdateMemoryCache(id, asset);
  281. m_DiskHits++;
  282. }
  283. catch (System.Runtime.Serialization.SerializationException e)
  284. {
  285. LogException(e);
  286. // If there was a problem deserializing the asset, the asset may
  287. // either be corrupted OR was serialized under an old format
  288. // {different version of AssetBase} -- we should attempt to
  289. // delete it and re-cache
  290. File.Delete(filename);
  291. }
  292. catch (Exception e)
  293. {
  294. LogException(e);
  295. }
  296. }
  297. #if WAIT_ON_INPROGRESS_REQUESTS
  298. // Check if we're already downloading this asset. If so, try to wait for it to
  299. // download.
  300. if (m_WaitOnInprogressTimeout > 0)
  301. {
  302. m_RequestsForInprogress++;
  303. ManualResetEvent waitEvent;
  304. if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
  305. {
  306. waitEvent.WaitOne(m_WaitOnInprogressTimeout);
  307. return Get(id);
  308. }
  309. }
  310. #else
  311. // Track how often we have the problem that an asset is requested while
  312. // it is still being downloaded by a previous request.
  313. if (m_CurrentlyWriting.Contains(filename))
  314. {
  315. m_RequestsForInprogress++;
  316. }
  317. #endif
  318. }
  319. if (((m_LogLevel >= 1)) && (m_HitRateDisplay != 0) && (m_Requests % m_HitRateDisplay == 0))
  320. {
  321. m_HitRateFile = (double)m_DiskHits / m_Requests * 100.0;
  322. m_log.InfoFormat("[ASSET CACHE]: Cache Get :: {0} :: {1}", id, asset == null ? "Miss" : "Hit");
  323. m_log.InfoFormat("[ASSET CACHE]: File Hit Rate {0}% for {1} requests", m_HitRateFile.ToString("0.00"), m_Requests);
  324. if (m_MemoryCacheEnabled)
  325. {
  326. m_HitRateMemory = (double)m_MemoryHits / m_Requests * 100.0;
  327. m_log.InfoFormat("[ASSET CACHE]: Memory Hit Rate {0}% for {1} requests", m_HitRateMemory.ToString("0.00"), m_Requests);
  328. }
  329. m_log.InfoFormat("[ASSET CACHE]: {0} unnessesary requests due to requests for assets that are currently downloading.", m_RequestsForInprogress);
  330. }
  331. return asset;
  332. }
  333. public void Expire(string id)
  334. {
  335. if (m_LogLevel >= 2)
  336. m_log.DebugFormat("[ASSET CACHE]: Expiring Asset {0}.", id);
  337. try
  338. {
  339. string filename = GetFileName(id);
  340. if (File.Exists(filename))
  341. {
  342. File.Delete(filename);
  343. }
  344. if (m_MemoryCacheEnabled)
  345. m_MemoryCache.Remove(id);
  346. }
  347. catch (Exception e)
  348. {
  349. LogException(e);
  350. }
  351. }
  352. public void Clear()
  353. {
  354. if (m_LogLevel >= 2)
  355. m_log.Debug("[ASSET CACHE]: Clearing Cache.");
  356. foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
  357. {
  358. Directory.Delete(dir);
  359. }
  360. if (m_MemoryCacheEnabled)
  361. m_MemoryCache.Clear();
  362. }
  363. private void CleanupExpiredFiles(object source, ElapsedEventArgs e)
  364. {
  365. if (m_LogLevel >= 2)
  366. m_log.DebugFormat("[ASSET CACHE]: Checking for expired files older then {0}.", m_FileExpiration.ToString());
  367. foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
  368. {
  369. foreach (string file in Directory.GetFiles(dir))
  370. {
  371. if (DateTime.Now - File.GetLastAccessTime(file) > m_FileExpiration)
  372. {
  373. File.Delete(file);
  374. }
  375. }
  376. }
  377. }
  378. private string GetFileName(string id)
  379. {
  380. // Would it be faster to just hash the darn thing?
  381. foreach (char c in m_InvalidChars)
  382. {
  383. id = id.Replace(c, '_');
  384. }
  385. string p = id.Substring(id.Length - 4);
  386. p = Path.Combine(p, id);
  387. return Path.Combine(m_CacheDirectory, p);
  388. }
  389. private void WriteFileCache(string filename, AssetBase asset)
  390. {
  391. try
  392. {
  393. // Make sure the target cache directory exists
  394. string directory = Path.GetDirectoryName(filename);
  395. if (!Directory.Exists(directory))
  396. {
  397. Directory.CreateDirectory(directory);
  398. }
  399. // Write file first to a temp name, so that it doesn't look
  400. // like it's already cached while it's still writing.
  401. string tempname = Path.Combine(directory, Path.GetRandomFileName());
  402. Stream stream = File.Open(tempname, FileMode.Create);
  403. BinaryFormatter bformatter = new BinaryFormatter();
  404. bformatter.Serialize(stream, asset);
  405. stream.Close();
  406. // Now that it's written, rename it so that it can be found.
  407. File.Move(tempname, filename);
  408. if (m_LogLevel >= 2)
  409. m_log.DebugFormat("[ASSET CACHE]: Cache Stored :: {0}", asset.ID);
  410. }
  411. catch (Exception e)
  412. {
  413. LogException(e);
  414. }
  415. finally
  416. {
  417. // Even if the write fails with an exception, we need to make sure
  418. // that we release the lock on that file, otherwise it'll never get
  419. // cached
  420. lock (m_CurrentlyWriting)
  421. {
  422. #if WAIT_ON_INPROGRESS_REQUESTS
  423. ManualResetEvent waitEvent;
  424. if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
  425. {
  426. m_CurrentlyWriting.Remove(filename);
  427. waitEvent.Set();
  428. }
  429. #else
  430. if (m_CurrentlyWriting.Contains(filename))
  431. {
  432. m_CurrentlyWriting.Remove(filename);
  433. }
  434. #endif
  435. }
  436. }
  437. }
  438. private static void LogException(Exception e)
  439. {
  440. string[] text = e.ToString().Split(new char[] { '\n' });
  441. foreach (string t in text)
  442. {
  443. m_log.ErrorFormat("[ASSET CACHE]: {0} ", t);
  444. }
  445. }
  446. }
  447. }