fix: provisioning race condition and noisy ClickHouse logs
Some checks failed
CI / build (push) Successful in 2m3s
CI / docker (push) Successful in 1m30s
SonarQube Analysis / sonarqube (push) Failing after 2m22s

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:
hsiegeln
2026-04-25 22:05:48 +02:00
parent dee1f39554
commit 171ed1a6ab
4 changed files with 57 additions and 18 deletions

View File

@@ -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);