From bbc7fa3aa8941dcb107565a76d39fc6cea256f98 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 18 Mar 2026 20:21:55 -0600 Subject: [PATCH] Clean backend reset flow --- android/.gitignore | 2 + android/app/.gitignore | 4 +- backend/.gitignore | 1 + backend/docker-compose.dev.yml | 2 +- .../petshop/backend/DockerComposeSupport.java | 62 +++++++++++-- .../petshop/backend/PortCleanupSupport.java | 89 +++++++++++++++++++ .../backend/ResetDatabaseApplication.java | 1 + 7 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/PortCleanupSupport.java diff --git a/android/.gitignore b/android/.gitignore index 9855261b..f7930e52 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -13,6 +13,8 @@ /.idea/caches/ /.idea/deploymentTargetSelector.xml /.idea/workspace.xml +/app/src/androidTest/ +/app/src/test/ .DS_Store /build /captures diff --git a/android/app/.gitignore b/android/app/.gitignore index 42afabfd..12b09d99 100644 --- a/android/app/.gitignore +++ b/android/app/.gitignore @@ -1 +1,3 @@ -/build \ No newline at end of file +/build +/src/test/ +/src/androidTest/ diff --git a/backend/.gitignore b/backend/.gitignore index 1a527ca6..4ade3c30 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -41,6 +41,7 @@ build/ .DS_Store ### Project Specific ### +src/test/ tmp/ uploads/ diff --git a/backend/docker-compose.dev.yml b/backend/docker-compose.dev.yml index 774e5393..9ea299cf 100644 --- a/backend/docker-compose.dev.yml +++ b/backend/docker-compose.dev.yml @@ -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 diff --git a/backend/src/main/java/com/petshop/backend/DockerComposeSupport.java b/backend/src/main/java/com/petshop/backend/DockerComposeSupport.java index 1ff43a77..4832de9b 100644 --- a/backend/src/main/java/com/petshop/backend/DockerComposeSupport.java +++ b/backend/src/main/java/com/petshop/backend/DockerComposeSupport.java @@ -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, CommandResult> commandRunner; + + DockerComposeSupport() { + this(Paths.get("").toAbsolutePath(), null); + } + + DockerComposeSupport(Path projectDir, Function, 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 composeCommand(String... args) { List command = new ArrayList<>(); command.add(resolveDockerExecutable()); @@ -90,12 +129,22 @@ final class DockerComposeSupport { } private void runCommand(List 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 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) { } + } diff --git a/backend/src/main/java/com/petshop/backend/PortCleanupSupport.java b/backend/src/main/java/com/petshop/backend/PortCleanupSupport.java new file mode 100644 index 00000000..939d5554 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/PortCleanupSupport.java @@ -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> pidLookup; + private final LongFunction> commandLineLookup; + private final LongConsumer terminator; + + PortCleanupSupport() { + this(PortCleanupSupport::findListeningPid, PortCleanupSupport::findCommandLine, PortCleanupSupport::terminateProcess); + } + + PortCleanupSupport(IntFunction> pidLookup, LongFunction> commandLineLookup, LongConsumer terminator) { + this.pidLookup = pidLookup; + this.commandLineLookup = commandLineLookup; + this.terminator = terminator; + } + + boolean cleanupIdentifiableProcessOnPort(int port) { + Optional pid = pidLookup.apply(port); + if (pid.isEmpty()) { + return false; + } + + Optional 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 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 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(); + } + }); + } +} diff --git a/backend/src/main/java/com/petshop/backend/ResetDatabaseApplication.java b/backend/src/main/java/com/petshop/backend/ResetDatabaseApplication.java index 056ec36e..72c43ac7 100644 --- a/backend/src/main/java/com/petshop/backend/ResetDatabaseApplication.java +++ b/backend/src/main/java/com/petshop/backend/ResetDatabaseApplication.java @@ -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(); }