diff --git a/.gitignore b/.gitignore index 67af86debb..ca1fd097b5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ mta-assembly .DS_Store !com.sap.cloud.lm.sl.cf.process/src/test/resources/com/sap/cloud/lm/sl/cf/process/steps/web.zip !multiapps-controller-client/src/test/resources/org/cloudfoundry/multiapps/controller/client/facade/staticfile.zip +**/.idea/ \ No newline at end of file diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java index 3c792a7380..38c2ba54ea 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java @@ -177,9 +177,6 @@ public final class Messages { public static final String FSS_CACHE_UPDATE_TIMEOUT = "Fss cache update timeout: {0} minutes"; public static final String THREAD_MONITOR_CACHE_TIMEOUT = "Flowable thread monitor cache timeout: {0} seconds"; public static final String SPACE_DEVELOPERS_CACHE_TIME_IN_SECONDS = "Cache for list of space developers per SpaceGUID: {0} seconds"; - public static final String APP_SHUTDOWN_REQUEST = "Application with id:\"{0}\", instance id:\"{1}\", instance index:\"{2}\", is requested to shutdown. Timeout to wait before shutdown of Flowable job executor:\"{3}\" seconds."; - public static final String APP_SUCCESSFULLY_SHUTDOWN = "Application with id:\"{0}\", instance id:\"{1}\", instance index:\"{2}\", is shutdown. Timeout to wait before shutdown of Flowable job executor:\"{3}\" seconds."; - public static final String APP_SHUTDOWN_STATUS_MONITOR = "Monitor shutdown status of application with id:\"{0}\", instance id:\"{1}\", instance index:\"{2}\". Status:\"{3}\"."; public static final String CONTROLLER_CLIENT_SSL_HANDSHAKE_TIMEOUT_IN_SECONDS = "Controller client SSL handshake timeout in seconds: {0}"; public static final String CONTROLLER_CLIENT_CONNECT_TIMEOUT_IN_SECONDS = "Controller client connect timeout in seconds: {0}"; public static final String CONTROLLER_CLIENT_CONNECTION_POOL_SIZE = "Controller client connection pool size: {0}"; diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java index b3e79b26a8..7214844fb1 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/Messages.java @@ -46,6 +46,8 @@ public final class Messages { public static final String ERROR_GETTING_FILES_CREATED_AFTER_0_AND_BEFORE_1 = "Error getting files created after {0} and before {1]"; public static final String BACKUP_DESCRIPTOR_FOR_MTA_ID_0_AND_ID_1_ALREADY_EXIST = "Backup descriptor for mta id \"{0}\" and id \"{1}\" already exist"; public static final String BACKUP_DESCRIPTOR_WITH_ID_NOT_EXIST = "Backup descriptor with ID \"{0}\" does not exist"; + public static final String APPLICATION_SHUTDOWN_WITH_APPLICATION_INSTANCE_ID_DOES_NOT_EXIST = "Application shutdown application instance ID \"{0}\" does not exist"; + public static final String APPLICATION_SHUTDOWN_WITH_APPLICATION_INSTANCE_ID_ALREADY_EXIST = "Application shutdown application instance ID \"{0}\" already exist"; public static final String DATABASE_HEALTH_CHECK_FAILED = "Database health check failed"; // ERROR log messages: diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/ApplicationShutdown.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/dto/ApplicationShutdown.java similarity index 71% rename from multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/ApplicationShutdown.java rename to multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/dto/ApplicationShutdown.java index 872dc84ed3..59c9a17917 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/ApplicationShutdown.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/dto/ApplicationShutdown.java @@ -1,9 +1,10 @@ -package org.cloudfoundry.multiapps.controller.core.model; +package org.cloudfoundry.multiapps.controller.persistence.dto; -import org.immutables.value.Value; +import java.util.Date; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.immutables.value.Value; @Value.Immutable @JsonSerialize(as = ImmutableApplicationShutdown.class) @@ -11,18 +12,20 @@ public interface ApplicationShutdown { enum Status { - FINISHED, RUNNING + FINISHED, RUNNING, INITIAL } - String getApplicationId(); + String getId(); - String getApplicationInstanceId(); + String getApplicationId(); int getApplicationInstanceIndex(); + Date getStartedAt(); + @Value.Default default Status getStatus() { - return Status.RUNNING; + return Status.INITIAL; } } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/dto/ApplicationShutdownDto.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/dto/ApplicationShutdownDto.java new file mode 100644 index 0000000000..87e903c593 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/dto/ApplicationShutdownDto.java @@ -0,0 +1,94 @@ +package org.cloudfoundry.multiapps.controller.persistence.dto; + +import java.util.Date; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Temporal; +import jakarta.persistence.TemporalType; +import org.cloudfoundry.multiapps.controller.persistence.model.PersistenceMetadata; + +@Entity +@Table(name = PersistenceMetadata.TableNames.APPLICATION_SHUTDOWN_TABLE) +public class ApplicationShutdownDto implements DtoWithPrimaryKey { + + public static class AttributeNames { + private AttributeNames() { + } + + public static final String ID = "id"; + public static final String APPLICATION_ID = "applicationId"; + public static final String APPLICATION_INSTANCE_INDEX = "applicationInstanceIndex"; + public static final String SHUTDOWN_STATUS = "shutdownStatus"; + public static final String STARTED_AT = "startedAt"; + } + + @Id + @Column(name = PersistenceMetadata.TableColumnNames.APPLICATION_SHUTDOWN_ID, nullable = false, unique = true) + private String id; + + @Column(name = PersistenceMetadata.TableColumnNames.APPLICATION_SHUTDOWN_APPLICATION_INSTANCE_INDEX, nullable = false, unique = true) + private int applicationInstanceIndex; + + @Column(name = PersistenceMetadata.TableColumnNames.APPLICATION_SHUTDOWN_APPLICATION_ID, nullable = false) + private String applicationId; + + @Column(name = PersistenceMetadata.TableColumnNames.APPLICATION_SHUTDOWN_SHUTDOWN_STATUS, nullable = false) + private ApplicationShutdown.Status shutdownStatus; + + @Column(name = PersistenceMetadata.TableColumnNames.APPLICATION_SHUTDOWN_STARTED_AT, nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date startedAt; + + public ApplicationShutdownDto() { + // Required by JPA + } + + public ApplicationShutdownDto(String id, String applicationId, int applicationInstanceIndex, ApplicationShutdown.Status shutdownStatus, + Date startedAt) { + this.id = id; + this.applicationId = applicationId; + this.applicationInstanceIndex = applicationInstanceIndex; + this.shutdownStatus = shutdownStatus; + this.startedAt = startedAt; + } + + @Override + public String getPrimaryKey() { + return id; + } + + @Override + public void setPrimaryKey(String id) { + this.id = id; + } + + public String getАpplicationId() { + return applicationId; + } + + public int getАpplicationIndex() { + return applicationInstanceIndex; + } + + public ApplicationShutdown.Status getShutdownStatus() { + return shutdownStatus; + } + + public Date getStartedAt() { + return startedAt; + } + + @Override + public String toString() { + return "ApplicationShutdownDto{" + + "id='" + id + '\'' + + ", applicationId='" + applicationId + '\'' + + ", shutdownStatus='" + shutdownStatus + '\'' + + ", applicationInstanceIndex='" + applicationInstanceIndex + '\'' + + ", startedAt='" + startedAt + '\'' + + '}'; + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/PersistenceMetadata.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/PersistenceMetadata.java index 7862074462..c5b7d94e98 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/PersistenceMetadata.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/model/PersistenceMetadata.java @@ -20,6 +20,7 @@ private TableNames() { public static final String LOCK_OWNERS_TABLE = "lock_owners"; public static final String ASYNC_UPLOAD_JOB_TABLE = "async_upload_job"; public static final String BACKUP_DESCRIPTOR_TABLE = "backup_descriptor"; + public static final String APPLICATION_SHUTDOWN_TABLE = "application_shutdown"; } @@ -113,6 +114,12 @@ private TableColumnNames() { public static final String BACKUP_DESCRIPTOR_SPACE_ID = "space_id"; public static final String BACKUP_DESCRIPTOR_NAMESPACE = "namespace"; public static final String BACKUP_DESCRIPTOR_TIMESTAMP = "timestamp"; + + public static final String APPLICATION_SHUTDOWN_ID = "id"; + public static final String APPLICATION_SHUTDOWN_APPLICATION_INSTANCE_INDEX = "application_instance_index"; + public static final String APPLICATION_SHUTDOWN_APPLICATION_ID = "application_id"; + public static final String APPLICATION_SHUTDOWN_SHUTDOWN_STATUS = "shutdown_status"; + public static final String APPLICATION_SHUTDOWN_STARTED_AT = "started_at"; } } diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/ApplicationShutdownQuery.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/ApplicationShutdownQuery.java new file mode 100644 index 0000000000..984ec6097a --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/ApplicationShutdownQuery.java @@ -0,0 +1,18 @@ +package org.cloudfoundry.multiapps.controller.persistence.query; + +import java.util.Date; + +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; + +public interface ApplicationShutdownQuery extends Query { + + ApplicationShutdownQuery id(String instanceId); + + ApplicationShutdownQuery applicationId(String applicationId); + + ApplicationShutdownQuery applicationInstanceIndex(int applicationInstanceIndex); + + ApplicationShutdownQuery shutdownStatus(ApplicationShutdown.Status shutdownStatus); + + ApplicationShutdownQuery startedAt(Date startedAt); +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/impl/ApplicationShutdownQueryImpl.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/impl/ApplicationShutdownQueryImpl.java new file mode 100644 index 0000000000..d91a3d6fe4 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/query/impl/ApplicationShutdownQueryImpl.java @@ -0,0 +1,100 @@ +package org.cloudfoundry.multiapps.controller.persistence.query.impl; + +import java.util.Date; +import java.util.List; + +import jakarta.persistence.EntityManager; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdownDto; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdownDto.AttributeNames; +import org.cloudfoundry.multiapps.controller.persistence.query.ApplicationShutdownQuery; +import org.cloudfoundry.multiapps.controller.persistence.query.criteria.ImmutableQueryAttributeRestriction; +import org.cloudfoundry.multiapps.controller.persistence.query.criteria.QueryCriteria; +import org.cloudfoundry.multiapps.controller.persistence.services.ApplicationShutdownService; + +public class ApplicationShutdownQueryImpl extends AbstractQueryImpl + implements ApplicationShutdownQuery { + private final QueryCriteria queryCriteria = new QueryCriteria(); + private final ApplicationShutdownService.ApplicationShutdownMapper applicationShutdownMapper; + + public ApplicationShutdownQueryImpl(EntityManager entityManager, + ApplicationShutdownService.ApplicationShutdownMapper applicationShutdownMapper) { + super(entityManager); + this.applicationShutdownMapper = applicationShutdownMapper; + } + + @Override + public ApplicationShutdownQuery id(String instanceId) { + queryCriteria.addRestriction(ImmutableQueryAttributeRestriction.builder() + .attribute(AttributeNames.ID) + .condition(getCriteriaBuilder()::equal) + .value(instanceId) + .build()); + return this; + } + + @Override + public ApplicationShutdownQuery applicationId(String applicationId) { + queryCriteria.addRestriction(ImmutableQueryAttributeRestriction.builder() + .attribute(AttributeNames.APPLICATION_ID) + .condition(getCriteriaBuilder()::equal) + .value(applicationId) + .build()); + return this; + } + + @Override + public ApplicationShutdownQuery applicationInstanceIndex(int applicationInstanceIndex) { + queryCriteria.addRestriction(ImmutableQueryAttributeRestriction.builder() + .attribute(AttributeNames.APPLICATION_INSTANCE_INDEX) + .condition(getCriteriaBuilder()::equal) + .value(applicationInstanceIndex) + .build()); + return this; + } + + @Override + public ApplicationShutdownQuery shutdownStatus(ApplicationShutdown.Status shutdownStatus) { + queryCriteria.addRestriction(ImmutableQueryAttributeRestriction.builder() + .attribute(AttributeNames.SHUTDOWN_STATUS) + .condition(getCriteriaBuilder()::equal) + .value(shutdownStatus) + .build()); + return this; + } + + @Override + public ApplicationShutdownQuery startedAt(Date startedAt) { + queryCriteria.addRestriction(ImmutableQueryAttributeRestriction.builder() + .attribute(AttributeNames.STARTED_AT) + .condition(getCriteriaBuilder()::equal) + .value(startedAt) + .build()); + return this; + } + + @Override + public ApplicationShutdown singleResult() { + ApplicationShutdownDto dto = executeInTransaction(manager -> createQuery(manager, queryCriteria, + ApplicationShutdownDto.class).getResultList() + .stream() + .findFirst() + .orElse(null)); + return dto != null ? applicationShutdownMapper.fromDto(dto) : null; + } + + @Override + public List list() { + List dtos = executeInTransaction(manager -> createQuery(manager, queryCriteria, + ApplicationShutdownDto.class).getResultList()); + + return dtos.stream() + .map(applicationShutdownMapper::fromDto) + .toList(); + } + + @Override + public int delete() { + return executeInTransaction(manager -> createDeleteQuery(manager, queryCriteria, ApplicationShutdownDto.class).executeUpdate()); + } +} diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ApplicationShutdownService.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ApplicationShutdownService.java new file mode 100644 index 0000000000..c2de692c02 --- /dev/null +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/ApplicationShutdownService.java @@ -0,0 +1,65 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import jakarta.inject.Named; +import jakarta.persistence.EntityManagerFactory; +import org.cloudfoundry.multiapps.common.ConflictException; +import org.cloudfoundry.multiapps.common.NotFoundException; +import org.cloudfoundry.multiapps.controller.persistence.Messages; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdownDto; +import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.query.ApplicationShutdownQuery; +import org.cloudfoundry.multiapps.controller.persistence.query.impl.ApplicationShutdownQueryImpl; + +@Named +public class ApplicationShutdownService extends PersistenceService { + + private final ApplicationShutdownService.ApplicationShutdownMapper applicationShutdownMapper; + + public ApplicationShutdownService(EntityManagerFactory entityManagerFactory, + ApplicationShutdownService.ApplicationShutdownMapper applicationShutdownMapper) { + super(entityManagerFactory); + this.applicationShutdownMapper = applicationShutdownMapper; + } + + @Override + protected PersistenceObjectMapper getPersistenceObjectMapper() { + return applicationShutdownMapper; + } + + public ApplicationShutdownQuery createQuery() { + return new ApplicationShutdownQueryImpl(createEntityManager(), applicationShutdownMapper); + } + + @Override + protected void onEntityConflict(ApplicationShutdownDto dto, Throwable t) { + throw new ConflictException(t, Messages.APPLICATION_SHUTDOWN_WITH_APPLICATION_INSTANCE_ID_ALREADY_EXIST, dto.getPrimaryKey()); + } + + @Override + protected void onEntityNotFound(String primaryKey) { + throw new NotFoundException(Messages.APPLICATION_SHUTDOWN_WITH_APPLICATION_INSTANCE_ID_DOES_NOT_EXIST, primaryKey); + } + + @Named + public static class ApplicationShutdownMapper implements PersistenceObjectMapper { + + @Override + public ApplicationShutdown fromDto(ApplicationShutdownDto dto) { + return ImmutableApplicationShutdown.builder() + .id(dto.getPrimaryKey()) + .applicationId(dto.getАpplicationId()) + .applicationInstanceIndex(dto.getАpplicationIndex()) + .status(dto.getShutdownStatus()) + .startedAt(dto.getStartedAt()) + .build(); + } + + @Override + public ApplicationShutdownDto toDto(ApplicationShutdown object) { + return new ApplicationShutdownDto(object.getId(), object.getApplicationId(), object.getApplicationInstanceIndex(), + object.getStatus(), object.getStartedAt()); + } + } + +} diff --git a/multiapps-controller-persistence/src/main/resources/META-INF/persistence.xml b/multiapps-controller-persistence/src/main/resources/META-INF/persistence.xml index 684a213264..41575342d8 100644 --- a/multiapps-controller-persistence/src/main/resources/META-INF/persistence.xml +++ b/multiapps-controller-persistence/src/main/resources/META-INF/persistence.xml @@ -14,11 +14,12 @@ org.cloudfoundry.multiapps.controller.persistence.dto.LockOwnerDto org.cloudfoundry.multiapps.controller.persistence.dto.AsyncUploadJobDto org.cloudfoundry.multiapps.controller.persistence.dto.BackupDescriptorDto + org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdownDto true - + + value="org.eclipse.persistence.logging.slf4j.SLF4JLogger"/> diff --git a/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog-2.36.0-persistence.xml b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog-2.36.0-persistence.xml new file mode 100644 index 0000000000..c022a545fa --- /dev/null +++ b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog-2.36.0-persistence.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml index a5da6591b7..6d946f7697 100644 --- a/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml +++ b/multiapps-controller-persistence/src/main/resources/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml @@ -40,4 +40,6 @@ + + diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ApplicationShutdownServiceTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ApplicationShutdownServiceTest.java new file mode 100644 index 0000000000..9a76766c9b --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/ApplicationShutdownServiceTest.java @@ -0,0 +1,138 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; +import org.cloudfoundry.multiapps.common.ConflictException; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.query.ApplicationShutdownQuery; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ApplicationShutdownServiceTest { + + private final String APPLICATION_ID = UUID.randomUUID() + .toString(); + private final String INSTANCE_ID = UUID.randomUUID() + .toString(); + private final String INSTANCE_ID_2 = UUID.randomUUID() + .toString(); + private final ApplicationShutdown APPLICATION_SHUTDOWN = createApplicationShutdownInstance(INSTANCE_ID, 0); + private final ApplicationShutdown APPLICATION_SHUTDOWN_2 = createApplicationShutdownInstance(INSTANCE_ID_2, 1); + + private final ApplicationShutdownService applicationShutdownService = createApplicationShutdownService(); + + @AfterEach + void cleanUp() { + applicationShutdownService.createQuery() + .delete(); + } + + @Test + void testAdd() { + applicationShutdownService.add(APPLICATION_SHUTDOWN); + assertEquals(1, applicationShutdownService.createQuery() + .list() + .size()); + + assertEquals(APPLICATION_SHUTDOWN.getId(), applicationShutdownService.createQuery() + .id(APPLICATION_SHUTDOWN.getId()) + .singleResult() + .getId()); + + } + + @Test + void testAddWithAlreadyExistingApplicationShutdown() { + applicationShutdownService.add(APPLICATION_SHUTDOWN); + assertApplicationShutdownExists(APPLICATION_SHUTDOWN.getId()); + + assertThrows(ConflictException.class, () -> applicationShutdownService.add(APPLICATION_SHUTDOWN)); + } + + @Test + void testAddWithMoreThanOneApplicationShutdown() { + addApplicationShutdown(List.of(APPLICATION_SHUTDOWN, APPLICATION_SHUTDOWN_2)); + assertApplicationShutdownExists(APPLICATION_SHUTDOWN.getId()); + assertApplicationShutdownExists(APPLICATION_SHUTDOWN_2.getId()); + + assertEquals(2, applicationShutdownService.createQuery() + .list() + .size()); + } + + @Test + void testQueryById() { + testQueryByCriteria((query, applicationShutdown) -> query.id(applicationShutdown.getId()), 1); + } + + @Test + void testQueryByApplicationId() { + testQueryByCriteria((query, applicationShutdown) -> query.applicationId(applicationShutdown.getApplicationId()), 2); + } + + @Test + void testQueryByShutdownStatus() { + testQueryByCriteria((query, applicationShutdown) -> query.shutdownStatus(applicationShutdown.getStatus()), 2); + } + + @Test + void testQueryByApplicationInstanceIndex() { + testQueryByCriteria( + (query, applicationShutdown) -> query.applicationInstanceIndex(applicationShutdown.getApplicationInstanceIndex()), 1); + } + + @Test + void testQueryByStartedAt() { + testQueryByCriteria((query, applicationShutdown) -> query.startedAt(applicationShutdown.getStartedAt()), 2); + } + + private interface ApplicationShutdownQueryBuilder { + ApplicationShutdownQuery build(ApplicationShutdownQuery applicationShutdownQuery, ApplicationShutdown applicationShutdown); + } + + private void testQueryByCriteria(ApplicationShutdownQueryBuilder applicationShutdownQueryBuilder, int resultCount) { + addApplicationShutdown(List.of(APPLICATION_SHUTDOWN, APPLICATION_SHUTDOWN_2)); + assertEquals(resultCount, applicationShutdownQueryBuilder.build(applicationShutdownService.createQuery(), APPLICATION_SHUTDOWN) + .list() + .size()); + assertEquals(resultCount, applicationShutdownQueryBuilder.build(applicationShutdownService.createQuery(), APPLICATION_SHUTDOWN) + .delete()); + assertApplicationShutdownExists(APPLICATION_SHUTDOWN_2.getId()); + } + + private void assertApplicationShutdownExists(String id) { + // If does not exist, will throw NoResultException + applicationShutdownService.createQuery() + .id(id); + } + + private void addApplicationShutdown(List applicationShutdowns) { + applicationShutdowns.forEach(applicationShutdownService::add); + } + + private ApplicationShutdown createApplicationShutdownInstance(String instanceId, int index) { + return ImmutableApplicationShutdown.builder() + .id(instanceId) + .applicationId(APPLICATION_ID) + .applicationInstanceIndex(index) + .startedAt(Date.from(Instant.now())) + .status(ApplicationShutdown.Status.FINISHED) + .build(); + } + + private ApplicationShutdownService createApplicationShutdownService() { + EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("TestDefault"); + ApplicationShutdownService.ApplicationShutdownMapper applicationShutdownMapper = new ApplicationShutdownService.ApplicationShutdownMapper(); + return new ApplicationShutdownService(entityManagerFactory, + applicationShutdownMapper); + } +} diff --git a/multiapps-controller-persistence/src/test/resources/META-INF/persistence.xml b/multiapps-controller-persistence/src/test/resources/META-INF/persistence.xml index c3a8d536d4..a981c281fd 100644 --- a/multiapps-controller-persistence/src/test/resources/META-INF/persistence.xml +++ b/multiapps-controller-persistence/src/test/resources/META-INF/persistence.xml @@ -12,16 +12,17 @@ org.cloudfoundry.multiapps.controller.persistence.dto.AccessTokenDto org.cloudfoundry.multiapps.controller.persistence.dto.TextAttributeConverter org.cloudfoundry.multiapps.controller.persistence.dto.BackupDescriptorDto + org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdownDto true - + - - + value="jdbc:h2:mem:configuration-subscriptions:DB_CLOSE_DELAY=-1"/> + + - - + + \ No newline at end of file diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java index 8e259ae732..37e8f091bb 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/Messages.java @@ -788,6 +788,8 @@ public class Messages { public static final String GETTING_FEATURES_FOR_APPLICATION_0 = "Getting features for application \"{0}\""; public static final String TOTAL_SIZE_OF_ALL_RESOLVED_CONTENT_0 = "Total size for all resolved content {0}"; + public static final String STARTED_SHUTTING_DOWN_APPLICATION_WITH_ID_AND_INDEX = "Started shutting down application with ID: \"{0}\" and index: \"{1}\""; + public static final String SHUT_DOWN_APPLICATION_WITH_ID_AND_INDEX_FINISHED_SUCCESSFULLY = "Shut down application with ID: \"{0}\" and index: \"{1}\" finished successfully"; // Not log messages public static final String SERVICE_TYPE = "{0}/{1}"; diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/ApplicationShutdownJob.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/ApplicationShutdownJob.java new file mode 100644 index 0000000000..55d8fe028b --- /dev/null +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/jobs/ApplicationShutdownJob.java @@ -0,0 +1,89 @@ +package org.cloudfoundry.multiapps.controller.process.jobs; + +import java.text.MessageFormat; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Named; +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown.Status; +import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.services.ApplicationShutdownService; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.cloudfoundry.multiapps.controller.process.flowable.FlowableFacade; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; + +@Named +public class ApplicationShutdownJob { + + private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationShutdownJob.class); + + private final FlowableFacade flowableFacade; + private final ApplicationShutdownService applicationShutdownService; + private final ApplicationConfiguration applicationConfiguration; + + public ApplicationShutdownJob(FlowableFacade flowableFacade, ApplicationShutdownService applicationShutdownService, + ApplicationConfiguration applicationConfiguration) { + this.flowableFacade = flowableFacade; + this.applicationShutdownService = applicationShutdownService; + this.applicationConfiguration = applicationConfiguration; + } + + @Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS) + public void run() { + ApplicationShutdown applicationShutdown = getApplicationToShutdown(); + + if (applicationShutdown == null || applicationShutdown.getStatus() + .equals(Status.FINISHED)) { + return; + } + if (applicationShutdown.getStatus() + .equals(Status.INITIAL)) { + shutdownApplication(applicationShutdown); + updateApplicationShutdownStatus(applicationShutdown, + Status.RUNNING); + } + if (getShutdownStatus().equals(Status.FINISHED)) { + updateApplicationShutdownStatus(applicationShutdown, + Status.FINISHED); + } + } + + private ApplicationShutdown getApplicationToShutdown() { + String applicationId = applicationConfiguration.getApplicationGuid(); + int applicationInstanceIndex = applicationConfiguration.getApplicationInstanceIndex(); + + return applicationShutdownService.createQuery() + .applicationId(applicationId) + .applicationInstanceIndex(applicationInstanceIndex) + .singleResult(); + } + + private void updateApplicationShutdownStatus(ApplicationShutdown oldApplicationShutdown, + ApplicationShutdown.Status status) { + ApplicationShutdown newApplicationShutdown = ImmutableApplicationShutdown.copyOf(oldApplicationShutdown) + .withStatus(status); + applicationShutdownService.update(oldApplicationShutdown, newApplicationShutdown); + } + + private Status getShutdownStatus() { + return flowableFacade.isJobExecutorActive() ? Status.RUNNING : Status.FINISHED; + } + + private void shutdownApplication(ApplicationShutdown applicationShutdown) { + CompletableFuture.runAsync(() -> { + logProgressOfShuttingDown(applicationShutdown, Messages.STARTED_SHUTTING_DOWN_APPLICATION_WITH_ID_AND_INDEX); + flowableFacade.shutdownJobExecutor(); + }) + .thenRun(() -> logProgressOfShuttingDown(applicationShutdown, + Messages.SHUT_DOWN_APPLICATION_WITH_ID_AND_INDEX_FINISHED_SUCCESSFULLY)); + } + + private void logProgressOfShuttingDown(ApplicationShutdown applicationShutdown, String message) { + LOGGER.info( + MessageFormat.format(message, applicationShutdown.getApplicationId(), applicationShutdown.getApplicationInstanceIndex())); + } +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/jobs/ApplicationShutdownJobTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/jobs/ApplicationShutdownJobTest.java new file mode 100644 index 0000000000..0a45bee5ae --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/jobs/ApplicationShutdownJobTest.java @@ -0,0 +1,107 @@ +package org.cloudfoundry.multiapps.controller.process.jobs; + +import java.time.Instant; +import java.util.Date; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.query.ApplicationShutdownQuery; +import org.cloudfoundry.multiapps.controller.persistence.services.ApplicationShutdownService; +import org.cloudfoundry.multiapps.controller.process.flowable.FlowableFacade; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ApplicationShutdownJobTest { + + @Mock + private FlowableFacade flowableFacade; + + @Mock + private ApplicationShutdownService applicationShutdownService; + + @Mock + private ApplicationConfiguration applicationConfiguration; + + @Mock + private ApplicationShutdownQuery applicationShutdownQuery; + + private ApplicationShutdownJob applicationShutdownJob; + private final String APPLICATION_ID = UUID.randomUUID() + .toString(); + private final String INSTANCE_ID = UUID.randomUUID() + .toString(); + private final int APPLICATION_INSTANCE_INDEX = 0; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + applicationShutdownJob = new ApplicationShutdownJob(flowableFacade, applicationShutdownService, applicationConfiguration); + + when(applicationConfiguration.getApplicationGuid()).thenReturn(APPLICATION_ID); + when(applicationConfiguration.getApplicationInstanceIndex()).thenReturn(APPLICATION_INSTANCE_INDEX); + + when(applicationShutdownService.createQuery()).thenReturn(applicationShutdownQuery); + when(applicationShutdownQuery.applicationId(anyString())).thenReturn(applicationShutdownQuery); + when(applicationShutdownQuery.applicationInstanceIndex(anyInt())).thenReturn(applicationShutdownQuery); + + } + + @Test + void testRunWithNotStoppedFlowableExecutor() { + ApplicationShutdown applicationShutdown = createApplicationShutdownInstance(ApplicationShutdown.Status.INITIAL); + when(flowableFacade.isJobExecutorActive()).thenReturn(true); + when(applicationShutdownQuery.singleResult()).thenReturn(applicationShutdown); + + applicationShutdownJob.run(); + + verify(applicationShutdownService).update(any(), any()); + } + + @Test + void testRunWithStoppedFlowableExecutor() { + ApplicationShutdown applicationShutdown = createApplicationShutdownInstance(ApplicationShutdown.Status.INITIAL); + when(applicationShutdownQuery.singleResult()).thenReturn(applicationShutdown); + + applicationShutdownJob.run(); + + verify(applicationShutdownService, times(2)).update(any(), any()); + } + + @Test + void testRunWithoutScheduledApplication() { + applicationShutdownJob.run(); + + verify(applicationShutdownService, times(0)).update(any(), any()); + } + + @Test + void testRunWithScheduledApplicationInFinishedState() { + ApplicationShutdown applicationShutdown = createApplicationShutdownInstance(ApplicationShutdown.Status.FINISHED); + when(applicationShutdownQuery.singleResult()).thenReturn(applicationShutdown); + applicationShutdownJob.run(); + + verify(applicationShutdownService, times(0)).update(any(), any()); + } + + private ApplicationShutdown createApplicationShutdownInstance(ApplicationShutdown.Status status) { + return ImmutableApplicationShutdown.builder() + .id(INSTANCE_ID) + .applicationId(APPLICATION_ID) + .applicationInstanceIndex(APPLICATION_INSTANCE_INDEX) + .startedAt(Date.from(Instant.now())) + .status(status) + .build(); + } +} diff --git a/multiapps-controller-shutdown-client/pom.xml b/multiapps-controller-shutdown-client/pom.xml index 4ec0801401..ff564600f8 100644 --- a/multiapps-controller-shutdown-client/pom.xml +++ b/multiapps-controller-shutdown-client/pom.xml @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 multiapps-controller-shutdown-client @@ -18,13 +18,38 @@ jakarta.servlet jakarta.servlet-api + + com.google.cloud + google-cloud-nio + compile + + + commons-logging + commons-logging + ${commons-logging.version} + + + org.slf4j + slf4j-api + compile + org.cloudfoundry.multiapps multiapps-common + + + org.slf4j + jcl-over-slf4j + + org.cloudfoundry.multiapps - multiapps-controller-core + multiapps-controller-database-migration + + + org.springframework + spring-orm \ No newline at end of file diff --git a/multiapps-controller-shutdown-client/src/main/java/module-info.java b/multiapps-controller-shutdown-client/src/main/java/module-info.java index 7273b1e0cb..0e11bd86a3 100644 --- a/multiapps-controller-shutdown-client/src/main/java/module-info.java +++ b/multiapps-controller-shutdown-client/src/main/java/module-info.java @@ -3,16 +3,13 @@ exports org.cloudfoundry.multiapps.controller.shutdown.client; exports org.cloudfoundry.multiapps.controller.shutdown.client.configuration; - requires transitive org.cloudfoundry.multiapps.controller.core; - requires com.fasterxml.jackson.annotation; - requires org.apache.httpcomponents.client5.httpclient5; - requires org.apache.httpcomponents.core5.httpcore5; - requires org.cloudfoundry.multiapps.controller.client; requires org.cloudfoundry.multiapps.common; + requires org.cloudfoundry.multiapps.controller.database.migration; requires org.slf4j; requires static java.compiler; requires static org.immutables.value; + requires spring.orm; } \ No newline at end of file diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationInstanceShutdownExecutor.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationInstanceShutdownExecutor.java deleted file mode 100644 index d5048514ee..0000000000 --- a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationInstanceShutdownExecutor.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.cloudfoundry.multiapps.controller.shutdown.client; - -import java.util.UUID; - -import org.cloudfoundry.multiapps.common.util.JsonUtil; -import org.cloudfoundry.multiapps.common.util.MiscUtil; -import org.cloudfoundry.multiapps.controller.core.model.ApplicationShutdown; -import org.cloudfoundry.multiapps.controller.shutdown.client.configuration.ShutdownClientConfiguration; -import org.cloudfoundry.multiapps.controller.shutdown.client.configuration.ShutdownConfiguration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class ApplicationInstanceShutdownExecutor { - - private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationInstanceShutdownExecutor.class); - - private static final long SHUTDOWN_POLLING_INTERVAL = 3000L; - - private final ShutdownConfiguration shutdownConfiguration; - private final ShutdownClientFactory shutdownClientFactory; - - public ApplicationInstanceShutdownExecutor(ShutdownConfiguration shutdownConfiguration, ShutdownClientFactory shutdownClientFactory) { - this.shutdownConfiguration = shutdownConfiguration; - this.shutdownClientFactory = shutdownClientFactory; - } - - public void execute(UUID applicationGuid, int applicationInstanceIndex) { - ShutdownClient shutdownClient = createShutdownClient(); - shutdown(shutdownClient, applicationGuid, applicationInstanceIndex); - } - - private ShutdownClient createShutdownClient() { - return shutdownClientFactory.createShutdownClient(createShutdownClientConfiguration()); - } - - private ShutdownClientConfiguration createShutdownClientConfiguration() { - return new ShutdownClientConfiguration(shutdownConfiguration); - } - - private static void shutdown(ShutdownClient shutdownClient, UUID applicationGuid, int applicationInstanceIndex) { - ApplicationShutdown shutdown = shutdownClient.triggerShutdown(applicationGuid, applicationInstanceIndex); - while (!hasFinished(shutdown)) { - print(shutdown); - MiscUtil.sleep(SHUTDOWN_POLLING_INTERVAL); - shutdown = shutdownClient.getStatus(applicationGuid, applicationInstanceIndex); - } - } - - private static boolean hasFinished(ApplicationShutdown shutdown) { - return ApplicationShutdown.Status.FINISHED.equals(shutdown.getStatus()); - } - - private static void print(ApplicationShutdown shutdown) { - LOGGER.info("Shutdown status of application with GUID {}, instance {}: {}", shutdown.getApplicationId(), - shutdown.getApplicationInstanceIndex(), JsonUtil.toJson(shutdown, true)); - } - -} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownExecutor.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownExecutor.java index 544f3ddead..b4c01a7e5c 100644 --- a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownExecutor.java +++ b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownExecutor.java @@ -1,63 +1,58 @@ package org.cloudfoundry.multiapps.controller.shutdown.client; -import java.net.MalformedURLException; -import java.net.URL; -import java.text.MessageFormat; -import java.util.UUID; - -import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; -import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClientImpl; -import org.cloudfoundry.multiapps.controller.client.facade.CloudCredentials; -import org.cloudfoundry.multiapps.controller.client.facade.domain.InstancesInfo; -import org.cloudfoundry.multiapps.controller.shutdown.client.configuration.EnvironmentBasedShutdownConfiguration; -import org.cloudfoundry.multiapps.controller.shutdown.client.configuration.ShutdownConfiguration; +import java.util.List; -public class ApplicationShutdownExecutor { +import jakarta.persistence.EntityManagerFactory; +import org.cloudfoundry.multiapps.common.util.MiscUtil; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.services.ApplicationShutdownService; +import org.cloudfoundry.multiapps.controller.shutdown.client.configuration.DatabaseConnector; +import org.cloudfoundry.multiapps.controller.shutdown.client.util.ShutdownUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; - public static void main(String[] args) { - new ApplicationShutdownExecutor().execute(); - } +public class ApplicationShutdownExecutor { - private final ShutdownConfiguration shutdownConfiguration = new EnvironmentBasedShutdownConfiguration(); - private final ShutdownClientFactory shutdownClientFactory = new ShutdownClientFactory(); - private final ApplicationInstanceShutdownExecutor instanceShutdownExecutor = new ApplicationInstanceShutdownExecutor( - shutdownConfiguration, - shutdownClientFactory); + private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationShutdownExecutor.class); + private static final long SHUTDOWN_POLLING_INTERVAL = 5000L; - public void execute() { - int applicationInstancesCount = getApplicationInstancesCount(shutdownConfiguration); - shutdownInstances(applicationInstancesCount); + public static void main(String[] args) { + String applicationId = args[0]; + int applicationInstanceCount = Integer.parseInt(args[1]); + new ApplicationShutdownExecutor().execute(applicationId, applicationInstanceCount); } - private void shutdownInstances(int applicationInstancesCount) { - UUID applicationGuid = shutdownConfiguration.getApplicationGuid(); - for (int i = 0; i < applicationInstancesCount; i++) { - instanceShutdownExecutor.execute(applicationGuid, i); + public void execute(String applicationId, int applicationInstanceCount) { + ApplicationShutdownScheduler applicationShutdownScheduler = getApplicationShutdownScheduler(); + List scheduledApplicationShutdowns = applicationShutdownScheduler.scheduleApplicationForShutdown(applicationId, + applicationInstanceCount); + List applicationShutdownInstancesIds = getApplicationShutdownInstancesIds(scheduledApplicationShutdowns); + List applicationShutdowns = applicationShutdownScheduler.getScheduledApplicationInstancesForShutdown( + applicationId, applicationShutdownInstancesIds); + + while (ShutdownUtil.areThereUnstoppedInstances(applicationShutdowns) && !ShutdownUtil.isTimeoutExceeded( + applicationShutdowns.get(0))) { + ShutdownUtil.logShutdownStatus(applicationShutdowns); + MiscUtil.sleep(SHUTDOWN_POLLING_INTERVAL); + applicationShutdowns = applicationShutdownScheduler.getScheduledApplicationInstancesForShutdown( + applicationId, applicationShutdownInstancesIds); } + LOGGER.info(Messages.FINISHED_SHUTTING_DOWN); } - private static int getApplicationInstancesCount(ShutdownConfiguration shutdownConfiguration) { - CloudControllerClient client = createCloudControllerClient(shutdownConfiguration); - InstancesInfo instances = client.getApplicationInstances(shutdownConfiguration.getApplicationGuid()); - return instances.getInstances() - .size(); + public List getApplicationShutdownInstancesIds(List applicationShutdowns) { + return applicationShutdowns.stream() + .map(ApplicationShutdown::getId) + .toList(); } - private static CloudControllerClient createCloudControllerClient(ShutdownConfiguration shutdownConfiguration) { - URL cloudControllerUrl = toURL(shutdownConfiguration.getCloudControllerUrl()); - return new CloudControllerClientImpl(cloudControllerUrl, createCloudCredentials(shutdownConfiguration)); - } + public ApplicationShutdownScheduler getApplicationShutdownScheduler() { + DatabaseConnector databaseConnector = new DatabaseConnector(); + EntityManagerFactory entityManagerFactory = databaseConnector.createEntityManagerFactory(); + ApplicationShutdownService.ApplicationShutdownMapper applicationShutdownMapper = new ApplicationShutdownService.ApplicationShutdownMapper(); + ApplicationShutdownService applicationShutdownService = new ApplicationShutdownService(entityManagerFactory, + applicationShutdownMapper); - private static URL toURL(String string) { - try { - return new URL(string); - } catch (MalformedURLException e) { - throw new IllegalArgumentException(MessageFormat.format("{0} is not a valid URL.", string)); - } + return new ApplicationShutdownScheduler(applicationShutdownService); } - - private static CloudCredentials createCloudCredentials(ShutdownConfiguration shutdownConfiguration) { - return new CloudCredentials(shutdownConfiguration.getUsername(), shutdownConfiguration.getPassword()); - } - } diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownScheduler.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownScheduler.java new file mode 100644 index 0000000000..acc6bd201c --- /dev/null +++ b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownScheduler.java @@ -0,0 +1,65 @@ +package org.cloudfoundry.multiapps.controller.shutdown.client; + +import java.text.MessageFormat; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.services.ApplicationShutdownService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ApplicationShutdownScheduler { + + private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationShutdownScheduler.class); + private final ApplicationShutdownService applicationShutdownService; + + public ApplicationShutdownScheduler(ApplicationShutdownService applicationShutdownService) { + this.applicationShutdownService = applicationShutdownService; + } + + public List scheduleApplicationForShutdown(String applicationId, int instancesCount) { + List applicationInstancesForShutdown = new ArrayList<>(); + for (int i = 0; i < instancesCount; i++) { + ApplicationShutdown applicationShutdown = buildApplicationShutdown(applicationId, i); + applicationShutdownService.add(applicationShutdown); + applicationInstancesForShutdown.add(applicationShutdown); + LOGGER.info(MessageFormat.format(Messages.APP_INSTANCE_WITH_ID_AND_INDEX_SCHEDULED_FOR_SHUTDOWN, applicationId, i)); + } + return applicationInstancesForShutdown; + } + + public List getScheduledApplicationInstancesForShutdown(String applicationId, + List instancesIds) { + List instances = new ArrayList<>(); + for (String instanceId : instancesIds) { + ApplicationShutdown applicationShutdown = getApplicationShutdownInstanceByInstanceId(instanceId, applicationId); + if (applicationShutdown != null) { + instances.add(applicationShutdown); + } + } + return instances; + } + + private ApplicationShutdown getApplicationShutdownInstanceByInstanceId(String instanceId, String applicationId) { + return applicationShutdownService.createQuery() + .id(instanceId) + .applicationId(applicationId) + .singleResult(); + } + + private ApplicationShutdown buildApplicationShutdown(String applicationId, + int applicationInstanceIndex) { + return ImmutableApplicationShutdown.builder() + .id(UUID.randomUUID() + .toString()) + .startedAt(Date.from(Instant.now())) + .applicationId(applicationId) + .applicationInstanceIndex(applicationInstanceIndex) + .build(); + } +} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/CsrfHttpClientFactory.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/CsrfHttpClientFactory.java deleted file mode 100644 index 42d216cfa7..0000000000 --- a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/CsrfHttpClientFactory.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.cloudfoundry.multiapps.controller.shutdown.client; - -import java.util.Map; - -import org.cloudfoundry.multiapps.controller.core.http.CsrfHttpClient; - -interface CsrfHttpClientFactory { - - CsrfHttpClient create(Map defaultHttpHeaders); - -} \ No newline at end of file diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/Messages.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/Messages.java new file mode 100644 index 0000000000..fbe1a1c94a --- /dev/null +++ b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/Messages.java @@ -0,0 +1,11 @@ +package org.cloudfoundry.multiapps.controller.shutdown.client; + +public class Messages { + + private Messages() { + } + + public static final String SHUTDOWN_STATUS_OF_APPLICATION_WITH_GUID_INSTANCE = "Shutdown status of application with GUID {0}, instance {1}: {2}"; + public static final String APP_INSTANCE_WITH_ID_AND_INDEX_SCHEDULED_FOR_SHUTDOWN = "Application with ID \"{0}\" and index \"{1}\" has been scheduled for shutdown"; + public static final String FINISHED_SHUTTING_DOWN = "Finished shutting down"; +} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ShutdownClient.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ShutdownClient.java deleted file mode 100644 index 994f3eb67b..0000000000 --- a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ShutdownClient.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.cloudfoundry.multiapps.controller.shutdown.client; - -import java.util.UUID; - -import org.cloudfoundry.multiapps.controller.core.model.ApplicationShutdown; - -public interface ShutdownClient { - - ApplicationShutdown triggerShutdown(UUID applicationGuid, int applicationInstanceIndex); - - ApplicationShutdown getStatus(UUID applicationGuid, int applicationInstanceIndex); - -} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ShutdownClientFactory.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ShutdownClientFactory.java deleted file mode 100644 index 1377cb4925..0000000000 --- a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ShutdownClientFactory.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.cloudfoundry.multiapps.controller.shutdown.client; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Map; -import org.apache.hc.client5.http.HttpRequestRetryStrategy; -import org.apache.hc.client5.http.config.ConnectionConfig; -import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; -import org.apache.hc.core5.http.HttpHeaders; -import org.apache.hc.core5.http.io.SocketConfig; -import org.apache.hc.core5.util.TimeValue; -import org.apache.hc.core5.util.Timeout; -import org.cloudfoundry.multiapps.common.util.MapUtil; -import org.cloudfoundry.multiapps.controller.core.http.CsrfHttpClient; -import org.cloudfoundry.multiapps.controller.shutdown.client.configuration.ShutdownClientConfiguration; - -public class ShutdownClientFactory { - - private static final String CSRF_TOKEN_ENDPOINT = "/api/v1/csrf-token"; - private static final int RETRY_COUNT = 5; - private static final int RETRY_INTERVAL_IN_MILLIS = 5000; - private static final Timeout CONNECT_TIMEOUT = Timeout.ofMinutes(2); - private static final Timeout SOCKET_TIMEOUT = Timeout.ofMinutes(5); - private static final Timeout RESPONSE_TIMEOUT = Timeout.ofMinutes(10); - - public ShutdownClient createShutdownClient(ShutdownClientConfiguration configuration) { - return new ShutdownClientImpl(configuration.getApplicationUrl(), - defaultHttpHeaders -> createCsrfHttpClient(configuration, defaultHttpHeaders)); - } - - private CsrfHttpClient createCsrfHttpClient(ShutdownClientConfiguration configuration, Map defaultHttpHeaders) { - CloseableHttpClient httpClient = createHttpClient(); - String csrfTokenUrl = computeCsrfTokenUrl(configuration); - Map enrichedDefaultHttpHeaders = MapUtil.merge(computeHeaders(configuration), defaultHttpHeaders); - return new CsrfHttpClient(httpClient, csrfTokenUrl, enrichedDefaultHttpHeaders); - } - - private CloseableHttpClient createHttpClient() { - return HttpClientBuilder.create() - .setRetryStrategy(createRetryStrategy()) - .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create() - .setDefaultSocketConfig( - SocketConfig.custom() - .setSoTimeout( - SOCKET_TIMEOUT) - .build()) - .setDefaultConnectionConfig( - ConnectionConfig.custom() - .setConnectTimeout( - CONNECT_TIMEOUT) - .setSocketTimeout( - SOCKET_TIMEOUT) - .build()) - .build()) - .setDefaultRequestConfig(RequestConfig.custom() - .setResponseTimeout(RESPONSE_TIMEOUT) - .build()) - .build(); - } - - private HttpRequestRetryStrategy createRetryStrategy() { - return new DefaultHttpRequestRetryStrategy(RETRY_COUNT, TimeValue.ofMilliseconds(RETRY_INTERVAL_IN_MILLIS)); - } - - private String computeCsrfTokenUrl(ShutdownClientConfiguration configuration) { - return configuration.getApplicationUrl() + CSRF_TOKEN_ENDPOINT; - } - - private Map computeHeaders(ShutdownClientConfiguration configuration) { - String credentials = computeBasicAuthorizationCredentials(configuration); - return Map.of(HttpHeaders.AUTHORIZATION, String.format("Basic %s", encode(credentials))); - } - - private String encode(String string) { - return Base64.getEncoder() - .encodeToString(string.getBytes(StandardCharsets.UTF_8)); - } - - private String computeBasicAuthorizationCredentials(ShutdownClientConfiguration configuration) { - return String.format("%s:%s", configuration.getUsername(), configuration.getPassword()); - } - -} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ShutdownClientImpl.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ShutdownClientImpl.java deleted file mode 100644 index 6e17b73587..0000000000 --- a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/ShutdownClientImpl.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.cloudfoundry.multiapps.controller.shutdown.client; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.text.MessageFormat; -import java.util.Map; -import java.util.UUID; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.classic.methods.HttpUriRequest; -import org.apache.hc.core5.http.ClassicHttpResponse; -import org.apache.hc.core5.http.ParseException; -import org.apache.hc.core5.http.io.entity.EntityUtils; -import org.cloudfoundry.multiapps.common.util.JsonUtil; -import org.cloudfoundry.multiapps.controller.core.http.CsrfHttpClient; -import org.cloudfoundry.multiapps.controller.core.model.ApplicationShutdown; - -class ShutdownClientImpl implements ShutdownClient { - - private static final String X_CF_APP_INSTANCE = "x-cf-app-instance"; - private static final String SHUTDOWN_ENDPOINT = "/admin/shutdown"; - - private final String applicationUrl; - /** - * We need to create a new instance of {@link CsrfHttpClient} for each request, because the default headers of the client may differ - * based on the application's GUID and instance index. - */ - private final CsrfHttpClientFactory httpClientFactory; - - ShutdownClientImpl(String applicationUrl, CsrfHttpClientFactory httpClientFactory) { - this.applicationUrl = applicationUrl; - this.httpClientFactory = httpClientFactory; - } - - @Override - public ApplicationShutdown triggerShutdown(UUID applicationGuid, int applicationInstanceIndex) { - HttpPost request = new HttpPost(getShutdownEndpoint()); - return makeShutdownApiRequest(applicationGuid, applicationInstanceIndex, request); - } - - @Override - public ApplicationShutdown getStatus(UUID applicationGuid, int applicationInstanceIndex) { - HttpGet request = new HttpGet(getShutdownEndpoint()); - return makeShutdownApiRequest(applicationGuid, applicationInstanceIndex, request); - } - - private String getShutdownEndpoint() { - return applicationUrl + SHUTDOWN_ENDPOINT; - } - - private ApplicationShutdown makeShutdownApiRequest(UUID applicationGuid, int applicationInstanceIndex, HttpUriRequest httpRequest) { - try (CsrfHttpClient csrfHttpClient = createCsrfHttpClient(applicationGuid, applicationInstanceIndex)) { - return csrfHttpClient.execute(httpRequest, ShutdownClientImpl::parse); - } catch (IOException e) { - throw new IllegalStateException(MessageFormat.format("Could not parse shutdown API response: {0}", e.getMessage()), e); - } - } - - private CsrfHttpClient createCsrfHttpClient(UUID applicationGuid, int applicationInstanceIndex) { - String applicationInstanceHeaderValue = computeApplicationInstanceHeaderValue(applicationGuid, applicationInstanceIndex); - return httpClientFactory.create(Map.of(X_CF_APP_INSTANCE, applicationInstanceHeaderValue)); - } - - private static String computeApplicationInstanceHeaderValue(UUID applicationGuid, int applicationInstanceIndex) { - return String.format("%s:%d", applicationGuid, applicationInstanceIndex); - } - - private static ApplicationShutdown parse(ClassicHttpResponse response) throws IOException, ParseException { - String body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); - return parse(body); - } - - private static ApplicationShutdown parse(String body) { - return JsonUtil.fromJson(body, ApplicationShutdown.class); - } - -} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/CustomShutdownConfiguration.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/CustomShutdownConfiguration.java deleted file mode 100644 index 828ed264c7..0000000000 --- a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/CustomShutdownConfiguration.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.cloudfoundry.multiapps.controller.shutdown.client.configuration; - -import org.immutables.value.Value; - -/** - * NOT USED PRODUCTIVELY! Can be used to run the "script" locally, for testing and development purposes. - * - */ -@Value.Immutable -public interface CustomShutdownConfiguration extends ShutdownConfiguration { - -} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/DatabaseConnector.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/DatabaseConnector.java new file mode 100644 index 0000000000..ab7775d15d --- /dev/null +++ b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/DatabaseConnector.java @@ -0,0 +1,42 @@ +package org.cloudfoundry.multiapps.controller.shutdown.client.configuration; + +import javax.sql.DataSource; + +import jakarta.persistence.EntityManagerFactory; +import org.cloudfoundry.multiapps.controller.database.migration.extractor.DataSourceEnvironmentExtractor; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter; + +public class DatabaseConnector { + + private static final String DEPLOY_SERVICE_DATABASE_NAME = "deploy-service-database"; + private static final String PERSIST_UNIT_NAME = "Default"; + private static final String PACKAGES_TO_SCAN = "org.cloudfoundry.multiapps"; + + public EntityManagerFactory createEntityManagerFactory() { + DataSourceEnvironmentExtractor environmentExtractor = new DataSourceEnvironmentExtractor(); + DataSource targetDataSource = environmentExtractor.extractDataSource(DEPLOY_SERVICE_DATABASE_NAME); + + return getLocalContainerEntityManagerFactoryBean(targetDataSource, eclipseLinkJpaVendorAdapter()).getObject(); + } + + private LocalContainerEntityManagerFactoryBean getLocalContainerEntityManagerFactoryBean(DataSource dataSource, + EclipseLinkJpaVendorAdapter eclipseLinkJpaVendorAdapter) { + LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + localContainerEntityManagerFactoryBean.setPersistenceUnitName(PERSIST_UNIT_NAME); + localContainerEntityManagerFactoryBean.setDataSource(dataSource); + localContainerEntityManagerFactoryBean.setJpaVendorAdapter(eclipseLinkJpaVendorAdapter); + localContainerEntityManagerFactoryBean.setPackagesToScan(PACKAGES_TO_SCAN); + localContainerEntityManagerFactoryBean.afterPropertiesSet(); + return localContainerEntityManagerFactoryBean; + } + + private EclipseLinkJpaVendorAdapter eclipseLinkJpaVendorAdapter() { + EclipseLinkJpaVendorAdapter eclipseLinkJpaVendorAdapter = new EclipseLinkJpaVendorAdapter(); + eclipseLinkJpaVendorAdapter.setShowSql(false); + eclipseLinkJpaVendorAdapter.setDatabase(Database.POSTGRESQL); + return eclipseLinkJpaVendorAdapter; + } + +} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/EnvironmentBasedShutdownConfiguration.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/EnvironmentBasedShutdownConfiguration.java deleted file mode 100644 index 5e5dc3c22e..0000000000 --- a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/EnvironmentBasedShutdownConfiguration.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.cloudfoundry.multiapps.controller.shutdown.client.configuration; - -import java.text.MessageFormat; -import java.util.UUID; - -import org.cloudfoundry.multiapps.common.util.JsonUtil; -import org.cloudfoundry.multiapps.controller.core.configuration.Environment; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class EnvironmentBasedShutdownConfiguration implements ShutdownConfiguration { - - private static final String VCAP_APPLICATION = "VCAP_APPLICATION"; - private static final String CFG_URL = "APPLICATION_URL"; - private static final String CFG_USERNAME = "SHUTDOWN_USERNAME"; - private static final String CFG_PASSWORD = "SHUTDOWN_PASSWORD"; - - private static final String ARG_0_IS_NOT_SPECIFIED_IN_APP_ENV = "{0} is not specified in the application's environment."; - - private final Environment environment; - - public EnvironmentBasedShutdownConfiguration() { - this(new Environment()); - } - - public EnvironmentBasedShutdownConfiguration(Environment environment) { - this.environment = environment; - } - - @Override - public UUID getApplicationGuid() { - UUID applicationGuid = getVcapApplication().applicationGuid; - if (applicationGuid == null) { - throw new IllegalStateException(MessageFormat.format("Could not find application GUID in {0}.", VCAP_APPLICATION)); - } - return applicationGuid; - } - - @Override - public String getApplicationUrl() { - String multiappsControllerUrl = environment.getString(CFG_URL); - if (multiappsControllerUrl == null) { - throw new IllegalStateException(MessageFormat.format(ARG_0_IS_NOT_SPECIFIED_IN_APP_ENV, CFG_URL)); - } - return multiappsControllerUrl; - } - - @Override - public String getCloudControllerUrl() { - String cloudControllerUrl = getVcapApplication().cloudControllerUrl; - if (cloudControllerUrl == null) { - throw new IllegalStateException(MessageFormat.format("Could not find cloud controller URL in {0}.", VCAP_APPLICATION)); - } - return cloudControllerUrl; - } - - @Override - public String getUsername() { - String username = environment.getString(CFG_USERNAME); - if (username == null) { - throw new IllegalStateException(MessageFormat.format(ARG_0_IS_NOT_SPECIFIED_IN_APP_ENV, CFG_USERNAME)); - } - return username; - } - - @Override - public String getPassword() { - String password = environment.getString(CFG_PASSWORD); - if (password == null) { - throw new IllegalStateException(MessageFormat.format(ARG_0_IS_NOT_SPECIFIED_IN_APP_ENV, CFG_PASSWORD)); - } - return password; - } - - private VcapApplication getVcapApplication() { - return JsonUtil.fromJson(environment.getString(VCAP_APPLICATION), VcapApplication.class); - } - - private static class VcapApplication { - - @JsonProperty("application_id") - private UUID applicationGuid; - @JsonProperty("cf_api") - private String cloudControllerUrl; - - } - -} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/ShutdownClientConfiguration.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/ShutdownClientConfiguration.java deleted file mode 100644 index 916c9be022..0000000000 --- a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/ShutdownClientConfiguration.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.cloudfoundry.multiapps.controller.shutdown.client.configuration; - -import org.cloudfoundry.multiapps.controller.shutdown.client.ShutdownClient; - -/** - * A small facade around {@link ShutdownConfiguration} that limits what the instances of {@link ShutdownClient} can see from the entire - * shutdown configuration. They shouldn't need to know the CF API URL, for example. - * - */ -public class ShutdownClientConfiguration { - - private final ShutdownConfiguration configuration; - - public ShutdownClientConfiguration(ShutdownConfiguration configuration) { - this.configuration = configuration; - } - - public String getApplicationUrl() { - return configuration.getApplicationUrl(); - } - - public String getUsername() { - return configuration.getUsername(); - } - - public String getPassword() { - return configuration.getPassword(); - } - -} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/ShutdownConfiguration.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/ShutdownConfiguration.java deleted file mode 100644 index 36ca960daf..0000000000 --- a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/configuration/ShutdownConfiguration.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.cloudfoundry.multiapps.controller.shutdown.client.configuration; - -import java.util.UUID; - -public interface ShutdownConfiguration { - - UUID getApplicationGuid(); - - String getApplicationUrl(); - - String getCloudControllerUrl(); - - String getUsername(); - - String getPassword(); - -} diff --git a/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/util/ShutdownUtil.java b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/util/ShutdownUtil.java new file mode 100644 index 0000000000..41fedcb282 --- /dev/null +++ b/multiapps-controller-shutdown-client/src/main/java/org/cloudfoundry/multiapps/controller/shutdown/client/util/ShutdownUtil.java @@ -0,0 +1,42 @@ +package org.cloudfoundry.multiapps.controller.shutdown.client.util; + +import java.text.MessageFormat; +import java.time.Instant; +import java.util.List; + +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.shutdown.client.Messages; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ShutdownUtil { + + private ShutdownUtil() { + } + + private static final Logger LOGGER = LoggerFactory.getLogger(ShutdownUtil.class); + + public static final int TIMEOUT_IN_SECONDS = 600; //10 minutes + + public static boolean areThereUnstoppedInstances(List shutdownInstances) { + return shutdownInstances.stream() + .anyMatch(shutdownInstance -> !shutdownInstance.getStatus() + .equals(ApplicationShutdown.Status.FINISHED)); + } + + public static boolean isTimeoutExceeded(ApplicationShutdown applicationShutdown) { + Instant tenMinutesAfterStartedDate = Instant.from(applicationShutdown.getStartedAt() + .toInstant()) + .plusSeconds(TIMEOUT_IN_SECONDS); + Instant timeNow = Instant.now(); + return timeNow.isAfter(tenMinutesAfterStartedDate); + } + + public static void logShutdownStatus(List shutdownInstances) { + for (ApplicationShutdown shutdownInstance : shutdownInstances) { + LOGGER.info( + MessageFormat.format(Messages.SHUTDOWN_STATUS_OF_APPLICATION_WITH_GUID_INSTANCE, shutdownInstance.getApplicationId(), + String.valueOf(shutdownInstance.getApplicationInstanceIndex()), shutdownInstance.getStatus())); + } + } +} diff --git a/multiapps-controller-shutdown-client/src/test/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownExecutorTest.java b/multiapps-controller-shutdown-client/src/test/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownExecutorTest.java new file mode 100644 index 0000000000..ba91b842eb --- /dev/null +++ b/multiapps-controller-shutdown-client/src/test/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownExecutorTest.java @@ -0,0 +1,87 @@ +package org.cloudfoundry.multiapps.controller.shutdown.client; + +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableApplicationShutdown; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ApplicationShutdownExecutorTest { + + private final ApplicationShutdownExecutor applicationShutdownExecutor = new ApplicationShutdownExecutorClone(); + + @Mock + private ApplicationShutdownScheduler applicationShutdownScheduler; + + private final String APPLICATION_ID = UUID.randomUUID() + .toString(); + private final String INSTANCE_ID = UUID.randomUUID() + .toString(); + private final String INSTANCE_ID_2 = UUID.randomUUID() + .toString(); + private final String INSTANCE_ID_3 = UUID.randomUUID() + .toString(); + private final int INSTANCE_COUNT = 5; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + } + + @Test + void testExecuteWithStoppedInstances() { + List instances = List.of(createApplicationShutdownInstance(INSTANCE_ID)); + List instanceIds = applicationShutdownExecutor.getApplicationShutdownInstancesIds(instances); + + when(applicationShutdownScheduler.scheduleApplicationForShutdown(APPLICATION_ID, INSTANCE_COUNT)).thenReturn(instances); + when(applicationShutdownScheduler.getScheduledApplicationInstancesForShutdown(APPLICATION_ID, instanceIds)).thenReturn(instances); + applicationShutdownExecutor.execute(APPLICATION_ID, INSTANCE_COUNT); + + verify(applicationShutdownScheduler).scheduleApplicationForShutdown(APPLICATION_ID, INSTANCE_COUNT); + verify(applicationShutdownScheduler, times(1)).getScheduledApplicationInstancesForShutdown(APPLICATION_ID, instanceIds); + } + + @Test + void testGetApplicationShutdownInstancesIds() { + List instances = List.of(createApplicationShutdownInstance(INSTANCE_ID), + createApplicationShutdownInstance(INSTANCE_ID_2), + createApplicationShutdownInstance(INSTANCE_ID_3)); + + List ids = applicationShutdownExecutor.getApplicationShutdownInstancesIds(instances); + assertEquals(3, ids.size()); + assertTrue(ids.contains(INSTANCE_ID)); + assertTrue(ids.contains(INSTANCE_ID_2)); + assertTrue(ids.contains(INSTANCE_ID_3)); + } + + class ApplicationShutdownExecutorClone extends ApplicationShutdownExecutor { + + @Override + public ApplicationShutdownScheduler getApplicationShutdownScheduler() { + return applicationShutdownScheduler; + } + } + + private ApplicationShutdown createApplicationShutdownInstance(String instanceId) { + return ImmutableApplicationShutdown.builder() + .id(instanceId) + .applicationId(APPLICATION_ID) + .applicationInstanceIndex(0) + .startedAt(Date.from(Instant.now())) + .status(ApplicationShutdown.Status.FINISHED) + .build(); + } +} diff --git a/multiapps-controller-shutdown-client/src/test/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownSchedulerTest.java b/multiapps-controller-shutdown-client/src/test/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownSchedulerTest.java new file mode 100644 index 0000000000..82ad0ceef8 --- /dev/null +++ b/multiapps-controller-shutdown-client/src/test/java/org/cloudfoundry/multiapps/controller/shutdown/client/ApplicationShutdownSchedulerTest.java @@ -0,0 +1,103 @@ +package org.cloudfoundry.multiapps.controller.shutdown.client; + +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.query.impl.ApplicationShutdownQueryImpl; +import org.cloudfoundry.multiapps.controller.persistence.services.ApplicationShutdownService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ApplicationShutdownSchedulerTest { + + private final String APPLICATION_ID = UUID.randomUUID() + .toString(); + private final String INSTANCE_ID = UUID.randomUUID() + .toString(); + private final String INSTANCE_ID_2 = UUID.randomUUID() + .toString(); + + @Mock + private ApplicationShutdownService applicationShutdownService; + + @Mock + private ApplicationShutdownQueryImpl applicationShutdownQuery; + + private ApplicationShutdownScheduler applicationShutdownScheduler; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + applicationShutdownScheduler = new ApplicationShutdownScheduler(applicationShutdownService); + } + + @Test + void testScheduleApplicationForShutdownWithZeroInstances() { + List instances = applicationShutdownScheduler.scheduleApplicationForShutdown(APPLICATION_ID, 0); + assertEquals(0, instances.size()); + } + + @Test + void testScheduleApplicationForShutdownWithFiveInstances() { + List instances = applicationShutdownScheduler.scheduleApplicationForShutdown(APPLICATION_ID, 5); + for (ApplicationShutdown instance : instances) { + assertNotNull(instance.getId()); + assertNotNull(instance.getStartedAt()); + assertEquals(APPLICATION_ID, instance.getApplicationId()); + } + assertEquals(5, instances.size()); + } + + @Test + void testGetScheduledApplicationInstancesForShutdownWithId() { + ApplicationShutdown applicationShutdown = createApplicationShutdownInstance(INSTANCE_ID); + when(applicationShutdownQuery.id(anyString())).thenReturn(applicationShutdownQuery); + when(applicationShutdownQuery.applicationId(anyString())).thenReturn(applicationShutdownQuery); + when(applicationShutdownQuery.singleResult()).thenReturn(applicationShutdown); + when(applicationShutdownService.createQuery()).thenReturn(applicationShutdownQuery); + + List instances = applicationShutdownScheduler.getScheduledApplicationInstancesForShutdown(APPLICATION_ID, + List.of( + INSTANCE_ID)); + for (ApplicationShutdown instance : instances) { + assertNotNull(instance.getId()); + assertNotNull(instance.getStartedAt()); + assertEquals(APPLICATION_ID, instance.getApplicationId()); + } + + assertEquals(1, instances.size()); + verify(applicationShutdownQuery).id(INSTANCE_ID); + verify(applicationShutdownQuery).applicationId(APPLICATION_ID); + } + + @Test + void testGetScheduledApplicationInstancesForShutdownWithoutIds() { + List instances = applicationShutdownScheduler.getScheduledApplicationInstancesForShutdown(APPLICATION_ID, + List.of()); + + assertEquals(0, instances.size()); + } + + private ApplicationShutdown createApplicationShutdownInstance(String instanceId) { + return ImmutableApplicationShutdown.builder() + .id(instanceId) + .applicationId(APPLICATION_ID) + .applicationInstanceIndex(0) + .startedAt(Date.from(Instant.now())) + .status(ApplicationShutdown.Status.FINISHED) + .build(); + } +} diff --git a/multiapps-controller-shutdown-client/src/test/java/org/cloudfoundry/multiapps/controller/shutdown/client/util/ShutdownUtilTest.java b/multiapps-controller-shutdown-client/src/test/java/org/cloudfoundry/multiapps/controller/shutdown/client/util/ShutdownUtilTest.java new file mode 100644 index 0000000000..3930f10cec --- /dev/null +++ b/multiapps-controller-shutdown-client/src/test/java/org/cloudfoundry/multiapps/controller/shutdown/client/util/ShutdownUtilTest.java @@ -0,0 +1,67 @@ +package org.cloudfoundry.multiapps.controller.shutdown.client.util; + +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableApplicationShutdown; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ShutdownUtilTest { + + public static final int TIMEOUT_IN_SECONDS = 300; //5 minutes + + @Test + void testAreThereUnstoppedInstancesWithUpstoppedInstances() { + List applicationShutdownInstances = List.of(createApplicationShutdownInstance(true, Date.from(Instant.now())), + createApplicationShutdownInstance(false, Date.from(Instant.now())), + createApplicationShutdownInstance(true, Date.from(Instant.now()))); + + assertTrue(ShutdownUtil.areThereUnstoppedInstances(applicationShutdownInstances)); + } + + @Test + void testAreThereUnstoppedInstancesWithAllStoppedInstances() { + List applicationShutdownInstances = List.of(createApplicationShutdownInstance(true, Date.from(Instant.now())), + createApplicationShutdownInstance(true, Date.from(Instant.now())), + createApplicationShutdownInstance(true, Date.from(Instant.now()))); + + assertFalse(ShutdownUtil.areThereUnstoppedInstances(applicationShutdownInstances)); + } + + @Test + void testIsTimeoutExceededWithTimeOutExceeded() { + Instant timeBeforeTenMinutes = Instant.now() + .minusSeconds(ShutdownUtil.TIMEOUT_IN_SECONDS); + ApplicationShutdown applicationShutdownInstance = createApplicationShutdownInstance(true, Date.from(timeBeforeTenMinutes)); + + assertTrue(ShutdownUtil.isTimeoutExceeded(applicationShutdownInstance)); + } + + @Test + void testIsTimeoutExceededWithTimeOutNotExceeded() { + Instant timeBeforeTenMinutes = Instant.now() + .minusSeconds(TIMEOUT_IN_SECONDS); + ApplicationShutdown applicationShutdownInstance = createApplicationShutdownInstance(true, Date.from(timeBeforeTenMinutes)); + + assertFalse(ShutdownUtil.isTimeoutExceeded(applicationShutdownInstance)); + } + + private ApplicationShutdown createApplicationShutdownInstance(boolean isInstanceStopped, Date startedAt) { + return ImmutableApplicationShutdown.builder() + .id(UUID.randomUUID() + .toString()) + .applicationId(UUID.randomUUID() + .toString()) + .applicationInstanceIndex(0) + .startedAt(startedAt) + .status( + isInstanceStopped ? ApplicationShutdown.Status.FINISHED : ApplicationShutdown.Status.RUNNING) + .build(); + } +} diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/Constants.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/Constants.java index 2e88f984a0..620de4f9f0 100644 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/Constants.java +++ b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/Constants.java @@ -57,7 +57,6 @@ private Resources() { } public static final String APPLICATION_HEALTH = "/public/application-health"; - public static final String APPLICATION_SHUTDOWN = "/rest/admin/shutdown"; public static final String CONFIGURATION_ENTRIES = "/rest/configuration-entries"; public static final String CSRF_TOKEN = "/rest/csrf-token"; public static final String HEALTH_CHECK = "/public/health"; diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/Messages.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/Messages.java index 04f831b707..24d2c046d5 100644 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/Messages.java +++ b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/Messages.java @@ -69,6 +69,7 @@ public final class Messages { public static final String CLEARED_LOCK_OWNER = "Cleared lock owner {0}"; public static final String OBJECT_STORE_WITH_PROVIDER_0_CREATED = "Object store with provider: {0} created"; public static final String JOB_WITH_ID_WAS_NOT_UPDATED_WITHIN_SECONDS = "Job with ID: {} was not updated within: {} seconds"; + public static final String CLEARING_OLD_ENTRY = "Clearing old entry with id: {0}"; // DEBUG log messages public static final String RECEIVED_UPLOAD_REQUEST = "Received upload request on URI: {}"; diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/bootstrap/BootstrapServlet.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/bootstrap/BootstrapServlet.java index 3f81f2ef5d..5890af2869 100644 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/bootstrap/BootstrapServlet.java +++ b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/bootstrap/BootstrapServlet.java @@ -1,18 +1,17 @@ package org.cloudfoundry.multiapps.controller.web.bootstrap; -import static java.text.MessageFormat.format; - import java.text.MessageFormat; +import javax.naming.NamingException; +import javax.sql.DataSource; import jakarta.inject.Inject; import jakarta.inject.Named; -import javax.naming.NamingException; import jakarta.servlet.ServletConfig; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; -import javax.sql.DataSource; - import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.services.ApplicationShutdownService; import org.cloudfoundry.multiapps.controller.persistence.services.FileService; import org.cloudfoundry.multiapps.controller.persistence.services.LockOwnerService; import org.cloudfoundry.multiapps.controller.process.util.LockOwnerReleaser; @@ -23,6 +22,8 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.web.context.support.SpringBeanAutowiringSupport; +import static java.text.MessageFormat.format; + public class BootstrapServlet extends HttpServlet { private static final long serialVersionUID = -1740423033397429145L; @@ -49,6 +50,9 @@ public class BootstrapServlet extends HttpServlet { @Inject protected LockOwnerReleaser lockOwnerReleaser; + @Inject + protected ApplicationShutdownService applicationShutdownService; + @Override public void init(ServletConfig config) throws ServletException { super.init(config); @@ -57,6 +61,7 @@ public void init(ServletConfig config) throws ServletException { initializeApplicationConfiguration(); initializeFileService(); initExtras(); + deleteOldScheduledApplication(); processEngine.getProcessEngineConfiguration() .getAsyncExecutor() .start(); @@ -98,6 +103,15 @@ protected void initExtras() throws NamingException { // Do nothing } + protected void deleteOldScheduledApplication() { + int applicationIndex = configuration.getApplicationInstanceIndex(); + ApplicationShutdown applicationShutdown = getApplicationShutdownByApplicationIndex(applicationIndex); + if (applicationShutdown != null) { + LOGGER.info(MessageFormat.format(Messages.CLEARING_OLD_ENTRY, applicationIndex)); + deleteApplicationShutdownsByIndex(applicationIndex); + } + } + private void clearLockOwner() { var lockOwner = processEngine.getProcessEngineConfiguration() .getAsyncExecutor() @@ -118,4 +132,16 @@ protected void destroyExtras() { // Do nothing } + private void deleteApplicationShutdownsByIndex(int applicationInstanceIndex) { + applicationShutdownService.createQuery() + .applicationInstanceIndex(applicationInstanceIndex) + .delete(); + } + + private ApplicationShutdown getApplicationShutdownByApplicationIndex(int applicationInstanceIndex) { + return applicationShutdownService.createQuery() + .applicationInstanceIndex(applicationInstanceIndex) + .singleResult(); + } + } diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/DatabaseConfiguration.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/DatabaseConfiguration.java index eb0beeb4df..6bdd42d975 100644 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/DatabaseConfiguration.java +++ b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/configuration/DatabaseConfiguration.java @@ -1,8 +1,9 @@ package org.cloudfoundry.multiapps.controller.web.configuration; -import jakarta.inject.Inject; import javax.sql.DataSource; +import jakarta.inject.Inject; +import liquibase.integration.spring.SpringLiquibase; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.persistence.DataSourceWithDialect; import org.cloudfoundry.multiapps.controller.persistence.dialects.DataSourceDialect; @@ -18,12 +19,10 @@ import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter; -import liquibase.integration.spring.SpringLiquibase; - @Configuration public class DatabaseConfiguration { - private static final String DATA_SOURCE_SERVICE_NAME = "deploy-service-database"; + public static final String DATA_SOURCE_SERVICE_NAME = "deploy-service-database"; private static final String LIQUIBASE_CHANGELOG = "classpath:/org/cloudfoundry/multiapps/controller/persistence/db/changelog/db-changelog.xml"; private static final String ENTITY_MANAGER_DEFAULT_PERSISTENCE_UNIT_NAME = "Default"; @@ -62,8 +61,8 @@ public LocalContainerEntityManagerFactoryBean defaultEntityManagerFactory(DataSo } protected LocalContainerEntityManagerFactoryBean - getLocalContainerEntityManagerFactoryBean(DataSource dataSource, EclipseLinkJpaVendorAdapter eclipseLinkJpaVendorAdapter, - String persistenceUnitName) { + getLocalContainerEntityManagerFactoryBean(DataSource dataSource, EclipseLinkJpaVendorAdapter eclipseLinkJpaVendorAdapter, + String persistenceUnitName) { LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); localContainerEntityManagerFactoryBean.setPersistenceUnitName(persistenceUnitName); localContainerEntityManagerFactoryBean.setDataSource(dataSource); diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ApplicationShutdownResource.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ApplicationShutdownResource.java deleted file mode 100644 index a69b9c5a24..0000000000 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/resources/ApplicationShutdownResource.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.cloudfoundry.multiapps.controller.web.resources; - -import java.text.MessageFormat; -import java.util.concurrent.CompletableFuture; - -import jakarta.inject.Inject; -import org.cloudfoundry.multiapps.controller.core.Messages; -import org.cloudfoundry.multiapps.controller.core.model.ApplicationShutdown; -import org.cloudfoundry.multiapps.controller.core.model.ImmutableApplicationShutdown; -import org.cloudfoundry.multiapps.controller.process.flowable.FlowableFacade; -import org.cloudfoundry.multiapps.controller.web.Constants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping(value = Constants.Resources.APPLICATION_SHUTDOWN) -public class ApplicationShutdownResource { - - private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationShutdownResource.class); - - @Inject - private FlowableFacade flowableFacade; - - @PostMapping(produces = { MediaType.APPLICATION_JSON_VALUE }) - public ApplicationShutdown - shutdownFlowableJobExecutor(@RequestHeader(name = "x-cf-applicationid", required = false) String applicationId, - @RequestHeader(name = "x-cf-instanceid", required = false) String applicationInstanceId, - @RequestHeader(name = "x-cf-instanceindex", required = false) String applicationInstanceIndex) { - - CompletableFuture.runAsync(() -> { - LOGGER.info(MessageFormat.format(Messages.APP_SHUTDOWN_REQUEST, applicationId, applicationInstanceId, - applicationInstanceIndex)); - flowableFacade.shutdownJobExecutor(); - }) - .thenRun(() -> LOGGER.info( - MessageFormat.format(Messages.APP_SUCCESSFULLY_SHUTDOWN, applicationId, applicationInstanceId, - applicationInstanceIndex))); - - return ImmutableApplicationShutdown.builder() - .status(getShutdownStatus()) - .applicationInstanceIndex(Integer.parseInt(applicationInstanceIndex)) - .applicationId(applicationId) - .applicationInstanceId(applicationInstanceId) - .build(); - } - - private ApplicationShutdown.Status getShutdownStatus() { - return flowableFacade.isJobExecutorActive() ? ApplicationShutdown.Status.RUNNING : ApplicationShutdown.Status.FINISHED; - } - - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) - public ApplicationShutdown - getFlowableJobExecutorShutdownStatus(@RequestHeader(name = "x-cf-applicationid", required = false) String applicationId, - @RequestHeader(name = "x-cf-instanceid", required = false) String applicationInstanceId, - @RequestHeader(name = "x-cf-instanceindex", required = false) String applicationInstanceIndex) { - - ApplicationShutdown applicationShutdown = ImmutableApplicationShutdown.builder() - .status(getShutdownStatus()) - .applicationInstanceIndex( - Integer.parseInt(applicationInstanceIndex)) - .applicationId(applicationId) - .applicationInstanceId(applicationInstanceId) - .build(); - - LOGGER.info(MessageFormat.format(Messages.APP_SHUTDOWN_STATUS_MONITOR, applicationId, applicationInstanceId, - applicationInstanceIndex, applicationShutdown.getStatus())); - - return applicationShutdown; - } - -} diff --git a/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/bootstrap/BootstrapServletTest.java b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/bootstrap/BootstrapServletTest.java new file mode 100644 index 0000000000..9c3141241a --- /dev/null +++ b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/bootstrap/BootstrapServletTest.java @@ -0,0 +1,84 @@ +package org.cloudfoundry.multiapps.controller.web.bootstrap; + +import java.time.Instant; +import java.util.Date; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.persistence.dto.ApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableApplicationShutdown; +import org.cloudfoundry.multiapps.controller.persistence.query.ApplicationShutdownQuery; +import org.cloudfoundry.multiapps.controller.persistence.services.ApplicationShutdownService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class BootstrapServletTest { + + @Mock + private ApplicationConfiguration applicationConfiguration; + + @Mock + private ApplicationShutdownService applicationShutdownService; + + @Mock + private ApplicationShutdownQuery applicationShutdownQuery; + + @InjectMocks + private BootstrapServlet bootstrapServlet; + private final int APPLICATION_INSTANCE_INDEX = 0; + private final String APPLICATION_ID = UUID.randomUUID() + .toString(); + private final String INSTANCE_ID = UUID.randomUUID() + .toString(); + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + + when(applicationConfiguration.getApplicationInstanceIndex()).thenReturn(APPLICATION_INSTANCE_INDEX); + when(applicationShutdownService.createQuery()).thenReturn(applicationShutdownQuery); + when(applicationShutdownQuery.applicationId(anyString())).thenReturn(applicationShutdownQuery); + when(applicationShutdownQuery.applicationInstanceIndex(anyInt())).thenReturn(applicationShutdownQuery); + } + + @Test + void testDeleteScheduledApplicationWithExistingApplication() { + ApplicationShutdown applicationShutdown = createApplicationShutdownInstance(); + when(applicationShutdownQuery.singleResult()).thenReturn(applicationShutdown); + + bootstrapServlet.deleteOldScheduledApplication(); + verify(applicationShutdownService, times(2)).createQuery(); + verify(applicationShutdownQuery, times(2)).applicationInstanceIndex(anyInt()); + verify(applicationShutdownQuery).delete(); + verify(applicationShutdownQuery).singleResult(); + } + + @Test + void testDeleteScheduledApplicationWithoutApplication() { + bootstrapServlet.deleteOldScheduledApplication(); + verify(applicationShutdownService, times(1)).createQuery(); + verify(applicationShutdownQuery, times(1)).applicationInstanceIndex(anyInt()); + verify(applicationShutdownQuery, times(0)).delete(); + verify(applicationShutdownQuery, times(1)).singleResult(); + } + + private ApplicationShutdown createApplicationShutdownInstance() { + return ImmutableApplicationShutdown.builder() + .id(INSTANCE_ID) + .applicationId(APPLICATION_ID) + .applicationInstanceIndex(APPLICATION_INSTANCE_INDEX) + .startedAt(Date.from(Instant.now())) + .status(ApplicationShutdown.Status.INITIAL) + .build(); + } +}