Clean backend reset flow

This commit is contained in:
2026-03-18 20:21:55 -06:00
parent 2b616b8306
commit bbc7fa3aa8
7 changed files with 153 additions and 8 deletions

2
android/.gitignore vendored
View File

@@ -13,6 +13,8 @@
/.idea/caches/
/.idea/deploymentTargetSelector.xml
/.idea/workspace.xml
/app/src/androidTest/
/app/src/test/
.DS_Store
/build
/captures

View File

@@ -1 +1,3 @@
/build
/build
/src/test/
/src/androidTest/

1
backend/.gitignore vendored
View File

@@ -41,6 +41,7 @@ build/
.DS_Store
### Project Specific ###
src/test/
tmp/
uploads/

View File

@@ -13,7 +13,7 @@ services:
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysql", "-uroot", "-proot", "-e", "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='Petstoredb' AND table_name='users';"]
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot"]
interval: 10s
timeout: 5s
retries: 30

View File

@@ -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) {
}
}

View File

@@ -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();
}
});
}
}

View File

@@ -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();
}