Addressing concurrency exceptions when incrementing the download count. #716
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
Automatic retry when incrementing the download count throws DbUpdateConcurrencyException.
Problem
When a package is requested, BaGet will update the package record, incrementing the Downloads field by 1. This is done with EF Core, where the record is first retrieved from the database, modified in memory, and then saved with a call to SaveChangesAsync().
If the record in the database is modified in between retrieving the record and the call to SaveChangesAsync, then a DbUpdateConcurrencyException is raised, leading to a 500 status code and the following error message:
This can happen when there are two requests for the same package around the same time, which should be expected for parallel CI pipelines running dotnet restore or for popular packages.
Solution
I have fixed the issue for myself (I think) and have created this PR in case you would like to merge it, or to help anyone else with the issue.
The solution works by retrying the operation up to 5 attempts, and then throwing the error.
Something I needed to do (which you might not be happy with) is to change the DbContext registration from scoped to transient. This is so that I can create a new DbContext for each attempt rather than fix up a DbContext in an invalid state.
I also needed to fix up the tests, which were failing in my local environment (because of UTC+8).
Another idea might be to find a platform independent way to run the following SQL, without first having to retrieve the record and therefore risk the concurrency error in the first place.