Files
sqltoolsservice/src/Microsoft.SqlTools.ServiceLayer/Connection/ReliableConnection/RetryPolicy.cs
ranasaria 09652cccd1 Enhanced Logging for sqltoolsservice (#695)
This change modifies the logging framework within sqltoolservice.
Moves away from custom Logger object to start using .Net tracing framework. It supports for the static Trace and TraceSource way of logging. For all new code it is recommend that we log the log messages using the existing static Logger class, while the code changes will continue to route the older Trace.Write* calls from the process to same log listeners (and thus the log targets) as used by the Logger class. Thus tracing in SMO code that uses Trace.Write* methods gets routed to the same file as the messages from rest of SQLTools Service code.
Make changes to start using .Net Frameworks codebase for all logging to unify our logging story.
Allows parameter to set tracingLevel filters that controls what kinds of message make it to the log file.
Allows a parameter to set a specific log file name so if these gets set by external code (the UI code using the tools service for example) then the external code is aware of the current log file in use.
Adding unittests to test out the existing and improved logging capabilities.


Sequences of checkins in development branch:
* Saving v1 of logging to prepare for code review. Minor cleanup and some end to end testing still remains
* Removing local launchSettings.json files
* added support for lazy listener to sqltoolsloglistener and removed incorrect changes to comments across files in previous checkin
* Converting time to local time when writing entries to the log
* move the hosting.v2 to new .net based logging code
* removing *.dgml files and addding them to .gitignore
* fixing typo of defaultTraceSource
* Addressing pull request feedback
* Adding a test to verify logging from SMO codebase
* propogating changes to v1 sqltools.hosting commandoptions.cs to the v2 version
* Fixing comments on start and stop callstack methods and whitespaces
* Commenting a test that got uncommented by mistake
* addding .gitattributes file as .sql file was observed to be misconstrued as a binary file
2018-09-24 23:55:59 -07:00

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 System.Data.SqlClient;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Threading;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.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; }
}
}
}
}