mirror of
https://github.com/bitwarden/server.git
synced 2025-12-10 00:42:07 -06:00
Configure EF to gracefully handle deadlocks
This commit is contained in:
parent
f5393fa643
commit
1c03ef110e
@ -47,22 +47,34 @@ public static class EntityFrameworkServiceCollectionExtensions
|
||||
{
|
||||
if (provider == SupportedDatabaseProviders.Postgres)
|
||||
{
|
||||
options.UseNpgsql(connectionString, b => b.MigrationsAssembly("PostgresMigrations"));
|
||||
options.UseNpgsql(connectionString, b =>
|
||||
{
|
||||
b.MigrationsAssembly("PostgresMigrations");
|
||||
b.EnableRetryOnFailure();
|
||||
});
|
||||
// Handle NpgSql Legacy Support for `timestamp without timezone` issue
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
}
|
||||
else if (provider == SupportedDatabaseProviders.MySql)
|
||||
{
|
||||
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString),
|
||||
b => b.MigrationsAssembly("MySqlMigrations"));
|
||||
b =>
|
||||
{
|
||||
b.MigrationsAssembly("MySqlMigrations");
|
||||
b.EnableRetryOnFailure();
|
||||
});
|
||||
}
|
||||
else if (provider == SupportedDatabaseProviders.Sqlite)
|
||||
{
|
||||
// SQLite doesn't support EnableRetryOnFailure
|
||||
options.UseSqlite(connectionString, b => b.MigrationsAssembly("SqliteMigrations"));
|
||||
}
|
||||
else if (provider == SupportedDatabaseProviders.SqlServer)
|
||||
{
|
||||
options.UseSqlServer(connectionString);
|
||||
options.UseSqlServer(connectionString, b =>
|
||||
{
|
||||
b.EnableRetryOnFailure();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -826,70 +826,76 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
// Use SERIALIZABLE isolation level to prevent race conditions during concurrent calls
|
||||
using var transaction = await dbContext.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
|
||||
// Use EF's execution strategy to handle transient failures (including deadlocks)
|
||||
var strategy = dbContext.Database.CreateExecutionStrategy();
|
||||
|
||||
try
|
||||
return await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
// Check if this organization user already has a default collection
|
||||
// SERIALIZABLE ensures this SELECT acquires range locks
|
||||
var existingDefaultCollection = await (
|
||||
from c in dbContext.Collections
|
||||
join cu in dbContext.CollectionUsers on c.Id equals cu.CollectionId
|
||||
where cu.OrganizationUserId == organizationUserId
|
||||
&& c.OrganizationId == organizationId
|
||||
&& c.Type == CollectionType.DefaultUserCollection
|
||||
select c
|
||||
).FirstOrDefaultAsync();
|
||||
// Use SERIALIZABLE isolation level to prevent race conditions during concurrent calls
|
||||
using var transaction = await dbContext.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
|
||||
|
||||
// If collection already exists, return false (not created)
|
||||
if (existingDefaultCollection != null)
|
||||
try
|
||||
{
|
||||
// Check if this organization user already has a default collection
|
||||
// SERIALIZABLE ensures this SELECT acquires range locks
|
||||
var existingDefaultCollection = await (
|
||||
from c in dbContext.Collections
|
||||
join cu in dbContext.CollectionUsers on c.Id equals cu.CollectionId
|
||||
where cu.OrganizationUserId == organizationUserId
|
||||
&& c.OrganizationId == organizationId
|
||||
&& c.Type == CollectionType.DefaultUserCollection
|
||||
select c
|
||||
).FirstOrDefaultAsync();
|
||||
|
||||
// If collection already exists, return false (not created)
|
||||
if (existingDefaultCollection != null)
|
||||
{
|
||||
await transaction.CommitAsync();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create new default collection
|
||||
var collectionId = CoreHelpers.GenerateComb();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Id = collectionId,
|
||||
OrganizationId = organizationId,
|
||||
Name = defaultCollectionName,
|
||||
ExternalId = null,
|
||||
CreationDate = now,
|
||||
RevisionDate = now,
|
||||
Type = CollectionType.DefaultUserCollection,
|
||||
DefaultUserCollectionEmail = null
|
||||
};
|
||||
|
||||
var collectionUser = new CollectionUser
|
||||
{
|
||||
CollectionId = collectionId,
|
||||
OrganizationUserId = organizationUserId,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true
|
||||
};
|
||||
|
||||
await dbContext.Collections.AddAsync(collection);
|
||||
await dbContext.CollectionUsers.AddAsync(collectionUser);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Bump user account revision dates
|
||||
await dbContext.UserBumpAccountRevisionDateByCollectionIdAsync(collectionId, organizationId);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
await transaction.CommitAsync();
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create new default collection
|
||||
var collectionId = CoreHelpers.GenerateComb();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var collection = new Collection
|
||||
catch
|
||||
{
|
||||
Id = collectionId,
|
||||
OrganizationId = organizationId,
|
||||
Name = defaultCollectionName,
|
||||
ExternalId = null,
|
||||
CreationDate = now,
|
||||
RevisionDate = now,
|
||||
Type = CollectionType.DefaultUserCollection,
|
||||
DefaultUserCollectionEmail = null
|
||||
};
|
||||
|
||||
var collectionUser = new CollectionUser
|
||||
{
|
||||
CollectionId = collectionId,
|
||||
OrganizationUserId = organizationUserId,
|
||||
ReadOnly = false,
|
||||
HidePasswords = false,
|
||||
Manage = true
|
||||
};
|
||||
|
||||
await dbContext.Collections.AddAsync(collection);
|
||||
await dbContext.CollectionUsers.AddAsync(collectionUser);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
// Bump user account revision dates
|
||||
await dbContext.UserBumpAccountRevisionDateByCollectionIdAsync(collectionId, organizationId);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
await transaction.CommitAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<HashSet<Guid>> GetOrgUserIdsWithDefaultCollectionAsync(DatabaseContext dbContext, Guid organizationId)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user