Skip to content

Commit 65961ec

Browse files
authored
Merge pull request #257 from CSID-DGU/develop
7월 6일 작업분
2 parents c6e008d + 4e13fc9 commit 65961ec

15 files changed

Lines changed: 354 additions & 127 deletions

File tree

src/main/java/DGU_AI_LAB/admin_be/domain/alarm/dto/SlackMessageDto.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,20 @@ public enum MessageType {
1818
DM // 사용자 개인 DM
1919
}
2020

21-
private MessageType type; // 메시지 타입 구분
22-
private String message; // 보낼 메시지 내용
21+
private MessageType type;
22+
private String message;
2323

2424
// Webhook용 필드
2525
private String webhookUrl;
2626

2727
// DM용 필드
2828
private String username;
2929
private String email;
30+
31+
@Builder.Default
32+
private int retryCount = 0;
33+
34+
public void incrementRetryCount() {
35+
this.retryCount++;
36+
}
3037
}

src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/request/SaveRequestRequestDTO.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import com.fasterxml.jackson.core.JsonProcessingException;
1212
import com.fasterxml.jackson.databind.ObjectMapper;
1313
import io.swagger.v3.oas.annotations.media.Schema;
14+
import jakarta.validation.constraints.Future;
1415
import jakarta.validation.constraints.NotBlank;
1516
import jakarta.validation.constraints.NotNull;
17+
import jakarta.validation.constraints.Positive;
1618
import jakarta.validation.constraints.Size;
1719
import lombok.Builder;
1820

