Addressing concurrency exceptions when incrementing the download count. - #716
Open
inkysquid wants to merge 6 commits into
Open
Addressing concurrency exceptions when incrementing the download count.#716inkysquid wants to merge 6 commits into
inkysquid wants to merge 6 commits into
Conversation
added 5 commits
January 12, 2022 18:54
…ices can manage the DbContext's lifetime.
BlythMeister
suggested changes
Apr 27, 2022
| private readonly Func<IContext> _newContext; | ||
|
|
||
| public PackageDatabase(IContext context) | ||
| public PackageDatabase(IContext context, Func<IContext> newContext) |
There was a problem hiding this comment.
Suggest naming this context generator so it's clearer what it is?
Author
There was a problem hiding this comment.
I prefer to name the factory closer to the idiomatic C# code, i.e.
using var context =_newContext();
as an analog to
using var context = new Context();
I can can probably be convinced to rename to _createContext ;)
Let's be honest though, we all nab the fixes and then apply our own naming conventions here.
|
|
||
| public async Task<PackageAddResult> AddAsync(Package package, CancellationToken cancellationToken) | ||
| { | ||
| using var context = _newContext(); |
There was a problem hiding this comment.
This read as confusing with the field _context and this.
Why is the field still required? Could it not always generate context?
Author
There was a problem hiding this comment.
Agree and have updated this. Would like to change this everywhere to be honest. I don't like relying on DI to manage DbContext lifetime.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.