{"record":{"id":"640c9406e231a843","repo":"fullstackhero/dotnet-starter-kit","slug":"provisioning-already-running-for-tenant-tenantid","errorCode":null,"errorMessage":"Provisioning already running for tenant {tenantId}.","messagePattern":"Provisioning already running for tenant (.+?)\\.","errorType":"exception","errorClass":"CustomException","httpStatus":400,"severity":"error","filePath":"src/Modules/Multitenancy/Modules.Multitenancy/Provisioning/TenantProvisioningService.cs","lineNumber":44,"sourceCode":"        IServiceScopeFactory scopeFactory,\n        ILogger<TenantProvisioningService> logger)\n    {\n        _dbContext = dbContext;\n        _tenantStore = tenantStore;\n        _jobService = jobService;\n        _scopeFactory = scopeFactory;\n        _logger = logger;\n    }\n\n    public async Task<TenantProvisioning> StartAsync(string tenantId, CancellationToken cancellationToken)\n    {\n        var tenant = await _tenantStore.GetAsync(tenantId).ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Tenant {tenantId} not found for provisioning.\");\n\n        var existing = await GetLatestAsync(tenantId, cancellationToken).ConfigureAwait(false);\n        if (existing is not null && (existing.Status is TenantProvisioningStatus.Running or TenantProvisioningStatus.Pending))\n        {\n            throw new CustomException($\"Provisioning already running for tenant {tenantId}.\");\n        }\n\n        var correlationId = Guid.NewGuid().ToString();\n        var provisioning = new TenantProvisioning(tenant.Id, correlationId);\n\n        provisioning.Steps.Add(new TenantProvisioningStep(provisioning.Id, TenantProvisioningStepName.Database));\n        provisioning.Steps.Add(new TenantProvisioningStep(provisioning.Id, TenantProvisioningStepName.Migrations));\n        provisioning.Steps.Add(new TenantProvisioningStep(provisioning.Id, TenantProvisioningStepName.Seeding));\n        provisioning.Steps.Add(new TenantProvisioningStep(provisioning.Id, TenantProvisioningStepName.CacheWarm));\n\n        _dbContext.Add(provisioning);\n        await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n\n        if (!TryEnsureJobStorage())\n        {\n            _logger.LogWarning(\"Background job storage not available; running provisioning inline for tenant {TenantId}.\", tenantId);\n            provisioning.SetJobId(\"inline\");\n            await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Multitenancy/Modules.Multitenancy/Provisioning/TenantProvisioningService.cs#L26-L62","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait provisioningService.StartAsync(tenantId, ct);\n\n// after\nvar status = await provisioningService.GetStatusAsync(tenantId, ct);\nif (status is not { Status: TenantProvisioningStatus.Running or TenantProvisioningStatus.Pending })\n{\n    await provisioningService.StartAsync(tenantId, ct);\n}","handlingStrategy":"try-catch","validationCode":"var latest = await provisioningService.GetStatusAsync(tenantId, ct);\nif (latest.Status is TenantProvisioningStatus.Running or TenantProvisioningStatus.Pending)\n    return; // already in flight","typeGuard":"bool CanStart(TenantProvisioningStatusDto? s) => s is null || s.Status is not (TenantProvisioningStatus.Running or TenantProvisioningStatus.Pending);","tryCatchPattern":"try\n{\n    await provisioningService.StartAsync(tenantId, ct);\n}\ncatch (CustomException ex) when (ex.Message.Contains(\"Provisioning already running\"))\n{\n    logger.LogInformation(\"Provisioning already in flight for {TenantId}\", tenantId);\n}","preventionTips":["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."],"tags":["multitenancy","provisioning","concurrency","conflict"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}