@@ -41,15 +43,20 @@ public record SaveRequestRequestDTO(
4143
String ubuntuPassword,
4244

4345
@Schema(description = "볼륨 사이즈 (GiB)", example = "20")
46+
@NotNull(message = "Volume size cannot be null")
47+
@Positive(message = "Volume size must be positive")
4448
Long volumeSizeGiB,
4549

4650
@Schema(description = "사용 목적", example = "딥러닝 모델 학습")
51+
@NotBlank(message = "Usage purpose cannot be blank")
4752
String usagePurpose,
4853

4954
@Schema(description = "폼 응답", example = "{\"question\": \"answer\"}")
5055
Map<String, Object> formAnswers,
5156

5257
@Schema(description = "서버 만료 일시", example = "2026-12-31T23:59:59")
58+
@NotNull(message = "Expires date cannot be null")
59+
@Future(message = "Expires date must be in the future")
5360
LocalDateTime expiresAt,
5461
@Schema(description = "Ubuntu GID 목록", example = "[1005, 1006]")
5562
Set<Long> ubuntuGids,

src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/response/AcceptInfoResponseDTO.java

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package DGU_AI_LAB.admin_be.domain.requests.dto.response;
22

3+
import DGU_AI_LAB.admin_be.domain.nodes.entity.Node;
34
import DGU_AI_LAB.admin_be.domain.portRequests.entity.PortRequests;
45
import DGU_AI_LAB.admin_be.domain.requests.entity.Request;
56
import io.swagger.v3.oas.annotations.media.Schema;
@@ -15,15 +16,37 @@ public record AcceptInfoResponseDTO(
1516
String username,
1617
@Schema(description = "컨테이너 이미지 (이름:버전)", example = "cuda:11.8")
1718
String image,
18-
@Schema(description = "Ubuntu GID 목록", example = "[1005, 1006]")
19-
List<Long> gid,
19+
@Schema(description = "그룹 목록 (GID + 그룹명)")
20+
List<GroupDTO> groups,
2021
@Schema(description = "볼륨 크기 (GiB)", example = "20")
2122
Long volume_size,
22-
@Schema(description = "GPU 필요 여부", example = "true")
23-
Boolean gpu_required,
23+
@Schema(description = "GPU 노드 목록")
24+
List<GpuNodeDTO> gpu_nodes,
2425
@Schema(description = "추가 포트 목록")
2526
List<AdditionalPortDTO> additional_ports
2627
) {
28+
@Schema(description = "그룹 정보")
29+
@Builder
30+
public record GroupDTO(
31+
@Schema(description = "Ubuntu GID", example = "10004")
32+
Long gid,
33+
@Schema(description = "그룹명", example = "ailab")
34+
String name
35+
) {}
36+
37+
@Schema(description = "GPU 노드 정보")
38+
@Builder
39+
public record GpuNodeDTO(
40+
@Schema(description = "노드명", example = "farm2")
41+
String node_name,
42+
@Schema(description = "GPU 수", example = "2")
43+
int num_gpu,
44+
@Schema(description = "CPU 제한 (k8s 포맷)", example = "4000m")
45+
String cpu_limit,
46+
@Schema(description = "메모리 제한 (k8s 포맷)", example = "8192Mi")
47+
String memory_limit
48+
) {}
49+
2750
@Schema(description = "추가 포트 정보")
2851
@Builder
2952
public record AdditionalPortDTO(
@@ -33,26 +56,38 @@ public record AdditionalPortDTO(
3356
String usage_purpose
3457
) {}
3558

36-
public static AcceptInfoResponseDTO fromEntity(Request request, List<PortRequests> portRequests) {
59+
public static AcceptInfoResponseDTO fromEntity(Request request, List<PortRequests> portRequests, List<Node> nodes) {
3760
var image = request.getContainerImage();
3861

62+
List<GroupDTO> groupDTOList = request.getRequestGroups().stream()
63+
.map(rg -> GroupDTO.builder()
64+
.gid(rg.getGroup().getUbuntuGid())
65+
.name(rg.getGroup().getGroupName())
66+
.build())
67+
.toList();
68+
69+
List<GpuNodeDTO> gpuNodeDTOList = nodes.stream()
70+
.map(node -> GpuNodeDTO.builder()
71+
.node_name(node.getNodeId())
72+
.num_gpu(node.getNumberGpu())
73+
.cpu_limit(node.getCpuCoreCount() * 1000 + "m")
74+
.memory_limit(node.getMemorySizeGB() * 1024 + "Mi")
75+
.build())
76+
.toList();
77+
3978
List<AdditionalPortDTO> additionalPortDTOList = portRequests.stream()
4079
.map(portRequest -> AdditionalPortDTO.builder()
4180
.internal_port(portRequest.getInternalPort())
4281
.usage_purpose(portRequest.getUsagePurpose())
43-
.build()
44-
).toList();
82+
.build())
83+
.toList();
4584

4685
return AcceptInfoResponseDTO.builder()
4786
.username(request.getUbuntuUsername())
4887
.image(image.getImageName() + ":" + image.getImageVersion())
49-
.gid(
50-
request.getRequestGroups().stream()
51-
.map(rg -> rg.getGroup().getUbuntuGid())
52-
.toList()
53-
)
88+
.groups(groupDTOList)
5489
.volume_size(request.getVolumeSizeGiB())
55-
.gpu_required(true)
90+
.gpu_nodes(gpuNodeDTOList)
5691
.additional_ports(additionalPortDTOList)
5792
.build();
5893
}

src/main/java/DGU_AI_LAB/admin_be/domain/requests/dto/response/ContainerInfoDTO.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ public record ContainerInfoDTO(
1616
String userName,
1717
@Schema(description = "Ubuntu 사용자명", example = "test2014")
1818
String ubuntuUsername,
19+
@Schema(description = "Pod 이름", example = "pod-test2014-mock")
20+
String podName,
21+
@Schema(description = "배포된 노드명", example = "farm1")
22+
String nodeName,
1923
@Schema(description = "Ubuntu GID 목록", example = "[1005, 1006]")
2024
List<Long> ubuntuGids,
2125
@Schema(description = "리소스 그룹 ID", example = "1")
@@ -32,6 +36,8 @@ public static ContainerInfoDTO fromEntity(Request request) {
3236
.userId(request.getUser().getUserId())
3337
.userName(request.getUser().getName())
3438
.ubuntuUsername(request.getUbuntuUsername())
39+
.podName(request.getPodName())
40+
.nodeName(request.getNodeName())
3541
.ubuntuGids(
3642
request.getRequestGroups().stream()
3743
.map(rg -> rg.getGroup().getUbuntuGid())

src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/ConfigRequestService.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package DGU_AI_LAB.admin_be.domain.requests.service;
22

3+
import DGU_AI_LAB.admin_be.domain.nodes.entity.Node;
4+
import DGU_AI_LAB.admin_be.domain.nodes.repository.NodeRepository;
35
import DGU_AI_LAB.admin_be.domain.portRequests.entity.PortRequests;
46
import DGU_AI_LAB.admin_be.domain.portRequests.repository.PortRequestRepository;
57
import DGU_AI_LAB.admin_be.domain.requests.dto.response.AcceptInfoResponseDTO;
@@ -21,6 +23,7 @@ public class ConfigRequestService {
2123

2224
private final RequestRepository requestRepository;
2325
private final PortRequestRepository portRequestRepository;
26+
private final NodeRepository nodeRepository;
2427

2528
/** ubuntu username 중복 검사 */
2629
@Transactional(readOnly = true)
@@ -29,10 +32,10 @@ public boolean isUbuntuUsernameAvailable(String username) {
2932
}
3033

3134
/** config server용 acceptinfo */
35+
@Transactional(readOnly = true)
3236
public AcceptInfoResponseDTO getAcceptInfo(String username) {
3337
log.info("사용자 승인 정보 조회를 시작합니다. username: {}", username);
3438

35-
// 사용자 요청 정보 조회
3639
Request request = requestRepository.findByUbuntuUsername(username)
3740
.orElseThrow(() -> {
3841
log.warn("사용자 '{}'에 대한 승인 정보가 존재하지 않습니다.", username);
@@ -41,12 +44,13 @@ public AcceptInfoResponseDTO getAcceptInfo(String username) {
4144

4245
log.debug("사용자 '{}'의 요청 정보를 성공적으로 찾았습니다. 요청 ID: {}", username, request.getRequestId());
4346

44-
// 포트 요청 정보 조회
4547
List<PortRequests> portRequests = portRequestRepository.findByRequestRequestId(request.getRequestId());
4648
log.debug("요청 '{}'에 속한 포트 요청 {}개를 조회했습니다.", request.getRequestId(), portRequests.size());
4749

48-
// 응답 DTO 생성 및 반환
49-
AcceptInfoResponseDTO response = AcceptInfoResponseDTO.fromEntity(request, portRequests);
50+
List<Node> nodes = nodeRepository.findAllByResourceGroup(request.getResourceGroup());
51+
log.debug("리소스 그룹 소속 노드 {}개를 조회했습니다.", nodes.size());
52+
53+
AcceptInfoResponseDTO response = AcceptInfoResponseDTO.fromEntity(request, portRequests, nodes);
5054
log.info("사용자 '{}'에 대한 AcceptInfoResponseDTO 생성을 완료했습니다.", username);
5155

5256
return response;

src/main/java/DGU_AI_LAB/admin_be/domain/requests/service/RequestExpiryService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class RequestExpiryService {
2020

2121
private final RequestRepository requestRepository;
2222
private final UbuntuAccountService ubuntuAccountService;
23+
private final PodService podService;
2324
private final ApplicationEventPublisher eventPublisher;
2425

2526
@Transactional
@@ -33,6 +34,7 @@ public void deleteExpiredRequest(Long requestId) {
3334
String ubuntuUsername = request.getUbuntuUsername();
3435
User user = request.getUser();
3536

37+
podService.deletePod(request.getPodName());
3638
ubuntuAccountService.deleteUbuntuAccount(ubuntuUsername);
3739

3840
request.delete();

src/main/java/DGU_AI_LAB/admin_be/domain/scheduler/RequestSchedulerService.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.springframework.transaction.annotation.Transactional;
1515

1616
import java.time.LocalDateTime;
17+
import java.time.ZoneId;
1718
import java.util.List;
1819

1920
/**
@@ -33,7 +34,7 @@ public class RequestSchedulerService {
3334
@Scheduled(cron = "0 00 08 * * ?", zone = "Asia/Seoul")
3435
public void runScheduler() {
3536
log.info("🗓️ [스케줄러 시작] 만료 계정 관리 작업");
36-
LocalDateTime now = LocalDateTime.now();
37+
LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
3738

3839
sendPreExpiryNotification(now.plusDays(7), "7일");
3940
sendPreExpiryNotification(now.plusDays(3), "3일");

src/main/java/DGU_AI_LAB/admin_be/domain/scheduler/SlackNotificationWorker.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
/**
1414
* Consumer / Worker
1515
* Redis 큐에 쌓인 알림 요청을 하나씩 꺼내서 실제로 처리하는 Consumer입니다.
16+
* 전송 실패 시 최대 MAX_RETRY_COUNT회 재시도합니다.
1617
*/
1718
@Component
1819
@RequiredArgsConstructor
@@ -24,15 +25,22 @@ public class SlackNotificationWorker {
2425
private final ObjectMapper objectMapper;
2526

2627
private static final String SLACK_QUEUE_KEY = "slack:notification:queue";
28+
private static final int MAX_RETRY_COUNT = 3;
2729

2830
@Scheduled(fixedDelay = 1000)
2931
public void processSlackQueue() {
30-
try {
31-
Object messageObj = redisTemplate.opsForList().leftPop(SLACK_QUEUE_KEY);
32-
if (messageObj == null) return;
32+
Object messageObj = redisTemplate.opsForList().leftPop(SLACK_QUEUE_KEY);
33+
if (messageObj == null) return;
3334

34-
SlackMessageDto dto = objectMapper.convertValue(messageObj, SlackMessageDto.class);
35+
SlackMessageDto dto;
36+
try {
37+
dto = objectMapper.convertValue(messageObj, SlackMessageDto.class);
38+
} catch (Exception e) {
39+
log.error("Slack 큐 메시지 역직렬화 실패 (폐기): {}", e.getMessage());
40+
return;
41+
}
3542

43+
try {
3644
if (dto.getType() == SlackMessageDto.MessageType.WEBHOOK) {
3745
slackApiService.sendWebhook(dto.getWebhookUrl(), dto.getMessage());
3846
log.info("Slack Webhook 전송 성공 (Queue)");
@@ -43,10 +51,23 @@ public void processSlackQueue() {
4351
}
4452

4553
} catch (BusinessException e) {
46-
log.warn("Slack 알림 처리 실패 (Business): {}", e.getMessage());
54+
// 비즈니스 예외(유저 없음 등)는 재시도해도 동일하게 실패하므로 바로 폐기
55+
log.warn("Slack 알림 처리 실패 (Business, 폐기): {}", e.getMessage());
4756

4857
} catch (Exception e) {
49-
log.error("Slack 큐 처리 중 시스템 오류 (재시도 필요 시 큐 복귀 고려)", e);
58+
// 일시적 장애(네트워크, Slack API 다운 등)는 재시도
59+
requeue(dto, e);
60+
}
61+
}
62+
63+
private void requeue(SlackMessageDto dto, Exception cause) {
64+
if (dto.getRetryCount() < MAX_RETRY_COUNT) {
65+
dto.incrementRetryCount();
66+
redisTemplate.opsForList().rightPush(SLACK_QUEUE_KEY, dto);
67+
log.warn("Slack 메시지 재시도 예약 ({}/{}회): {}", dto.getRetryCount(), MAX_RETRY_COUNT, cause.getMessage());
68+
} else {
69+
log.error("Slack 메시지 최대 재시도({}) 초과, 폐기: {} | 원인: {}",
70+
MAX_RETRY_COUNT, dto.getMessage(), cause.getMessage());
5071
}
5172
}
52-
}
73+
}

0 commit comments

Comments
 (0)