Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use [DebuggerDisableUserUnhandledExceptions] #2254

Draft
wants to merge 13 commits into
base: dotnet-vnext
Choose a base branch
from

Conversation

martincostello
Copy link
Member

@martincostello martincostello commented Jul 31, 2024

  • Add usage of [DebuggerDisableUserUnhandledExceptions] in StrategyHelper and RetryEngine to avoid newer versions of the Visual Studio debugger for breaking for exceptions we are intentionally handling.
  • Add copy of [DebuggerDisableUserUnhandledExceptions] for use in downlevel versions of .NET that do not contain the attribute.
  • Add net9.0 TFM to potentially include to use the built-in definition of the attribute.
  • Add net8.0 TFM to Polly to resolve dependency resolution issues in the Snippets project.

Break for Async User-Unhandled exceptions in the Visual Studio Debugger

/cc @halter73 @gregg-miskelly

Copy link

codecov bot commented Jul 31, 2024

Codecov Report

Attention: Patch coverage is 95.23810% with 1 line in your changes missing coverage. Please review.

Project coverage is 85.28%. Comparing base (e548cc6) to head (52ff760).

Files with missing lines Patch % Lines
src/Polly/Timeout/AsyncTimeoutEngine.cs 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@               Coverage Diff                @@
##           dotnet-vnext    #2254      +/-   ##
================================================
- Coverage         85.29%   85.28%   -0.02%     
================================================
  Files               312      312              
  Lines              7426     7426              
  Branches           1118     1119       +1     
================================================
- Hits               6334     6333       -1     
  Misses              746      746              
- Partials            346      347       +1     
Flag Coverage Δ
linux 85.28% <95.23%> (-0.02%) ⬇️
macos 85.28% <95.23%> (-0.02%) ⬇️
windows 85.28% <95.23%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@gregg-miskelly
Copy link

When I try this locally, in the sync case, I am seeing the exception caught in the various lambdas such as this one:

static (context, state) =>

@gregg-miskelly
Copy link

I am seeing something similar in async where it is the lambda's in ResiliencePipeline.Async.cs that is actually calling the user's delegate and catching the exception, so that is where the [DebuggerDisableUserUnhandledExceptions] attribute needs to go.

@martincostello
Copy link
Member Author

martincostello commented Jul 31, 2024

Thanks for hunting those down. I didn't do a proper dig through the code (clearly) to find the closest place to the user's original code.

Tomorrow I'll look at moving the attributes around.

@martincostello
Copy link
Member Author

