Reputation: 117
using: Asp.net Core, Entityframework Core, ABP 4.5
I have a user registration and initialization flow. But it takes a long time. I want to parallelize this. This is due to updating from the same entity, but with a different field.
My goal: 1. The endpoint should respond as soon as possible; 2. Long initialization is processed in the background;
Code-before (minor details omitted for brevity)
public async Task<ResponceDto> Rgistration(RegModel input)
{
var user = await _userRegistrationManager.RegisterAsync(input.EmailAddress, input.Password, false );
var result = await _userManager.AddToRoleAsync(user, defaultRoleName);
user.Code = GenerateCode();
await SendEmail(user.EmailAddress, user.Code);
await AddSubEntities(user);
await AddSubCollectionEntities(user);
await CurrentUnitOfWork.SaveChangesAsync();
return user.MapTo<ResponceDto>();
}
private async Task AddSubEntities(User user)
{
var newSubEntity = new newSubEntity { User = user, UserId = user.Id };
await _subEntityRepo.InsertAsync(newSubEntity);
//few another One-to-One entities...
}
private async Task AddSubEntities(User user)
{
List<AnotherEntity> collection = GetSomeCollection(user.Type);
await _anotherEntitieRepo.GetDbContext().AddRangeAsync(collection);
//few another One-to-Many collections...
}
Try change:
public async Task<ResponceDto> Rgistration(RegModel input)
{
var user = await _userRegistrationManager.RegisterAsync(input.EmailAddress, input.Password, false );
Task.Run(async () => {
var result = await _userManager.AddToRoleAsync(user, defaultRoleName);
});
Task.Run(async () => {
user.Code = GenerateCode();
await SendEmail(user.EmailAddress, user.Code);
});
Task.Run(async () => {
using (var unitOfWork = UnitOfWorkManager.Begin())
{//long operation. defalt unitOfWork out of scope
try
{
await AddSubEntities(user);
}
finally
{
unitOfWork.Complete();
}
}
});
Task.Run(async () => {
using (var unitOfWork = UnitOfWorkManager.Begin())
{
try
{
await AddSubCollectionEntities(user);
}
finally
{
unitOfWork.Complete();
}
}
});
await CurrentUnitOfWork.SaveChangesAsync();
return user.MapTo<ResponceDto>();
}
Errors: here I get a lot of different errors related to competition. frequent:
A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext.
In few registratin calls: Cannot insert duplicate key row in object 'XXX' with unique index 'YYY'. The duplicate key value is (70). The statement has been terminated.
I thought on the server every request in its stream, but apparently not.
how to keep the user entity “open” for updating and at the same time “closed” for changes initiated by other requests? How to make this code thread safe and fast, can anyone help with advice?
Upvotes: 1
Views: 1393
Reputation: 239440
Task.Run
is not the same as parallel. It takes a new thread from the pool and runs the work on that thread, and since you're not awaiting it, the rest of the code can move on. However, that's because you're essentially orphaning that thread. When the action returns, all the scoped services will be disposed, which includes things like your context. Any threads that haven't finished, yet, will error out as a result.
The thread pool is a limited resource, and within the context of a web application, it equates directly to the throughput of your server. Every thread you take is one less request you can service. As a result, you're more likely to end up queuing requests, which will only add to processing time. It's virtually never appropriate to use Task.Run
in a web environment.
Also, EF Core (or old EF, for that matter) does not support parallelization. So, even without the other problems described above, it will stop you cold from doing what you're trying to do here, regardless.
The queries you have here are not complex. Even if you were trying to insert 100s of things at once, it should still take only milliseconds to complete. If there is any significant delay here, you need to look at the resources of your database server and your network latency, first.
More likely than not, the slow-down is coming from the sending of the email. That too can likely be optimized, though. I was in a situation once where it was taking emails 30 seconds to send, until I finally figured out that it was an issue with our Exchange server, where an IT admin had idiotically introduced a 30 second delay on purpose. Regardless, it is generally always preferable to background things like sending emails, since they aren't core to your app's functionality. However, that means actually processing them in background, i.e. queue them and process them via something like a hosted service or an entirely different worker process.
Upvotes: 3
Reputation: 20373
Using Task.Run
in ASP.NET is rarely a good idea.
Async methods run on the thread pool anyway, so wrapping them in Task.Run
is simply adding overhead without any benefit.
The purpose of using async in ASP.NET is simply to prevent threads being blocked so they are able to serve other HTTP requests.
Ultimately, your database is the bottleneck; if all these operations need to happen before you return a response to the client, then there's not much you can do other than to let them happen.
If it is possible to return early and allow some operations to continue running on the background, then there are details here showing how that can be done.
Upvotes: 4