File: System\Net\TimerThread.cs
Web Access
Project: src\src\libraries\System.Net.Requests\src\System.Net.Requests.csproj (System.Net.Requests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
 
namespace System.Net
{
    /// <summary>
    /// <para>Acts as countdown timer, used to measure elapsed time over a sync operation.</para>
    /// </summary>
    internal static class TimerThread
    {
        /// <summary>
        /// <para>Represents a queue of timers, which all have the same duration.</para>
        /// </summary>
        internal abstract class Queue
        {
            private readonly int _durationMilliseconds;
 
            internal Queue(int durationMilliseconds)
            {
                _durationMilliseconds = durationMilliseconds;
            }
 
            /// <summary>
            /// <para>The duration in milliseconds of timers in this queue.</para>
            /// </summary>
            internal int Duration => _durationMilliseconds;
 
            /// <summary>
            /// <para>Creates and returns a handle to a new timer with attached context.</para>
            /// </summary>
            internal abstract Timer CreateTimer(Callback callback, object? context);
        }
 
        /// <summary>
        /// <para>Represents a timer and provides a mechanism to cancel.</para>
        /// </summary>
        internal abstract class Timer : IDisposable
        {
            private readonly int _startTimeMilliseconds;
            private readonly int _durationMilliseconds;
 
            internal Timer(int durationMilliseconds)
            {
                _durationMilliseconds = durationMilliseconds;
                _startTimeMilliseconds = Environment.TickCount;
            }
 
            /// <summary>
            /// <para>The time (relative to Environment.TickCount) when the timer started.</para>
            /// </summary>
            internal int StartTime => _startTimeMilliseconds;
 
            /// <summary>
            /// <para>The time (relative to Environment.TickCount) when the timer will expire.</para>
            /// </summary>
            internal int Expiration => unchecked(_startTimeMilliseconds + _durationMilliseconds);
 
            /// <summary>
            /// <para>Cancels the timer.  Returns true if the timer hasn't and won't fire; false if it has or will.</para>
            /// </summary>
            internal abstract bool Cancel();
 
            /// <summary>
            /// <para>Whether or not the timer has expired.</para>
            /// </summary>
            internal abstract bool HasExpired { get; }
 
            public void Dispose() => Cancel();
        }
 
        /// <summary>
        /// <para>Prototype for the callback that is called when a timer expires.</para>
        /// </summary>
        internal delegate void Callback(Timer timer, int timeNoticed, object? context);
 
        private const int ThreadIdleTimeoutMilliseconds = 30 * 1000;
        private const int CacheScanPerIterations = 32;
        private const int TickCountResolution = 15;
 
        private static readonly LinkedList<WeakReference> s_queues = new LinkedList<WeakReference>();
        private static readonly LinkedList<WeakReference> s_newQueues = new LinkedList<WeakReference>();
        private static int s_threadState = (int)TimerThreadState.Idle;  // Really a TimerThreadState, but need an int for Interlocked.
        private static readonly AutoResetEvent s_threadReadyEvent = new AutoResetEvent(false);
        private static readonly ManualResetEvent s_threadShutdownEvent = new ManualResetEvent(false);
        private static readonly WaitHandle[] s_threadEvents = { s_threadShutdownEvent, s_threadReadyEvent };
        private static int s_cacheScanIteration;
        private static readonly Hashtable s_queuesCache = new Hashtable();
 
        /// <summary>
        /// <para>The possible states of the timer thread.</para>
        /// </summary>
        private enum TimerThreadState
        {
            Idle,
            Running,
            Stopped
        }
 
        /// <summary>
        /// <para>Queue factory.  Always synchronized.</para>
        /// </summary>
        internal static Queue GetOrCreateQueue(int durationMilliseconds)
        {
            if (durationMilliseconds == Timeout.Infinite)
            {
                return new InfiniteTimerQueue();
            }
 
            ArgumentOutOfRangeException.ThrowIfNegative(durationMilliseconds);
 
            TimerQueue? queue;
            object key = durationMilliseconds; // Box once.
            WeakReference? weakQueue = (WeakReference?)s_queuesCache[key];
            if (weakQueue == null || (queue = (TimerQueue?)weakQueue.Target) == null)
            {
                lock (s_newQueues)
                {
                    weakQueue = (WeakReference?)s_queuesCache[key];
                    if (weakQueue == null || (queue = (TimerQueue?)weakQueue.Target) == null)
                    {
                        queue = new TimerQueue(durationMilliseconds);
                        weakQueue = new WeakReference(queue);
                        s_newQueues.AddLast(weakQueue);
                        s_queuesCache[key] = weakQueue;
 
                        // Take advantage of this lock to periodically scan the table for garbage.
                        if (++s_cacheScanIteration % CacheScanPerIterations == 0)
                        {
                            var garbage = new List<object>();
                            // Manual use of IDictionaryEnumerator instead of foreach to avoid DictionaryEntry box allocations.
                            IDictionaryEnumerator e = s_queuesCache.GetEnumerator();
                            while (e.MoveNext())
                            {
                                DictionaryEntry pair = e.Entry;
                                if (((WeakReference)pair.Value!).Target == null)
                                {
                                    garbage.Add(pair.Key);
                                }
                            }
                            for (int i = 0; i < garbage.Count; i++)
                            {
                                s_queuesCache.Remove(garbage[i]);
                            }
                        }
                    }
                }
            }
 
            return queue;
        }
 
        /// <summary>
        /// <para>Represents a queue of timers of fixed duration.</para>
        /// </summary>
        private sealed class TimerQueue : Queue
        {
            // This is a GCHandle that holds onto the TimerQueue when active timers are in it.
            // The TimerThread only holds WeakReferences to it so that it can be collected when the user lets go of it.
            // But we don't want the user to HAVE to keep a reference to it when timers are active in it.
            // It gets created when the first timer gets added, and cleaned up when the TimerThread notices it's empty.
            // The TimerThread will always notice it's empty eventually, since the TimerThread will always wake up and
            // try to fire the timer, even if it was cancelled and removed prematurely.
            private IntPtr _thisHandle;
 
            // This sentinel TimerNode acts as both the head and the tail, allowing nodes to go in and out of the list without updating
            // any TimerQueue members.  _timers.Next is the true head, and .Prev the true tail.  This also serves as the list's lock.
            private readonly TimerNode _timers;
 
            /// <summary>
            /// <para>Create a new TimerQueue.  TimerQueues must be created while s_NewQueues is locked in
            /// order to synchronize with Shutdown().</para>
            /// </summary>
            /// <param name="durationMilliseconds"></param>
            internal TimerQueue(int durationMilliseconds) :
                base(durationMilliseconds)
            {
                // Create the doubly-linked list with a sentinel head and tail so that this member never needs updating.
                _timers = new TimerNode();
                _timers.Next = _timers;
                _timers.Prev = _timers;
            }
 
            /// <summary>
            /// <para>Creates new timers.  This method is thread-safe.</para>
            /// </summary>
            internal override Timer CreateTimer(Callback callback, object? context)
            {
                TimerNode timer = new TimerNode(callback, context, Duration, _timers);
 
                // Add this on the tail.  (Actually, one before the tail - _timers is the sentinel tail.)
                bool needProd = false;
                lock (_timers)
                {
                    Debug.Assert(_timers.Prev!.Next == _timers, $"Tail corruption.");
 
                    // If this is the first timer in the list, we need to create a queue handle and prod the timer thread.
                    if (_timers.Next == _timers)
                    {
                        if (_thisHandle == IntPtr.Zero)
                        {
                            _thisHandle = (IntPtr)GCHandle.Alloc(this);
                        }
                        needProd = true;
                    }
 
                    timer.Next = _timers;
                    timer.Prev = _timers.Prev;
                    _timers.Prev.Next = timer;
                    _timers.Prev = timer;
                }
 
                // If, after we add the new tail, there is a chance that the tail is the next
                // node to be processed, we need to wake up the timer thread.
                if (needProd)
                {
                    TimerThread.Prod();
                }
 
                return timer;
            }
 
            /// <summary>
            /// <para>Called by the timer thread to fire the expired timers.  Returns true if there are future timers
            /// in the queue, and if so, also sets nextExpiration.</para>
            /// </summary>
            internal bool Fire(out int nextExpiration)
            {
                while (true)
                {
                    // Check if we got to the end.  If so, free the handle.
                    TimerNode timer = _timers.Next!;
                    if (timer == _timers)
                    {
                        lock (_timers)
                        {
                            timer = _timers.Next!;
                            if (timer == _timers)
                            {
                                if (_thisHandle != IntPtr.Zero)
                                {
                                    ((GCHandle)_thisHandle).Free();
                                    _thisHandle = IntPtr.Zero;
                                }
 
                                nextExpiration = 0;
                                return false;
                            }
                        }
                    }
 
                    if (!timer.Fire())
                    {
                        nextExpiration = timer.Expiration;
                        return true;
                    }
                }
            }
        }
 
        /// <summary>
        /// <para>A special dummy implementation for a queue of timers of infinite duration.</para>
        /// </summary>
        private sealed class InfiniteTimerQueue : Queue
        {
            internal InfiniteTimerQueue() : base(Timeout.Infinite) { }
 
            /// <summary>
            /// <para>Always returns a dummy infinite timer.</para>
            /// </summary>
            internal override Timer CreateTimer(Callback callback, object? context) => new InfiniteTimer();
        }
 
        /// <summary>
        /// <para>Internal representation of an individual timer.</para>
        /// </summary>
        private sealed class TimerNode : Timer
        {
            private TimerState _timerState;
            private Callback? _callback;
            private object? _context;
            private readonly object _queueLock = null!;
            private TimerNode? _next;
            private TimerNode? _prev;
 
            /// <summary>
            /// <para>Status of the timer.</para>
            /// </summary>
            private enum TimerState
            {
                Ready,
                Fired,
                Cancelled,
                Sentinel
            }
 
            internal TimerNode(Callback callback, object? context, int durationMilliseconds, object queueLock) : base(durationMilliseconds)
            {
                if (callback != null)
                {
                    _callback = callback;
                    _context = context;
                }
                _timerState = TimerState.Ready;
                _queueLock = queueLock;
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime}");
            }
 
            // A sentinel node - both the head and tail are one, which prevent the head and tail from ever having to be updated.
            internal TimerNode() : base(0)
            {
                _timerState = TimerState.Sentinel;
            }
 
            internal override bool HasExpired => _timerState == TimerState.Fired;
 
            internal TimerNode? Next
            {
                get { return _next; }
                set { _next = value; }
            }
 
            internal TimerNode? Prev
            {
                get { return _prev; }
                set { _prev = value; }
            }
 
            /// <summary>
            /// <para>Cancels the timer.  Returns true if it hasn't and won't fire; false if it has or will, or has already been cancelled.</para>
            /// </summary>
            internal override bool Cancel()
            {
                if (_timerState == TimerState.Ready)
                {
                    lock (_queueLock)
                    {
                        if (_timerState == TimerState.Ready)
                        {
                            // Remove it from the list.  This keeps the list from getting too big when there are a lot of rapid creations
                            // and cancellations.  This is done before setting it to Cancelled to try to prevent the Fire() loop from
                            // seeing it, or if it does, of having to take a lock to synchronize with the state of the list.
                            Next!.Prev = Prev;
                            Prev!.Next = Next;
 
                            // Just cleanup.  Doesn't need to be in the lock but is easier to have here.
                            Next = null;
                            Prev = null;
                            _callback = null;
                            _context = null;
 
                            _timerState = TimerState.Cancelled;
 
                            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime} Cancel (success)");
                            return true;
                        }
                    }
                }
 
                if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime} Cancel (failure)");
                return false;
            }
 
            /// <summary>
            /// <para>Fires the timer if it is still active and has expired.  Returns
            /// true if it can be deleted, or false if it is still timing.</para>
            /// </summary>
            internal bool Fire()
            {
                if (_timerState == TimerState.Sentinel)
                {
                    if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, "TimerQueue tried to Fire a Sentinel.");
                }
 
                if (_timerState != TimerState.Ready)
                {
                    return true;
                }
 
                // Must get the current tick count within this method so it is guaranteed not to be before
                // StartTime, which is set in the constructor.
                int nowMilliseconds = Environment.TickCount;
                if (IsTickBetween(StartTime, Expiration, nowMilliseconds))
                {
                    if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime}::Fire() Not firing ({StartTime} <= {nowMilliseconds} < {Expiration})");
                    return false;
                }
 
                bool needCallback = false;
                lock (_queueLock)
                {
                    if (_timerState == TimerState.Ready)
                    {
                        if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"TimerThreadTimer#{StartTime}::Fire() Firing ({StartTime} <= {nowMilliseconds} >= " + Expiration + ")");
                        _timerState = TimerState.Fired;
 
                        // Remove it from the list.
                        Next!.Prev = Prev;
                        Prev!.Next = Next;
 
                        Next = null;
                        Prev = null;
                        needCallback = _callback != null;
                    }
                }
 
                if (needCallback)
                {
                    try
                    {
                        Callback callback = _callback!;
                        object? context = _context;
                        _callback = null;
                        _context = null;
                        callback(this, nowMilliseconds, context);
                    }
                    catch (Exception exception)
                    {
                        if (ExceptionCheck.IsFatal(exception))
                            throw;
 
                        if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"exception in callback: {exception}");
 
                        // This thread is not allowed to go into user code, so we should never get an exception here.
                        // So, in debug, throw it up, killing the AppDomain.  In release, we'll just ignore it.
#if DEBUG
                        throw;
#endif
                    }
                }
 
                return true;
            }
        }
 
        /// <summary>
        /// <para>A dummy infinite timer.</para>
        /// </summary>
        private sealed class InfiniteTimer : Timer
        {
            internal InfiniteTimer() : base(Timeout.Infinite) { }
 
            private bool _canceled;
 
            internal override bool HasExpired => false;
 
            /// <summary>
            /// <para>Cancels the timer.  Returns true the first time, false after that.</para>
            /// </summary>
            internal override bool Cancel() => !Interlocked.Exchange(ref _canceled, true);
        }
 
        /// <summary>
        /// <para>Internal mechanism used when timers are added to wake up / create the thread.</para>
        /// </summary>
        private static void Prod()
        {
            s_threadReadyEvent.Set();
            TimerThreadState oldState = (TimerThreadState)Interlocked.CompareExchange(
                ref s_threadState,
                (int)TimerThreadState.Running,
                (int)TimerThreadState.Idle);
 
            if (oldState == TimerThreadState.Idle)
            {
                new Thread(new ThreadStart(ThreadProc))
                {
                    IsBackground = true,
                    Name = ".NET Network Timer"
                }.Start();
            }
        }
 
        /// <summary>
        /// <para>Thread for the timer.  Ignores all exceptions.  If no activity occurs for a while,
        /// the thread will shut down.</para>
        /// </summary>
        private static void ThreadProc()
        {
            // Keep a permanent lock on s_Queues.  This lets for example Shutdown() know when this thread isn't running.
            lock (s_queues)
            {
                // If shutdown was recently called, abort here.
                if (Interlocked.CompareExchange(ref s_threadState, (int)TimerThreadState.Running, (int)TimerThreadState.Running) !=
                    (int)TimerThreadState.Running)
                {
                    return;
                }
 
                bool running = true;
                while (running)
                {
                    try
                    {
                        s_threadReadyEvent.Reset();
 
                        while (true)
                        {
                            // Copy all the new queues to the real queues.  Since only this thread modifies the real queues, it doesn't have to lock it.
                            if (s_newQueues.Count > 0)
                            {
                                lock (s_newQueues)
                                {
                                    for (LinkedListNode<WeakReference>? node = s_newQueues.First; node != null; node = s_newQueues.First)
                                    {
                                        s_newQueues.Remove(node);
                                        s_queues.AddLast(node);
                                    }
                                }
                            }
 
                            int now = Environment.TickCount;
                            int nextTick = 0;
                            bool haveNextTick = false;
                            for (LinkedListNode<WeakReference>? node = s_queues.First; node != null; /* node = node.Next must be done in the body */)
                            {
                                TimerQueue? queue = (TimerQueue?)node.Value.Target;
                                if (queue == null)
                                {
                                    LinkedListNode<WeakReference>? next = node.Next;
                                    s_queues.Remove(node);
                                    node = next;
                                    continue;
                                }
 
                                // Fire() will always return values that should be interpreted as later than 'now' (that is, even if 'now' is
                                // returned, it is 0x100000000 milliseconds in the future).  There's also a chance that Fire() will return a value
                                // intended as > 0x100000000 milliseconds from 'now'.  Either case will just cause an extra scan through the timers.
                                int nextTickInstance;
                                if (queue.Fire(out nextTickInstance) && (!haveNextTick || IsTickBetween(now, nextTick, nextTickInstance)))
                                {
                                    nextTick = nextTickInstance;
                                    haveNextTick = true;
                                }
 
                                node = node.Next;
                            }
 
                            // Figure out how long to wait, taking into account how long the loop took.
                            // Add 15 ms to compensate for poor TickCount resolution (want to guarantee a firing).
                            int newNow = Environment.TickCount;
                            int waitDuration = haveNextTick ?
                                (int)(IsTickBetween(now, nextTick, newNow) ?
                                    Math.Min(unchecked((uint)(nextTick - newNow)), (uint)(int.MaxValue - TickCountResolution)) + TickCountResolution :
                                    0) :
                                ThreadIdleTimeoutMilliseconds;
 
                            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"Waiting for {waitDuration}ms");
 
                            int waitResult = WaitHandle.WaitAny(s_threadEvents, waitDuration, false);
 
                            // 0 is s_ThreadShutdownEvent - die.
                            if (waitResult == 0)
                            {
                                if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, "Awoke, cause: Shutdown");
                                running = false;
                                break;
                            }
 
                            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"Awoke, cause {(waitResult == WaitHandle.WaitTimeout ? "Timeout" : "Prod")}");
 
                            // If we timed out with nothing to do, shut down.
                            if (waitResult == WaitHandle.WaitTimeout && !haveNextTick)
                            {
                                Interlocked.CompareExchange(ref s_threadState, (int)TimerThreadState.Idle, (int)TimerThreadState.Running);
                                // There could have been one more prod between the wait and the exchange.  Check, and abort if necessary.
                                if (s_threadReadyEvent.WaitOne(0, false))
                                {
                                    if (Interlocked.CompareExchange(ref s_threadState, (int)TimerThreadState.Running, (int)TimerThreadState.Idle) ==
                                        (int)TimerThreadState.Idle)
                                    {
                                        continue;
                                    }
                                }
 
                                running = false;
                                break;
                            }
                        }
                    }
                    catch (Exception exception)
                    {
                        if (ExceptionCheck.IsFatal(exception))
                            throw;
 
                        if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, exception);
 
                        // The only options are to continue processing and likely enter an error-loop,
                        // shut down timers for this AppDomain, or shut down the AppDomain.  Go with shutting
                        // down the AppDomain in debug, and going into a loop in retail, but try to make the
                        // loop somewhat slow.  Note that in retail, this can only be triggered by OutOfMemory or StackOverflow,
                        // or an exception thrown within TimerThread - the rest are caught in Fire().
#if !DEBUG
                        Thread.Sleep(1000);
#else
                        throw;
#endif
                    }
                }
            }
 
            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, "Stop");
        }
 
        /// <summary>
        /// <para>Helper for deciding whether a given TickCount is before or after a given expiration
        /// tick count assuming that it can't be before a given starting TickCount.</para>
        /// </summary>
        private static bool IsTickBetween(int start, int end, int comparand)
        {
            // Assumes that if start and end are equal, they are the same time.
            // Assumes that if the comparand and start are equal, no time has passed,
            // and that if the comparand and end are equal, end has occurred.
            return ((start <= comparand) == (end <= comparand)) != (start <= end);
        }
    }
}