Setup lifecycle manager

This commit is contained in:
Andrei Isvoran 2024-07-03 13:10:12 +03:00
parent b77233c89c
commit 6d45d9943c
15 changed files with 552 additions and 0 deletions

41
.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
gradle.properties
gradlew
gradlew.bat
gradle/
**/.gradle
**/build

21
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,21 @@
include:
- project: 'gitlab/gitlab'
ref: 'main'
file: 'ci-templates/gradle_java.yml'
deploy:
stage: deploy
tags:
- dind
script:
- echo "Building with gradle version ${BUILDVERSION}"
- gradle -Pversion=${BUILDVERSION} publish
- echo "BUILDVERSION=$BUILDVERSION" >> version.env
artifacts:
reports:
dotenv: version.env
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH =~ /^release/
- if: $CI_COMMIT_TAG

116
build.gradle.kts Normal file
View File

@ -0,0 +1,116 @@
plugins {
`java-library`
`maven-publish`
`kotlin-dsl`
pmd
checkstyle
jacoco
id("io.freefair.lombok") version "8.4"
id("org.sonarqube") version "4.4.1.3373"
}
val springBootVersion = "3.1.4"
val springCloudVersion = "4.0.4"
val lombokVersion = "1.18.30"
configurations {
all {
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
exclude(group = "ch.qos.logback", module = "logback-classic")
exclude(group = "org.apache.logging.log4j", module = "log4j-to-slf4j")
}
}
dependencies {
api("org.springframework.boot:spring-boot-starter-amqp:${springBootVersion}")
api("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
api("org.springframework.cloud:spring-cloud-starter-openfeign:${springCloudVersion}")
api("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
api("org.projectlombok:lombok:${lombokVersion}")
runtimeOnly("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootVersion}")
}
group = "com.knecon.fforesight"
description = "lifecycle-commons"
java.sourceCompatibility = JavaVersion.VERSION_17
java.targetCompatibility = JavaVersion.VERSION_17
repositories {
mavenLocal()
maven {
url = uri("https://nexus.knecon.com/repository/gindev/");
credentials {
username = providers.gradleProperty("mavenUser").getOrNull();
password = providers.gradleProperty("mavenPassword").getOrNull();
}
}
mavenCentral()
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
}
}
repositories {
maven {
url = uri("https://nexus.knecon.com/repository/red-platform-releases/")
credentials {
username = providers.gradleProperty("mavenUser").getOrNull();
password = providers.gradleProperty("mavenPassword").getOrNull();
}
}
}
}
tasks.withType<PublishToMavenRepository> {
onlyIf { publication.name == "mavenJava" }
}
pmd {
isConsoleOutput = true
}
tasks.pmdMain {
pmd.ruleSetFiles = files("${rootDir}/config/pmd/pmd.xml")
}
tasks.pmdTest {
pmd.ruleSetFiles = files("${rootDir}/config/pmd/test_pmd.xml")
}
tasks.named<Test>("test") {
useJUnitPlatform()
reports {
junitXml.outputLocation.set(layout.buildDirectory.dir("reports/junit"))
}
}
sonarqube {
properties {
property("sonar.login", providers.gradleProperty("sonarToken").getOrNull())
property("sonar.host.url", "https://sonarqube.knecon.com")
}
}
tasks.test {
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
csv.required.set(false)
html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml"))
}
}
java {
withJavadocJar()
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<module name="Checker">
<property
name="severity"
value="error"/>
<module name="TreeWalker">
<module name="SuppressWarningsHolder"/>
<module name="MissingDeprecated"/>
<module name="MissingOverride"/>
<module name="AnnotationLocation"/>
<module name="JavadocStyle"/>
<module name="NonEmptyAtclauseDescription"/>
<module name="IllegalImport"/>
<module name="RedundantImport"/>
<module name="RedundantModifier"/>
<module name="EmptyBlock"/>
<module name="DefaultComesLast"/>
<module name="EmptyStatement"/>
<module name="EqualsHashCode"/>
<module name="ExplicitInitialization"/>
<module name="IllegalInstantiation"/>
<module name="ModifiedControlVariable"/>
<module name="MultipleVariableDeclarations"/>
<module name="PackageDeclaration"/>
<module name="ParameterAssignment"/>
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
<module name="StringLiteralEquality"/>
<module name="OneStatementPerLine"/>
<module name="FinalClass"/>
<module name="ArrayTypeStyle"/>
<module name="UpperEll"/>
<module name="OuterTypeFilename"/>
</module>
<module name="FileTabCharacter"/>
<module name="SuppressWarningsFilter"/>
</module>

20
config/pmd/pmd.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0"?>
<ruleset name="Custom ruleset"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
<description>
Knecon ruleset checks the code for bad stuff
</description>
<rule ref="category/java/errorprone.xml">
<exclude name="MissingSerialVersionUID"/>
<exclude name="AvoidLiteralsInIfCondition"/>
<exclude name="AvoidDuplicateLiterals"/>
<exclude name="NullAssignment"/>
<exclude name="AssignmentInOperand"/>
<exclude name="BeanMembersShouldSerialize"/>
</rule>
</ruleset>

22
config/pmd/test_pmd.xml Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0"?>
<ruleset name="Custom ruleset"
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
<description>
Knecon test ruleset checks the code for bad stuff
</description>
<rule ref="category/java/errorprone.xml">
<exclude name="MissingSerialVersionUID"/>
<exclude name="AvoidLiteralsInIfCondition"/>
<exclude name="AvoidDuplicateLiterals"/>
<exclude name="NullAssignment"/>
<exclude name="AssignmentInOperand"/>
<exclude name="TestClassWithoutTestCases"/>
<exclude name="BeanMembersShouldSerialize"/>
</rule>
</ruleset>

1
gradle.properties.kts Normal file
View File

@ -0,0 +1 @@
version = 0.1-SNAPSHOT

5
settings.gradle.kts Normal file
View File

@ -0,0 +1,5 @@
/*
* This file was generated by the Gradle 'init' task.
*/
rootProject.name = "tenant-commons"

View File

@ -0,0 +1,59 @@
package com.knecon.fforesight.lifecyclecommons;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.amqp.AmqpRejectAndDontRequeueException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class LifecycleAspect {
private final LifecycleManager lifecycleManager;
@Value("${aspect.base-package}")
private String basePackage;
@Around("@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener) || "
+ "@annotation(org.springframework.web.bind.annotation.GetMapping) || "
+ "@annotation(org.springframework.web.bind.annotation.PostMapping) || "
+ "@annotation(org.springframework.web.bind.annotation.PutMapping) || "
+ "@annotation(org.springframework.web.bind.annotation.DeleteMapping) || "
+ "@annotation(org.springframework.web.bind.annotation.RequestMapping))")
public Object checkLifecycle(ProceedingJoinPoint joinPoint) throws Throwable {
String targetClassName = joinPoint.getTarget().getClass().getPackageName();
if (!targetClassName.startsWith(basePackage)) {
return joinPoint.proceed();
}
synchronized (lifecycleManager) {
if (!lifecycleManager.isRunning()) {
log.info("Application is shutting down, rejecting new messages.");
throw new AmqpRejectAndDontRequeueException("Application is shutting down, rejecting new messages.");
}
lifecycleManager.incrementAndGet();
}
try {
return joinPoint.proceed();
} finally {
int remainingTasks = lifecycleManager.decrementAndGet();
synchronized (lifecycleManager) {
if (remainingTasks == 0 && !lifecycleManager.isRunning()) {
lifecycleManager.countDown();
log.info("All tasks are done, ready for shutdown.");
}
}
}
}
}

View File

@ -0,0 +1,106 @@
package com.knecon.fforesight.lifecyclecommons;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.context.ApplicationListener;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@RequiredArgsConstructor
public class LifecycleManager implements SmartLifecycle, ApplicationListener<ContextClosedEvent> {
private volatile boolean running = false;
private volatile boolean shutdownInitiated = false;
private final Object shutdownMonitor = new Object();
private final AtomicInteger activeTasks = new AtomicInteger(0);
private final CountDownLatch latch = new CountDownLatch(1);
public void countDown() {
latch.countDown();
}
public int incrementAndGet() {
return activeTasks.incrementAndGet();
}
public int decrementAndGet() {
return activeTasks.decrementAndGet();
}
@Override
public void start() {
synchronized (shutdownMonitor) {
running = true;
}
}
@Override
public void stop() {
synchronized (shutdownMonitor) {
running = false;
if (activeTasks.get() == 0) {
latch.countDown(); // No active tasks, release the latch immediately
}
}
}
@Override
public boolean isRunning() {
return running;
}
@Override
public boolean isAutoStartup() {
return true;
}
@Override
public int getPhase() {
return Integer.MAX_VALUE; // Start this component last and stop it first
}
@Override
public void onApplicationEvent(ContextClosedEvent event) {
synchronized (shutdownMonitor) {
if (shutdownInitiated) {
return; // Avoid multiple shutdown initiations
}
shutdownInitiated = true;
}
stop();
log.info("Context is closing, waiting for ongoing tasks to complete.");
try {
latch.await(); // Wait for the latch to count down to zero
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Shutdown was interrupted", e);
}
}
}

View File

@ -0,0 +1,2 @@
aspect:
base-package: com.knecon

View File

@ -0,0 +1,73 @@
package com.knecon.fforesight.lifecyclecommons.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.amqp.AmqpRejectAndDontRequeueException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.knecon.fforesight.lifecyclecommons.LifecycleManager;
import com.knecon.fforesight.lifecyclecommons.config.utils.AspectTestConfig;
import com.knecon.fforesight.lifecyclecommons.config.utils.TestController;
import lombok.SneakyThrows;
@ExtendWith({SpringExtension.class, MockitoExtension.class})
@SpringBootTest(classes = {AspectTestConfig.class, TestController.class})
@Import(AspectTestConfig.class)
public class LifecycleAspectTest {
@Autowired
private TestController testController;
@Autowired
private LifecycleManager lifecycleManager;
@BeforeEach
public void setup() {
lifecycleManager.start();
}
@Test
@SneakyThrows
public void testHttpGetAspectWhenRunning() {
String response = testController.testGet();
assertEquals("Test Get", response);
}
@Test
@SneakyThrows
public void testStopLifecycleWhenRunning() {
String response = testController.testGet();
lifecycleManager.stop();
assertEquals("Test Get", response);
}
@Test
public void testHttpGetAspectWhenNotRunning() {
lifecycleManager.stop();
Exception exception = assertThrows(AmqpRejectAndDontRequeueException.class, () -> {
testController.testGet();
});
assertEquals("Application is shutting down, rejecting new messages.", exception.getMessage());
}
}

View File

@ -0,0 +1,28 @@
package com.knecon.fforesight.lifecyclecommons.config.utils;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import com.knecon.fforesight.lifecyclecommons.LifecycleAspect;
import com.knecon.fforesight.lifecyclecommons.LifecycleManager;
@TestConfiguration
@EnableAspectJAutoProxy
public class AspectTestConfig {
@Bean
public LifecycleManager lifecycleManager() {
return new LifecycleManager();
}
@Bean
public LifecycleAspect lifecycleAspect(LifecycleManager lifecycleManager) {
return new LifecycleAspect(lifecycleManager);
}
}

View File

@ -0,0 +1,17 @@
package com.knecon.fforesight.lifecyclecommons.config.utils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/test-get")
public String testGet() throws InterruptedException {
Thread.sleep(5000);
return "Test Get";
}
}

View File

@ -0,0 +1,2 @@
aspect:
base-package: com.knecon