diff --git a/src/main/java/com/deepdirect/deepwebide_be/DeepwebideBeApplication.java b/src/main/java/com/deepdirect/deepwebide_be/DeepwebideBeApplication.java index 7d402f7a..e32d3dad 100644 --- a/src/main/java/com/deepdirect/deepwebide_be/DeepwebideBeApplication.java +++ b/src/main/java/com/deepdirect/deepwebide_be/DeepwebideBeApplication.java @@ -2,10 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableScheduling +@Async public class DeepwebideBeApplication { public static void main(String[] args) { diff --git a/src/main/java/com/deepdirect/deepwebide_be/global/config/SchedulerConfig.java b/src/main/java/com/deepdirect/deepwebide_be/global/config/SchedulerConfig.java new file mode 100644 index 00000000..4eba77d7 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/global/config/SchedulerConfig.java @@ -0,0 +1,18 @@ +package com.deepdirect.deepwebide_be.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +@Configuration +@EnableScheduling +public class SchedulerConfig { + @Bean + public ThreadPoolTaskScheduler taskScheduler() { + ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(2); + scheduler.setThreadNamePrefix("auto-stop-scheduler-"); + return scheduler; + } +} 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..bcf7ee0c 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,13 +1,11 @@ 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; import com.deepdirect.deepwebide_be.repository.dto.response.RepositoryStopResponse; -import com.deepdirect.deepwebide_be.repository.repository.RepositoryRepository; +import com.deepdirect.deepwebide_be.repository.service.AutoStopSchedulerService; import com.deepdirect.deepwebide_be.repository.service.RepositoryRunService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -30,7 +28,7 @@ public class RepositoryRunController { private final RepositoryRunService repositoryRunService; - private final RepositoryRepository repositoryRepository; + private final AutoStopSchedulerService autoStopSchedulerService; @PostMapping("/{repositoryId}/execute") @Operation(summary = "레포지토리 실행", description = "레포지토리를 실행하고 실행 결과를 반환합니다.") @@ -39,6 +37,7 @@ public ResponseEntity> executeReposito @PathVariable Long repositoryId ) { RepositoryExecuteResponse resp = repositoryRunService.executeRepository(repositoryId, userDetails.getId()); + autoStopSchedulerService.scheduleAutoStop(repositoryId, resp.getUuid(), 10); return ResponseEntity.ok(ApiResponseDto.of(200, "레포지토리 실행 요청 완료", resp)); } 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..6c5eb9fd 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 @@ -9,8 +9,8 @@ public interface PortRegistryRepository extends JpaRepository { - Optional findFirstByStatusOrderByPortAsc(PortStatus status); - Optional findByRepository(Repository repository); Optional findFirstByStatus(PortStatus status); + Optional findByRepositoryId(Long repositoryId); + } diff --git a/src/main/java/com/deepdirect/deepwebide_be/repository/service/AutoStopSchedulerService.java b/src/main/java/com/deepdirect/deepwebide_be/repository/service/AutoStopSchedulerService.java new file mode 100644 index 00000000..00cb60e5 --- /dev/null +++ b/src/main/java/com/deepdirect/deepwebide_be/repository/service/AutoStopSchedulerService.java @@ -0,0 +1,23 @@ +package com.deepdirect.deepwebide_be.repository.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; + +import java.util.Date; + + +@Service +@RequiredArgsConstructor +public class AutoStopSchedulerService { + private final RepositoryRunService repositoryRunService; + private final TaskScheduler taskScheduler; + + // 10분 후 자동 중지 예약 + public void scheduleAutoStop(Long repositoryId, String uuid, int minutes) { + taskScheduler.schedule( + () -> repositoryRunService.stopIfTimeout(repositoryId, uuid), + new Date(System.currentTimeMillis() + minutes * 60 * 1000L) + ); + } +} 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..a649736c 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,12 +20,10 @@ 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.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.*; @@ -196,6 +194,14 @@ public boolean stopRepository(Long repositoryId, Long userId) { RunningContainer container = containerOpt.get(); + // 포트 정보 해제 로직 추가 + Optional portRegistryOpt = portRegistryRepository.findByRepositoryId(repositoryId); + portRegistryOpt.ifPresent(portRegistry -> { + portRegistry.release(); + portRegistryRepository.save(portRegistry); + log.info("Released port {} for repository {}", portRegistry.getPort(), repositoryId); + }); + // 샌드박스 서버에 중지 요청 boolean success = sandboxService.stopContainer(container.getUuid()); @@ -446,9 +452,11 @@ public Map getRepositoryLogs(Long repositoryId, Long userId, int Optional containerOpt = runningContainerRepository.findByRepositoryId(repositoryId); + Integer portValue = containerOpt.map(RunningContainer::getPort).orElse(-1); + if (containerOpt.isEmpty()) { return Map.of( - "port", null, + "port", portValue, // -1로 반환 "logs", "실행 중인 컨테이너가 없습니다." ); } @@ -473,44 +481,52 @@ public Map getRepositoryLogs(Long repositoryId, Long userId, int runningContainerRepository.save(container); return Map.of( - "port", null, + "port", portValue, "logs", "컨테이너가 존재하지 않아 중지되었습니다." ); } // 정상 응답 - 포트와 로그만 반환 String logs = extractLogs(response); + if (logs == null) logs = ""; return Map.of( - "port", container.getPort(), + "port", portValue, "logs", logs ); } return Map.of( - "port", container.getPort(), + "port", portValue, "logs", "로그를 가져올 수 없습니다." ); } catch (Exception httpEx) { log.error("HTTP request failed - url: {}", url, httpEx); + String msg = httpEx.getMessage(); + if (msg == null) msg = "알 수 없는 오류"; + // HTTP 오류 시에도 컨테이너 상태 확인 - if (httpEx.getMessage().contains("404") || httpEx.getMessage().contains("Not Found")) { + if (msg.contains("404") || msg.contains("Not Found")) { container.stop(); runningContainerRepository.save(container); } return Map.of( - "port", container.getPort(), - "logs", "로그 조회 중 오류가 발생했습니다: " + httpEx.getMessage() + "port", portValue, + "logs", "로그 조회 중 오류가 발생했습니다: " + msg ); } } catch (Exception e) { log.error("Failed to get repository logs: {}", repositoryId, e); + + String msg = e.getMessage(); + if (msg == null) msg = "알 수 없는 오류"; + return Map.of( - "port", null, - "logs", "오류가 발생했습니다: " + e.getMessage() + "port", -1, + "logs", "오류가 발생했습니다: " + msg ); } } @@ -536,6 +552,19 @@ private String extractLogs(Map response) { return result.isEmpty() ? "로그가 없습니다." : result; } + @Transactional + public void stopIfTimeout(Long repositoryId, String uuid) { + Optional containerOpt = runningContainerRepository.findByRepositoryId(repositoryId); + if (containerOpt.isPresent() && "RUNNING".equals(containerOpt.get().getStatus())) { + log.info("자동 중지 조건 충족: repositoryId={}, uuid={}", repositoryId, uuid); + Repository repo = repositoryRepository.findById(repositoryId) + .orElseThrow(() -> new GlobalException(ErrorCode.REPOSITORY_NOT_FOUND)); + stopRepository(repositoryId, repo.getOwner().getId()); + } else { + log.info("이미 중지된 컨테이너: repositoryId={}, uuid={}", repositoryId, uuid); + } + } + private Map createContainerInfo(RunningContainer container, String actualStatus) { return Map.of( "uuid", container.getUuid(),