-
Notifications
You must be signed in to change notification settings - Fork 684
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
Addressing concurrency exceptions when incrementing the download count. #716
base: main
Are you sure you want to change the base?
Conversation
…ices can manage the DbContext's lifetime.
src/BaGet.Core/PackageDatabase.cs
Outdated
|
||
public PackageDatabase(IContext context) | ||
public PackageDatabase(IContext context, Func<IContext> newContext) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest naming this context generator so it's clearer what it is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This read as confusing with the field _context and this.
Why is the field still required? Could it not always generate context?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.