mirror of
https://github.com/bitwarden/server
synced 2025-12-24 04:03:25 +00:00
[PM-25138] Reduce db locking when creating default collections (#6308)
* Use single method for default collection creation * Use GenerateComb to create sequential guids * Pre-sort data for SqlBulkCopy * Add SqlBulkCopy options per dbops recommendations
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Infrastructure.Dapper.Utilities;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Bit.Infrastructure.Dapper.AdminConsole.Helpers;
|
||||
@@ -8,11 +9,25 @@ namespace Bit.Infrastructure.Dapper.AdminConsole.Helpers;
|
||||
public static class BulkResourceCreationService
|
||||
{
|
||||
private const string _defaultErrorMessage = "Must have at least one record for bulk creation.";
|
||||
public static async Task CreateCollectionsUsersAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable<CollectionUser> collectionUsers, string errorMessage = _defaultErrorMessage)
|
||||
public static async Task CreateCollectionsUsersAsync(SqlConnection connection, SqlTransaction transaction,
|
||||
IEnumerable<CollectionUser> collectionUsers, string errorMessage = _defaultErrorMessage)
|
||||
{
|
||||
// Offload some work from SQL Server by pre-sorting before insert.
|
||||
// This lets us use the SqlBulkCopy.ColumnOrderHints to improve performance and reduce deadlocks.
|
||||
var sortedCollectionUsers = collectionUsers
|
||||
.OrderBySqlGuid(cu => cu.CollectionId)
|
||||
.ThenBySqlGuid(cu => cu.OrganizationUserId)
|
||||
.ToList();
|
||||
|
||||
using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction);
|
||||
bulkCopy.DestinationTableName = "[dbo].[CollectionUser]";
|
||||
var dataTable = BuildCollectionsUsersTable(bulkCopy, collectionUsers, errorMessage);
|
||||
bulkCopy.BatchSize = 500;
|
||||
bulkCopy.BulkCopyTimeout = 120;
|
||||
bulkCopy.EnableStreaming = true;
|
||||
bulkCopy.ColumnOrderHints.Add("CollectionId", SortOrder.Ascending);
|
||||
bulkCopy.ColumnOrderHints.Add("OrganizationUserId", SortOrder.Ascending);
|
||||
|
||||
var dataTable = BuildCollectionsUsersTable(bulkCopy, sortedCollectionUsers, errorMessage);
|
||||
await bulkCopy.WriteToServerAsync(dataTable);
|
||||
}
|
||||
|
||||
@@ -96,11 +111,21 @@ public static class BulkResourceCreationService
|
||||
return table;
|
||||
}
|
||||
|
||||
public static async Task CreateCollectionsAsync(SqlConnection connection, SqlTransaction transaction, IEnumerable<Collection> collections, string errorMessage = _defaultErrorMessage)
|
||||
public static async Task CreateCollectionsAsync(SqlConnection connection, SqlTransaction transaction,
|
||||
IEnumerable<Collection> collections, string errorMessage = _defaultErrorMessage)
|
||||
{
|
||||
// Offload some work from SQL Server by pre-sorting before insert.
|
||||
// This lets us use the SqlBulkCopy.ColumnOrderHints to improve performance and reduce deadlocks.
|
||||
var sortedCollections = collections.OrderBySqlGuid(c => c.Id).ToList();
|
||||
|
||||
using var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction);
|
||||
bulkCopy.DestinationTableName = "[dbo].[Collection]";
|
||||
var dataTable = BuildCollectionsTable(bulkCopy, collections, errorMessage);
|
||||
bulkCopy.BatchSize = 500;
|
||||
bulkCopy.BulkCopyTimeout = 120;
|
||||
bulkCopy.EnableStreaming = true;
|
||||
bulkCopy.ColumnOrderHints.Add("Id", SortOrder.Ascending);
|
||||
|
||||
var dataTable = BuildCollectionsTable(bulkCopy, sortedCollections, errorMessage);
|
||||
await bulkCopy.WriteToServerAsync(dataTable);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Infrastructure.Dapper.AdminConsole.Helpers;
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
@@ -326,9 +327,10 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> affectedOrgUserIds, string defaultCollectionName)
|
||||
public async Task UpsertDefaultCollectionsAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, string defaultCollectionName)
|
||||
{
|
||||
if (!affectedOrgUserIds.Any())
|
||||
organizationUserIds = organizationUserIds.ToList();
|
||||
if (!organizationUserIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -340,7 +342,7 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
{
|
||||
var orgUserIdWithDefaultCollection = await GetOrgUserIdsWithDefaultCollectionAsync(connection, transaction, organizationId);
|
||||
|
||||
var missingDefaultCollectionUserIds = affectedOrgUserIds.Except(orgUserIdWithDefaultCollection);
|
||||
var missingDefaultCollectionUserIds = organizationUserIds.Except(orgUserIdWithDefaultCollection);
|
||||
|
||||
var (collectionUsers, collections) = BuildDefaultCollectionForUsers(organizationId, missingDefaultCollectionUserIds, defaultCollectionName);
|
||||
|
||||
@@ -393,7 +395,7 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
|
||||
foreach (var orgUserId in missingDefaultCollectionUserIds)
|
||||
{
|
||||
var collectionId = Guid.NewGuid();
|
||||
var collectionId = CoreHelpers.GenerateComb();
|
||||
|
||||
collections.Add(new Collection
|
||||
{
|
||||
|
||||
26
src/Infrastructure.Dapper/Utilities/SqlGuidHelpers.cs
Normal file
26
src/Infrastructure.Dapper/Utilities/SqlGuidHelpers.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Data.SqlTypes;
|
||||
|
||||
namespace Bit.Infrastructure.Dapper.Utilities;
|
||||
|
||||
public static class SqlGuidHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Sorts the source IEnumerable by the specified Guid property using the <see cref="SqlGuid"/> comparison logic.
|
||||
/// This is required because MSSQL server compares (and therefore sorts) Guids differently to C#.
|
||||
/// Ref: https://learn.microsoft.com/en-us/sql/connect/ado-net/sql/compare-guid-uniqueidentifier-values
|
||||
/// </summary>
|
||||
public static IOrderedEnumerable<T> OrderBySqlGuid<T>(
|
||||
this IEnumerable<T> source,
|
||||
Func<T, Guid> keySelector)
|
||||
{
|
||||
return source.OrderBy(x => new SqlGuid(keySelector(x)));
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="OrderBySqlGuid"/>
|
||||
public static IOrderedEnumerable<T> ThenBySqlGuid<T>(
|
||||
this IOrderedEnumerable<T> source,
|
||||
Func<T, Guid> keySelector)
|
||||
{
|
||||
return source.ThenBy(x => new SqlGuid(keySelector(x)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user