Skip to content

Commit

Permalink
Use [DebuggerDisableUserUnhandledExceptions] (#2254)
Browse files Browse the repository at this point in the history
- Add usage of `[DebuggerDisableUserUnhandledExceptions]` to avoid newer versions of the Visual Studio debugger for breaking for exceptions on async code paths we are intentionally handling.
- Add copy of `[DebuggerDisableUserUnhandledExceptions]` for use in downlevel versions of .NET that do not contain the attribute.
  • Loading branch information
martincostello authored Oct 16, 2024
1 parent d090973 commit 73c7feb
Show file tree
Hide file tree
Showing 24 changed files with 74 additions and 21 deletions.
2 changes: 2 additions & 0 deletions src/Polly.Core/Hedging/Controller/TaskExecution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ public async ValueTask ResetAsync()
_stopExecutionTimestamp = 0;
}

[DebuggerDisableUserUnhandledExceptions]
private async Task ExecuteSecondaryActionAsync(Func<ValueTask<Outcome<T>>> action)
{
Outcome<T> outcome;
Expand All @@ -218,6 +219,7 @@ private async Task ExecuteSecondaryActionAsync(Func<ValueTask<Outcome<T>>> actio

private async Task ExecuteCreateActionException(Exception e) => await UpdateOutcomeAsync(Polly.Outcome.FromException<T>(e)).ConfigureAwait(Context.ContinueOnCapturedContext);

[DebuggerDisableUserUnhandledExceptions]
private async Task ExecutePrimaryActionAsync<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> primaryCallback, TState state)
{
Outcome<T> outcome;
Expand Down
1 change: 0 additions & 1 deletion src/Polly.Core/Outcome.TResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,4 @@ internal TResult GetResultOrRethrow()
ExceptionDispatchInfo?.Throw();
return Result!;
}

}
4 changes: 4 additions & 0 deletions src/Polly.Core/Polly.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@
<PackageReference Include="System.ComponentModel.Annotations" Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'netcoreapp3.1'))" />
</ItemGroup>

<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net9.0'))">
<Compile Include="$(MSBuildThisFileDirectory)..\Shared\DebuggerDisableUserUnhandledExceptionsAttribute.cs" Link="DebuggerDisableUserUnhandledExceptionsAttribute.cs" />
</ItemGroup>

</Project>
2 changes: 2 additions & 0 deletions src/Polly.Core/ResilienceContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ internal void InitializeFrom(ResilienceContext context, CancellationToken cancel
Properties.AddOrReplaceProperties(context.Properties);
}

#pragma warning disable S3236 // Remove this argument from the method call; it hides the caller information.
[ExcludeFromCodeCoverage]
[Conditional("DEBUG")]
internal void AssertInitialized() => Debug.Assert(IsInitialized, "The resilience context is not initialized.");
#pragma warning restore S3236 // Remove this argument from the method call; it hides the caller information.

