Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v1.1.0 #5

Merged
merged 5 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion handler/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
}

group = 'io.jeyong'
version = '1.0.3'
version = '1.1.0'

ext {
artifactName = 'k8s-sigterm-handler'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import sun.misc.SignalHandler;

public interface ApplicationTerminator {
public abstract class ApplicationTerminator {

SignalHandler handleTermination(final int status);
public SignalHandler handleTermination() {
return signal -> System.exit(getExitCode());
}

protected abstract int getExitCode();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@

public class SignalHandlerRegistrar {

public static final String SIGNAL_TYPE = "TERM";
public static final int EXIT_CODE = 0;

private final ApplicationTerminator applicationTerminator;
private final String signalType;

public SignalHandlerRegistrar(final ApplicationTerminator applicationTerminator) {
public SignalHandlerRegistrar(final ApplicationTerminator applicationTerminator, final String signalType) {
this.applicationTerminator = applicationTerminator;
this.signalType = signalType;
}

@PostConstruct
public void registerHandler() {
Signal.handle(new Signal(SIGNAL_TYPE), applicationTerminator.handleTermination(EXIT_CODE));
Signal.handle(new Signal(signalType), applicationTerminator.handleTermination());
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package io.jeyong.handler;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SigtermHandlerConfiguration {

private static final String SIGNAL_TYPE = "TERM";
private static final int EXIT_CODE = 0;

@Bean
public ApplicationTerminator applicationTerminator() {
return new SystemTerminator();
public ApplicationTerminator applicationTerminator(final ApplicationContext applicationContext) {
return new SpringContextTerminator(applicationContext, EXIT_CODE);
}

@Bean
public SignalHandlerRegistrar sigtermHandlerRegister(final ApplicationTerminator applicationTerminator) {
return new SignalHandlerRegistrar(applicationTerminator);
return new SignalHandlerRegistrar(applicationTerminator, SIGNAL_TYPE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.jeyong.handler;

import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;

public class SpringContextTerminator extends ApplicationTerminator {

private final ApplicationContext applicationContext;
private final int exitCode;

public SpringContextTerminator(final ApplicationContext applicationContext, final int exitCode) {
this.applicationContext = applicationContext;
this.exitCode = exitCode;
}

@Override
protected int getExitCode() {
return SpringApplication.exit(applicationContext, () -> exitCode);
}
}
11 changes: 0 additions & 11 deletions handler/src/main/java/io/jeyong/handler/SystemTerminator.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.jeyong.test.cleanup;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CleanupConfiguration {

@Bean
public PreDestroyHandler preDestroyHandler() {
return new PreDestroyHandler();
}

@Bean
public ContextClosedEventHandler contextClosedEventHandler() {
return new ContextClosedEventHandler();
}

@Bean
public ShutdownHookHandler shutdownHookHandler() {
ShutdownHookHandler shutdownHookHandler = new ShutdownHookHandler();
shutdownHookHandler.registerShutdownHook();
return shutdownHookHandler;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.jeyong.test.cleanup;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;

@Slf4j
public class ContextClosedEventHandler {

public static final String CONTEXT_CLOSED_EVENT_LOG = "Handling ContextClosedEvent...";

@EventListener(ContextClosedEvent.class)
public void handleContextClosed() {
log.info(CONTEXT_CLOSED_EVENT_LOG);
}
}
15 changes: 15 additions & 0 deletions test/src/main/java/io/jeyong/test/cleanup/PreDestroyHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.jeyong.test.cleanup;

import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class PreDestroyHandler {

public static final String PRE_DESTROY_LOG = "Executing @PreDestroy cleanup logic...";

@PreDestroy
public void cleanup() {
log.info(PRE_DESTROY_LOG);
}
}
13 changes: 13 additions & 0 deletions test/src/main/java/io/jeyong/test/cleanup/ShutdownHookHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.jeyong.test.cleanup;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ShutdownHookHandler {

public static final String SHUTDOWN_HOOK_LOG = "Executing JVM Shutdown Hook...";

public void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> log.info(SHUTDOWN_HOOK_LOG)));
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,70 @@
package io.jeyong.test.integration;

import static io.jeyong.test.cleanup.ContextClosedEventHandler.CONTEXT_CLOSED_EVENT_LOG;
import static io.jeyong.test.cleanup.PreDestroyHandler.PRE_DESTROY_LOG;
import static io.jeyong.test.cleanup.ShutdownHookHandler.SHUTDOWN_HOOK_LOG;
import static org.assertj.core.api.Assertions.assertThat;
import static org.testcontainers.containers.wait.strategy.Wait.forLogMessage;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.io.FileUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.images.builder.ImageFromDockerfile;

@SpringBootTest
@DisplayName("SigtermHandler Integration Test")
public class SigtermHandlerTest {

private final Logger logger = LoggerFactory.getLogger(SigtermHandlerTest.class);
private static ImageFromDockerfile dockerImage;

@BeforeAll
static void setUp() {
dockerImage = buildImage();
}

@Test
@DisplayName("Container should exit with code 0 on SIGTERM")
void testExitCode() throws Exception {
// given
GenericContainer<?> container = new GenericContainer<>(dockerImage)
.waitingFor(forLogMessage(".*Started TestApplication.*\\n", 1));
container.start();

// when
sendSigtermToContainer(container);

// then
Long exitCode = container.getCurrentContainerInfo().getState().getExitCodeLong();
assertThat(exitCode).isEqualTo(0);
}

@Test
@DisplayName("Application should exit with code 0 on SIGTERM")
void testSigtermHandling() throws Exception {
@DisplayName("Container should exit with cleanup performed on SIGTERM")
void testCleanUp() throws Exception {
// given
Path tempDir = Files.createTempDirectory("docker-context");
copyFile("../gradlew", tempDir.resolve("gradlew"));
copyDirectory("../gradle", tempDir.resolve("gradle"));
copyFile("../settings.gradle", tempDir.resolve("settings.gradle"));
copyDirectory("../handler", tempDir.resolve("handler"));
copyDirectory("../test", tempDir.resolve("test"));

ImageFromDockerfile image = new ImageFromDockerfile()
GenericContainer<?> container = new GenericContainer<>(dockerImage)
.waitingFor(forLogMessage(".*Started TestApplication.*\\n", 1));
container.start();

// when
sendSigtermToContainer(container);

// then
String logs = container.getLogs();
assertThat(logs).contains(PRE_DESTROY_LOG);
assertThat(logs).contains(CONTEXT_CLOSED_EVENT_LOG);
assertThat(logs).contains(SHUTDOWN_HOOK_LOG);
}

private static ImageFromDockerfile buildImage() {
Path tempDir = createTempDirectory();
return new ImageFromDockerfile()
.withFileFromPath(".", tempDir)
.withDockerfileFromBuilder(builder -> {
builder.from("eclipse-temurin:17")
Expand All @@ -47,32 +79,37 @@ void testSigtermHandling() throws Exception {
.cmd("java", "-jar", "/app/test/build/libs/test-0.0.1-SNAPSHOT.jar")
.build();
});
}

// when
try (GenericContainer<?> container = new GenericContainer<>(image)
.withLogConsumer(new Slf4jLogConsumer(logger))) {
container.start();
sendSigtermToContainer(container);

// then
Long exitCode = container.getCurrentContainerInfo().getState().getExitCodeLong();
assertThat(exitCode).isEqualTo(0);
private static Path createTempDirectory() {
try {
Path tempDir = Files.createTempDirectory("docker-context");
copyFile("../gradlew", tempDir.resolve("gradlew"));
copyDirectory("../gradle", tempDir.resolve("gradle"));
copyFile("../settings.gradle", tempDir.resolve("settings.gradle"));
copyDirectory("../handler", tempDir.resolve("handler"));
copyDirectory("../test", tempDir.resolve("test"));
return tempDir;
} catch (Exception e) {
throw new RuntimeException("Failed to create temp directory for Docker context", e);
}
}

private void sendSigtermToContainer(GenericContainer<?> container) {
private static void copyFile(String source, Path destination) throws Exception {
FileUtils.copyFile(new File(source), destination.toFile());
}

private static void copyDirectory(String source, Path destination) throws Exception {
FileUtils.copyDirectory(new File(source), destination.toFile());
}

private static void sendSigtermToContainer(GenericContainer<?> container) throws Exception {
String containerId = container.getContainerId();
container.getDockerClient()
.killContainerCmd(containerId)
.withSignal("SIGTERM")
.exec();
}

private void copyFile(String source, Path destination) throws Exception {
FileUtils.copyFile(new File(source), destination.toFile());
}

private void copyDirectory(String source, Path destination) throws Exception {
FileUtils.copyDirectory(new File(source), destination.toFile());
Thread.sleep(1000);
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
package io.jeyong.test.unit;

import static com.github.stefanbirkner.systemlambda.SystemLambda.catchSystemExit;
import static io.jeyong.handler.SignalHandlerRegistrar.EXIT_CODE;
import static org.assertj.core.api.Assertions.assertThat;

import io.jeyong.handler.SystemTerminator;
import io.jeyong.handler.ApplicationTerminator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import sun.misc.SignalHandler;

@DisplayName("SystemTerminator Unit Test")
public class SystemTerminatorTest {
@DisplayName("ApplicationTerminator Unit Test")
public class ApplicationTerminatorTest {

@Test
@DisplayName("System.exit should be called with the expected status code")
@DisplayName("System.exit should be called with the expected exit code")
void testHandleTerminationCallsSystemExit() throws Exception {
// given
SystemTerminator terminator = new SystemTerminator();
int expectedStatusCode = EXIT_CODE;
SignalHandler handler = terminator.handleTermination(expectedStatusCode);
int expectedExitCode = 0;
ApplicationTerminator terminator = new ApplicationTerminator() {

@Override
protected int getExitCode() {
return expectedExitCode;
}
};
SignalHandler handler = terminator.handleTermination();

// when
int actualExitCode = catchSystemExit(() -> handler.handle(null));

// then
assertThat(actualExitCode).isEqualTo(expectedStatusCode);
assertThat(actualExitCode).isEqualTo(expectedExitCode);
}
}
Loading