Using a search for catch (Exception as a guide, I've added a bunch of additional attributes to various methods.

Are there any other heuristics I can use to find likely useful places for this? Otherwise I'll need to rely on you both to see if it's behaving how you'd expect 😄

@gregg-miskelly
Copy link

Using a search for catch (Exception as a guide, I've added a bunch of additional attributes to various methods.

Searching for catch (Exception is a great way to start. DebuggerDisableUserUnhandledExceptionsAttribute only makes sense in methods with a catch and I would be surprised if there are any methods in Polly that catches a specific exception that would need this attribute.

Once you have found a method like that, then the follow up question is if there is a way for the exception being caught to be one which (1) is from user code -and- (2) should usually be ignored as Polly is very likely to retry the operation.

If you want to be able to test this work, Visual Studio 2022 version 17.12 preview 1 will support DebuggerDisableUserUnhandledExceptionsAttribute. 17.12 preview 1 isn't public yet, but it should be fairly soon. If you aren't familiar with Visual Studio previews, you can install them side by side your regular builds from the preview channel.

You can also somewhat test things today thusly --

  • In cases where the exception is thrown by a synchronous user method, you can test this with any version of .NET and Visual Studio.
  • In cases where the exception is thrown by an asynchronous user method, you can test this if you use a test app running on .NET 9 preview 6 + Visual Studio 2022 version 17.11, which can be obtained from the preview channel.

Sorry if this is obvious, but the basic way to test is:

  1. Create a new test executable that supplies a call back method that throws
  2. Ensure that Debug->Options->Enable Just My Code is checked.
  3. Ensure that the test app is using a Release-configuration version of Polly
  4. Run the test app under the debugger

If you are using 17.11 (async) / 17.11 or earlier (sync) you should see the debugger break "User-Unhandled". You want to make sure you have added DebuggerDisableUserUnhandledExceptions to whichever frame on the call stack has the catch block that will first catch the exception.

If you are using 17.12, you want to see that the debugger no longer breaks user unhandled.

I hope this helps and thanks so much for working on this!

src/Polly/Bulkhead/AsyncBulkheadEngine.cs Outdated Show resolved Hide resolved
src/Polly/Bulkhead/BulkheadEngine.cs Outdated Show resolved Hide resolved
src/Polly/Caching/AsyncCacheEngine.cs Show resolved Hide resolved
src/Polly/NoOp/NoOpEngine.cs Outdated Show resolved Hide resolved
src/Polly/RateLimit/AsyncRateLimitEngine.cs Outdated Show resolved Hide resolved
src/Polly/RateLimit/RateLimitEngine.cs Outdated Show resolved Hide resolved
@martincostello
Copy link
Member Author

@gregg-miskelly Thanks for the information. I already have previews installed, so I think I'll just wait until I get updated to 17.12 and then run out test suites inside the IDE in Release mode and see where the debugger breaks (or not).

@gregg-miskelly
Copy link

@gregg-miskelly Thanks for the information. I already have previews installed, so I think I'll just wait until I get updated to 17.12 and then run out test suites inside the IDE in Release mode and see where the debugger breaks (or not).

FYI 17.12 preview 1 was released today

@martincostello martincostello force-pushed the DebuggerDisableUserUnhandledExceptions branch from 75bafbf to 483a4e5 Compare August 14, 2024 09:43
@martincostello
Copy link
Member Author

@gregg-miskelly I was having a play around with this branch in 17.11 and 17.12 and I didn't really notice any difference with and without the attribute.

I'm probably doing something stupid, but I followed the instructions you posted the other week.

Do you have a simple GitHub repo that I could clone that shows the desired behavioural difference so I can check I've got things configured properly before I re-test with Polly?

@gregg-miskelly
Copy link

@martincostello I am sorry. Unfortunately, the preview 1 version of this work has a rather large bug that we are working to fix for preview 2. If you want to work around it rather than waiting for preview 2 --

  1. Start debugging, and hit a breakpoint before the exception is raised
  2. Open the modules window
  3. Load symbols for the module containing the [DebuggerDisableUserUnhandledExceptions]
  4. Verify the value of 'Optimized', 'User Code', and 'Symbol Status' look like this:

image

- Build with the .NET 9 (preview 6) SDK.
- Add tests TFM for `net9.0`.
- Remove tests for `net6.0` TFM.
- Run workflows on PRs to `dotnet-vnext` branch.
- Only audit direct NuGet dependencies.
- Fix IDE0022 warning.
- Temporarily disable some tests that are failing in GitHub Actions.
- Add usage of `[DebuggerDisableUserUnhandledExceptions]` in `StrategyHelper` and `RetryEngine` to avoid newer versions of the Visual Studio debugger for breaking for exceptions we are intentionally handling.
- Add copy of `[DebuggerDisableUserUnhandledExceptions]` for use in downlevel versions of .NET that do not contain the attribute.
- Add `net9.0` TFM to potentially include to use the built-in definition of the attribute.
- Add `net8.0` TFM to Polly to resolve dependency resolution issues in the Snippets project.
Remove redundant usage of `[DebuggerNonUserCode]`.
Add `[DebuggerDisableUserUnhandledExceptions]` to all the methods containing `catch (Exception...)` that appear to be executing the user's callback.
Not required due to lack of catch.
Remove redundant attributes.
Do not include DebuggerDisableUserUnhandledExceptionsAttribute for .NET 9 as it is built-in as of preview 7.
Suppress S3236 warnings.
Suppress S3236 warning.
@martincostello martincostello force-pushed the DebuggerDisableUserUnhandledExceptions branch from a374368 to 14b80f0 Compare September 15, 2024 08:17
Re-enable tests.
@martincostello
Copy link
Member Author

Just come back to this, and I'm still failing to see a difference in behaviour with 17.12.0 Preview 2.0. I used the Polly-Samples repo and ran the EFCore sample.

My understanding was that with Polly 8.4.0, Visual Studio would break here, but with the changes from this branch it wouldn't. It practice, I'm seeing the same behaviour with both - Visual Studio doesn't break and I just see the exceptions logged to the console.

I'm consuming Polly via a local NuGet package source using the .nupkg files out of CI so it's more representative of what a user would experience (rather than Polly locally compiled). I disabled Just My Code to step into Polly.Core, and then the Modules window shows this:

image

I feel like I must be missing something here.

Could you share a simple GitHub project that's set up in a way that should illustrate the difference? Then I can pull this branch's code into it and verify if I see the same.

Seems to work with `net9.0` now.
Accidentally removed.
@gregg-miskelly
Copy link

My understanding was that with Polly 8.4.0, Visual Studio would break here, but with the changes from this branch it wouldn't.

Your understanding of the expected behavior is correct. What version of the .NET Runtime is the process you are debugging using? For async-user unhandled, you need to modify the client to use .NET 9. Also, if you are on .NET 9, do you have Just My Code enabled?

@martincostello
Copy link
Member Author

Ah, that's probably what I missed - I didn't update the sample to .NET 9 and was just using the CI build for net8.0 with the copy of the attribute in it. I'll update the host project tomorrow and try again.

@martincostello
Copy link
Member Author

Yep, that's what it was.

Using App-vNext/Polly-Samples#107, the debugger breaks here when an exception is thrown now when the app is targeting net9.0. With net8.0, it just gets logged to the console and the debugger doesn't break.

That suggests to me that I'm missing one or more attributes as it should be being ignored and not breaking as it's being retried?

@martincostello
Copy link
Member Author

This is what I see in Visual Studio:

image

@gregg-miskelly
Copy link

That suggests to me that I'm missing one or more attributes as it should be being ignored and not breaking as it's being retried?

Agreed. If show external code, and inspect the frames on the call stack, you can hopefully find the frame with the catch block.

martincostello added a commit to App-vNext/Polly-Samples that referenced this pull request Sep 19, 2024
Add `[DebuggerDisableUserUnhandledExceptions]` to `StrategyHelper`.
@martincostello
Copy link
Member Author

@gregg-miskelly I thought I'd found one I'd missed, but ingesting that change still doesn't seem to have changed the behaviour - would you mind cloning App-vNext/Polly-Samples#107 locally and taking a look to see what I'm missing?

@gregg-miskelly
Copy link

@martincostello Thanks for continuing to work on this!

The attribute usage in Polly is correct. The issue is that there is a non-user async method on the stack above Polly's catch handler (Microsoft.EntityFrameworkCore.dll!Microsoft.EntityFrameworkCore.ExecutionStrategyExtensions.ExecuteAsync.AnonymousMethod__4_0). Any async method will have a compiler-generated catch handler. In hindsight this should have been obvious to us, but we didn't think about those compiler-generated catch blocks when we added DebuggerDisableUserUnhandledExceptions support. So the debugger sees the catch handler from EntityFramework and thinks this exception should be user-unhandled. We think it shouldn't be too hard to teach the debugger to ignore these catch handlers and let the exception continue propagating. We hope we will be able to fix this for Preview 3, but that is shutting down soon so we will see.

Thanks again and sorry for all the problems.

@martincostello
Copy link
Member Author

No worries, that's the point of testing 😄

Let me know when a fix makes it into a public preview of 17.12 and then I'll try again.

@martincostello
Copy link
Member Author

One more question: do we need to use the attribute in methods with finally blocks without a catch?

Running the other demos in the same project, there's another one that is breaking in the debugger that has this method on the stack - do we need to mark those methods with the attribute too, or is this similar to the EFCore case?

@gregg-miskelly
Copy link

One more question: do we need to use the attribute in methods with finally blocks without a catch?

No, you only need to worry about catch blocks.

Running the other demos in the same project, there's another one that is breaking in the debugger that has this method on the stack - do we need to mark those methods with the attribute too, or is this similar to the EFCore case?

It looks like the same issue as the EFCore case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants