[backend/federation] Fix race condition when updating a user during a request

This commit is contained in:
Laura Hausmann 2024-02-17 19:42:15 +01:00
parent 0884f462c0
commit f073018e95
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
5 changed files with 29 additions and 16 deletions

View file

@ -26,7 +26,7 @@ public class DriveFile : IEntity
/// The created date of the DriveFile.
/// </summary>
[Column("createdAt")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime CreatedAt { get; set; }
/// <summary>
/// The owner ID.
@ -188,7 +188,7 @@ public class DriveFile : IEntity
[Key]
[Column("id")]
[StringLength(32)]
public string Id { get; set; } = IdHelpers.GenerateSlowflakeId();
public string Id { get; set; } = null!;
public class FileProperties
{

View file

@ -16,11 +16,9 @@ public class ActivityDeliverService(ILogger<ActivityDeliverService> logger, Queu
await queueService.PreDeliverQueue.EnqueueAsync(new PreDeliverJob
{
ActorId = actor.Id,
RecipientIds = recipients.Select(p => p.Id).ToList(),
SerializedActivity =
JsonConvert.SerializeObject(activity,
LdHelpers.JsonSerializerSettings),
ActorId = actor.Id,
RecipientIds = recipients.Select(p => p.Id).ToList(),
SerializedActivity = JsonConvert.SerializeObject(activity, LdHelpers.JsonSerializerSettings),
DeliverToFollowers = true
});
}
@ -32,11 +30,9 @@ public class ActivityDeliverService(ILogger<ActivityDeliverService> logger, Queu
await queueService.PreDeliverQueue.EnqueueAsync(new PreDeliverJob
{
ActorId = actor.Id,
RecipientIds = recipients.Select(p => p.Id).ToList(),
SerializedActivity =
JsonConvert.SerializeObject(activity,
LdHelpers.JsonSerializerSettings),
ActorId = actor.Id,
RecipientIds = recipients.Select(p => p.Id).ToList(),
SerializedActivity = JsonConvert.SerializeObject(activity, LdHelpers.JsonSerializerSettings),
DeliverToFollowers = false
});
}

View file

@ -112,13 +112,18 @@ public class UserResolver(
private async Task<User> GetUpdatedUser(User user)
{
if (!user.NeedsUpdate) return user;
user.LastFetchedAt = DateTime.UtcNow; // Prevent multiple background tasks from being started
try
{
var task = followupTaskSvc.ExecuteTask("UpdateUserAsync", async provider =>
{
// Get a fresh UserService instance in a new scope
var bgUserSvc = provider.GetRequiredService<UserService>();
await bgUserSvc.UpdateUserAsync(user);
// Use the id overload so it doesn't attempt to insert in the main thread's DbContext
var fetchedUser = await bgUserSvc.UpdateUserAsync(user.Id);
user = fetchedUser;
});
// Return early, but continue execution in background

View file

@ -4,6 +4,7 @@ using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Federation.Cryptography;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Middleware;
using Iceshrimp.Backend.Core.Queues;
using Microsoft.EntityFrameworkCore;
@ -240,6 +241,8 @@ public class DriveService(
file = new DriveFile
{
Id = IdHelpers.GenerateSlowflakeId(),
CreatedAt = DateTime.UtcNow,
User = user,
UserHost = user.Host,
Sha256 = digest,
@ -332,6 +335,8 @@ file static class DriveFileExtensions
return new DriveFile
{
Id = IdHelpers.GenerateSlowflakeId(),
CreatedAt = DateTime.UtcNow,
User = user,
Blurhash = file.Blurhash,
Type = file.Type,

View file

@ -129,7 +129,7 @@ public class UserService(
try
{
await db.AddRangeAsync(user, profile, publicKey);
// We need to do this after calling db.Add(Range) to ensure data consistency
var processPendingDeletes = await ResolveAvatarAndBanner(user, actor);
await db.SaveChangesAsync();
@ -158,9 +158,16 @@ public class UserService(
}
}
public async Task<User> UpdateUserAsync(User user, ASActor? actor = null)
public async Task<User> UpdateUserAsync(string id)
{
if (!user.NeedsUpdate && actor == null) return user;
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id) ??
throw new Exception("Cannot update nonexistent user");
return await UpdateUserAsync(user, force: true);
}
public async Task<User> UpdateUserAsync(User user, ASActor? actor = null, bool force = false)
{
if (!user.NeedsUpdate && actor == null && !force) return user;
if (actor is { IsUnresolved: true } or { Username: null })
actor = null; // This will trigger a fetch a couple lines down