#region Copyright 2011-2014 by Roger Knapp, Licensed under the Apache License, Version 2.0
/* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#endregion
using System;
using System.Threading;
namespace CSharpTest.Net.Utils
{
///
/// Provides an interface for tracking a limited number of references to objects for use in a WeakReference
/// cache.
///
public interface IObjectKeepAlive
{
///
/// Clears the entire keep-alive cache
///
void Clear();
///
/// Can be called periodically by external threads to ensure cleanup instead of depending upon calls to Add()
///
void Tick();
///
/// Cleans up expired items and adds the object to the list of items to keep alive.
///
void Add(object item);
}
///
/// Provides a means of forcing the garbage collector to wait on objects aquired from permanent
/// storage while only holding WeakReference's of the object. Essentially uses a simple lockless
/// algorithm to track the most recently loaded objects so that they will stay alive longer.
///
[System.Diagnostics.DebuggerDisplay("{_head.Start}-{_tail.Last}")]
public class ObjectKeepAlive : IObjectKeepAlive
{
private const int BucketSize = 100;
private readonly int _minItems;
private readonly int _maxItems;
private readonly bool _externalTicks;
private readonly long _maxAge;
private long _position;
private Entry _head;
private Entry _tail;
///
/// Configures the keep-alive policy for this container
///
/// The minimum number of items desired in the list (kept event after age expires)
/// The maximum number of items desired in the list (discarded even if age has not expired)
/// Determines how long to keep an object if the count is between min and max
public ObjectKeepAlive(int minItems, int maxItems, TimeSpan maxAge)
: this(minItems, maxItems, maxAge, false)
{ }
///
/// Configures the keep-alive policy for this container
///
/// The minimum number of items desired in the list (kept event after age expires)
/// The maximum number of items desired in the list (discarded even if age has not expired)
/// Determines how long to keep an object if the count is between min and max
/// True if you want to perform cleanup exclusivly on another thread by calling Tick(), otherwise false
public ObjectKeepAlive(int minItems, int maxItems, TimeSpan maxAge, bool externalTicks)
{
_minItems = minItems;
_maxItems = maxItems;
_externalTicks = externalTicks;
_maxAge = maxAge.Ticks;
_position = -1;
_head = _tail = new Entry(0);
}
///
/// Clears the entire keep-alive cache
///
public void Clear()
{
_position = -1;
_head = _tail = new Entry(0);
}
///
/// Can be called periodically by external threads to ensure cleanup instead of depending upon calls to Add()
///
public void Tick()
{
Tick(DateTime.UtcNow.Ticks);
}
///
/// Cleans up expired items and adds the object to the list of items to keep alive.
///
public void Add(object item)
{
if (_maxItems == 0)
return;
long dtNow = DateTime.UtcNow.Ticks;
if(!_externalTicks)
Tick(dtNow);
Entry current;
long myPos;
do {
current = _tail;
myPos = Interlocked.Increment(ref _position);
} while (myPos < current.Start);
int myOffset = (int)(myPos - current.Start);
while (myOffset >= BucketSize)
{
if (current.Next == null)
{
Entry next = new Entry(current.Start + BucketSize);
Interlocked.CompareExchange(ref current.Next, next, null);
}
Interlocked.CompareExchange(ref _tail, current.Next, current);
current = current.Next;
myOffset = (int)(myPos - current.Start);
}
current.Age = dtNow;
current.Items[myOffset] = item;
long lastPos = current.Last;
while (lastPos <= myPos)
{
long test = Interlocked.CompareExchange(ref current.Last, myPos + 1, lastPos);
if (lastPos == test)
break;
lastPos = test;
}
}
private void Tick(long dtNow)
{
long expireBefore = dtNow - _maxAge;
killAnother:
Entry item = _head;
bool killEntry = false;
long itemsFollowing = _position - item.Last;//how many items are we holding after this Entry
killEntry |= itemsFollowing > _maxItems;//exceeding our maxItems limit?
killEntry |= itemsFollowing > _minItems && item.Age < expireBefore;//timeout of all items?
killEntry &= _head.Next != null && !ReferenceEquals(_head, _tail); //only kill when something follows.
if (killEntry)
{
if (ReferenceEquals(item, Interlocked.CompareExchange(ref _head, item.Next, item)))
if((itemsFollowing - BucketSize) > _maxItems)
goto killAnother;
return;
}
int offset = item.OffsetClear;
long entryCount = item.Last - item.Start - offset;
while (entryCount > 0 && offset < BucketSize && (entryCount > _maxItems || (item.Age < expireBefore && entryCount > _minItems)))
{
if (offset != Interlocked.CompareExchange(ref item.OffsetClear, offset + 1, offset))
break;
item.Items[offset] = null;
offset++;
entryCount--;
}
}
[System.Diagnostics.DebuggerDisplay("{Start}-{Last}")]
class Entry
{
public readonly long Start;
public readonly object[] Items;
public long Last;
public long Age;
public int OffsetClear;
public Entry Next;
public Entry(long start)
{
Start = Last = start;
OffsetClear = 0;
Age = long.MaxValue;
Items = new object[BucketSize];
Next = null;
}
}
}
}