mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 01:25:40 -05:00
* Move unused forked code to external directory * Fix SLN build errors * Add back resource provider core since it's referenced by main resource provider project * Update PackageProjects step of pipeline
543 lines
23 KiB
C#
543 lines
23 KiB
C#
//
|
|
// Copyright (c) Microsoft. All rights reserved.
|
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
//
|
|
|
|
// This code is copied from the source described in the comment below.
|
|
|
|
// =======================================================================================
|
|
// Microsoft Windows Server AppFabric Customer Advisory Team (CAT) Best Practices Series
|
|
//
|
|
// This sample is supplemental to the technical guidance published on the community
|
|
// blog at http://blogs.msdn.com/appfabriccat/ and copied from
|
|
// sqlmain ./sql/manageability/mfx/common/
|
|
//
|
|
// =======================================================================================
|
|
// Copyright © 2012 Microsoft Corporation. All rights reserved.
|
|
//
|
|
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
|
|
// EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. YOU BEAR THE RISK OF USING IT.
|
|
// =======================================================================================
|
|
|
|
// namespace Microsoft.SQL.CAT.BestPractices.SqlAzure.Framework
|
|
// namespace Microsoft.SqlServer.Management.Common
|
|
|
|
using System;
|
|
using Microsoft.Data.SqlClient;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.Contracts;
|
|
using System.Globalization;
|
|
using System.Threading;
|
|
using Microsoft.SqlTools.Hosting.Utility;
|
|
|
|
namespace Microsoft.SqlTools.CoreServices.Connection.ReliableConnection
|
|
{
|
|
/// <summary>
|
|
/// Implements a policy defining and implementing the retry mechanism for unreliable actions.
|
|
/// </summary>
|
|
internal abstract partial class RetryPolicy
|
|
{
|
|
/// <summary>
|
|
/// Defines a callback delegate which will be invoked whenever a retry condition is encountered.
|
|
/// </summary>
|
|
/// <param name="retryState">The state of current retry attempt.</param>
|
|
internal delegate void RetryCallbackDelegate(RetryState retryState);
|
|
|
|
/// <summary>
|
|
/// Defines a callback delegate which will be invoked whenever an error is ignored on retry.
|
|
/// </summary>
|
|
/// <param name="retryState">The state of current retry attempt.</param>
|
|
internal delegate void IgnoreErrorCallbackDelegate(RetryState retryState);
|
|
|
|
private readonly IErrorDetectionStrategy _errorDetectionStrategy;
|
|
|
|
protected RetryPolicy(IErrorDetectionStrategy strategy)
|
|
{
|
|
Contract.Assert(strategy != null);
|
|
|
|
_errorDetectionStrategy = strategy;
|
|
this.FastFirstRetry = true;
|
|
|
|
//TODO Defect 1078447 Validate whether CommandTimeout needs to be used differently in schema/data scenarios
|
|
this.CommandTimeoutInSeconds = AmbientSettings.LongRunningQueryTimeoutSeconds;
|
|
}
|
|
|
|
/// <summary>
|
|
/// An instance of a callback delegate which will be invoked whenever a retry condition is encountered.
|
|
/// </summary>
|
|
public event RetryCallbackDelegate RetryOccurred;
|
|
|
|
/// <summary>
|
|
/// An instance of a callback delegate which will be invoked whenever an error is ignored on retry.
|
|
/// </summary>
|
|
public event IgnoreErrorCallbackDelegate IgnoreErrorOccurred;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether or not the very first retry attempt will be made immediately
|
|
/// whereas the subsequent retries will remain subject to retry interval.
|
|
/// </summary>
|
|
public bool FastFirstRetry { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the timeout in seconds of sql commands
|
|
/// </summary>
|
|
public int CommandTimeoutInSeconds
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the error detection strategy of this retry policy
|
|
/// </summary>
|
|
internal IErrorDetectionStrategy ErrorDetectionStrategy
|
|
{
|
|
get
|
|
{
|
|
return _errorDetectionStrategy;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// We should only ignore errors if they happen after the first retry.
|
|
/// This flag is used to allow the ignore even on first try, for testing purposes.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This flag is currently being used for TESTING PURPOSES ONLY.
|
|
/// </remarks>
|
|
internal bool ShouldIgnoreOnFirstTry
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
protected static bool IsLessThanMaxRetryCount(int currentRetryCount, int maxRetryCount)
|
|
{
|
|
return currentRetryCount <= maxRetryCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Repetitively executes the specified action while it satisfies the current retry policy.
|
|
/// </summary>
|
|
/// <param name="action">A delegate representing the executable action which doesn't return any results.</param>
|
|
/// <param name="token">Cancellation token to cancel action between retries.</param>
|
|
public void ExecuteAction(Action action, CancellationToken? token = null)
|
|
{
|
|
ExecuteAction(
|
|
_ => action(), token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Repetitively executes the specified action while it satisfies the current retry policy.
|
|
/// </summary>
|
|
/// <param name="action">A delegate representing the executable action which doesn't return any results.</param>
|
|
/// <param name="token">Cancellation token to cancel action between retries.</param>
|
|
public void ExecuteAction(Action<RetryState> action, CancellationToken? token = null)
|
|
{
|
|
ExecuteAction<object>(
|
|
retryState =>
|
|
{
|
|
action(retryState);
|
|
return null;
|
|
}, token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Repetitively executes the specified action while it satisfies the current retry policy.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of result expected from the executable action.</typeparam>
|
|
/// <param name="func">A delegate representing the executable action which returns the result of type T.</param>
|
|
/// <param name="token">Cancellation token to cancel action between retries.</param>
|
|
/// <returns>The result from the action.</returns>
|
|
public T ExecuteAction<T>(Func<T> func, CancellationToken? token = null)
|
|
{
|
|
return ExecuteAction(
|
|
_ => func(), token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Repetitively executes the specified action while it satisfies the current retry policy.
|
|
/// </summary>
|
|
/// <typeparam name="R">The type of result expected from the executable action.</typeparam>
|
|
/// <param name="func">A delegate representing the executable action which returns the result of type R.</param>
|
|
/// <param name="token">Cancellation token to cancel action between retries.</param>
|
|
/// <returns>The result from the action.</returns>
|
|
public R ExecuteAction<R>(Func<RetryState, R> func, CancellationToken? token = null)
|
|
{
|
|
RetryState retryState = CreateRetryState();
|
|
|
|
if (token != null)
|
|
{
|
|
token.Value.ThrowIfCancellationRequested();
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
try
|
|
{
|
|
return func(retryState);
|
|
}
|
|
catch (RetryLimitExceededException limitExceededEx)
|
|
{
|
|
// The user code can throw a RetryLimitExceededException to force the exit from the retry loop.
|
|
// The RetryLimitExceeded exception can have an inner exception attached to it. This is the exception
|
|
// which we will have to throw up the stack so that callers can handle it.
|
|
if (limitExceededEx.InnerException != null)
|
|
{
|
|
throw limitExceededEx.InnerException;
|
|
}
|
|
|
|
return default(R);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
retryState.LastError = ex;
|
|
|
|
if (retryState.RetryCount > 0 || this.ShouldIgnoreOnFirstTry)
|
|
{
|
|
// If we can ignore this error, then break out of the loop and consider this execution as passing
|
|
// We return the default value for the type R
|
|
if (ShouldIgnoreError(retryState))
|
|
{
|
|
OnIgnoreErrorOccurred(retryState);
|
|
return default(R);
|
|
}
|
|
}
|
|
|
|
retryState.RetryCount++;
|
|
|
|
if (!ShouldRetry(retryState))
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
|
|
OnRetryOccurred(retryState);
|
|
|
|
if ((retryState.RetryCount > 1 || !FastFirstRetry) && !retryState.IsDelayDisabled)
|
|
{
|
|
Thread.Sleep(retryState.Delay);
|
|
}
|
|
|
|
// check for cancellation after delay.
|
|
if (token != null)
|
|
{
|
|
token.Value.ThrowIfCancellationRequested();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual RetryState CreateRetryState()
|
|
{
|
|
return new RetryState();
|
|
}
|
|
|
|
public bool IsRetryableException(Exception ex)
|
|
{
|
|
return ErrorDetectionStrategy.CanRetry(ex);
|
|
}
|
|
|
|
public bool ShouldRetry(RetryState retryState)
|
|
{
|
|
bool canRetry = ErrorDetectionStrategy.CanRetry(retryState.LastError);
|
|
bool shouldRetry = canRetry
|
|
&& ShouldRetryImpl(retryState);
|
|
|
|
Logger.Write(TraceEventType.Error,
|
|
string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"Retry requested: Retry count = {0}. Delay = {1}, SQL Error Number = {2}, Can retry error = {3}, Will retry = {4}",
|
|
retryState.RetryCount,
|
|
retryState.Delay,
|
|
GetErrorNumber(retryState.LastError),
|
|
canRetry,
|
|
shouldRetry));
|
|
|
|
// Perform an extra check in the delay interval. Should prevent from accidentally ending up with the value of -1 which will block a thread indefinitely.
|
|
// In addition, any other negative numbers will cause an ArgumentOutOfRangeException fault which will be thrown by Thread.Sleep.
|
|
if (retryState.Delay.TotalMilliseconds < 0)
|
|
{
|
|
retryState.Delay = TimeSpan.Zero;
|
|
}
|
|
return shouldRetry;
|
|
}
|
|
|
|
public bool ShouldIgnoreError(RetryState retryState)
|
|
{
|
|
bool shouldIgnoreError = ErrorDetectionStrategy.ShouldIgnoreError(retryState.LastError);
|
|
|
|
Logger.Write(TraceEventType.Error,
|
|
string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"Ignore Error requested: Retry count = {0}. Delay = {1}, SQL Error Number = {2}, Should Ignore Error = {3}",
|
|
retryState.RetryCount,
|
|
retryState.Delay,
|
|
GetErrorNumber(retryState.LastError),
|
|
shouldIgnoreError));
|
|
|
|
return shouldIgnoreError;
|
|
}
|
|
|
|
/* TODO - Error code does not exist in SqlException for .NET Core
|
|
private static int? GetErrorCode(Exception ex)
|
|
{
|
|
SqlException sqlEx= ex as SqlException;
|
|
if (sqlEx == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return sqlEx.ErrorCode;
|
|
}
|
|
*/
|
|
|
|
internal static int? GetErrorNumber(Exception ex)
|
|
{
|
|
SqlException sqlEx = ex as SqlException;
|
|
if (sqlEx == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return sqlEx.Number;
|
|
}
|
|
|
|
protected abstract bool ShouldRetryImpl(RetryState retryState);
|
|
|
|
/// <summary>
|
|
/// Notifies the subscribers whenever a retry condition is encountered.
|
|
/// </summary>
|
|
/// <param name="retryState">The state of current retry attempt.</param>
|
|
protected virtual void OnRetryOccurred(RetryState retryState)
|
|
{
|
|
var retryOccurred = RetryOccurred;
|
|
if (retryOccurred != null)
|
|
{
|
|
retryOccurred(retryState);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Notifies the subscribers whenever an error is ignored on retry.
|
|
/// </summary>
|
|
/// <param name="retryState">The state of current retry attempt.</param>
|
|
protected virtual void OnIgnoreErrorOccurred(RetryState retryState)
|
|
{
|
|
var ignoreErrorOccurred = IgnoreErrorOccurred;
|
|
if (ignoreErrorOccurred != null)
|
|
{
|
|
ignoreErrorOccurred(retryState);
|
|
}
|
|
}
|
|
|
|
internal class FixedDelayPolicy : RetryPolicy
|
|
{
|
|
private readonly int _maxRetryCount;
|
|
private readonly TimeSpan _intervalBetweenRetries;
|
|
|
|
/// <summary>
|
|
/// Constructs a new instance of the TRetryPolicy class with the specified number of retry attempts and time interval between retries.
|
|
/// </summary>
|
|
/// <param name="strategy">The <see cref="RetryPolicy.IErrorDetectionStrategy"/> to use when checking whether an error is retryable</param>
|
|
/// <param name="maxRetryCount">The max number of retry attempts. Should be 1-indexed.</param>
|
|
/// <param name="intervalBetweenRetries">The interval between retries.</param>
|
|
public FixedDelayPolicy(IErrorDetectionStrategy strategy, int maxRetryCount, TimeSpan intervalBetweenRetries)
|
|
: base(strategy)
|
|
{
|
|
Contract.Assert(maxRetryCount >= 0, "maxRetryCount cannot be a negative number");
|
|
Contract.Assert(intervalBetweenRetries.Ticks >= 0, "intervalBetweenRetries cannot be negative");
|
|
|
|
_maxRetryCount = maxRetryCount;
|
|
_intervalBetweenRetries = intervalBetweenRetries;
|
|
}
|
|
|
|
protected override bool ShouldRetryImpl(RetryState retryState)
|
|
{
|
|
Contract.Assert(retryState != null);
|
|
|
|
if (IsLessThanMaxRetryCount(retryState.RetryCount, _maxRetryCount))
|
|
{
|
|
retryState.Delay = _intervalBetweenRetries;
|
|
return true;
|
|
}
|
|
|
|
retryState.Delay = TimeSpan.Zero;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal class ProgressiveRetryPolicy : RetryPolicy
|
|
{
|
|
private readonly int _maxRetryCount;
|
|
private readonly TimeSpan _initialInterval;
|
|
private readonly TimeSpan _increment;
|
|
|
|
/// <summary>
|
|
/// Constructs a new instance of the TRetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries.
|
|
/// </summary>
|
|
/// <param name="strategy">The <see cref="RetryPolicy.IErrorDetectionStrategy"/> to use when checking whether an error is retryable</param>
|
|
/// <param name="maxRetryCount">The maximum number of retry attempts. Should be 1-indexed.</param>
|
|
/// <param name="initialInterval">The initial interval which will apply for the first retry.</param>
|
|
/// <param name="increment">The incremental time value which will be used for calculating the progressive delay between retries.</param>
|
|
public ProgressiveRetryPolicy(IErrorDetectionStrategy strategy, int maxRetryCount, TimeSpan initialInterval, TimeSpan increment)
|
|
: base(strategy)
|
|
{
|
|
Contract.Assert(maxRetryCount >= 0, "maxRetryCount cannot be a negative number");
|
|
Contract.Assert(initialInterval.Ticks >= 0, "retryInterval cannot be negative");
|
|
Contract.Assert(increment.Ticks >= 0, "retryInterval cannot be negative");
|
|
|
|
_maxRetryCount = maxRetryCount;
|
|
_initialInterval = initialInterval;
|
|
_increment = increment;
|
|
}
|
|
|
|
protected override bool ShouldRetryImpl(RetryState retryState)
|
|
{
|
|
Contract.Assert(retryState != null);
|
|
|
|
if (IsLessThanMaxRetryCount(retryState.RetryCount, _maxRetryCount))
|
|
{
|
|
retryState.Delay = TimeSpan.FromMilliseconds(_initialInterval.TotalMilliseconds + (_increment.TotalMilliseconds * (retryState.RetryCount - 1)));
|
|
return true;
|
|
}
|
|
|
|
retryState.Delay = TimeSpan.Zero;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal class ExponentialDelayRetryPolicy : RetryPolicy
|
|
{
|
|
private readonly int _maxRetryCount;
|
|
private readonly double _intervalFactor;
|
|
private readonly TimeSpan _minInterval;
|
|
private readonly TimeSpan _maxInterval;
|
|
|
|
/// <summary>
|
|
/// Constructs a new instance of the TRetryPolicy class with the specified number of retry attempts and parameters defining the progressive delay between retries.
|
|
/// </summary>
|
|
/// <param name="strategy">The <see cref="RetryPolicy.IErrorDetectionStrategy"/> to use when checking whether an error is retryable</param>
|
|
/// <param name="maxRetryCount">The maximum number of retry attempts.</param>
|
|
/// <param name="intervalFactor">Controls the speed at which the delay increases - the retryCount is raised to this power as
|
|
/// part of the function </param>
|
|
/// <param name="minInterval">Minimum interval between retries. The basis for all backoff calculations</param>
|
|
/// <param name="maxInterval">Maximum interval between retries. Backoff will not take longer than this period.</param>
|
|
public ExponentialDelayRetryPolicy(IErrorDetectionStrategy strategy, int maxRetryCount, double intervalFactor, TimeSpan minInterval, TimeSpan maxInterval)
|
|
: base(strategy)
|
|
{
|
|
Contract.Assert(maxRetryCount >= 0, "maxRetryCount cannot be a negative number");
|
|
Contract.Assert(intervalFactor > 1, "intervalFactor Must be > 1 so that the delay increases exponentially");
|
|
Contract.Assert(minInterval.Ticks >= 0, "minInterval cannot be negative");
|
|
Contract.Assert(maxInterval.Ticks >= 0, "maxInterval cannot be negative");
|
|
Contract.Assert(maxInterval.Ticks >= minInterval.Ticks, "maxInterval must be greater than minInterval");
|
|
|
|
_maxRetryCount = maxRetryCount;
|
|
_intervalFactor = intervalFactor;
|
|
_minInterval = minInterval;
|
|
_maxInterval = maxInterval;
|
|
}
|
|
|
|
protected override bool ShouldRetryImpl(RetryState retryState)
|
|
{
|
|
Contract.Assert(retryState != null);
|
|
|
|
if (IsLessThanMaxRetryCount(retryState.RetryCount, _maxRetryCount))
|
|
{
|
|
retryState.Delay = RetryPolicyUtils.CalcExponentialRetryDelay(retryState.RetryCount, _intervalFactor, _minInterval, _maxInterval);
|
|
return true;
|
|
}
|
|
|
|
retryState.Delay = TimeSpan.Zero;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
internal class TimeBasedRetryPolicy : RetryPolicy
|
|
{
|
|
private readonly TimeSpan _minTotalRetryTimeLimit;
|
|
private readonly TimeSpan _maxTotalRetryTimeLimit;
|
|
private readonly double _totalRetryTimeLimitRate;
|
|
|
|
private readonly TimeSpan _minInterval;
|
|
private readonly TimeSpan _maxInterval;
|
|
private readonly double _intervalFactor;
|
|
|
|
private readonly Stopwatch _stopwatch;
|
|
|
|
public TimeBasedRetryPolicy(
|
|
IErrorDetectionStrategy strategy,
|
|
TimeSpan minTotalRetryTimeLimit,
|
|
TimeSpan maxTotalRetryTimeLimit,
|
|
double totalRetryTimeLimitRate,
|
|
TimeSpan minInterval,
|
|
TimeSpan maxInterval,
|
|
double intervalFactor)
|
|
: base(strategy)
|
|
{
|
|
Contract.Assert(minTotalRetryTimeLimit.Ticks >= 0);
|
|
Contract.Assert(maxTotalRetryTimeLimit.Ticks >= minTotalRetryTimeLimit.Ticks);
|
|
Contract.Assert(totalRetryTimeLimitRate >= 0);
|
|
|
|
Contract.Assert(minInterval.Ticks >= 0);
|
|
Contract.Assert(maxInterval.Ticks >= minInterval.Ticks);
|
|
Contract.Assert(intervalFactor >= 1);
|
|
|
|
_minTotalRetryTimeLimit = minTotalRetryTimeLimit;
|
|
_maxTotalRetryTimeLimit = maxTotalRetryTimeLimit;
|
|
_totalRetryTimeLimitRate = totalRetryTimeLimitRate;
|
|
|
|
_minInterval = minInterval;
|
|
_maxInterval = maxInterval;
|
|
_intervalFactor = intervalFactor;
|
|
|
|
_stopwatch = Stopwatch.StartNew();
|
|
}
|
|
|
|
protected override bool ShouldRetryImpl(RetryState retryStateObj)
|
|
{
|
|
Contract.Assert(retryStateObj is RetryStateEx);
|
|
RetryStateEx retryState = (RetryStateEx)retryStateObj;
|
|
|
|
// Calculate the delay as exponential value based on the number of retries.
|
|
retryState.Delay =
|
|
RetryPolicyUtils.CalcExponentialRetryDelay(
|
|
retryState.RetryCount,
|
|
_intervalFactor,
|
|
_minInterval,
|
|
_maxInterval);
|
|
|
|
// Add the delay to the total retry time
|
|
retryState.TotalRetryTime = retryState.TotalRetryTime + retryState.Delay;
|
|
|
|
// Calculate the maximum total retry time depending on how long ago was the task (this retry policy) started.
|
|
// Longer running tasks are less eager to abort since, more work is has been done.
|
|
TimeSpan totalRetryTimeLimit = checked(TimeSpan.FromMilliseconds(
|
|
Math.Max(
|
|
Math.Min(
|
|
_stopwatch.ElapsedMilliseconds * _totalRetryTimeLimitRate,
|
|
_maxTotalRetryTimeLimit.TotalMilliseconds),
|
|
_minTotalRetryTimeLimit.TotalMilliseconds)));
|
|
|
|
if (retryState.TotalRetryTime <= totalRetryTimeLimit)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
retryState.Delay = TimeSpan.Zero;
|
|
return false;
|
|
}
|
|
|
|
protected override RetryState CreateRetryState()
|
|
{
|
|
return new RetryStateEx { TotalRetryTime = TimeSpan.Zero };
|
|
}
|
|
|
|
internal sealed class RetryStateEx : RetryState
|
|
{
|
|
public TimeSpan TotalRetryTime { get; set; }
|
|
}
|
|
}
|
|
}
|
|
}
|