Clean backend reset flow
This commit is contained in:
2
android/.gitignore
vendored
2
android/.gitignore
vendored
@@ -13,6 +13,8 @@
|
|||||||
/.idea/caches/
|
/.idea/caches/
|
||||||
/.idea/deploymentTargetSelector.xml
|
/.idea/deploymentTargetSelector.xml
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
|
/app/src/androidTest/
|
||||||
|
/app/src/test/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
|
|||||||
4
android/app/.gitignore
vendored
4
android/app/.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
/build
|
/build
|
||||||
|
/src/test/
|
||||||
|
/src/androidTest/
|
||||||
|
|||||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -41,6 +41,7 @@ build/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
### Project Specific ###
|
### Project Specific ###
|
||||||
|
src/test/
|
||||||
tmp/
|
tmp/
|
||||||
uploads/
|
uploads/
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- db_data:/var/lib/mysql
|
- db_data:/var/lib/mysql
|
||||||
healthcheck:
|
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
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 30
|
retries: 30
|
||||||
|
|||||||
@@ -9,10 +9,23 @@ import java.nio.file.Paths;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
final class DockerComposeSupport {
|
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() {
|
void ensureDockerAvailable() {
|
||||||
CommandResult versionResult = runCommand(List.of(resolveDockerExecutable(), "version"), false);
|
CommandResult versionResult = runCommand(List.of(resolveDockerExecutable(), "version"), false);
|
||||||
@@ -22,7 +35,8 @@ final class DockerComposeSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void startDatabase() {
|
void startDatabase() {
|
||||||
runCommand(composeCommand("up", "-d", "--wait", "db"));
|
runCommand(composeCommand("up", "-d", "db"));
|
||||||
|
waitForDatabaseHealth();
|
||||||
}
|
}
|
||||||
|
|
||||||
void stopDatabase() {
|
void stopDatabase() {
|
||||||
@@ -31,6 +45,8 @@ final class DockerComposeSupport {
|
|||||||
|
|
||||||
void resetDatabase() {
|
void resetDatabase() {
|
||||||
runCommand(composeCommand("down", "-v", "--remove-orphans"));
|
runCommand(composeCommand("down", "-v", "--remove-orphans"));
|
||||||
|
removeContainerIfPresent("petshop-api");
|
||||||
|
removeContainerIfPresent("petshop-db");
|
||||||
String volumeName = getDatabaseVolumeName();
|
String volumeName = getDatabaseVolumeName();
|
||||||
if (volumeExists(volumeName)) {
|
if (volumeExists(volumeName)) {
|
||||||
runCommand(List.of(resolveDockerExecutable(), "volume", "rm", "-f", volumeName));
|
runCommand(List.of(resolveDockerExecutable(), "volume", "rm", "-f", volumeName));
|
||||||
@@ -48,7 +64,7 @@ final class DockerComposeSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean volumeExists(String volumeName) {
|
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) {
|
if (result.exitCode != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -61,6 +77,29 @@ final class DockerComposeSupport {
|
|||||||
return projectDir.getFileName().toString() + "_db_data";
|
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) {
|
private List<String> composeCommand(String... args) {
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add(resolveDockerExecutable());
|
command.add(resolveDockerExecutable());
|
||||||
@@ -90,12 +129,22 @@ final class DockerComposeSupport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void runCommand(List<String> command) {
|
private void runCommand(List<String> command) {
|
||||||
CommandResult result = runCommand(command, true);
|
CommandResult result = commandRunner.apply(command);
|
||||||
if (result.exitCode != 0) {
|
if (result.exitCode != 0) {
|
||||||
throw new IllegalStateException(describeCommandFailure(command, result.output));
|
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) {
|
private CommandResult runCommand(List<String> command, boolean printOutput) {
|
||||||
ProcessBuilder builder = new ProcessBuilder(command);
|
ProcessBuilder builder = new ProcessBuilder(command);
|
||||||
builder.directory(projectDir.toFile());
|
builder.directory(projectDir.toFile());
|
||||||
@@ -144,7 +193,7 @@ final class DockerComposeSupport {
|
|||||||
if (output != null) {
|
if (output != null) {
|
||||||
lowerOutput = output.toLowerCase(Locale.ROOT);
|
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")) {
|
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.";
|
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;
|
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) {
|
public static void main(String[] args) {
|
||||||
DockerComposeSupport docker = new DockerComposeSupport();
|
DockerComposeSupport docker = new DockerComposeSupport();
|
||||||
|
new PortCleanupSupport().cleanupIdentifiableProcessOnPort(8080);
|
||||||
docker.ensureDockerAvailable();
|
docker.ensureDockerAvailable();
|
||||||
docker.resetDatabase();
|
docker.resetDatabase();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user