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

Addressing concurrency exceptions when incrementing the download count. #716

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

inkysquid
Copy link

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:

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.

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.

UPDATE
    Packages
SET
    Downloads = Downloads + 1 
WHERE 
    Id = @id AND
    NormalizedVersionString = @normalizedVersionString 


public PackageDatabase(IContext context)
public PackageDatabase(IContext context, Func<IContext> newContext)

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?

Copy link
Author

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();

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?

Copy link
Author

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.

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