587 lines
16 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|
|
} |