// ----------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// ----------------------------------------------------------------------------
namespace Microsoft.Singularity.Security.AccessControl
{
using System;
using System.Text;
using System.Collections;
///
/// Implement an cache with maximum size, timeouts, and LRU pruning.
///
public class ICacheValue
{
public DateTime expiry;
public bool Expired { get { return (this.expiry > DateTime.Now); } }
public void ResetExpiry(Cache! cache, DateTime minExpiry)
{
expiry = cache.ComputeExpiry(minExpiry);
}
public ICacheValue(Cache! cache, DateTime minExpiry)
{
expiry = cache.ComputeExpiry(minExpiry);
}
}
public class Cache : Hashtable
{
internal struct Stats {
public int maxEntries;
public bool enabled;
public int hits;
public int misses;
public int timeout;
public int prunePercent;
public string statHeader;
public Stats (int _maxSize, int _timeout, int _prunePercent, string! _statHdr)
{
maxEntries = _maxSize;
enabled = true;
hits = 0;
misses = 0;
timeout = _timeout;
prunePercent = _prunePercent;
statHeader = _statHdr;
}
}
internal class CacheEntry
{
public string! key;
public ulong lastAccess;
public ICacheValue! value; // the client value
public CacheEntry(string! key, ICacheValue! value)
{
this.key = key;
this.value = value;
this.lastAccess = lastAccessSource++;
}
}
private const int DefaultMaxCacheSize = 1000;
private const int DefaultTimeout = 60 * 60;
private const int DefaultPrunePercent = 25;
// we don't access this inside a lock, because if we lose
// a click we don't care
private static ulong lastAccessSource = 0L;
private Stats stats;
public Cache()
{
this.stats = new Stats(DefaultMaxCacheSize, DefaultTimeout, DefaultPrunePercent, "");
}
public Cache(int maxEntries, int timeout, int prunePercent, string! statHeader)
{
this.stats = new Stats(maxEntries, timeout, prunePercent, statHeader);
}
public DateTime ComputeExpiry(DateTime minExpiry)
{
DateTime exp = DateTime.Now;
exp.AddSeconds(stats.timeout);
if (exp > minExpiry)
exp = minExpiry;
return exp;
}
///
/// Retrieve the corresponding entry
///
public ICacheValue GetEntry(string! key)
{
// no lock because HTs are lock-free for read
if (!stats.enabled)
return null;
CacheEntry e = (CacheEntry) this[key];
if (e == null) {
stats.misses++;
return null;
}
stats.hits++;
e.lastAccess = lastAccessSource++;
return e.value;
}
///
/// Add or replace the entry in the cache
///
public void AddEntry(string! key, ICacheValue! value)
{
lock (this) {
if (!stats.enabled)
return;
CacheEntry e = new CacheEntry(key, value);
if (this.Count >= stats.maxEntries)
Prune(false);
this[e.key] = e;
}
}
///
/// Make room in the cache
///
public void Prune(bool all)
{
// assume that Expired entries will come to the head of the LRU
lock (this) {
if (all)
this.Clear();
else {
int target = (this.stats.maxEntries * stats.prunePercent) / 100;
CacheEntry[] candidates = new CacheEntry[target];
CacheEntry lastCandidate = null;
foreach (DictionaryEntry de in this) {
CacheEntry! e = (CacheEntry!) de.Value;
if (lastCandidate == null || e.lastAccess < lastCandidate.lastAccess) {
lastCandidate = InsertPruningCandidate(e, candidates);
}
}
for (int i = 0; i < target; i++) {
CacheEntry e = candidates[i];
if (e != null)
this.Remove(e.key);
}
}
}
}
private CacheEntry InsertPruningCandidate(CacheEntry! e, CacheEntry[]! ccs)
{
for (int i = ccs.Length - 1; i >= 0; i--) {
CacheEntry ee = ccs[i];
if (ee == null) {
ccs[i] = e;
break;
}
if (e.lastAccess < ee.lastAccess) {
// push everyone down, insert here
while (ee != null) {
ccs[i] = e;
if (i-- == 0)
break;
e = ee;
ee = ccs[i];
}
break;
}
// otherwise keep looking
}
return ccs[0];
}
// these methods are for testing purposes
public void ResetStats()
{
lock (this) {
this.stats.hits = 0;
this.stats.misses = 0;
}
}
public void SetMaxEntries(int max)
{
lock (this) {
stats.maxEntries = max;
Prune(false);
}
}
public void SetTimeout(int seconds)
{
lock (this) {
stats.timeout = seconds;
}
}
public void SetPrunePercent(int percent)
{
lock (this) {
stats.prunePercent = percent;
}
}
public void Enable()
{
lock (this) {
stats.enabled = true;
}
}
public void Disable()
{
lock (this) {
stats.enabled = false;
}
}
public void DumpStats(StringBuilder! sb)
{
lock (this) {
sb.AppendFormat("{0} Hit:{1}, Miss:{2}, N:{3}, Max:{4}, T0:{5}, PP:{6}\n",
stats.statHeader, stats.hits, stats.misses,
this.Count, stats.maxEntries, stats.timeout,
stats.prunePercent);
}
}
public void DumpCache(StringBuilder! sb)
{
lock (this) {
foreach (DictionaryEntry de in this) {
CacheEntry! e = (CacheEntry!) de.Value;
sb.AppendFormat("Key: {0}, Expiry: {1}, LastAccess: {2}\n",
e.key, e.value.expiry.ToString("t"), e.lastAccess);
}
}
}
}
}