From f8dccaae2b9a3cc02fa1fd5be11be402d7912a59 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 23 Apr 2026 01:01:00 +0200 Subject: [PATCH] fix(deploy): stop previous active deployment before START_REPLICAS (fixes 409) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container names are deterministic: {tenant}-{envSlug}-{appSlug}-{replica}. The prior code did the stop-existing step at SWAP_TRAFFIC, *after* START_REPLICAS had already tried to create containers with the same names — so a redeploy against a RUNNING app consistently failed with Docker 409 "container name already in use". Move the stop-existing block to run right after CREATE_NETWORK and before START_REPLICAS. SWAP_TRAFFIC becomes a label-only marker (traffic is swapped implicitly by Traefik labels once new replicas are healthy). Also: add `findActiveByAppIdAndEnvironmentIdExcluding` so the SQL excludes the current deployment by id — previously the Java-side `!id.equals(me)` guard failed because the newly-inserted row has status=STARTING (DB default) and ORDER BY created_at DESC LIMIT 1 picked the new row, hiding the actual previous deployment. Trade-off: this is destroy-then-start rather than true blue/green — brief downtime during the swap. Matches the pre-unified-page behavior and is what users reasonably expect. True blue/green would require per-deployment container names. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/runtime/DeploymentExecutor.java | 27 +++++++++++++------ .../storage/PostgresDeploymentRepository.java | 10 +++++++ .../core/runtime/DeploymentRepository.java | 1 + 3 files changed, 30 insertions(+), 8 deletions(-) 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);