< Summary - CoreWCF Coverage — PR #1766

Information
Class: CoreWCF.Runtime.TimeoutHelper
Assembly: CoreWCF.ConfigurationManager
File(s): /home/runner/work/CoreWCF/CoreWCF/src/Common/src/CoreWCF/Runtime/TimeoutHelper.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 71
Coverable lines: 71
Total lines: 331
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 42
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/CoreWCF/CoreWCF/src/Common/src/CoreWCF/Runtime/TimeoutHelper.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System;
 5using System.Collections.Concurrent;
 6using System.Threading;
 7
 8namespace CoreWCF.Runtime
 9{
 10    internal struct TimeoutHelper
 11    {
 12        // This is a brief plan on how to recover original timeout from CancellationToken
 13        // The coalescing is needed to prevent hammering at the timer queue otherwise we
 14        // get a lot of contention on it. It's really only the timer that needs to be coalesced.
 15        // 1. Change the coalescing to keep track of IOThreadTimer object
 16        // 2. Create a class which derives from/encapsulates an IOThreadTimer which can be used to
 17        //    register CancellationTokenSource objects to call cancel when the timer fires. This
 18        //    is the class that we coalesce.
 19        // 3. Create RecoverableTimeoutCancellationTokenSource which derives from Cancellation token source.
 20        //    It has the following behavior:
 21        //    a. Takes the derived IOThreadTimer from (2) in the constructor and registers cancelling itself
 22        //       when the timer fires.
 23        //    b. Takes the requested timeout in the constructor and saves it.
 24        //    c. Override the GetHashCode method to return a value representing the original timeout value.
 25        //       CancellationToken defers to the owning CancellationTokenSource to implement GetHashCode()
 26        // 4. Add a TimeoutHelper method which returns a TimeSpan from a CancellationToken. It queries the
 27        //    GetHashCode method to get the original requested timeout.
 028        public static readonly TimeSpan MaxWait = TimeSpan.FromMilliseconds(int.MaxValue);
 029        private static readonly CancellationToken s_precancelledToken = new CancellationToken(true);
 30
 31        private bool _cancellationTokenInitialized;
 32        private bool _deadlineSet;
 33
 34        private CancellationToken _cancellationToken;
 35        private DateTime _deadline;
 36        private TimeSpan _originalTimeout;
 37
 38        public TimeoutHelper(TimeSpan timeout)
 39        {
 40            Fx.Assert(timeout >= TimeSpan.Zero || timeout == Timeout.InfiniteTimeSpan,
 41                $"timeout must be non-negative or {Timeout.InfiniteTimeSpan}");
 42
 043            _cancellationToken = default;
 044            _cancellationTokenInitialized = false;
 045            _originalTimeout = timeout;
 046            _deadline = DateTime.MaxValue;
 047            _deadlineSet = (timeout == TimeSpan.MaxValue || timeout == Timeout.InfiniteTimeSpan);
 048        }
 49
 50        public CancellationToken GetCancellationToken()
 51        {
 052            if (!_cancellationTokenInitialized)
 53            {
 054                TimeSpan timeout = RemainingTime();
 055                if (timeout >= MaxWait || timeout == Timeout.InfiniteTimeSpan)
 56                {
 057                    _cancellationToken = CancellationToken.None;
 58                }
 059                else if (timeout > TimeSpan.Zero)
 60                {
 061                    _cancellationToken = TimeoutTokenSource.FromTimeout((int)timeout.TotalMilliseconds);
 62                }
 63                else
 64                {
 065                    _cancellationToken = s_precancelledToken;
 66                }
 067                _cancellationTokenInitialized = true;
 68            }
 69
 070            return _cancellationToken;
 71        }
 72
 73
 74        public TimeSpan OriginalTimeout
 75        {
 076            get { return _originalTimeout; }
 77        }
 78
 79        public static bool IsTooLarge(TimeSpan timeout)
 80        {
 081            return (timeout > MaxWait) && (timeout != TimeSpan.MaxValue);
 82        }
 83
 84        public static TimeSpan FromMilliseconds(int milliseconds)
 85        {
 086            if (milliseconds == Timeout.Infinite)
 87            {
 088                return TimeSpan.MaxValue;
 89            }
 90            else
 91            {
 092                return TimeSpan.FromMilliseconds(milliseconds);
 93            }
 94        }
 95
 96        public static int ToMilliseconds(TimeSpan timeout)
 97        {
 098            if (timeout == TimeSpan.MaxValue)
 99            {
 0100                return Timeout.Infinite;
 101            }
 102            else
 103            {
 0104                long ticks = Ticks.FromTimeSpan(timeout);
 0105                if (ticks / TimeSpan.TicksPerMillisecond > int.MaxValue)
 106                {
 0107                    return int.MaxValue;
 108                }
 0109                return Ticks.ToMilliseconds(ticks);
 110            }
 111        }
 112
 113        public static TimeSpan Min(TimeSpan val1, TimeSpan val2)
 114        {
 0115            if (val1 > val2)
 116            {
 0117                return val2;
 118            }
 119            else
 120            {
 0121                return val1;
 122            }
 123        }
 124
 125        public static TimeSpan Add(TimeSpan timeout1, TimeSpan timeout2)
 126        {
 0127            return Ticks.ToTimeSpan(Ticks.Add(Ticks.FromTimeSpan(timeout1), Ticks.FromTimeSpan(timeout2)));
 128        }
 129
 130        public static DateTime Add(DateTime time, TimeSpan timeout)
 131        {
 0132            if (timeout >= TimeSpan.Zero && DateTime.MaxValue - time <= timeout)
 133            {
 0134                return DateTime.MaxValue;
 135            }
 0136            if (timeout <= TimeSpan.Zero && DateTime.MinValue - time >= timeout)
 137            {
 0138                return DateTime.MinValue;
 139            }
 0140            return time + timeout;
 141        }
 142
 143        public static DateTime Subtract(DateTime time, TimeSpan timeout)
 144        {
 0145            return Add(time, TimeSpan.Zero - timeout);
 146        }
 147
 148        public static TimeSpan Divide(TimeSpan timeout, int factor)
 149        {
 0150            if (timeout == TimeSpan.MaxValue)
 151            {
 0152                return TimeSpan.MaxValue;
 153            }
 154
 0155            return Ticks.ToTimeSpan((Ticks.FromTimeSpan(timeout) / factor) + 1);
 156        }
 157
 158        public TimeSpan RemainingTime()
 159        {
 0160            if (!_deadlineSet)
 161            {
 0162                SetDeadline();
 0163                return _originalTimeout;
 164            }
 0165            else if (_deadline == DateTime.MaxValue)
 166            {
 0167                return TimeSpan.MaxValue;
 168            }
 169            else
 170            {
 0171                TimeSpan remaining = _deadline - DateTime.UtcNow;
 0172                if (remaining <= TimeSpan.Zero)
 173                {
 0174                    return TimeSpan.Zero;
 175                }
 176                else
 177                {
 0178                    return remaining;
 179                }
 180            }
 181        }
 182
 183        public TimeSpan ElapsedTime()
 184        {
 0185            return _originalTimeout - RemainingTime();
 186        }
 187
 188        private void SetDeadline()
 189        {
 190            Fx.Assert(!_deadlineSet, "TimeoutHelper deadline set twice.");
 0191            _deadline = DateTime.UtcNow + _originalTimeout;
 0192            _deadlineSet = true;
 0193        }
 194
 195        public static TimeSpan GetOriginalTimeout(CancellationToken token)
 196        {
 0197            return RecoverableTimeoutCancellationTokenSource.GetOriginalTimeout(token);
 198        }
 199
 200        public static void ThrowIfNegativeArgument(TimeSpan timeout)
 201        {
 0202            ThrowIfNegativeArgument(timeout, "timeout");
 0203        }
 204
 205        public static void ThrowIfNegativeArgument(TimeSpan timeout, string argumentName)
 206        {
 0207            if (timeout < TimeSpan.Zero)
 208            {
 0209                throw Fx.Exception.ArgumentOutOfRange(argumentName, timeout, SRCommon.Format(SRCommon.TimeoutMustBeNonNe
 210            }
 0211        }
 212
 213        public static void ThrowIfNonPositiveArgument(TimeSpan timeout)
 214        {
 0215            ThrowIfNonPositiveArgument(timeout, "timeout");
 0216        }
 217
 218        public static void ThrowIfNonPositiveArgument(TimeSpan timeout, string argumentName)
 219        {
 0220            if (timeout <= TimeSpan.Zero)
 221            {
 0222                throw Fx.Exception.ArgumentOutOfRange(argumentName, timeout, SRCommon.Format(SRCommon.TimeoutMustBePosit
 223            }
 0224        }
 225
 226        public static bool WaitOne(WaitHandle waitHandle, TimeSpan timeout)
 227        {
 0228            ThrowIfNegativeArgument(timeout);
 0229            if (timeout == TimeSpan.MaxValue)
 230            {
 0231                waitHandle.WaitOne();
 0232                return true;
 233            }
 234            else
 235            {
 236                // http://msdn.microsoft.com/en-us/library/85bbbxt9(v=vs.110).aspx
 237                // with exitContext was used in Desktop which is not supported in Net Native or CoreClr
 0238                return waitHandle.WaitOne(timeout);
 239            }
 240        }
 241
 242        internal static TimeoutException CreateEnterTimedOutException(TimeSpan timeout)
 243        {
 0244            return new TimeoutException(SRCommon.Format(SRCommon.LockTimeoutExceptionMessage, timeout));
 245        }
 246    }
 247
 248    /// <summary>
 249    /// This class coalesces timeout tokens because cancelation tokens with timeouts are more expensive to expose.
 250    /// Disposing too many such tokens will cause thread contentions in high throughput scenario.
 251    ///
 252    /// Tokens with target cancelation time 15ms apart would resolve to the same instance.
 253    /// </summary>
 254    internal static class TimeoutTokenSource
 255    {
 256        /// <summary>
 257        /// These are constants use to calculate timeout coalescing, for more description see method FromTimeoutAsync
 258        /// </summary>
 259        private const int CoalescingFactor = 15;
 260        private const int GranularityFactor = 2000;
 261        private const int SegmentationFactor = CoalescingFactor * GranularityFactor;
 262
 263        private static readonly ConcurrentDictionary<long, CancellationTokenSourceIOThreadTimer> s_timerCache =
 264            new ConcurrentDictionary<long, CancellationTokenSourceIOThreadTimer>();
 265
 266        private static readonly Action<object> s_deregisterTimer = (object state) =>
 267        {
 268            long targetTime = (long)state;
 269            s_timerCache.TryRemove(targetTime, out CancellationTokenSourceIOThreadTimer ignored);
 270        };
 271
 272        public static CancellationToken FromTimeout(int millisecondsTimeout)
 273        {
 274            // Note that CancellationTokenSource constructor requires input to be >= -1,
 275            // restricting millisecondsTimeout to be >= -1 would enforce that
 276            if (millisecondsTimeout < -1)
 277            {
 278                throw new ArgumentOutOfRangeException("Invalid millisecondsTimeout value " + millisecondsTimeout);
 279            }
 280
 281            // To prevent s_tokenCache growing too large, we have to adjust the granularity of the our coalesce dependin
 282            // on the value of millisecondsTimeout. The coalescing span scales proportionally with millisecondsTimeout w
 283            // would guarantee constant s_tokenCache size in the case where similar millisecondsTimeout values are accep
 284            // If the method is given a wildly different millisecondsTimeout values all the time, the dictionary would s
 285            // only grow logarithmically with respect to the range of the input values
 286
 287            uint currentTime = (uint)Environment.TickCount;
 288            long targetTime = millisecondsTimeout + currentTime;
 289
 290            // Formula for our coalescing span:
 291            // Divide millisecondsTimeout by SegmentationFactor and take the highest bit and then multiply CoalescingFac
 292            int segmentValue = millisecondsTimeout / SegmentationFactor;
 293            int coalescingSpanMs = CoalescingFactor;
 294            while (segmentValue > 0)
 295            {
 296                segmentValue >>= 1;
 297                coalescingSpanMs <<= 1;
 298            }
 299            targetTime = ((targetTime + (coalescingSpanMs - 1)) / coalescingSpanMs) * coalescingSpanMs;
 300
 301            if (!s_timerCache.TryGetValue(targetTime, out CancellationTokenSourceIOThreadTimer ctsTimer))
 302            {
 303                ctsTimer = new CancellationTokenSourceIOThreadTimer();
 304
 305                // only a single thread may succeed adding its timer into the cache
 306                if (s_timerCache.TryAdd(targetTime, ctsTimer))
 307                {
 308                    // Clean up cache when timer fires
 309                    ctsTimer.SetCompletionCallback(s_deregisterTimer, targetTime);
 310                    ctsTimer.Set((int)(targetTime - currentTime));
 311                }
 312                else
 313                {
 314                    // for threads that failed when calling TryAdd, there should be one already in the cache
 315                    if (!s_timerCache.TryGetValue(targetTime, out ctsTimer))
 316                    {
 317                        // In unlikely scenario the timer has already fired, we would not find it in cache.
 318                        // In this case we would simply create a CTS which doesn't use the coalesced timer.
 319                        var cts = new RecoverableTimeoutCancellationTokenSource(millisecondsTimeout);
 320                        cts.CancelAfter(millisecondsTimeout);
 321                        return cts.Token;
 322                    }
 323                }
 324            }
 325
 326            var tokenSource = new RecoverableTimeoutCancellationTokenSource(millisecondsTimeout);
 327            ctsTimer.RegisterTokenSourceForCancellation(tokenSource);
 328            return tokenSource.Token;
 329        }
 330    }
 331}