// Process a timer event
private void ThreadTimer(Object state)
{
//
// Theory of operation.
//
// To timeout transactions we must walk down the list starting from the head
// until we find a link with an absolute timeout that is greater than our own.
// At that point everything further down in the list is elegable to be timed
// out. So simply remove that link in the list and walk down from that point
// timing out any transaction that is found.
//
// There could be a race between this callback being queued and the timer
// being disabled. If we get here when the timer is disabled, just return.
if (!_timerEnabled)
{
return;
}
// Increment the number of ticks
_ticks++;
_lastTimerTime = DateTime.UtcNow.Ticks;
//
// First find the starting point of transactions that should time out. Every transaction after
// that point will timeout so once we've found it then it is just a matter of traversing the
// structure.
//
BucketSet lastBucketSet = null;
BucketSet currentBucketSet = _headBucketSet; // The list always has a head.
// Acquire a writer lock before checking to see if we should disable the timer.
// Adding of transactions acquires a reader lock and might insert a new BucketSet.
// If that races with our check for a BucketSet existing, we may not timeout that
// transaction that is being added.
WeakReference nextWeakSet = null;
BucketSet nextBucketSet = null;
nextWeakSet = (WeakReference)currentBucketSet.nextSetWeak;
if (nextWeakSet != null)
{
nextBucketSet = (BucketSet)nextWeakSet.Target;
}
if (nextBucketSet == null)
{
_rwLock.EnterWriteLock();
try
{
// Access the nextBucketSet again in writer lock to account for any race before disabling the timeout.
nextWeakSet = (WeakReference)currentBucketSet.nextSetWeak;
if (nextWeakSet != null)
{
nextBucketSet = (BucketSet)nextWeakSet.Target;
}
if (nextBucketSet == null)
{
//
// Special case to allow for disabling the timer.
//
// If there are no transactions on the timeout list we can disable the
// timer.
if (!_timer.Change(Timeout.Infinite, Timeout.Infinite))
{
throw TransactionException.CreateInvalidOperationException(
SR.TraceSourceLtm,
SR.UnexpectedTimerFailure,
null
);
}
_timerEnabled = false;
return;
}
}
finally
{
_rwLock.ExitWriteLock();
}
}
// Note it is slightly subtle that we always skip the head node. This is done
// on purpose because the head node contains transactions with essentially
// an infinite timeout.
do
{
do
{
nextWeakSet = (WeakReference)currentBucketSet.nextSetWeak;
if (nextWeakSet == null)
{
// Nothing more to do.
return;
}
nextBucketSet = (BucketSet)nextWeakSet.Target;
if (nextBucketSet == null)
{
// Again nothing more to do.
return;
}
lastBucketSet = currentBucketSet;
currentBucketSet = nextBucketSet;
}while (currentBucketSet.AbsoluteTimeout > _ticks);
//
// Pinch off the list at this point making sure it is still the correct set.
//
// Note: We may lose a race with an "Add" thread that is inserting a BucketSet in this location in
// the list. If that happens, this CompareExchange will not be performed and the returned abortingSetsWeak
// value will NOT equal nextWeakSet. But we check for that and if this condition occurs, this iteration of
// the timer thread will simply return, not timing out any transactions. When the next timer interval
// expires, the thread will walk the list again, find the appropriate BucketSet to pinch off, and
// then time out the transactions. This means that it is possible for a transaction to live a bit longer,
// but not much.
WeakReference abortingSetsWeak =
(WeakReference)Interlocked.CompareExchange(ref lastBucketSet.nextSetWeak, null, nextWeakSet);
if (abortingSetsWeak == nextWeakSet)
{
// Yea - now proceed to abort the transactions.
BucketSet abortingBucketSets = null;
do
{
if (abortingSetsWeak != null)
{
abortingBucketSets = (BucketSet)abortingSetsWeak.Target;
}
else
{
abortingBucketSets = null;
}
if (abortingBucketSets != null)
{
abortingBucketSets.TimeoutTransactions();
abortingSetsWeak = (WeakReference)abortingBucketSets.nextSetWeak;
}
}while (abortingBucketSets != null);
// That's all we needed to do.
break;
}
// We missed pulling the right transactions off. Loop back up and try again.
currentBucketSet = lastBucketSet;
}while (true);
}