Шардировать пакетную миграцию схем тенантов
Обход реестра идёт страницами (ITenantRepository.ListPageAsync, DefaultPageSize=200) с параллелизмом внутри страницы и изоляцией сбоев — масштаб на 1000+ схем без загрузки всего реестра. Закрывает BL-SCALE-1000.
This commit is contained in:
@@ -38,6 +38,24 @@ public sealed class TenantRepository(DealDbContext dbContext) : ITenantRepositor
|
||||
var entities = await dbContext.Tenants
|
||||
.AsNoTracking()
|
||||
.OrderBy(t => t.CreatedAt)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(t => ToTenantRecordDto(t))
|
||||
.ToListAsync(ct);
|
||||
return entities;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<TenantRecordDto>> ListPageAsync(
|
||||
int offset,
|
||||
int limit,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var entities = await dbContext.Tenants
|
||||
.AsNoTracking()
|
||||
.OrderBy(t => t.CreatedAt)
|
||||
.ThenBy(t => t.Id)
|
||||
.Skip(offset)
|
||||
.Take(limit)
|
||||
.Select(t => ToTenantRecordDto(t))
|
||||
.ToListAsync(ct);
|
||||
return entities;
|
||||
|
||||
@@ -8,7 +8,8 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Deal.Infrastructure.Tenancy;
|
||||
|
||||
/// <summary>
|
||||
/// Пакетная (maintenance) миграция схем ВСЕХ существующих тенантов реестра с ограниченным параллелизмом и логированием прогресса — для провижининга SaaS на сотни/тысячи схем.
|
||||
/// Пакетная (maintenance) миграция схем ВСЕХ существующих тенантов реестра: шардированный обход
|
||||
/// страницами с ограниченным параллелизмом и логированием прогресса — для провижининга на сотни/тысячи схем.
|
||||
/// </summary>
|
||||
public sealed class TenantSchemaMigrationService(
|
||||
ITenantRepository tenantRepository,
|
||||
@@ -20,69 +21,114 @@ public sealed class TenantSchemaMigrationService(
|
||||
/// </summary>
|
||||
public const int DefaultMaxParallelism = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Размер страницы обхода реестра по умолчанию (шард).
|
||||
/// </summary>
|
||||
public const int DefaultPageSize = 200;
|
||||
|
||||
// Нижняя граница параллелизма (ноль/отрицательное значение недопустимо).
|
||||
private const int MinMaxParallelism = 1;
|
||||
|
||||
// Верхняя граница параллелизма: защита Postgres от лавины одновременных DDL-подключений.
|
||||
private const int MaxMaxParallelism = 32;
|
||||
|
||||
private const int MinPageSize = 1;
|
||||
|
||||
private const int MaxPageSize = 5000;
|
||||
|
||||
/// <summary>
|
||||
/// Мигрирует схемы всех тенантов реестра с параллелизмом по умолчанию.
|
||||
/// Мигрирует схемы всех тенантов реестра с параметрами по умолчанию.
|
||||
/// </summary>
|
||||
/// <returns>Итоговая сводка пакетной миграции.</returns>
|
||||
public Task<TenantMigrationSummary> MigrateAllAsync(CancellationToken ct)
|
||||
=> MigrateAllAsync(DefaultMaxParallelism, ct);
|
||||
=> MigrateAllAsync(DefaultMaxParallelism, DefaultPageSize, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Мигрирует схемы всех тенантов реестра с заданным параллелизмом
|
||||
/// Мигрирует схемы всех тенантов реестра с заданным параллелизмом.
|
||||
/// </summary>
|
||||
/// <param name="maxParallelism">Желаемое число схем, мигрируемых одновременно.</param>
|
||||
/// <returns>Итоговая сводка пакетной миграции.</returns>
|
||||
public async Task<TenantMigrationSummary> MigrateAllAsync(int maxParallelism, CancellationToken ct)
|
||||
public Task<TenantMigrationSummary> MigrateAllAsync(int maxParallelism, CancellationToken ct)
|
||||
=> MigrateAllAsync(maxParallelism, DefaultPageSize, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Мигрирует схемы всех тенантов реестра шардами по страницам.
|
||||
/// </summary>
|
||||
/// <param name="maxParallelism">Желаемое число схем, мигрируемых одновременно.</param>
|
||||
/// <param name="pageSize">Размер страницы (шарда) обхода реестра.</param>
|
||||
/// <returns>Итоговая сводка пакетной миграции.</returns>
|
||||
public async Task<TenantMigrationSummary> MigrateAllAsync(
|
||||
int maxParallelism,
|
||||
int pageSize,
|
||||
CancellationToken ct)
|
||||
{
|
||||
int parallelism = Math.Clamp(maxParallelism, MinMaxParallelism, MaxMaxParallelism);
|
||||
IReadOnlyList<TenantRecordDto> tenants = await tenantRepository.ListAsync(ct).ConfigureAwait(false);
|
||||
int shardSize = Math.Clamp(pageSize, MinPageSize, MaxPageSize);
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var failedSchemas = new ConcurrentBag<string>();
|
||||
int total = 0;
|
||||
int migrated = 0;
|
||||
int shardIndex = 0;
|
||||
|
||||
if (tenants.Count == 0)
|
||||
while (true)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
IReadOnlyList<TenantRecordDto> shard = await tenantRepository
|
||||
.ListPageAsync(total, shardSize, ct)
|
||||
.ConfigureAwait(false);
|
||||
if (shard.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
shardIndex++;
|
||||
total += shard.Count;
|
||||
int processedSoFar = total;
|
||||
|
||||
await Parallel.ForEachAsync(
|
||||
shard,
|
||||
new ParallelOptions { MaxDegreeOfParallelism = parallelism, CancellationToken = ct },
|
||||
async (tenant, token) =>
|
||||
{
|
||||
var tenantId = new TenantId(tenant.Id.ToString("N"));
|
||||
try
|
||||
{
|
||||
await tenantProvisioner.ProvisionAsync(tenantId, token).ConfigureAwait(false);
|
||||
int done = Interlocked.Increment(ref migrated);
|
||||
logger.LogInformation(
|
||||
"Пакетная миграция схем: шард {Shard}, {Done}/{Total} — {Schema} готова",
|
||||
shardIndex,
|
||||
done,
|
||||
processedSoFar,
|
||||
tenantId.SchemaName);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
failedSchemas.Add(tenantId.SchemaName);
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Пакетная миграция схем: шард {Shard}, {Schema} не мигрирована",
|
||||
shardIndex,
|
||||
tenantId.SchemaName);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (shard.Count < shardSize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
|
||||
if (total == 0)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
logger.LogInformation("Пакетная миграция схем: в реестре нет тенантов — мигрировать нечего");
|
||||
return new TenantMigrationSummary(0, 0, 0, stopwatch.ElapsedMilliseconds, []);
|
||||
}
|
||||
|
||||
var failedSchemas = new ConcurrentBag<string>();
|
||||
int migrated = 0;
|
||||
|
||||
await Parallel.ForEachAsync(
|
||||
tenants,
|
||||
new ParallelOptions { MaxDegreeOfParallelism = parallelism, CancellationToken = ct },
|
||||
async (tenant, token) =>
|
||||
{
|
||||
var tenantId = new TenantId(tenant.Id.ToString("N"));
|
||||
try
|
||||
{
|
||||
await tenantProvisioner.ProvisionAsync(tenantId, token).ConfigureAwait(false);
|
||||
int done = Interlocked.Increment(ref migrated);
|
||||
logger.LogInformation(
|
||||
"Пакетная миграция схем: {Done}/{Total} — {Schema} готова",
|
||||
done,
|
||||
tenants.Count,
|
||||
tenantId.SchemaName);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
failedSchemas.Add(tenantId.SchemaName);
|
||||
logger.LogError(
|
||||
exception,
|
||||
"Пакетная миграция схем: {Schema} не мигрирована",
|
||||
tenantId.SchemaName);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
stopwatch.Stop();
|
||||
var summary = new TenantMigrationSummary(
|
||||
tenants.Count,
|
||||
total,
|
||||
migrated,
|
||||
failedSchemas.Count,
|
||||
stopwatch.ElapsedMilliseconds,
|
||||
|
||||
Reference in New Issue
Block a user