From 573fad0df8fc745cf64d8f7c5aa75e73bc59b898 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Sun, 3 Aug 2025 01:11:32 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(RepositoryRunController):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20import?= =?UTF-8?q?=EB=AC=B8=20=EC=82=AD=EC=A0=9C=20DP-200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/controller/RepositoryRunController.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/controller/RepositoryRunController.java b/src/main/java/com/deepdirect/deepwebide_be/repository/controller/RepositoryRunController.java index 7f27f54e..b761ba45 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/controller/RepositoryRunController.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/controller/RepositoryRunController.java @@ -1,8 +1,6 @@ package com.deepdirect.deepwebide_be.repository.controller; import com.deepdirect.deepwebide_be.global.dto.ApiResponseDto; -import com.deepdirect.deepwebide_be.global.exception.ErrorCode; -import com.deepdirect.deepwebide_be.global.exception.GlobalException; import com.deepdirect.deepwebide_be.global.security.CustomUserDetails; import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryExecuteResponse; import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryStatusResponse; @@ -17,7 +15,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.util.Map; From c60592aacd867cc857eebb5c87ab5cd98e9d801a Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Sun, 3 Aug 2025 02:14:50 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(RepositoryRunService):=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=9E=90=EB=8F=99=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20DP-200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/domain/PortRegistry.java | 14 +-- .../repository/PortRegistryRepository.java | 5 + .../RunningContainerRepository.java | 3 + .../service/RepositoryRunService.java | 104 +++++++++++++----- 4 files changed, 89 insertions(+), 37 deletions(-) diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/domain/PortRegistry.java b/src/main/java/com/deepdirect/deepwebide_be/repository/domain/PortRegistry.java index 807b07e3..64815840 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/domain/PortRegistry.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/domain/PortRegistry.java @@ -1,7 +1,9 @@ package com.deepdirect.deepwebide_be.repository.domain; import jakarta.persistence.*; +import lombok.Getter; +@Getter @Entity @Table(name = "port_registry") public class PortRegistry { @@ -17,13 +19,7 @@ public class PortRegistry { @OneToOne private Repository repository; -// public void setStatus(PortStatus status) { -// this.status = status; -// } -// -// public void setRepository(Repository repository) { -// this.repository = repository; -// } + public PortRegistry() {} // JPA 기본 생성자 public void assignToRepository(Repository repository) { this.status = PortStatus.IN_USE; @@ -35,8 +31,4 @@ public void release() { this.repository = null; } - public Integer getPort() { - return this.port; - } } - diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/repository/PortRegistryRepository.java b/src/main/java/com/deepdirect/deepwebide_be/repository/repository/PortRegistryRepository.java index f2b8c769..726373fb 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/repository/PortRegistryRepository.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/repository/PortRegistryRepository.java @@ -5,6 +5,7 @@ import com.deepdirect.deepwebide_be.repository.domain.Repository; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface PortRegistryRepository extends JpaRepository { @@ -13,4 +14,8 @@ public interface PortRegistryRepository extends JpaRepository findByRepository(Repository repository); Optional findFirstByStatus(PortStatus status); + + Optional findByPort(Integer port); + List findAllByStatus(PortStatus status); + } diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/repository/RunningContainerRepository.java b/src/main/java/com/deepdirect/deepwebide_be/repository/repository/RunningContainerRepository.java index 3705099a..ee0d3748 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/repository/RunningContainerRepository.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/repository/RunningContainerRepository.java @@ -29,4 +29,7 @@ public interface RunningContainerRepository extends JpaRepository findAllByStatusAndCreatedAtBefore(String status, LocalDateTime dateTime); + } diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryRunService.java b/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryRunService.java index 268811fe..a70e4d22 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryRunService.java +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/service/RepositoryRunService.java @@ -20,18 +20,18 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpMethod; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.client.RestTemplate; -import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; - import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.LocalDateTime; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -53,33 +53,26 @@ public class RepositoryRunService { @Value("${sandbox.api.base-url}") private String sandboxBaseUrl; + // === 컨테이너 실행 === @Transactional public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userId) { log.info("Starting repository execution - repositoryId: {}, userId: {}", repositoryId, userId); File zipFile = null; try { - // 1. 기존 실행 중인 컨테이너 중지 요청 stopExistingContainer(repositoryId); - // 2. 권한 체크 & 레포지토리 조회 Repository repo = repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); - // 3. framework, port 등 정보 추출 String framework = convertTypeToFramework(repo.getRepositoryType()); - Integer port = allocateOrGetPort(repo); + Integer port = allocateOrGetPort(repo); // 랜덤 할당 - // 4. 새로운 UUID 생성 String uuid = UUID.randomUUID().toString(); - // 5. 파일트리를 zip으로 변환 zipFile = fileTreeToZip(repositoryId, uuid); - - // 6. S3 업로드 String s3Url = uploadToS3(zipFile, uuid); - // 7. 샌드박스 실행 요청 SandboxExecutionRequest request = SandboxExecutionRequest.builder() .uuid(uuid) .url(s3Url) @@ -89,9 +82,11 @@ public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userI SandboxExecutionResponse result = sandboxService.requestExecution(request); - // 8. 실행 중인 컨테이너 정보 저장 saveRunningContainer(repositoryId, uuid, "sandbox-" + uuid, port, framework, s3Url); + // **컨테이너 10분 후 자동 만료 비동기 스케줄** + scheduleAutoStopAndRelease(uuid, port); + log.info("Repository execution completed - repositoryId: {}, uuid: {}, port: {}", repositoryId, uuid, port); return RepositoryExecuteResponse.builder() @@ -117,6 +112,46 @@ public RepositoryExecuteResponse executeRepository(Long repositoryId, Long userI } } + // === 컨테이너 만료 비동기 스케줄 === + @Async + public void scheduleAutoStopAndRelease(String uuid, Integer port) { + try { + Thread.sleep(600_000); // 10분 대기 + + boolean stopped = sandboxService.stopContainer(uuid); + if (stopped) { + RunningContainer container = runningContainerRepository.findByUuid(uuid).orElse(null); + if (container != null) { + container.stop(); + runningContainerRepository.save(container); + + PortRegistry portReg = portRegistryRepository.findByPort(port).orElse(null); + if (portReg != null) { + portReg.release(); + portRegistryRepository.save(portReg); + } + } + log.info("컨테이너 {}가 만료되어 중지 및 포트 {} 반납 완료", uuid, port); + } + } catch (Exception e) { + log.error("컨테이너 만료 자동 중지 실패 - uuid: {}", uuid, e); + } + } + + // === 만료 컨테이너 1분마다 백업성 스케줄 === + @Scheduled(fixedDelay = 60_000) + public void autoCleanupExpiredContainers() { + LocalDateTime now = LocalDateTime.now(); + List expired = runningContainerRepository + .findAllByStatusAndCreatedAtBefore("RUNNING", now.minusMinutes(10)); + for (RunningContainer container : expired) { + log.info("자동정리: 10분 초과 컨테이너 발견 {}", container.getUuid()); + stopRepository(container.getRepositoryId(), null); + } + } + + + /** * 기존 실행 중인 컨테이너 중지 */ @@ -158,8 +193,7 @@ public void saveRunningContainer(Long repositoryId, String uuid, String containe Integer port, String framework, String s3Url) { try { RunningContainer container = RunningContainer.builder() - .repositoryId(repositoryId) - .uuid(uuid) + .repositoryId(repositoryId).uuid(uuid) .containerName(containerName) .port(port) .status("RUNNING") @@ -183,9 +217,16 @@ public void saveRunningContainer(Long repositoryId, String uuid, String containe @Transactional public boolean stopRepository(Long repositoryId, Long userId) { try { - // 권한 체크 - repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) - .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + // userId가 null이 아니면 권한 체크, null이면 skip + if (userId != null) { + repositoryRepository.findByIdAndMemberOrOwner(repositoryId, userId) + .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + } else { + // 그냥 존재하는지 체크 (없으면 그냥 진행) + if (!repositoryRepository.findById(repositoryId).isPresent()) { + log.warn("stopRepository: repositoryId={} not found, but will cleanup container/port anyway.", repositoryId); + } + } Optional containerOpt = runningContainerRepository.findByRepositoryId(repositoryId); @@ -202,10 +243,18 @@ public boolean stopRepository(Long repositoryId, Long userId) { if (success) { container.stop(); runningContainerRepository.save(container); + + // 포트 반납도 여기서! + PortRegistry portReg = portRegistryRepository.findByPort(container.getPort()).orElse(null); + if (portReg != null) { + portReg.release(); + portRegistryRepository.save(portReg); + } + log.info("Successfully stopped repository: {} (uuid: {})", repositoryId, container.getUuid()); } - return success; + return true; } catch (Exception e) { log.error("Failed to stop repository: {}", repositoryId, e); @@ -264,6 +313,7 @@ private String convertTypeToFramework(RepositoryType type) { }; } + // === 랜덤 포트 할당 방식으로 수정 === private Integer allocateOrGetPort(Repository repo) { return portRegistryRepository.findByRepository(repo) .map(PortRegistry::getPort) @@ -273,16 +323,18 @@ private Integer allocateOrGetPort(Repository repo) { @Transactional public PortRegistry allocateNewPortForRepository(Repository repo) { log.debug("Allocating new port for repository: {}", repo.getId()); + List availablePorts = portRegistryRepository.findAllByStatus(PortStatus.AVAILABLE); - // 1. 사용 가능한 포트 목록 조회 (status == AVAILABLE) - PortRegistry available = portRegistryRepository.findFirstByStatus(PortStatus.AVAILABLE) - .orElseThrow(() -> new GlobalException(ErrorCode.NO_AVAILABLE_PORT)); + if (availablePorts.isEmpty()) { + throw new GlobalException(ErrorCode.NO_AVAILABLE_PORT); + } - // 2. 해당 포트 할당/저장 - available.assignToRepository(repo); - PortRegistry savedRegistry = portRegistryRepository.save(available); + Collections.shuffle(availablePorts); + PortRegistry selected = availablePorts.get(0); + selected.assignToRepository(repo); + PortRegistry savedRegistry = portRegistryRepository.save(selected); - log.info("Port {} allocated to repository {}", available.getPort(), repo.getId()); + log.info("Port {} allocated to repository {}", selected.getPort(), repo.getId()); return savedRegistry; }