From 890d3c5dddae81772d59dbb596a927d88f574932 Mon Sep 17 00:00:00 2001 From: Iliyan Velichkov Date: Mon, 5 Oct 2026 15:58:48 +0300 Subject: [PATCH] tenants: shut the delayed tenant provisioning down with its context TenantsInitializer scheduled the provisioning 30 s after ApplicationReady on a ScheduledThreadPoolExecutor it never shut down. A context closed within those 30 s still ran it later, through the closed context's beans, which Spring re-creates on demand - a new SystemDB pool, an EntityManagerFactory and the SystemDB Liquibase update - against a database that by then belonged to the next context. On the PostgreSQL CI leg the one LocalNativeAppLifecycleIT left behind fired 0.3 s after DirigibleCleaner had wiped the system schema and left the Liquibase changelog lock held: every later IT class in the slow shard then waited five minutes for the lock and failed to load its context. The executor is now a field of the bean and shut down in destroy(), so a provisioning that has not started when its context closes never runs. Co-Authored-By: Claude Opus 5.5 --- .../provisioning/TenantsInitializer.java | 37 ++++++++++- .../provisioning/TenantsInitializerTest.java | 63 +++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 components/core/core-tenants/src/test/java/org/eclipse/dirigible/components/tenants/provisioning/TenantsInitializerTest.java diff --git a/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/provisioning/TenantsInitializer.java b/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/provisioning/TenantsInitializer.java index 4e330cb7d51..05f8047edb7 100644 --- a/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/provisioning/TenantsInitializer.java +++ b/components/core/core-tenants/src/main/java/org/eclipse/dirigible/components/tenants/provisioning/TenantsInitializer.java @@ -12,30 +12,56 @@ import org.eclipse.dirigible.components.base.ApplicationListenersOrder.ApplicationReadyEventListeners; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +/** + * Provisions the tenants a while after the application is ready. + * + *

+ * The delayed provisioning belongs to this bean's application context: the executor is shut down + * with the context, so a provisioning that has not started yet never runs. Left to fire after the + * context closed, it reached through the closed context's beans, which Spring re-creates on demand + * - an entity manager factory and with it the SystemDB Liquibase update - against a database that + * meanwhile belongs to another context, where the stray update could leave the changelog lock held + * and block every later boot on it. + */ @Order(ApplicationReadyEventListeners.TENANTS_INITIALIZER) @Component -class TenantsInitializer implements ApplicationListener { +class TenantsInitializer implements ApplicationListener, DisposableBean { private static final Logger LOGGER = LoggerFactory.getLogger(TenantsInitializer.class); + private static final Duration PROVISIONING_DELAY = Duration.ofSeconds(30); + private final TenantsProvisioner tenantsProvisioner; + private final Duration provisioningDelay; + + private final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1); + + @Autowired TenantsInitializer(TenantsProvisioner tenantsProvisioner) { + this(tenantsProvisioner, PROVISIONING_DELAY); + } + + TenantsInitializer(TenantsProvisioner tenantsProvisioner, Duration provisioningDelay) { this.tenantsProvisioner = tenantsProvisioner; + this.provisioningDelay = provisioningDelay; } @Override public void onApplicationEvent(ApplicationReadyEvent event) { - ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); - executor.schedule(this::provisionTenants, 30, TimeUnit.SECONDS); + executor.schedule(this::provisionTenants, provisioningDelay.toMillis(), TimeUnit.MILLISECONDS); } private void provisionTenants() { @@ -46,4 +72,9 @@ private void provisionTenants() { LOGGER.debug("Tenants have been initialized."); } + @Override + public void destroy() { + executor.shutdownNow(); + } + } diff --git a/components/core/core-tenants/src/test/java/org/eclipse/dirigible/components/tenants/provisioning/TenantsInitializerTest.java b/components/core/core-tenants/src/test/java/org/eclipse/dirigible/components/tenants/provisioning/TenantsInitializerTest.java new file mode 100644 index 00000000000..44bcc0ef4d9 --- /dev/null +++ b/components/core/core-tenants/src/test/java/org/eclipse/dirigible/components/tenants/provisioning/TenantsInitializerTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.tenants.provisioning; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.after; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + +import java.time.Duration; +import java.util.Set; + +import org.eclipse.dirigible.components.tenants.domain.TenantStatus; +import org.eclipse.dirigible.components.tenants.service.TenantService; +import org.eclipse.dirigible.components.tenants.tenant.TenantFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * The delayed provisioning belongs to the context that scheduled it: once that context is closed, a + * provisioning that has not started must never run. + */ +class TenantsInitializerTest { + + private static final Duration DELAY = Duration.ofMillis(200); + + private final TenantService tenantService = mock(TenantService.class); + + private final TenantsInitializer initializer = + new TenantsInitializer(new TenantsProvisioner(tenantService, Set.of(), Set.of(), new TenantFactory()), DELAY); + + @AfterEach + void shutDown() { + initializer.destroy(); + } + + @Test + void provisionsTheTenantsAfterTheDelay() { + initializer.onApplicationEvent(null); + + verify(tenantService, timeout(5_000)).findByStatus(TenantStatus.INITIAL); + } + + @Test + void aProvisioningScheduledBeforeTheContextClosedNeverRuns() { + initializer.onApplicationEvent(null); + + initializer.destroy(); + + verify(tenantService, after(DELAY.multipliedBy(5) + .toMillis()).never()).findByStatus(any()); + } + +}