OpenSimMirror/OpenSim/Framework/Cache.cs

587 lines
16 KiB
C#

/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the OpenSimulator Project nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections.Generic;
using OpenMetaverse;
namespace OpenSim.Framework
{
// The delegate we will use for performing fetch from backing store
//
public delegate Object FetchDelegate(string index);
public delegate bool ExpireDelegate(string index);
// Strategy
//
// Conservative = Minimize memory. Expire items quickly.
// Balanced = Expire items with few hits quickly.
// Aggressive = Keep cache full. Expire only when over 90% and adding
//
public enum CacheStrategy
{
Conservative = 0,
Balanced = 1,
Aggressive = 2
}
// Select classes to store data on different media
//
public enum CacheMedium
{
Memory = 0,
File = 1
}
public enum CacheFlags
{
CacheMissing = 1,
AllowUpdate = 2
}
// The base class of all cache objects. Implements comparison and sorting
// by the string member.
//
// This is not abstract because we need to instantiate it briefly as a
// method parameter
//
public class CacheItemBase : IEquatable<CacheItemBase>, IComparable<CacheItemBase>
{
public string uuid;
public DateTime entered;
public DateTime lastUsed;
public DateTime expires = new DateTime(0);
public int hits = 0;
public virtual Object Retrieve()
{
return null;
}
public virtual void Store(Object data)
{
}
public CacheItemBase(string index)
{
uuid = index;
entered = DateTime.UtcNow;
lastUsed = entered;
}
public CacheItemBase(string index, DateTime ttl)
{
uuid = index;
entered = DateTime.UtcNow;
lastUsed = entered;
expires = ttl;
}
public virtual bool Equals(CacheItemBase item)
{
return uuid == item.uuid;
}
public virtual int CompareTo(CacheItemBase item)
{
return uuid.CompareTo(item.uuid);
}
public virtual bool IsLocked()
{
return false;
}
}
// Simple in-memory storage. Boxes the object and stores it in a variable
//
public class MemoryCacheItem : CacheItemBase
{
private Object m_Data;
public MemoryCacheItem(string index) :
base(index)
{
}
public MemoryCacheItem(string index, DateTime ttl) :
base(index, ttl)
{
}
public MemoryCacheItem(string index, Object data) :
base(index)
{
Store(data);
}
public MemoryCacheItem(string index, DateTime ttl, Object data) :
base(index, ttl)
{
Store(data);
}
public override Object Retrieve()
{
return m_Data;
}
public override void Store(Object data)
{
m_Data = data;
}
}
// Simple persistent file storage
//
public class FileCacheItem : CacheItemBase
{
public FileCacheItem(string index) :
base(index)
{
}
public FileCacheItem(string index, DateTime ttl) :
base(index, ttl)
{
}
public FileCacheItem(string index, Object data) :
base(index)
{
Store(data);
}
public FileCacheItem(string index, DateTime ttl, Object data) :
base(index, ttl)
{
Store(data);
}
public override Object Retrieve()
{
//TODO: Add file access code
return null;
}
public override void Store(Object data)
{
//TODO: Add file access code
}
}
// The main cache class. This is the class you instantiate to create
// a cache
//
public class Cache
{
/// <summary>
/// Must only be accessed under lock.
/// </summary>
private List<CacheItemBase> m_Index = new List<CacheItemBase>();
/// <summary>
/// Must only be accessed under m_Index lock.
/// </summary>
private Dictionary<string, CacheItemBase> m_Lookup =
new Dictionary<string, CacheItemBase>();
private CacheStrategy m_Strategy;
private CacheMedium m_Medium;
private CacheFlags m_Flags = 0;
private int m_Size = 1024;
private TimeSpan m_DefaultTTL = new TimeSpan(0);
private DateTime m_nextExpire;
private TimeSpan m_expiresTime = new TimeSpan(0,0,30);
public ExpireDelegate OnExpire;
// Comparison interfaces
//
private class SortLRU : IComparer<CacheItemBase>
{
public int Compare(CacheItemBase a, CacheItemBase b)
{
if (a == null && b == null)
return 0;
if (a == null)
return -1;
if (b == null)
return 1;
return(a.lastUsed.CompareTo(b.lastUsed));
}
}
// same as above, reverse order
private class SortLRUrev : IComparer<CacheItemBase>
{
public int Compare(CacheItemBase a, CacheItemBase b)
{
if (a == null && b == null)
return 0;
if (a == null)
return -1;
if (b == null)
return 1;
return(b.lastUsed.CompareTo(a.lastUsed));
}
}
// Convenience constructors
//
public Cache()
{
m_Strategy = CacheStrategy.Balanced;
m_Medium = CacheMedium.Memory;
m_Flags = 0;
m_nextExpire = DateTime.UtcNow + m_expiresTime;
m_Strategy = CacheStrategy.Aggressive;
}
public Cache(CacheMedium medium) :
this(medium, CacheStrategy.Balanced)
{
}
public Cache(CacheMedium medium, CacheFlags flags) :
this(medium, CacheStrategy.Balanced, flags)
{
}
public Cache(CacheMedium medium, CacheStrategy strategy) :
this(medium, strategy, 0)
{
}
public Cache(CacheStrategy strategy, CacheFlags flags) :
this(CacheMedium.Memory, strategy, flags)
{
}
public Cache(CacheFlags flags) :
this(CacheMedium.Memory, CacheStrategy.Balanced, flags)
{
}
public Cache(CacheMedium medium, CacheStrategy strategy,
CacheFlags flags)
{
m_Strategy = strategy;
m_Medium = medium;
m_Flags = flags;
}
// Count of the items currently in cache
//
public int Count
{
get { lock (m_Index) { return m_Index.Count; } }
}
// Maximum number of items this cache will hold
//
public int Size
{
get { return m_Size; }
set { SetSize(value); }
}
private void SetSize(int newSize)
{
lock (m_Index)
{
int target = newSize;
if(m_Strategy == CacheStrategy.Aggressive)
target = (int)(newSize * 0.9);
if(Count > target)
{
m_Index.Sort(new SortLRUrev());
m_Index.RemoveRange(newSize, Count - target);
m_Lookup.Clear();
foreach (CacheItemBase item in m_Index)
m_Lookup[item.uuid] = item;
}
m_Size = newSize;
}
}
public TimeSpan DefaultTTL
{
get { return m_DefaultTTL; }
set { m_DefaultTTL = value; }
}
// Get an item from cache. Return the raw item, not it's data
//
protected virtual CacheItemBase GetItem(string index)
{
CacheItemBase item = null;
lock (m_Index)
{
if (m_Lookup.ContainsKey(index))
item = m_Lookup[index];
if (item == null)
{
Expire(true);
return null;
}
item.hits++;
item.lastUsed = DateTime.UtcNow;
Expire(true);
}
return item;
}
// Get an item from cache. Do not try to fetch from source if not
// present. Just return null
//
public virtual Object Get(string index)
{
CacheItemBase item = GetItem(index);
if (item == null)
return null;
return item.Retrieve();
}
// Fetch an object from backing store if not cached, serve from
// cache if it is.
//
public virtual Object Get(string index, FetchDelegate fetch)
{
CacheItemBase item = GetItem(index);
if (item != null)
return item.Retrieve();
Object data = fetch(index);
if (data == null)
{
if((m_Flags & CacheFlags.CacheMissing) != 0)
{
lock (m_Index)
{
CacheItemBase missing = new CacheItemBase(index);
if (!m_Index.Contains(missing))
{
m_Index.Add(missing);
m_Lookup[index] = missing;
}
}
}
return null;
}
Store(index, data);
return data;
}
// Find an object in cache by delegate.
//
public Object Find(Predicate<CacheItemBase> d)
{
CacheItemBase item;
lock (m_Index)
item = m_Index.Find(d);
if (item == null)
return null;
return item.Retrieve();
}
public virtual void Store(string index, Object data)
{
Type container;
switch (m_Medium)
{
case CacheMedium.Memory:
container = typeof(MemoryCacheItem);
break;
case CacheMedium.File:
return;
default:
return;
}
Store(index, data, container);
}
public virtual void Store(string index, Object data, Type container)
{
Store(index, data, container, new Object[] { index });
}
public virtual void Store(string index, Object data, Type container,
Object[] parameters)
{
CacheItemBase item;
lock (m_Index)
{
Expire(false);
if (m_Index.Contains(new CacheItemBase(index)))
{
if ((m_Flags & CacheFlags.AllowUpdate) != 0)
{
item = GetItem(index);
item.hits++;
item.lastUsed = DateTime.UtcNow;
if (m_DefaultTTL.Ticks != 0)
item.expires = DateTime.UtcNow + m_DefaultTTL;
item.Store(data);
}
return;
}
item = (CacheItemBase)Activator.CreateInstance(container,
parameters);
if (m_DefaultTTL.Ticks != 0)
item.expires = DateTime.UtcNow + m_DefaultTTL;
m_Index.Add(item);
m_Lookup[index] = item;
}
item.Store(data);
}
/// <summary>
/// Expire items as appropriate.
/// </summary>
/// <remarks>
/// Callers must lock m_Index.
/// </remarks>
/// <param name='getting'></param>
protected virtual void Expire(bool getting)
{
if (getting && (m_Strategy == CacheStrategy.Aggressive))
return;
DateTime now = DateTime.UtcNow;
if(now < m_nextExpire)
return;
m_nextExpire = now + m_expiresTime;
if (m_DefaultTTL.Ticks != 0)
{
foreach (CacheItemBase item in new List<CacheItemBase>(m_Index))
{
if (item.expires.Ticks == 0 ||
item.expires <= now)
{
m_Index.Remove(item);
m_Lookup.Remove(item.uuid);
}
}
}
switch (m_Strategy)
{
case CacheStrategy.Aggressive:
int target = (int)((float)Size * 0.9);
if (Count < target) // Cover ridiculous cache sizes
return;
target = (int)((float)Size * 0.8);
m_Index.Sort(new SortLRUrev());
ExpireDelegate doExpire = OnExpire;
if (doExpire != null)
{
List<CacheItemBase> candidates =
m_Index.GetRange(target, Count - target);
foreach (CacheItemBase i in candidates)
{
if (doExpire(i.uuid))
{
m_Index.Remove(i);
m_Lookup.Remove(i.uuid);
}
}
}
else
{
m_Index.RemoveRange(target, Count - target);
m_Lookup.Clear();
foreach (CacheItemBase item in m_Index)
m_Lookup[item.uuid] = item;
}
break;
default:
break;
}
}
public void Invalidate(string uuid)
{
lock (m_Index)
{
if (!m_Lookup.ContainsKey(uuid))
return;
CacheItemBase item = m_Lookup[uuid];
m_Lookup.Remove(uuid);
m_Index.Remove(item);
}
}
public void Clear()
{
lock (m_Index)
{
m_Index.Clear();
m_Lookup.Clear();
}
}
}
}