| File: Internals\System\Runtime\TimeoutHelper.cs | Web Access |
| Project: src\src\System.ServiceModel.Primitives\src\System.ServiceModel.Primitives.csproj (System.ServiceModel.Primitives) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Concurrent; using System.Diagnostics.Contracts; using System.Threading; using System.Threading.Tasks; namespace System.Runtime { internal struct TimeoutHelper { public static readonly TimeSpan MaxWait = TimeSpan.FromMilliseconds(int.MaxValue); private static readonly CancellationToken s_precancelledToken = new CancellationToken(true); private bool _cancellationTokenInitialized; private bool _deadlineSet; private CancellationToken _cancellationToken; private DateTime _deadline; public TimeoutHelper(TimeSpan timeout) { Contract.Assert(timeout >= TimeSpan.Zero, "timeout must be non-negative"); _cancellationToken = default; _cancellationTokenInitialized = false; OriginalTimeout = timeout; _deadline = DateTime.MaxValue; _deadlineSet = (timeout == TimeSpan.MaxValue); } public CancellationToken GetCancellationToken() { return GetCancellationTokenAsync().Result; } public async Task<CancellationToken> GetCancellationTokenAsync() { if (!_cancellationTokenInitialized) { var timeout = RemainingTime(); if (timeout >= MaxWait || timeout == Timeout.InfiniteTimeSpan) { _cancellationToken = CancellationToken.None; } else if (timeout > TimeSpan.Zero) { _cancellationToken = await TimeoutTokenSource.FromTimeoutAsync((int)timeout.TotalMilliseconds); } else { _cancellationToken = s_precancelledToken; } _cancellationTokenInitialized = true; } return _cancellationToken; } public TimeSpan OriginalTimeout { get; } public static bool IsTooLarge(TimeSpan timeout) { return (timeout > MaxWait) && (timeout != TimeSpan.MaxValue); } public static TimeSpan FromMilliseconds(int milliseconds) { if (milliseconds == Timeout.Infinite) { return TimeSpan.MaxValue; } else { return TimeSpan.FromMilliseconds(milliseconds); } } public static int ToMilliseconds(TimeSpan timeout) { if (timeout == TimeSpan.MaxValue) { return Timeout.Infinite; } else { long ticks = Ticks.FromTimeSpan(timeout); if (ticks / TimeSpan.TicksPerMillisecond > int.MaxValue) { return int.MaxValue; } return Ticks.ToMilliseconds(ticks); } } public static TimeSpan Min(TimeSpan val1, TimeSpan val2) { if (val1 > val2) { return val2; } else { return val1; } } public static TimeSpan Add(TimeSpan timeout1, TimeSpan timeout2) { return Ticks.ToTimeSpan(Ticks.Add(Ticks.FromTimeSpan(timeout1), Ticks.FromTimeSpan(timeout2))); } public static DateTime Add(DateTime time, TimeSpan timeout) { if (timeout >= TimeSpan.Zero && DateTime.MaxValue - time <= timeout) { return DateTime.MaxValue; } if (timeout <= TimeSpan.Zero && DateTime.MinValue - time >= timeout) { return DateTime.MinValue; } return time + timeout; } public static DateTime Subtract(DateTime time, TimeSpan timeout) { return Add(time, TimeSpan.Zero - timeout); } public static TimeSpan Divide(TimeSpan timeout, int factor) { if (timeout == TimeSpan.MaxValue) { return TimeSpan.MaxValue; } return Ticks.ToTimeSpan((Ticks.FromTimeSpan(timeout) / factor) + 1); } public TimeSpan RemainingTime() { if (!_deadlineSet) { SetDeadline(); return OriginalTimeout; } else if (_deadline == DateTime.MaxValue) { return TimeSpan.MaxValue; } else { TimeSpan remaining = _deadline - DateTime.UtcNow; if (remaining <= TimeSpan.Zero) { return TimeSpan.Zero; } else { return remaining; } } } public TimeSpan ElapsedTime() { return OriginalTimeout - RemainingTime(); } private void SetDeadline() { Contract.Assert(!_deadlineSet, "TimeoutHelper deadline set twice."); _deadline = DateTime.UtcNow + OriginalTimeout; _deadlineSet = true; } public static void ThrowIfNegativeArgument(TimeSpan timeout) { ThrowIfNegativeArgument(timeout, "timeout"); } public static void ThrowIfNegativeArgument(TimeSpan timeout, string argumentName) { if (timeout < TimeSpan.Zero) { throw Fx.Exception.ArgumentOutOfRange(argumentName, timeout, InternalSR.TimeoutMustBeNonNegative(argumentName, timeout)); } } public static void ThrowIfNonPositiveArgument(TimeSpan timeout) { ThrowIfNonPositiveArgument(timeout, "timeout"); } public static void ThrowIfNonPositiveArgument(TimeSpan timeout, string argumentName) { if (timeout <= TimeSpan.Zero) { throw Fx.Exception.ArgumentOutOfRange(argumentName, timeout, InternalSR.TimeoutMustBePositive(argumentName, timeout)); } } public static bool WaitOne(WaitHandle waitHandle, TimeSpan timeout) { ThrowIfNegativeArgument(timeout); if (timeout == TimeSpan.MaxValue) { waitHandle.WaitOne(); return true; } else { // http://msdn.microsoft.com/en-us/library/85bbbxt9(v=vs.110).aspx // with exitContext was used in Desktop which is not supported in Net Native or CoreClr return waitHandle.WaitOne(timeout); } } public static bool Wait(ManualResetEventSlim mres, TimeSpan timeout) { ThrowIfNegativeArgument(timeout); if (timeout == TimeSpan.MaxValue) { mres.Wait(); return true; } else { return mres.Wait(timeout); } } internal static TimeoutException CreateEnterTimedOutException(TimeSpan timeout) { return new TimeoutException(SRP.Format(SRP.LockTimeoutExceptionMessage, timeout)); } } /// <summary> /// This class coalesces timeout tokens because cancelation tokens with timeouts are more expensive to expose. /// Disposing too many such tokens will cause thread contentions in high throughput scenario. /// /// Tokens with target cancelation time 15ms apart would resolve to the same instance. /// </summary> internal static class TimeoutTokenSource { /// <summary> /// These are constants use to calculate timeout coalescing, for more description see method FromTimeoutAsync /// </summary> private const int CoalescingFactor = 15; private const int GranularityFactor = 2000; private const int SegmentationFactor = CoalescingFactor * GranularityFactor; private static readonly ConcurrentDictionary<long, Task<CancellationToken>> s_tokenCache = new ConcurrentDictionary<long, Task<CancellationToken>>(); private static readonly Action<object> s_deregisterToken = (object state) => { var args = (Tuple<long, CancellationTokenSource>)state; Task<CancellationToken> ignored; try { s_tokenCache.TryRemove(args.Item1, out ignored); } finally { args.Item2.Dispose(); } }; public static CancellationToken FromTimeout(int millisecondsTimeout) { return FromTimeoutAsync(millisecondsTimeout).Result; } public static Task<CancellationToken> FromTimeoutAsync(int millisecondsTimeout) { // Note that CancellationTokenSource constructor requires input to be >= -1, // restricting millisecondsTimeout to be >= -1 would enforce that if (millisecondsTimeout < -1) { throw new ArgumentOutOfRangeException("Invalid millisecondsTimeout value " + millisecondsTimeout); } // To prevent s_tokenCache growing too large, we have to adjust the granularity of the our coalesce depending // on the value of millisecondsTimeout. The coalescing span scales proportionally with millisecondsTimeout which // would guarantee constant s_tokenCache size in the case where similar millisecondsTimeout values are accepted. // If the method is given a wildly different millisecondsTimeout values all the time, the dictionary would still // only grow logarithmically with respect to the range of the input values uint currentTime = (uint)Environment.TickCount; long targetTime = millisecondsTimeout + currentTime; // Formula for our coalescing span: // Divide millisecondsTimeout by SegmentationFactor and take the highest bit and then multiply CoalescingFactor back var segmentValue = millisecondsTimeout / SegmentationFactor; var coalescingSpanMs = CoalescingFactor; while (segmentValue > 0) { segmentValue >>= 1; coalescingSpanMs <<= 1; } targetTime = ((targetTime + (coalescingSpanMs - 1)) / coalescingSpanMs) * coalescingSpanMs; Task<CancellationToken> tokenTask; if (!s_tokenCache.TryGetValue(targetTime, out tokenTask)) { var tcs = new TaskCompletionSource<CancellationToken>(TaskCreationOptions.RunContinuationsAsynchronously); // only a single thread may succeed adding its task into the cache if (s_tokenCache.TryAdd(targetTime, tcs.Task)) { // Since this thread was successful reserving a spot in the cache, it would be the only thread // that construct the CancellationTokenSource var tokenSource = new CancellationTokenSource((int)(targetTime - currentTime)); var token = tokenSource.Token; // Clean up cache when Token is canceled token.UnsafeRegister(s_deregisterToken, Tuple.Create(targetTime, tokenSource)); // set the result so other thread may observe the token, and return tcs.TrySetResult(token); tokenTask = tcs.Task; } else { // for threads that failed when calling TryAdd, there should be one already in the cache if (!s_tokenCache.TryGetValue(targetTime, out tokenTask)) { // In unlikely scenario the token was already cancelled and timed out, we would not find it in cache. // In this case we would simply create a non-coalesced token tokenTask = Task.FromResult(new CancellationTokenSource(millisecondsTimeout).Token); } } } return tokenTask; } } }