OpenSim
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros
FlotsamAssetCache.cs
Go to the documentation of this file.
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 
28 // Uncomment to make asset Get requests for existing
29 // #define WAIT_ON_INPROGRESS_REQUESTS
30 
31 using System;
32 using System.IO;
33 using System.Collections.Generic;
34 using System.Linq;
35 using System.Reflection;
36 using System.Runtime.Serialization;
37 using System.Runtime.Serialization.Formatters.Binary;
38 using System.Threading;
39 using System.Timers;
40 using log4net;
41 using Nini.Config;
42 using Mono.Addins;
43 using OpenMetaverse;
44 using OpenSim.Framework;
45 using OpenSim.Framework.Console;
46 using OpenSim.Framework.Monitoring;
47 using OpenSim.Region.Framework.Interfaces;
48 using OpenSim.Region.Framework.Scenes;
49 using OpenSim.Services.Interfaces;
50 
51 
52 //[assembly: Addin("FlotsamAssetCache", "1.1")]
53 //[assembly: AddinDependency("OpenSim", "0.8.1")]
54 
55 namespace OpenSim.Region.CoreModules.Asset
56 {
57  [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "FlotsamAssetCache")]
59  {
60  private static readonly ILog m_log =
61  LogManager.GetLogger(
62  MethodBase.GetCurrentMethod().DeclaringType);
63 
64  private bool m_Enabled;
65 
66  private const string m_ModuleName = "FlotsamAssetCache";
67  private const string m_DefaultCacheDirectory = "./assetcache";
68  private string m_CacheDirectory = m_DefaultCacheDirectory;
69 
70  private readonly List<char> m_InvalidChars = new List<char>();
71 
72  private int m_LogLevel = 0;
73  private ulong m_HitRateDisplay = 100; // How often to display hit statistics, given in requests
74 
75  private static ulong m_Requests;
76  private static ulong m_RequestsForInprogress;
77  private static ulong m_DiskHits;
78  private static ulong m_MemoryHits;
79 
80 #if WAIT_ON_INPROGRESS_REQUESTS
81  private Dictionary<string, ManualResetEvent> m_CurrentlyWriting = new Dictionary<string, ManualResetEvent>();
82  private int m_WaitOnInprogressTimeout = 3000;
83 #else
84  private HashSet<string> m_CurrentlyWriting = new HashSet<string>();
85 #endif
86 
87  private bool m_FileCacheEnabled = true;
88 
89  private ExpiringCache<string, AssetBase> m_MemoryCache;
90  private bool m_MemoryCacheEnabled = false;
91 
92  // Expiration is expressed in hours.
93  private const double m_DefaultMemoryExpiration = 2;
94  private const double m_DefaultFileExpiration = 48;
95  private TimeSpan m_MemoryExpiration = TimeSpan.FromHours(m_DefaultMemoryExpiration);
96  private TimeSpan m_FileExpiration = TimeSpan.FromHours(m_DefaultFileExpiration);
97  private TimeSpan m_FileExpirationCleanupTimer = TimeSpan.FromHours(0.166);
98 
99  private static int m_CacheDirectoryTiers = 1;
100  private static int m_CacheDirectoryTierLen = 3;
101  private static int m_CacheWarnAt = 30000;
102 
103  private System.Timers.Timer m_CacheCleanTimer;
104 
105  private IAssetService m_AssetService;
106  private List<Scene> m_Scenes = new List<Scene>();
107 
109  {
110  m_InvalidChars.AddRange(Path.GetInvalidPathChars());
111  m_InvalidChars.AddRange(Path.GetInvalidFileNameChars());
112  }
113 
114  public Type ReplaceableInterface
115  {
116  get { return null; }
117  }
118 
119  public string Name
120  {
121  get { return m_ModuleName; }
122  }
123 
124  public void Initialise(IConfigSource source)
125  {
126  IConfig moduleConfig = source.Configs["Modules"];
127 
128  if (moduleConfig != null)
129  {
130  string name = moduleConfig.GetString("AssetCaching", String.Empty);
131 
132  if (name == Name)
133  {
134  m_MemoryCache = new ExpiringCache<string, AssetBase>();
135  m_Enabled = true;
136 
137  m_log.InfoFormat("[FLOTSAM ASSET CACHE]: {0} enabled", this.Name);
138 
139  IConfig assetConfig = source.Configs["AssetCache"];
140  if (assetConfig == null)
141  {
142  m_log.Debug(
143  "[FLOTSAM ASSET CACHE]: AssetCache section missing from config (not copied config-include/FlotsamCache.ini.example? Using defaults.");
144  }
145  else
146  {
147  m_FileCacheEnabled = assetConfig.GetBoolean("FileCacheEnabled", m_FileCacheEnabled);
148  m_CacheDirectory = assetConfig.GetString("CacheDirectory", m_DefaultCacheDirectory);
149 
150  m_MemoryCacheEnabled = assetConfig.GetBoolean("MemoryCacheEnabled", m_MemoryCacheEnabled);
151  m_MemoryExpiration = TimeSpan.FromHours(assetConfig.GetDouble("MemoryCacheTimeout", m_DefaultMemoryExpiration));
152 
153  #if WAIT_ON_INPROGRESS_REQUESTS
154  m_WaitOnInprogressTimeout = assetConfig.GetInt("WaitOnInprogressTimeout", 3000);
155  #endif
156 
157  m_LogLevel = assetConfig.GetInt("LogLevel", m_LogLevel);
158  m_HitRateDisplay = (ulong)assetConfig.GetLong("HitRateDisplay", (long)m_HitRateDisplay);
159 
160  m_FileExpiration = TimeSpan.FromHours(assetConfig.GetDouble("FileCacheTimeout", m_DefaultFileExpiration));
161  m_FileExpirationCleanupTimer
162  = TimeSpan.FromHours(
163  assetConfig.GetDouble("FileCleanupTimer", m_FileExpirationCleanupTimer.TotalHours));
164 
165  m_CacheDirectoryTiers = assetConfig.GetInt("CacheDirectoryTiers", m_CacheDirectoryTiers);
166  m_CacheDirectoryTierLen = assetConfig.GetInt("CacheDirectoryTierLength", m_CacheDirectoryTierLen);
167 
168  m_CacheWarnAt = assetConfig.GetInt("CacheWarnAt", m_CacheWarnAt);
169  }
170 
171  m_log.InfoFormat("[FLOTSAM ASSET CACHE]: Cache Directory {0}", m_CacheDirectory);
172 
173  if (m_FileCacheEnabled && (m_FileExpiration > TimeSpan.Zero) && (m_FileExpirationCleanupTimer > TimeSpan.Zero))
174  {
175  m_CacheCleanTimer = new System.Timers.Timer(m_FileExpirationCleanupTimer.TotalMilliseconds);
176  m_CacheCleanTimer.AutoReset = true;
177  m_CacheCleanTimer.Elapsed += CleanupExpiredFiles;
178  lock (m_CacheCleanTimer)
179  m_CacheCleanTimer.Start();
180  }
181 
182  if (m_CacheDirectoryTiers < 1)
183  {
184  m_CacheDirectoryTiers = 1;
185  }
186  else if (m_CacheDirectoryTiers > 3)
187  {
188  m_CacheDirectoryTiers = 3;
189  }
190 
191  if (m_CacheDirectoryTierLen < 1)
192  {
193  m_CacheDirectoryTierLen = 1;
194  }
195  else if (m_CacheDirectoryTierLen > 4)
196  {
197  m_CacheDirectoryTierLen = 4;
198  }
199 
200  MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache status", "fcache status", "Display cache status", HandleConsoleCommand);
201  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);
202  MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache assets", "fcache assets", "Attempt a deep scan and cache of all assets in all scenes", HandleConsoleCommand);
203  MainConsole.Instance.Commands.AddCommand("Assets", true, "fcache expire", "fcache expire <datetime>", "Purge cached assets older then the specified date/time", HandleConsoleCommand);
204  }
205  }
206  }
207 
208  public void PostInitialise()
209  {
210  }
211 
212  public void Close()
213  {
214  }
215 
216  public void AddRegion(Scene scene)
217  {
218  if (m_Enabled)
219  {
220  scene.RegisterModuleInterface<IImprovedAssetCache>(this);
221  m_Scenes.Add(scene);
222 
223  }
224  }
225 
226  public void RemoveRegion(Scene scene)
227  {
228  if (m_Enabled)
229  {
230  scene.UnregisterModuleInterface<IImprovedAssetCache>(this);
231  m_Scenes.Remove(scene);
232  }
233  }
234 
235  public void RegionLoaded(Scene scene)
236  {
237  if (m_Enabled && m_AssetService == null)
238  m_AssetService = scene.RequestModuleInterface<IAssetService>();
239  }
240 
242  // IImprovedAssetCache
243  //
244 
245  private void UpdateMemoryCache(string key, AssetBase asset)
246  {
247  m_MemoryCache.AddOrUpdate(key, asset, m_MemoryExpiration);
248  }
249 
250  private void UpdateFileCache(string key, AssetBase asset)
251  {
252  string filename = GetFileName(key);
253 
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 
278 #else
279  if (m_CurrentlyWriting.Contains(filename))
280  {
281  return;
282  }
283  else
284  {
285  m_CurrentlyWriting.Add(filename);
286  }
287 #endif
288 
289  }
290 
291  Util.FireAndForget(
292  delegate { WriteFileCache(filename, asset); }, null, "FlotsamAssetCache.UpdateFileCache");
293  }
294  }
295  catch (Exception e)
296  {
297  m_log.ErrorFormat(
298  "[FLOTSAM ASSET CACHE]: Failed to update cache for asset {0}. Exception {1} {2}",
299  asset.ID, e.Message, e.StackTrace);
300  }
301  }
302 
303  public void Cache(AssetBase asset)
304  {
305  // TODO: Spawn this off to some seperate thread to do the actual writing
306  if (asset != null)
307  {
308  //m_log.DebugFormat("[FLOTSAM ASSET CACHE]: Caching asset with id {0}", asset.ID);
309 
310  if (m_MemoryCacheEnabled)
311  UpdateMemoryCache(asset.ID, asset);
312 
313  if (m_FileCacheEnabled)
314  UpdateFileCache(asset.ID, asset);
315  }
316  }
317 
323  private bool UpdateFileLastAccessTime(string filename)
324  {
325  try
326  {
327  File.SetLastAccessTime(filename, DateTime.Now);
328  return true;
329  }
330  catch
331  {
332  return false;
333  }
334  }
335 
341  private AssetBase GetFromMemoryCache(string id)
342  {
343  AssetBase asset = null;
344 
345  if (m_MemoryCache.TryGetValue(id, out asset))
346  m_MemoryHits++;
347 
348  return asset;
349  }
350 
351  private bool CheckFromMemoryCache(string id)
352  {
353  return m_MemoryCache.Contains(id);
354  }
355 
361  private AssetBase GetFromFileCache(string id)
362  {
363  string filename = GetFileName(id);
364 
365 #if WAIT_ON_INPROGRESS_REQUESTS
366  // Check if we're already downloading this asset. If so, try to wait for it to
367  // download.
368  if (m_WaitOnInprogressTimeout > 0)
369  {
370  m_RequestsForInprogress++;
371 
372  ManualResetEvent waitEvent;
373  if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
374  {
375  waitEvent.WaitOne(m_WaitOnInprogressTimeout);
376  return Get(id);
377  }
378  }
379 #else
380  // Track how often we have the problem that an asset is requested while
381  // it is still being downloaded by a previous request.
382  if (m_CurrentlyWriting.Contains(filename))
383  {
384  m_RequestsForInprogress++;
385  return null;
386  }
387 #endif
388 
389  AssetBase asset = null;
390 
391  if (File.Exists(filename))
392  {
393  try
394  {
395  using (FileStream stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
396  {
397  BinaryFormatter bformatter = new BinaryFormatter();
398 
399  asset = (AssetBase)bformatter.Deserialize(stream);
400 
401  m_DiskHits++;
402  }
403  }
404  catch (System.Runtime.Serialization.SerializationException e)
405  {
406  m_log.WarnFormat(
407  "[FLOTSAM ASSET CACHE]: Failed to get file {0} for asset {1}. Exception {2} {3}",
408  filename, id, e.Message, e.StackTrace);
409 
410  // If there was a problem deserializing the asset, the asset may
411  // either be corrupted OR was serialized under an old format
412  // {different version of AssetBase} -- we should attempt to
413  // delete it and re-cache
414  File.Delete(filename);
415  }
416  catch (Exception e)
417  {
418  m_log.WarnFormat(
419  "[FLOTSAM ASSET CACHE]: Failed to get file {0} for asset {1}. Exception {2} {3}",
420  filename, id, e.Message, e.StackTrace);
421  }
422  }
423 
424  return asset;
425  }
426 
427  private bool CheckFromFileCache(string id)
428  {
429  bool found = false;
430 
431  string filename = GetFileName(id);
432 
433  if (File.Exists(filename))
434  {
435  try
436  {
437  using (FileStream stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
438  {
439  if (stream != null)
440  found = true;
441  }
442  }
443  catch (Exception e)
444  {
445  m_log.ErrorFormat(
446  "[FLOTSAM ASSET CACHE]: Failed to check file {0} for asset {1}. Exception {2} {3}",
447  filename, id, e.Message, e.StackTrace);
448  }
449  }
450 
451  return found;
452  }
453 
454  public AssetBase Get(string id)
455  {
456  m_Requests++;
457 
458  AssetBase asset = null;
459 
460  if (m_MemoryCacheEnabled)
461  asset = GetFromMemoryCache(id);
462 
463  if (asset == null && m_FileCacheEnabled)
464  {
465  asset = GetFromFileCache(id);
466 
467  if (m_MemoryCacheEnabled && asset != null)
468  UpdateMemoryCache(id, asset);
469  }
470 
471  if (((m_LogLevel >= 1)) && (m_HitRateDisplay != 0) && (m_Requests % m_HitRateDisplay == 0))
472  {
473  m_log.InfoFormat("[FLOTSAM ASSET CACHE]: Cache Get :: {0} :: {1}", id, asset == null ? "Miss" : "Hit");
474 
475  GenerateCacheHitReport().ForEach(l => m_log.InfoFormat("[FLOTSAM ASSET CACHE]: {0}", l));
476  }
477 
478  return asset;
479  }
480 
481  public bool Check(string id)
482  {
483  if (m_MemoryCacheEnabled && CheckFromMemoryCache(id))
484  return true;
485 
486  if (m_FileCacheEnabled && CheckFromFileCache(id))
487  return true;
488  return false;
489  }
490 
491  public AssetBase GetCached(string id)
492  {
493  return Get(id);
494  }
495 
496  public void Expire(string id)
497  {
498  if (m_LogLevel >= 2)
499  m_log.DebugFormat("[FLOTSAM ASSET CACHE]: Expiring Asset {0}", id);
500 
501  try
502  {
503  if (m_FileCacheEnabled)
504  {
505  string filename = GetFileName(id);
506  if (File.Exists(filename))
507  {
508  File.Delete(filename);
509  }
510  }
511 
512  if (m_MemoryCacheEnabled)
513  m_MemoryCache.Remove(id);
514  }
515  catch (Exception e)
516  {
517  m_log.WarnFormat(
518  "[FLOTSAM ASSET CACHE]: Failed to expire cached file {0}. Exception {1} {2}",
519  id, e.Message, e.StackTrace);
520  }
521  }
522 
523  public void Clear()
524  {
525  if (m_LogLevel >= 2)
526  m_log.Debug("[FLOTSAM ASSET CACHE]: Clearing caches.");
527 
528  if (m_FileCacheEnabled)
529  {
530  foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
531  {
532  Directory.Delete(dir);
533  }
534  }
535 
536  if (m_MemoryCacheEnabled)
537  m_MemoryCache.Clear();
538  }
539 
540  private void CleanupExpiredFiles(object source, ElapsedEventArgs e)
541  {
542  if (m_LogLevel >= 2)
543  m_log.DebugFormat("[FLOTSAM ASSET CACHE]: Checking for expired files older then {0}.", m_FileExpiration);
544 
545  // Purge all files last accessed prior to this point
546  DateTime purgeLine = DateTime.Now - m_FileExpiration;
547 
548  // An asset cache may contain local non-temporary assets that are not in the asset service. Therefore,
549  // before cleaning up expired files we must scan the objects in the scene to make sure that we retain
550  // such local assets if they have not been recently accessed.
551  TouchAllSceneAssets(false);
552 
553  foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
554  {
555  CleanExpiredFiles(dir, purgeLine);
556  }
557  }
558 
566  private void CleanExpiredFiles(string dir, DateTime purgeLine)
567  {
568  try
569  {
570  foreach (string file in Directory.GetFiles(dir))
571  {
572  if (File.GetLastAccessTime(file) < purgeLine)
573  {
574  File.Delete(file);
575  }
576  }
577 
578  // Recurse into lower tiers
579  foreach (string subdir in Directory.GetDirectories(dir))
580  {
581  CleanExpiredFiles(subdir, purgeLine);
582  }
583 
584  // Check if a tier directory is empty, if so, delete it
585  int dirSize = Directory.GetFiles(dir).Length + Directory.GetDirectories(dir).Length;
586  if (dirSize == 0)
587  {
588  Directory.Delete(dir);
589  }
590  else if (dirSize >= m_CacheWarnAt)
591  {
592  m_log.WarnFormat(
593  "[FLOTSAM ASSET CACHE]: Cache folder exceeded CacheWarnAt limit {0} {1}. Suggest increasing tiers, tier length, or reducing cache expiration",
594  dir, dirSize);
595  }
596  }
597  catch (DirectoryNotFoundException)
598  {
599  // If we get here, another node on the same box has
600  // already removed the directory. Continue with next.
601  }
602  catch (Exception e)
603  {
604  m_log.Warn(
605  string.Format("[FLOTSAM ASSET CACHE]: Could not complete clean of expired files in {0}, exception ", dir), e);
606  }
607  }
608 
614  private string GetFileName(string id)
615  {
616  // Would it be faster to just hash the darn thing?
617  foreach (char c in m_InvalidChars)
618  {
619  id = id.Replace(c, '_');
620  }
621 
622  string path = m_CacheDirectory;
623  for (int p = 1; p <= m_CacheDirectoryTiers; p++)
624  {
625  string pathPart = id.Substring((p - 1) * m_CacheDirectoryTierLen, m_CacheDirectoryTierLen);
626  path = Path.Combine(path, pathPart);
627  }
628 
629  return Path.Combine(path, id);
630  }
631 
638  private void WriteFileCache(string filename, AssetBase asset)
639  {
640  Stream stream = null;
641 
642  // Make sure the target cache directory exists
643  string directory = Path.GetDirectoryName(filename);
644 
645  // Write file first to a temp name, so that it doesn't look
646  // like it's already cached while it's still writing.
647  string tempname = Path.Combine(directory, Path.GetRandomFileName());
648 
649  try
650  {
651  try
652  {
653  if (!Directory.Exists(directory))
654  {
655  Directory.CreateDirectory(directory);
656  }
657 
658  stream = File.Open(tempname, FileMode.Create);
659  BinaryFormatter bformatter = new BinaryFormatter();
660  bformatter.Serialize(stream, asset);
661  }
662  catch (IOException e)
663  {
664  m_log.WarnFormat(
665  "[FLOTSAM ASSET CACHE]: Failed to write asset {0} to temporary location {1} (final {2}) on cache in {3}. Exception {4} {5}.",
666  asset.ID, tempname, filename, directory, e.Message, e.StackTrace);
667 
668  return;
669  }
670  finally
671  {
672  if (stream != null)
673  stream.Close();
674  }
675 
676  try
677  {
678  // Now that it's written, rename it so that it can be found.
679  //
680  // File.Copy(tempname, filename, true);
681  // File.Delete(tempname);
682  //
683  // For a brief period, this was done as a separate copy and then temporary file delete operation to
684  // avoid an IOException caused by move if some competing thread had already written the file.
685  // However, this causes exceptions on Windows when other threads attempt to read a file
686  // which is still being copied. So instead, go back to moving the file and swallow any IOException.
687  //
688  // This situation occurs fairly rarely anyway. We assume in this that moves are atomic on the
689  // filesystem.
690  File.Move(tempname, filename);
691 
692  if (m_LogLevel >= 2)
693  m_log.DebugFormat("[FLOTSAM ASSET CACHE]: Cache Stored :: {0}", asset.ID);
694  }
695  catch (IOException)
696  {
697  // If we see an IOException here it's likely that some other competing thread has written the
698  // cache file first, so ignore. Other IOException errors (e.g. filesystem full) should be
699  // signally by the earlier temporary file writing code.
700  }
701  }
702  finally
703  {
704  // Even if the write fails with an exception, we need to make sure
705  // that we release the lock on that file, otherwise it'll never get
706  // cached
707  lock (m_CurrentlyWriting)
708  {
709 #if WAIT_ON_INPROGRESS_REQUESTS
710  ManualResetEvent waitEvent;
711  if (m_CurrentlyWriting.TryGetValue(filename, out waitEvent))
712  {
713  m_CurrentlyWriting.Remove(filename);
714  waitEvent.Set();
715  }
716 #else
717  m_CurrentlyWriting.Remove(filename);
718 #endif
719  }
720  }
721  }
722 
728  private int GetFileCacheCount(string dir)
729  {
730  int count = Directory.GetFiles(dir).Length;
731 
732  foreach (string subdir in Directory.GetDirectories(dir))
733  {
734  count += GetFileCacheCount(subdir);
735  }
736 
737  return count;
738  }
739 
744  private void StampRegionStatusFile(UUID regionID)
745  {
746  string RegionCacheStatusFile = Path.Combine(m_CacheDirectory, "RegionStatus_" + regionID.ToString() + ".fac");
747 
748  try
749  {
750  if (File.Exists(RegionCacheStatusFile))
751  {
752  File.SetLastWriteTime(RegionCacheStatusFile, DateTime.Now);
753  }
754  else
755  {
756  File.WriteAllText(
757  RegionCacheStatusFile,
758  "Please do not delete this file unless you are manually clearing your Flotsam Asset Cache.");
759  }
760  }
761  catch (Exception e)
762  {
763  m_log.Warn(
764  string.Format(
765  "[FLOTSAM ASSET CACHE]: Could not stamp region status file for region {0}. Exception ",
766  regionID),
767  e);
768  }
769  }
770 
780  private int TouchAllSceneAssets(bool storeUncached)
781  {
782  UuidGatherer gatherer = new UuidGatherer(m_AssetService);
783 
784  Dictionary<UUID, bool> assetsFound = new Dictionary<UUID, bool>();
785 
786  foreach (Scene s in m_Scenes)
787  {
788  StampRegionStatusFile(s.RegionInfo.RegionID);
789 
790  s.ForEachSOG(delegate(SceneObjectGroup e)
791  {
792  gatherer.AddForInspection(e);
793  gatherer.GatherAll();
794 
795  foreach (UUID assetID in gatherer.GatheredUuids.Keys)
796  {
797  if (!assetsFound.ContainsKey(assetID))
798  {
799  string filename = GetFileName(assetID.ToString());
800 
801  if (File.Exists(filename))
802  {
803  UpdateFileLastAccessTime(filename);
804  }
805  else if (storeUncached)
806  {
807  AssetBase cachedAsset = m_AssetService.Get(assetID.ToString());
808  if (cachedAsset == null && gatherer.GatheredUuids[assetID] != (sbyte)AssetType.Unknown)
809  assetsFound[assetID] = false;
810  else
811  assetsFound[assetID] = true;
812  }
813  }
814  else if (!assetsFound[assetID])
815  {
816  m_log.DebugFormat(
817  "[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",
818  assetID, gatherer.GatheredUuids[assetID], e.Name, e.AbsolutePosition, s.Name);
819  }
820  }
821 
822  gatherer.GatheredUuids.Clear();
823  });
824  }
825 
826  return assetsFound.Count;
827  }
828 
832  private void ClearFileCache()
833  {
834  foreach (string dir in Directory.GetDirectories(m_CacheDirectory))
835  {
836  try
837  {
838  Directory.Delete(dir, true);
839  }
840  catch (Exception e)
841  {
842  m_log.WarnFormat(
843  "[FLOTSAM ASSET CACHE]: Couldn't clear asset cache directory {0} from {1}. Exception {2} {3}",
844  dir, m_CacheDirectory, e.Message, e.StackTrace);
845  }
846  }
847 
848  foreach (string file in Directory.GetFiles(m_CacheDirectory))
849  {
850  try
851  {
852  File.Delete(file);
853  }
854  catch (Exception e)
855  {
856  m_log.WarnFormat(
857  "[FLOTSAM ASSET CACHE]: Couldn't clear asset cache file {0} from {1}. Exception {1} {2}",
858  file, m_CacheDirectory, e.Message, e.StackTrace);
859  }
860  }
861  }
862 
863  private List<string> GenerateCacheHitReport()
864  {
865  List<string> outputLines = new List<string>();
866 
867  double fileHitRate = (double)m_DiskHits / m_Requests * 100.0;
868  outputLines.Add(
869  string.Format("File Hit Rate: {0}% for {1} requests", fileHitRate.ToString("0.00"), m_Requests));
870 
871  if (m_MemoryCacheEnabled)
872  {
873  double memHitRate = (double)m_MemoryHits / m_Requests * 100.0;
874 
875  outputLines.Add(
876  string.Format("Memory Hit Rate: {0}% for {1} requests", memHitRate.ToString("0.00"), m_Requests));
877  }
878 
879  outputLines.Add(
880  string.Format(
881  "Unnecessary requests due to requests for assets that are currently downloading: {0}",
882  m_RequestsForInprogress));
883 
884  return outputLines;
885  }
886 
887  #region Console Commands
888  private void HandleConsoleCommand(string module, string[] cmdparams)
889  {
890  ICommandConsole con = MainConsole.Instance;
891 
892  if (cmdparams.Length >= 2)
893  {
894  string cmd = cmdparams[1];
895 
896  switch (cmd)
897  {
898  case "status":
899  if (m_MemoryCacheEnabled)
900  con.OutputFormat("Memory Cache: {0} assets", m_MemoryCache.Count);
901  else
902  con.OutputFormat("Memory cache disabled");
903 
904  if (m_FileCacheEnabled)
905  {
906  int fileCount = GetFileCacheCount(m_CacheDirectory);
907  con.OutputFormat("File Cache: {0} assets", fileCount);
908  }
909  else
910  {
911  con.Output("File cache disabled");
912  }
913 
914  GenerateCacheHitReport().ForEach(l => con.Output(l));
915 
916  if (m_FileCacheEnabled)
917  {
918  con.Output("Deep scans have previously been performed on the following regions:");
919 
920  foreach (string s in Directory.GetFiles(m_CacheDirectory, "*.fac"))
921  {
922  string RegionID = s.Remove(0,s.IndexOf("_")).Replace(".fac","");
923  DateTime RegionDeepScanTMStamp = File.GetLastWriteTime(s);
924  con.OutputFormat("Region: {0}, {1}", RegionID, RegionDeepScanTMStamp.ToString("MM/dd/yyyy hh:mm:ss"));
925  }
926  }
927 
928  break;
929 
930  case "clear":
931  if (cmdparams.Length < 2)
932  {
933  con.Output("Usage is fcache clear [file] [memory]");
934  break;
935  }
936 
937  bool clearMemory = false, clearFile = false;
938 
939  if (cmdparams.Length == 2)
940  {
941  clearMemory = true;
942  clearFile = true;
943  }
944  foreach (string s in cmdparams)
945  {
946  if (s.ToLower() == "memory")
947  clearMemory = true;
948  else if (s.ToLower() == "file")
949  clearFile = true;
950  }
951 
952  if (clearMemory)
953  {
954  if (m_MemoryCacheEnabled)
955  {
956  m_MemoryCache.Clear();
957  con.Output("Memory cache cleared.");
958  }
959  else
960  {
961  con.Output("Memory cache not enabled.");
962  }
963  }
964 
965  if (clearFile)
966  {
967  if (m_FileCacheEnabled)
968  {
969  ClearFileCache();
970  con.Output("File cache cleared.");
971  }
972  else
973  {
974  con.Output("File cache not enabled.");
975  }
976  }
977 
978  break;
979 
980  case "assets":
981  con.Output("Ensuring assets are cached for all scenes.");
982 
983  WorkManager.RunInThread(delegate
984  {
985  int assetReferenceTotal = TouchAllSceneAssets(true);
986  con.OutputFormat("Completed check with {0} assets.", assetReferenceTotal);
987  }, null, "TouchAllSceneAssets");
988 
989  break;
990 
991  case "expire":
992  if (cmdparams.Length < 3)
993  {
994  con.OutputFormat("Invalid parameters for Expire, please specify a valid date & time", cmd);
995  break;
996  }
997 
998  string s_expirationDate = "";
999  DateTime expirationDate;
1000 
1001  if (cmdparams.Length > 3)
1002  {
1003  s_expirationDate = string.Join(" ", cmdparams, 2, cmdparams.Length - 2);
1004  }
1005  else
1006  {
1007  s_expirationDate = cmdparams[2];
1008  }
1009 
1010  if (!DateTime.TryParse(s_expirationDate, out expirationDate))
1011  {
1012  con.OutputFormat("{0} is not a valid date & time", cmd);
1013  break;
1014  }
1015 
1016  if (m_FileCacheEnabled)
1017  CleanExpiredFiles(m_CacheDirectory, expirationDate);
1018  else
1019  con.OutputFormat("File cache not active, not clearing.");
1020 
1021  break;
1022  default:
1023  con.OutputFormat("Unknown command {0}", cmd);
1024  break;
1025  }
1026  }
1027  else if (cmdparams.Length == 1)
1028  {
1029  con.Output("fcache assets - Attempt a deep cache of all assets in all scenes");
1030  con.Output("fcache expire <datetime> - Purge assets older then the specified date & time");
1031  con.Output("fcache clear [file] [memory] - Remove cached assets");
1032  con.Output("fcache status - Display cache status");
1033  }
1034  }
1035 
1036  #endregion
1037 
1038  #region IAssetService Members
1039 
1040  public AssetMetadata GetMetadata(string id)
1041  {
1042  AssetBase asset = Get(id);
1043  return asset.Metadata;
1044  }
1045 
1046  public byte[] GetData(string id)
1047  {
1048  AssetBase asset = Get(id);
1049  return asset.Data;
1050  }
1051 
1052  public bool Get(string id, object sender, AssetRetrieved handler)
1053  {
1054  AssetBase asset = Get(id);
1055  handler(id, sender, asset);
1056  return true;
1057  }
1058 
1059  public bool[] AssetsExist(string[] ids)
1060  {
1061  bool[] exist = new bool[ids.Length];
1062 
1063  for (int i = 0; i < ids.Length; i++)
1064  {
1065  exist[i] = Check(ids[i]);
1066  }
1067 
1068  return exist;
1069  }
1070 
1071  public string Store(AssetBase asset)
1072  {
1073  if (asset.FullID == UUID.Zero)
1074  {
1075  asset.FullID = UUID.Random();
1076  }
1077 
1078  Cache(asset);
1079 
1080  return asset.ID;
1081  }
1082 
1083  public bool UpdateContent(string id, byte[] data)
1084  {
1085  AssetBase asset = Get(id);
1086  asset.Data = data;
1087  Cache(asset);
1088  return true;
1089  }
1090 
1091  public bool Delete(string id)
1092  {
1093  Expire(id);
1094  return true;
1095  }
1096 
1097  #endregion
1098  }
1099 }
void Expire(string id)
Expire an asset from the cache.
bool[] AssetsExist(string[] ids)
Check if assets exist in the database.
void Initialise(IConfigSource source)
This is called to initialize the region module. For shared modules, this is called exactly once...
Gather uuids for a given entity.
Definition: UuidGatherer.cs:54
bool Check(string id)
Check whether an asset with the specified id exists in the cache.
AssetBase GetCached(string id)
Synchronously fetches an asset from the local cache only.
bool Get(string id, object sender, AssetRetrieved handler)
void RemoveRegion(Scene scene)
This is called whenever a Scene is removed. For shared modules, this can happen several times...
byte[] GetData(string id)
Get an asset's data, ignoring the metadata.
A scene object group is conceptually an object in the scene. The object is constituted of SceneObject...
AssetMetadata GetMetadata(string id)
Get an asset's metadata
void Cache(AssetBase asset)
Cache the specified asset.
OpenSim.Region.ScriptEngine.Shared.LSL_Types.LSLString key
Definition: ICM_Api.cs:31
Asset class. All Assets are reference by this class or a class derived from this class ...
Definition: AssetBase.cs:49
Non-texture assets
void RegionLoaded(Scene scene)
This will be called once for every scene loaded. In a shared module this will be multiple times in on...
IDictionary< UUID, sbyte > GatheredUuids
The dictionary of UUIDs gathered so far. If Complete == true then this is all the reachable UUIDs...
Definition: UuidGatherer.cs:67
bool UpdateContent(string id, byte[] data)
Update an asset's content
UUID FullID
Asset UUID
Definition: AssetBase.cs:168
void Close()
This is the inverse to Initialise. After a Close(), this instance won't be usable anymore...
delegate void AssetRetrieved(string id, Object sender, AssetBase asset)
void PostInitialise()
This is called exactly once after all the shared region-modules have been instanciated and IRegionMod...
AssetBase Get(string id)
Get an asset by its id.
void AddRegion(Scene scene)
This is called whenever a Scene is added. For shared modules, this can happen several times...
string ID
Asset MetaData ID (transferring from UUID to string ID)
Definition: AssetBase.cs:177
string Store(AssetBase asset)
Creates a new asset