Clean backend reset flow
This commit is contained in:
@@ -9,10 +9,23 @@ import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Function;
|
||||
|
||||
final class DockerComposeSupport {
|
||||
|
||||
private final Path projectDir = Paths.get("").toAbsolutePath();
|
||||
private static final int DATABASE_WAIT_ATTEMPTS = 30;
|
||||
|
||||
private final Path projectDir;
|
||||
private final Function<List<String>, CommandResult> commandRunner;
|
||||
|
||||
DockerComposeSupport() {
|
||||
this(Paths.get("").toAbsolutePath(), null);
|
||||
}
|
||||
|
||||
DockerComposeSupport(Path projectDir, Function<List<String>, CommandResult> commandRunner) {
|
||||
this.projectDir = projectDir;
|
||||
this.commandRunner = commandRunner != null ? commandRunner : command -> runCommand(command, true);
|
||||
}
|
||||
|
||||
void ensureDockerAvailable() {
|
||||
CommandResult versionResult = runCommand(List.of(resolveDockerExecutable(), "version"), false);
|
||||
@@ -22,7 +35,8 @@ final class DockerComposeSupport {
|
||||
}
|
||||
|
||||
void startDatabase() {
|
||||
runCommand(composeCommand("up", "-d", "--wait", "db"));
|
||||
runCommand(composeCommand("up", "-d", "db"));
|
||||
waitForDatabaseHealth();
|
||||
}
|
||||
|
||||
void stopDatabase() {
|
||||
@@ -31,6 +45,8 @@ final class DockerComposeSupport {
|
||||
|
||||
void resetDatabase() {
|
||||
runCommand(composeCommand("down", "-v", "--remove-orphans"));
|
||||
removeContainerIfPresent("petshop-api");
|
||||
removeContainerIfPresent("petshop-db");
|
||||
String volumeName = getDatabaseVolumeName();
|
||||
if (volumeExists(volumeName)) {
|
||||
runCommand(List.of(resolveDockerExecutable(), "volume", "rm", "-f", volumeName));
|
||||
@@ -48,7 +64,7 @@ final class DockerComposeSupport {
|
||||
}
|
||||
|
||||
private boolean volumeExists(String volumeName) {
|
||||
CommandResult result = runCommand(List.of(resolveDockerExecutable(), "volume", "ls", "--format", "{{.Name}}"), false);
|
||||
CommandResult result = commandRunner.apply(List.of(resolveDockerExecutable(), "volume", "ls", "--format", "{{.Name}}"));
|
||||
if (result.exitCode != 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -61,6 +77,29 @@ final class DockerComposeSupport {
|
||||
return projectDir.getFileName().toString() + "_db_data";
|
||||
}
|
||||
|
||||
private void waitForDatabaseHealth() {
|
||||
for (int attempt = 0; attempt < DATABASE_WAIT_ATTEMPTS; attempt++) {
|
||||
if (isContainerHealthy("petshop-db")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Database startup interrupted while waiting for MySQL.", ex);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Database startup failed because the petshop-db container did not become healthy in time.");
|
||||
}
|
||||
|
||||
private boolean isContainerHealthy(String containerName) {
|
||||
CommandResult result = commandRunner.apply(List.of(resolveDockerExecutable(), "inspect", "--format", "{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}", containerName));
|
||||
if (result.exitCode != 0 || result.output == null) {
|
||||
return false;
|
||||
}
|
||||
return "healthy".equalsIgnoreCase(result.output.trim());
|
||||
}
|
||||
|
||||
private List<String> composeCommand(String... args) {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(resolveDockerExecutable());
|
||||
@@ -90,12 +129,22 @@ final class DockerComposeSupport {
|
||||
}
|
||||
|
||||
private void runCommand(List<String> command) {
|
||||
CommandResult result = runCommand(command, true);
|
||||
CommandResult result = commandRunner.apply(command);
|
||||
if (result.exitCode != 0) {
|
||||
throw new IllegalStateException(describeCommandFailure(command, result.output));
|
||||
}
|
||||
}
|
||||
|
||||
private void removeContainerIfPresent(String containerName) {
|
||||
CommandResult result = commandRunner.apply(List.of(resolveDockerExecutable(), "rm", "-f", containerName));
|
||||
if (result.exitCode != 0) {
|
||||
String output = result.output == null ? "" : result.output.toLowerCase(Locale.ROOT);
|
||||
if (!output.contains("no such container")) {
|
||||
throw new IllegalStateException(describeCommandFailure(List.of(resolveDockerExecutable(), "rm", "-f", containerName), result.output));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CommandResult runCommand(List<String> command, boolean printOutput) {
|
||||
ProcessBuilder builder = new ProcessBuilder(command);
|
||||
builder.directory(projectDir.toFile());
|
||||
@@ -144,7 +193,7 @@ final class DockerComposeSupport {
|
||||
if (output != null) {
|
||||
lowerOutput = output.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
if (renderedCommand.contains(" up ") || renderedCommand.endsWith(" up -d --wait db")) {
|
||||
if (renderedCommand.contains(" up ") || renderedCommand.endsWith(" up -d db")) {
|
||||
if (lowerOutput.contains("port is already allocated") || lowerOutput.contains("address already in use")) {
|
||||
return "Database startup failed because port 3306 is already in use. Stop the conflicting MySQL service or container, then run Reset Database.";
|
||||
}
|
||||
@@ -168,6 +217,7 @@ final class DockerComposeSupport {
|
||||
return output;
|
||||
}
|
||||
|
||||
private record CommandResult(int exitCode, String output) {
|
||||
record CommandResult(int exitCode, String output) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.petshop.backend;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.function.LongConsumer;
|
||||
import java.util.function.LongFunction;
|
||||
|
||||
final class PortCleanupSupport {
|
||||
|
||||
private final IntFunction<Optional<Long>> pidLookup;
|
||||
private final LongFunction<Optional<String>> commandLineLookup;
|
||||
private final LongConsumer terminator;
|
||||
|
||||
PortCleanupSupport() {
|
||||
this(PortCleanupSupport::findListeningPid, PortCleanupSupport::findCommandLine, PortCleanupSupport::terminateProcess);
|
||||
}
|
||||
|
||||
PortCleanupSupport(IntFunction<Optional<Long>> pidLookup, LongFunction<Optional<String>> commandLineLookup, LongConsumer terminator) {
|
||||
this.pidLookup = pidLookup;
|
||||
this.commandLineLookup = commandLineLookup;
|
||||
this.terminator = terminator;
|
||||
}
|
||||
|
||||
boolean cleanupIdentifiableProcessOnPort(int port) {
|
||||
Optional<Long> pid = pidLookup.apply(port);
|
||||
if (pid.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Optional<String> commandLine = commandLineLookup.apply(pid.get());
|
||||
if (commandLine.isEmpty() || !isIdentifiableBackendCommand(commandLine.get())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
terminator.accept(pid.get());
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isIdentifiableBackendCommand(String commandLine) {
|
||||
String normalized = commandLine.toLowerCase(Locale.ROOT);
|
||||
return normalized.contains("com.petshop.backend.backendapplication")
|
||||
|| normalized.contains("com.petshop.backend.devstackapplication")
|
||||
|| normalized.contains("exec:java@dev-stack")
|
||||
|| normalized.contains("group-2-threaded-project-petshop/backend")
|
||||
|| normalized.contains("/backend/target/classes")
|
||||
|| normalized.contains("com.petshop:backend");
|
||||
}
|
||||
|
||||
private static Optional<Long> findListeningPid(int port) {
|
||||
ProcessBuilder builder = new ProcessBuilder("sh", "-lc", "lsof -tiTCP:" + port + " -sTCP:LISTEN | head -n 1");
|
||||
builder.redirectErrorStream(true);
|
||||
try {
|
||||
Process process = builder.start();
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
||||
String line = reader.readLine();
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0 || line == null || line.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(Long.parseLong(line.trim()));
|
||||
}
|
||||
} catch (IOException | InterruptedException | NumberFormatException ex) {
|
||||
if (ex instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<String> findCommandLine(long pid) {
|
||||
return ProcessHandle.of(pid)
|
||||
.flatMap(handle -> handle.info().commandLine());
|
||||
}
|
||||
|
||||
private static void terminateProcess(long pid) {
|
||||
ProcessHandle.of(pid).ifPresent(handle -> {
|
||||
handle.destroy();
|
||||
try {
|
||||
handle.onExit().get();
|
||||
} catch (Exception ex) {
|
||||
handle.destroyForcibly();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ public class ResetDatabaseApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
DockerComposeSupport docker = new DockerComposeSupport();
|
||||
new PortCleanupSupport().cleanupIdentifiableProcessOnPort(8080);
|
||||
docker.ensureDockerAvailable();
|
||||
docker.resetDatabase();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user