mirror of
https://github.com/bitwarden/server.git
synced 2025-12-11 04:34:37 -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)
|
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
|
// Handle NpgSql Legacy Support for `timestamp without timezone` issue
|
||||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||||
}
|
}
|
||||||
else if (provider == SupportedDatabaseProviders.MySql)
|
else if (provider == SupportedDatabaseProviders.MySql)
|
||||||
{
|
{
|
||||||
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString),
|
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString),
|
||||||
b => b.MigrationsAssembly("MySqlMigrations"));
|
b =>
|
||||||
|
{
|
||||||
|
b.MigrationsAssembly("MySqlMigrations");
|
||||||
|
b.EnableRetryOnFailure();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else if (provider == SupportedDatabaseProviders.Sqlite)
|
else if (provider == SupportedDatabaseProviders.Sqlite)
|
||||||
{
|
{
|
||||||
|
// SQLite doesn't support EnableRetryOnFailure
|
||||||
options.UseSqlite(connectionString, b => b.MigrationsAssembly("SqliteMigrations"));
|
options.UseSqlite(connectionString, b => b.MigrationsAssembly("SqliteMigrations"));
|
||||||
}
|
}
|
||||||
else if (provider == SupportedDatabaseProviders.SqlServer)
|
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();
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
// Use SERIALIZABLE isolation level to prevent race conditions during concurrent calls
|
// Use EF's execution strategy to handle transient failures (including deadlocks)
|
||||||
using var transaction = await dbContext.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
|
var strategy = dbContext.Database.CreateExecutionStrategy();
|
||||||
|
|
||||||
try
|
return await strategy.ExecuteAsync(async () =>
|
||||||
{
|
{
|
||||||
// Check if this organization user already has a default collection
|
// Use SERIALIZABLE isolation level to prevent race conditions during concurrent calls
|
||||||
// SERIALIZABLE ensures this SELECT acquires range locks
|
using var transaction = await dbContext.Database.BeginTransactionAsync(System.Data.IsolationLevel.Serializable);
|
||||||
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)
|
try
|
||||||
if (existingDefaultCollection != null)
|
|
||||||
{
|
{
|
||||||
|
// 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();
|
await transaction.CommitAsync();
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
// Create new default collection
|
|
||||||
var collectionId = CoreHelpers.GenerateComb();
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
|
|
||||||
var collection = new Collection
|
|
||||||
{
|
{
|
||||||
Id = collectionId,
|
await transaction.RollbackAsync();
|
||||||
OrganizationId = organizationId,
|
throw;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HashSet<Guid>> GetOrgUserIdsWithDefaultCollectionAsync(DatabaseContext dbContext, Guid organizationId)
|
private async Task<HashSet<Guid>> GetOrgUserIdsWithDefaultCollectionAsync(DatabaseContext dbContext, Guid organizationId)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user