internal ResilienceContext Initialize<TResult>(bool isSynchronous)
{
Expand Down
8 changes: 4 additions & 4 deletions src/Polly.Core/ResiliencePipeline.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public async ValueTask ExecuteAsync<TState>(
InitializeAsyncContext(context);

var outcome = await Component.ExecuteCore(
static async (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static async (context, state) =>
{
try
{
Expand Down Expand Up @@ -60,7 +60,7 @@ public async ValueTask ExecuteAsync(
InitializeAsyncContext(context);

var outcome = await Component.ExecuteCore(
static async (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static async (context, state) =>
{
try
{
Expand Down Expand Up @@ -99,7 +99,7 @@ public async ValueTask ExecuteAsync<TState>(
try
{
var outcome = await Component.ExecuteCore(
static async (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static async (context, state) =>
{
try
{
Expand Down Expand Up @@ -140,7 +140,7 @@ public async ValueTask ExecuteAsync(
try
{
var outcome = await Component.ExecuteCore(
static async (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static async (context, state) =>
{
try
{
Expand Down
8 changes: 4 additions & 4 deletions src/Polly.Core/ResiliencePipeline.AsyncT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async ValueTask<TResult> ExecuteAsync<TResult, TState>(
InitializeAsyncContext<TResult>(context);

var outcome = await Component.ExecuteCore(
static async (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static async (context, state) =>
{
try
{
Expand Down Expand Up @@ -88,7 +88,7 @@ public async ValueTask<TResult> ExecuteAsync<TResult>(
InitializeAsyncContext<TResult>(context);

var outcome = await Component.ExecuteCore(
static async (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static async (context, state) =>
{
try
{
Expand Down Expand Up @@ -127,7 +127,7 @@ public async ValueTask<TResult> ExecuteAsync<TResult, TState>(
try
{
var outcome = await Component.ExecuteCore(
static async (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static async (context, state) =>
{
try
{
Expand Down Expand Up @@ -168,7 +168,7 @@ public async ValueTask<TResult> ExecuteAsync<TResult>(
try
{
var outcome = await Component.ExecuteCore(
static async (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static async (context, state) =>
{
try
{
Expand Down
12 changes: 6 additions & 6 deletions src/Polly.Core/ResiliencePipeline.Sync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void Execute<TState>(
InitializeSyncContext(context);

Component.ExecuteCoreSync(
static (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static (context, state) =>
{
try
{
Expand Down Expand Up @@ -56,7 +56,7 @@ public void Execute(
InitializeSyncContext(context);

Component.ExecuteCoreSync(
static (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static (context, state) =>
{
try
{
Expand Down Expand Up @@ -92,7 +92,7 @@ public void Execute<TState>(
try
{
Component.ExecuteCoreSync(
static (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static (context, state) =>
{
try
{
Expand Down Expand Up @@ -130,7 +130,7 @@ public void Execute(
try
{
Component.ExecuteCoreSync(
static (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static (context, state) =>
{
try
{
Expand Down Expand Up @@ -169,7 +169,7 @@ public void Execute<TState>(
try
{
Component.ExecuteCoreSync(
static (_, state) =>
[DebuggerDisableUserUnhandledExceptions] static (_, state) =>
{
try
{
Expand Down Expand Up @@ -204,7 +204,7 @@ public void Execute(Action callback)
try
{
Component.ExecuteCoreSync(
static (_, state) =>
[DebuggerDisableUserUnhandledExceptions] static (_, state) =>
{
try
{
Expand Down
12 changes: 6 additions & 6 deletions src/Polly.Core/ResiliencePipeline.SyncT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public TResult Execute<TResult, TState>(
InitializeSyncContext<TResult>(context);

return Component.ExecuteCoreSync(
static (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static (context, state) =>
{
try
{
Expand Down Expand Up @@ -60,7 +60,7 @@ public TResult Execute<TResult>(
InitializeSyncContext<TResult>(context);

return Component.ExecuteCoreSync(
static (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static (context, state) =>
{
try
{
Expand Down Expand Up @@ -95,7 +95,7 @@ public TResult Execute<TResult>(
try
{
return Component.ExecuteCoreSync(
static (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static (context, state) =>
{
try
{
Expand Down Expand Up @@ -131,7 +131,7 @@ public TResult Execute<TResult>(Func<TResult> callback)
try
{
return Component.ExecuteCoreSync(
static (_, state) =>
[DebuggerDisableUserUnhandledExceptions] static (_, state) =>
{
try
{
Expand Down Expand Up @@ -169,7 +169,7 @@ public TResult Execute<TResult, TState>(Func<TState, TResult> callback, TState s
try
{
return Component.ExecuteCoreSync(
static (_, state) =>
[DebuggerDisableUserUnhandledExceptions] static (_, state) =>
{
try
{
Expand Down Expand Up @@ -211,7 +211,7 @@ public TResult Execute<TResult, TState>(
try
{
return Component.ExecuteCoreSync(
static (context, state) =>
[DebuggerDisableUserUnhandledExceptions] static (context, state) =>
{
try
{
Expand Down
2 changes: 2 additions & 0 deletions src/Polly.Core/Retry/RetryHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ private static TimeSpan DecorrelatedJitterBackoffV2(int attempt, TimeSpan baseDe

long ticks = (long)Math.Min(formulaIntrinsicValue * RpScalingFactor * targetTicksFirstDelay, MaxTimeSpanTicks);

#pragma warning disable S3236 // Remove this argument from the method call; it hides the caller information.
Debug.Assert(ticks >= 0, "ticks cannot be negative");
#pragma warning restore S3236 // Remove this argument from the method call; it hides the caller information.

return TimeSpan.FromTicks(ticks);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Polly.Core/Retry/RetryResilienceStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ protected internal override async ValueTask<Outcome<T>> ExecuteCore<TState>(Func
}
}

#pragma warning disable S3236 // Remove this argument from the method call; it hides the caller information.
Debug.Assert(delay >= TimeSpan.Zero, "The delay cannot be negative.");
#pragma warning restore S3236 // Remove this argument from the method call; it hides the caller information.

var onRetryArgs = new OnRetryArguments<T>(context, outcome, attempt, delay, executionTime);
_telemetry.Report<OnRetryArguments<T>, T>(new(ResilienceEventSeverity.Warning, RetryConstants.OnRetryEvent), onRetryArgs);
Expand Down
2 changes: 2 additions & 0 deletions src/Polly.Core/Utils/StrategyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

internal static class StrategyHelper
{
[DebuggerDisableUserUnhandledExceptions]
public static ValueTask<Outcome<TResult>> ExecuteCallbackSafeAsync<TResult, TState>(
Func<ResilienceContext, TState, ValueTask<Outcome<TResult>>> callback,
ResilienceContext context,
Expand All @@ -29,6 +30,7 @@ public static ValueTask<Outcome<TResult>> ExecuteCallbackSafeAsync<TResult, TSta
return new ValueTask<Outcome<TResult>>(Outcome.FromException<TResult>(e));
}

[DebuggerDisableUserUnhandledExceptions]
static async ValueTask<Outcome<T>> AwaitTask<T>(ValueTask<Outcome<T>> task, bool continueOnCapturedContext)
{
try
Expand Down
1 change: 1 addition & 0 deletions src/Polly.Core/Utils/TaskHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Polly.Utils;

#pragma warning disable S3236 // Remove this argument from the method call; it hides the caller information.
#pragma warning disable S5034 // "ValueTask" should be consumed correctly

internal static class TaskHelper
Expand Down
1 change: 1 addition & 0 deletions src/Polly/Caching/AsyncCacheEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Polly.Caching;

internal static class AsyncCacheEngine
{
[DebuggerDisableUserUnhandledExceptions]
internal static async Task<TResult> ImplementationAsync<TResult>(
IAsyncCacheProvider<TResult> cacheProvider,
ITtlStrategy<TResult> ttlStrategy,
Expand Down
1 change: 1 addition & 0 deletions src/Polly/Caching/CacheEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Polly.Caching;

internal static class CacheEngine
{
[DebuggerDisableUserUnhandledExceptions]
internal static TResult Implementation<TResult>(
ISyncCacheProvider<TResult> cacheProvider,
ITtlStrategy<TResult> ttlStrategy,
Expand Down
1 change: 1 addition & 0 deletions src/Polly/CircuitBreaker/AsyncCircuitBreakerEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

internal static class AsyncCircuitBreakerEngine
{
[DebuggerDisableUserUnhandledExceptions]
internal static async Task<TResult> ImplementationAsync<TResult>(
Func<Context, CancellationToken, Task<TResult>> action,
Context context,
Expand Down
1 change: 1 addition & 0 deletions src/Polly/CircuitBreaker/CircuitBreakerEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Polly.CircuitBreaker;

internal static class CircuitBreakerEngine
{
[DebuggerDisableUserUnhandledExceptions]
internal static TResult Implementation<TResult>(
Func<Context, CancellationToken, TResult> action,
Context context,
Expand Down
1 change: 1 addition & 0 deletions src/Polly/Fallback/AsyncFallbackEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Polly.Fallback;

internal static class AsyncFallbackEngine
{
[DebuggerDisableUserUnhandledExceptions]
internal static async Task<TResult> ImplementationAsync<TResult>(
Func<Context, CancellationToken, Task<TResult>> action,
Context context,
Expand Down
1 change: 1 addition & 0 deletions src/Polly/Fallback/FallbackEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Polly.Fallback;

internal static class FallbackEngine
{
[DebuggerDisableUserUnhandledExceptions]
internal static TResult Implementation<TResult>(
Func<Context, CancellationToken, TResult> action,
Context context,
Expand Down
4 changes: 4 additions & 0 deletions src/Polly/Polly.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@
<ProjectReference Include="..\Polly.Core\Polly.Core.csproj" />
</ItemGroup>

<ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net9.0'))">
<Compile Include="$(MSBuildThisFileDirectory)..\Shared\DebuggerDisableUserUnhandledExceptionsAttribute.cs" Link="DebuggerDisableUserUnhandledExceptionsAttribute.cs" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions src/Polly/Retry/AsyncRetryEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Polly.Retry;

internal static class AsyncRetryEngine
{
[DebuggerDisableUserUnhandledExceptions]
internal static async Task<TResult> ImplementationAsync<TResult>(
Func<Context, CancellationToken, Task<TResult>> action,
Context context,
Expand Down
1 change: 1 addition & 0 deletions src/Polly/Retry/RetryEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace Polly.Retry;

internal static class RetryEngine
{
[DebuggerDisableUserUnhandledExceptions]
internal static TResult Implementation<TResult>(
Func<Context, CancellationToken, TResult> action,
Context context,
Expand Down
5 changes: 5 additions & 0 deletions src/Polly/Timeout/AsyncTimeoutEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

internal static class AsyncTimeoutEngine
{
[DebuggerDisableUserUnhandledExceptions]
internal static async Task<TResult> ImplementationAsync<TResult>(
Func<Context, CancellationToken, Task<TResult>> action,
Context context,
Expand Down Expand Up @@ -57,7 +58,11 @@ internal static async Task<TResult> ImplementationAsync<TResult>(
// See https://github.com/App-vNext/Polly/issues/722.
if (!combinedTokenSource.IsCancellationRequested && timeoutCancellationTokenSource.IsCancellationRequested)
{
#if NET8_0_OR_GREATER
await combinedTokenSource.CancelAsync().ConfigureAwait(false);
#else
combinedTokenSource.Cancel();
#endif
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/Polly/Timeout/TimeoutEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Polly.Timeout;

internal static class TimeoutEngine
{
[DebuggerDisableUserUnhandledExceptions]
internal static TResult Implementation<TResult>(
Func<Context, CancellationToken, TResult> action,
Context context,
Expand Down
21 changes: 21 additions & 0 deletions src/Shared/DebuggerDisableUserUnhandledExceptionsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma warning disable
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// Adapted from https://github.com/dotnet/runtime/blob/bffe34e0c7ff8a05e79d884ed8447426aae17bfb/src/libraries/System.Private.CoreLib/src/System/Diagnostics/DebuggerDisableUserUnhandledExceptionsAttribute.cs
// See the following links for more information and context:
// https://github.com/dotnet/runtime/issues/103105,
// https://github.com/dotnet/runtime/pull/104813
// https://github.com/dotnet/aspnetcore/issues/57085

namespace System.Diagnostics;

/// <summary>
/// If a .NET Debugger is attached which supports the Debugger.BreakForUserUnhandledException(Exception) API,
/// this attribute will prevent the debugger from breaking on user-unhandled exceptions when the
/// exception is caught by a method with this attribute, unless BreakForUserUnhandledException is called.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
internal sealed class DebuggerDisableUserUnhandledExceptionsAttribute : Attribute
{
}

0 comments on commit 73c7feb

Please sign in to comment.