< Summary - CoreWCF Coverage — PR #1766

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

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.cctor()100%110%
FromTimeout(...)0%10100%

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.
 28        public static readonly TimeSpan MaxWait = TimeSpan.FromMilliseconds(int.MaxValue);
 29        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
 43            _cancellationToken = default;
 44            _cancellationTokenInitialized = false;
 45            _originalTimeout = timeout;
 46            _deadline = DateTime.MaxValue;
 47            _deadlineSet = (timeout == TimeSpan.MaxValue || timeout == Timeout.InfiniteTimeSpan);
 48        }
 49
 50        public CancellationToken GetCancellationToken()
 51        {
 52            if (!_cancellationTokenInitialized)
 53            {
 54                TimeSpan timeout = RemainingTime();
 55                if (timeout >= MaxWait || timeout == Timeout.InfiniteTimeSpan)
 56                {
 57                    _cancellationToken = CancellationToken.None;
 58                }
 59                else if (timeout > TimeSpan.Zero)
 60                {
 61                    _cancellationToken = TimeoutTokenSource.FromTimeout((int)timeout.TotalMilliseconds);
 62                }
 63                else
 64                {
 65                    _cancellationToken = s_precancelledToken;
 66                }
 67                _cancellationTokenInitialized = true;
 68            }
 69
 70            return _cancellationToken;
 71        }
 72
 73
 74        public TimeSpan OriginalTimeout
 75        {
 76            get { return _originalTimeout; }
 77        }
 78
 79        public static bool IsTooLarge(TimeSpan timeout)
 80        {
 81            return (timeout > MaxWait) && (timeout != TimeSpan.MaxValue);
 82        }
 83
 84        public static TimeSpan FromMilliseconds(int milliseconds)
 85        {
 86            if (milliseconds == Timeout.Infinite)
 87            {
 88                return TimeSpan.MaxValue;
 89            }
 90            else
 91            {
 92                return TimeSpan.FromMilliseconds(milliseconds);
 93            }
 94        }
 95
 96        public static int ToMilliseconds(TimeSpan timeout)
 97        {
 98            if (timeout == TimeSpan.MaxValue)
 99            {
 100                return Timeout.Infinite;
 101            }
 102            else
 103            {
 104                long ticks = Ticks.FromTimeSpan(timeout);
 105                if (ticks / TimeSpan.TicksPerMillisecond > int.MaxValue)
 106                {
 107                    return int.MaxValue;
 108                }
 109                return Ticks.ToMilliseconds(ticks);
 110            }
 111        }
 112
 113        public static TimeSpan Min(TimeSpan val1, TimeSpan val2)
 114        {
 115            if (val1 > val2)
 116            {
 117                return val2;
 118            }
 119            else
 120            {
 121                return val1;
 122            }
 123        }
 124
 125        public static TimeSpan Add(TimeSpan timeout1, TimeSpan timeout2)
 126        {
 127            return Ticks.ToTimeSpan(Ticks.Add(Ticks.FromTimeSpan(timeout1), Ticks.FromTimeSpan(timeout2)));
 128        }
 129
 130        public static DateTime Add(DateTime time, TimeSpan timeout)
 131        {
 132            if (timeout >= TimeSpan.Zero && DateTime.MaxValue - time <= timeout)
 133            {
 134                return DateTime.MaxValue;
 135            }
 136            if (timeout <= TimeSpan.Zero && DateTime.MinValue - time >= timeout)
 137            {
 138                return DateTime.MinValue;
 139            }
 140            return time + timeout;
 141        }
 142
 143        public static DateTime Subtract(DateTime time, TimeSpan timeout)
 144        {
 145            return Add(time, TimeSpan.Zero - timeout);
 146        }
 147
 148        public static TimeSpan Divide(TimeSpan timeout, int factor)
 149        {
 150            if (timeout == TimeSpan.MaxValue)
 151            {
 152                return TimeSpan.MaxValue;
 153            }
 154
 155            return Ticks.ToTimeSpan((Ticks.FromTimeSpan(timeout) / factor) + 1);
 156        }
 157
 158        public TimeSpan RemainingTime()
 159        {
 160            if (!_deadlineSet)
 161            {
 162                SetDeadline();
 163                return _originalTimeout;
 164            }
 165            else if (_deadline == DateTime.MaxValue)
 166            {
 167                return TimeSpan.MaxValue;
 168            }
 169            else
 170            {
 171                TimeSpan remaining = _deadline - DateTime.UtcNow;
 172                if (remaining <= TimeSpan.Zero)
 173                {
 174                    return TimeSpan.Zero;
 175                }
 176                else
 177                {
 178                    return remaining;
 179                }
 180            }
 181        }
 182
 183        public TimeSpan ElapsedTime()
 184        {
 185            return _originalTimeout - RemainingTime();
 186        }
 187
 188        private void SetDeadline()
 189        {
 190            Fx.Assert(!_deadlineSet, "TimeoutHelper deadline set twice.");
 191            _deadline = DateTime.UtcNow + _originalTimeout;
 192            _deadlineSet = true;
 193        }
 194
 195        public static TimeSpan GetOriginalTimeout(CancellationToken token)
 196        {
 197            return RecoverableTimeoutCancellationTokenSource.GetOriginalTimeout(token);
 198        }
 199
 200        public static void ThrowIfNegativeArgument(TimeSpan timeout)
 201        {
 202            ThrowIfNegativeArgument(timeout, "timeout");
 203        }
 204
 205        public static void ThrowIfNegativeArgument(TimeSpan timeout, string argumentName)
 206        {
 207            if (timeout < TimeSpan.Zero)
 208            {
 209                throw Fx.Exception.ArgumentOutOfRange(argumentName, timeout, SRCommon.Format(SRCommon.TimeoutMustBeNonNe
 210            }
 211        }
 212
 213        public static void ThrowIfNonPositiveArgument(TimeSpan timeout)
 214        {
 215            ThrowIfNonPositiveArgument(timeout, "timeout");
 216        }
 217
 218        public static void ThrowIfNonPositiveArgument(TimeSpan timeout, string argumentName)
 219        {
 220            if (timeout <= TimeSpan.Zero)
 221            {
 222                throw Fx.Exception.ArgumentOutOfRange(argumentName, timeout, SRCommon.Format(SRCommon.TimeoutMustBePosit
 223            }
 224        }
 225
 226        public static bool WaitOne(WaitHandle waitHandle, TimeSpan timeout)
 227        {
 228            ThrowIfNegativeArgument(timeout);
 229            if (timeout == TimeSpan.MaxValue)
 230            {
 231                waitHandle.WaitOne();
 232                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
 238                return waitHandle.WaitOne(timeout);
 239            }
 240        }
 241
 242        internal static TimeoutException CreateEnterTimedOutException(TimeSpan timeout)
 243        {
 244            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
 0263        private static readonly ConcurrentDictionary<long, CancellationTokenSourceIOThreadTimer> s_timerCache =
 0264            new ConcurrentDictionary<long, CancellationTokenSourceIOThreadTimer>();
 265
 0266        private static readonly Action<object> s_deregisterTimer = (object state) =>
 0267        {
 0268            long targetTime = (long)state;
 0269            s_timerCache.TryRemove(targetTime, out CancellationTokenSourceIOThreadTimer ignored);
 0270        };
 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
 0276            if (millisecondsTimeout < -1)
 277            {
 0278                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
 0287            uint currentTime = (uint)Environment.TickCount;
 0288            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
 0292            int segmentValue = millisecondsTimeout / SegmentationFactor;
 0293            int coalescingSpanMs = CoalescingFactor;
 0294            while (segmentValue > 0)
 295            {
 0296                segmentValue >>= 1;
 0297                coalescingSpanMs <<= 1;
 298            }
 0299            targetTime = ((targetTime + (coalescingSpanMs - 1)) / coalescingSpanMs) * coalescingSpanMs;
 300
 0301            if (!s_timerCache.TryGetValue(targetTime, out CancellationTokenSourceIOThreadTimer ctsTimer))
 302            {
 0303                ctsTimer = new CancellationTokenSourceIOThreadTimer();
 304
 305                // only a single thread may succeed adding its timer into the cache
 0306                if (s_timerCache.TryAdd(targetTime, ctsTimer))
 307                {
 308                    // Clean up cache when timer fires
 0309                    ctsTimer.SetCompletionCallback(s_deregisterTimer, targetTime);
 0310                    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
 0315                    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.
 0319                        var cts = new RecoverableTimeoutCancellationTokenSource(millisecondsTimeout);
 0320                        cts.CancelAfter(millisecondsTimeout);
 0321                        return cts.Token;
 322                    }
 323                }
 324            }
 325
 0326            var tokenSource = new RecoverableTimeoutCancellationTokenSource(millisecondsTimeout);
 0327            ctsTimer.RegisterTokenSourceForCancellation(tokenSource);
 0328            return tokenSource.Token;
 329        }
 330    }
 331}

Methods/Properties

.cctor()
FromTimeout(System.Int32)