diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DeploymentExecutor.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DeploymentExecutor.java index 7ddfd02a..a6adfbc9 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DeploymentExecutor.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DeploymentExecutor.java @@ -167,6 +167,21 @@ public class DeploymentExecutor { } } + // === STOP PREVIOUS ACTIVE DEPLOYMENT === + // Container names are deterministic ({tenant}-{env}-{app}-{replica}), so a + // previous active deployment holds the Docker names we need. Stop + remove + // it before starting new replicas to avoid a 409 name conflict. Excluding + // the current deployment id by SQL (not Java) because the newly created + // row already has status=STARTING and would otherwise be picked by + // findActiveByAppIdAndEnvironmentId ORDER BY created_at DESC LIMIT 1. + Optional previous = deploymentRepository.findActiveByAppIdAndEnvironmentIdExcluding( + deployment.appId(), deployment.environmentId(), deployment.id()); + if (previous.isPresent()) { + log.info("Stopping previous deployment {} before starting new replicas", previous.get().id()); + stopDeploymentContainers(previous.get()); + deploymentService.markStopped(previous.get().id()); + } + // === START REPLICAS === updateStage(deployment.id(), DeployStage.START_REPLICAS); @@ -244,16 +259,12 @@ public class DeploymentExecutor { pgDeployRepo.updateReplicaStates(deployment.id(), replicaStates); // === SWAP TRAFFIC === + // Traffic is routed via Traefik Docker labels, so the "swap" happens + // implicitly once the new replicas are healthy and the old containers + // are gone. The old deployment was already stopped before START_REPLICAS + // to free the deterministic container names. updateStage(deployment.id(), DeployStage.SWAP_TRAFFIC); - Optional existing = deploymentRepository.findActiveByAppIdAndEnvironmentId( - deployment.appId(), deployment.environmentId()); - if (existing.isPresent() && !existing.get().id().equals(deployment.id())) { - stopDeploymentContainers(existing.get()); - deploymentService.markStopped(existing.get().id()); - log.info("Stopped previous deployment {} for replacement", existing.get().id()); - } - // === COMPLETE === updateStage(deployment.id(), DeployStage.COMPLETE); diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java index 4edddeaf..51924e87 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresDeploymentRepository.java @@ -63,6 +63,16 @@ public class PostgresDeploymentRepository implements DeploymentRepository { return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); } + @Override + public Optional findActiveByAppIdAndEnvironmentIdExcluding(UUID appId, UUID environmentId, UUID excludeDeploymentId) { + var results = jdbc.query( + "SELECT " + SELECT_COLS + " FROM deployments WHERE app_id = ? AND environment_id = ? " + + "AND status IN ('STARTING', 'RUNNING', 'DEGRADED') AND id <> ? " + + "ORDER BY created_at DESC LIMIT 1", + (rs, rowNum) -> mapRow(rs), appId, environmentId, excludeDeploymentId); + return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0)); + } + public List findByStatus(List statuses) { String placeholders = String.join(",", statuses.stream().map(s -> "'" + s.name() + "'").toList()); return jdbc.query( diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentRepository.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentRepository.java index 4512bdfa..ccb61695 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentRepository.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/DeploymentRepository.java @@ -9,6 +9,7 @@ public interface DeploymentRepository { List findByEnvironmentId(UUID environmentId); Optional findById(UUID id); Optional findActiveByAppIdAndEnvironmentId(UUID appId, UUID environmentId); + Optional findActiveByAppIdAndEnvironmentIdExcluding(UUID appId, UUID environmentId, UUID excludeDeploymentId); UUID create(UUID appId, UUID appVersionId, UUID environmentId, String containerName); void updateStatus(UUID id, DeploymentStatus status, String containerId, String errorMessage); void markDeployed(UUID id);