Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>
* 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<ApplicationReadyEvent> {
class TenantsInitializer implements ApplicationListener<ApplicationReadyEvent>, 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() {
Expand All @@ -46,4 +72,9 @@ private void provisionTenants() {
LOGGER.debug("Tenants have been initialized.");
}

@Override
public void destroy() {
executor.shutdownNow();
}

}
Original file line number Diff line number Diff line change
@@ -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());
}

}
Loading