Шардировать пакетную миграцию схем тенантов

Обход реестра идёт страницами (ITenantRepository.ListPageAsync, DefaultPageSize=200) с параллелизмом внутри страницы и изоляцией сбоев — масштаб на 1000+ схем без загрузки всего реестра. Закрывает BL-SCALE-1000.
This commit is contained in:
Rustam Khalimov
2026-09-11 17:43:27 +03:00
parent 38561528ab
commit 1d7c9980ba
12 changed files with 273 additions and 44 deletions
@@ -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,