fullstackhero/dotnet-starter-kit · error · CustomException
Provisioning already running for tenant
Error message
Provisioning already running for tenant {tenantId}. What it means
TenantProvisioningService.StartAsync refuses to start a new provisioning run when the latest TenantProvisioning record for the tenant is still in Running or Pending status. Provisioning is treated as a single-flight operation per tenant, so a concurrent or stale in-flight run blocks new ones. The thrown CustomException surfaces to the caller as a 400/409-style business error naming the tenant.
Solutions
- Wait for the current provisioning run to finish (poll GetStatusAsync until Status is Completed or Failed) before retrying.
- If the record is stuck in Running/Pending from a crashed job, mark it Failed or delete/reset the TenantProvisioning row, then call StartAsync again.
- Add idempotency on the client: check GetStatusAsync before calling start, or serialize provisioning behind a queue.
- If the Hangfire job died, verify job storage (TryEnsureJobStorage) and requeue the job so the record is driven to a terminal status.
Example fix
// before
await provisioningService.StartAsync(tenantId, ct);
// after
var status = await provisioningService.GetStatusAsync(tenantId, ct);
if (status is not { Status: TenantProvisioningStatus.Running or TenantProvisioningStatus.Pending })
{
await provisioningService.StartAsync(tenantId, ct);
} Defensive patterns
Strategy: try-catch
Validate before calling
var latest = await provisioningService.GetStatusAsync(tenantId, ct);
if (latest.Status is TenantProvisioningStatus.Running or TenantProvisioningStatus.Pending)
return; // already in flight Type guard
bool CanStart(TenantProvisioningStatusDto? s) => s is null || s.Status is not (TenantProvisioningStatus.Running or TenantProvisioningStatus.Pending);
Try / catch
try
{
await provisioningService.StartAsync(tenantId, ct);
}
catch (CustomException ex) when (ex.Message.Contains("Provisioning already running"))
{
logger.LogInformation("Provisioning already in flight for {TenantId}", tenantId);
} Prevention
- Poll GetStatusAsync before starting or retrying provisioning.
- Disable the provisioning/retry button in the UI while status is Running/Pending.
- Monitor for provisioning records stuck in Running and alert/mark them Failed.
- Make retry idempotent: catch this exception and treat as in-progress.
When it happens
Trigger: Calling StartAsync (directly or via RetryAsync) while a TenantProvisioning row for tenantId exists with Status == TenantProvisioningStatus.Running or Pending. Typical when a Hangfire provisioning job is still executing, or a previous run was never marked Failed/Completed (e.g. API crashed mid-run) and no cleanup happened.
Common situations: Double-clicking a 'provision tenant' button; two admins provisioning simultaneously; a crashed or hung background job leaving the record stuck in Running; retrying before the prior run finishes; deploying a new provisioning job while the old one is pending.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Tenant not found during provisioning.
- Tenant not found for provisioning.
- Provisioning not found for tenant
- Tenant is not provisioned. Status: .
- Provisioning for tenant not found.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/640c9406e231a843.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Multitenancy/Modules.Multitenancy/Provisioning/TenantProvisioningService.cs:44
IServiceScopeFactory scopeFactory,
ILogger<TenantProvisioningService> logger)
{
_dbContext = dbContext;
_tenantStore = tenantStore;
_jobService = jobService;
_scopeFactory = scopeFactory;
_logger = logger;
}
public async Task<TenantProvisioning> StartAsync(string tenantId, CancellationToken cancellationToken)
{
var tenant = await _tenantStore.GetAsync(tenantId).ConfigureAwait(false)
?? throw new NotFoundException($"Tenant {tenantId} not found for provisioning.");
var existing = await GetLatestAsync(tenantId, cancellationToken).ConfigureAwait(false);
if (existing is not null && (existing.Status is TenantProvisioningStatus.Running or TenantProvisioningStatus.Pending))
{
throw new CustomException($"Provisioning already running for tenant {tenantId}.");
}
var correlationId = Guid.NewGuid().ToString();
var provisioning = new TenantProvisioning(tenant.Id, correlationId);
provisioning.Steps.Add(new TenantProvisioningStep(provisioning.Id, TenantProvisioningStepName.Database));
provisioning.Steps.Add(new TenantProvisioningStep(provisioning.Id, TenantProvisioningStepName.Migrations));
provisioning.Steps.Add(new TenantProvisioningStep(provisioning.Id, TenantProvisioningStepName.Seeding));
provisioning.Steps.Add(new TenantProvisioningStep(provisioning.Id, TenantProvisioningStepName.CacheWarm));
_dbContext.Add(provisioning);
await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
if (!TryEnsureJobStorage())
{
_logger.LogWarning("Background job storage not available; running provisioning inline for tenant {TenantId}.", tenantId);
provisioning.SetJobId("inline");
await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);View on GitHub (pinned to 3f2959e683)