fix: provisioning race condition and noisy ClickHouse logs
Defer provisionAsync() until after the transaction commits using TransactionSynchronization.afterCommit(). Previously the @Async thread raced the @Transactional commit — findById returned null because the tenant INSERT wasn't visible yet. Downgrade ClickHouse UNKNOWN_TABLE errors to DEBUG level in InfrastructureService. These are expected on fresh installs before any cameleer-server has created the tables. Make the onboarding slug field read-only (derived from org name). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,11 +17,14 @@ import net.siegeln.cameleer.saas.tenant.TenantService;
|
||||
import net.siegeln.cameleer.saas.tenant.TenantStatus;
|
||||
import net.siegeln.cameleer.saas.tenant.Tier;
|
||||
import net.siegeln.cameleer.saas.tenant.dto.CreateTenantRequest;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -89,6 +92,28 @@ class VendorTenantServiceTest {
|
||||
tenantService, tenantRepository, licenseService,
|
||||
tenantProvisioner, serverApiClient, logtoClient, logtoConfig,
|
||||
auditService, provisioningProps, dataCleanupService, tenantDatabaseService, vendorTenantService);
|
||||
|
||||
// Enable transaction synchronization so afterCommit callbacks can be registered
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
}
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
}
|
||||
|
||||
/** Simulate transaction commit — runs all registered afterCommit callbacks. */
|
||||
private void flushAfterCommit() {
|
||||
var syncs = TransactionSynchronizationManager.getSynchronizations();
|
||||
for (TransactionSynchronization sync : syncs) {
|
||||
sync.afterCommit();
|
||||
}
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
@@ -155,8 +180,9 @@ class VendorTenantServiceTest {
|
||||
when(tenantRepository.save(any(TenantEntity.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
vendorTenantService.createAndProvision(request, actorId);
|
||||
flushAfterCommit();
|
||||
|
||||
// provisionAsync modifies the tenant entity in-place (runs synchronously in unit tests)
|
||||
// provisionAsync runs via afterCommit callback (synchronously in unit tests)
|
||||
assertThat(tenant.getStatus()).isEqualTo(TenantStatus.ACTIVE);
|
||||
assertThat(tenant.getServerEndpoint()).isEqualTo("http://server:8080");
|
||||
assertThat(tenant.getProvisionError()).isNull();
|
||||
@@ -178,8 +204,9 @@ class VendorTenantServiceTest {
|
||||
when(tenantRepository.save(any(TenantEntity.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
vendorTenantService.createAndProvision(request, actorId);
|
||||
flushAfterCommit();
|
||||
|
||||
// provisionAsync modifies the tenant entity in-place (runs synchronously in unit tests)
|
||||
// provisionAsync runs via afterCommit callback (synchronously in unit tests)
|
||||
assertThat(tenant.getProvisionError()).isEqualTo("Docker failure");
|
||||
assertThat(tenant.getStatus()).isEqualTo(TenantStatus.PROVISIONING);
|
||||
verify(tenantRepository, times(2)).save(tenant);
|
||||
|
||||
Reference in New Issue
Block a user