Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class DecisionCommitMatchService {
Expand Down Expand Up @@ -227,13 +229,21 @@ private void saveRecommendations(Decision decision, List<Long> repositoryIds, Js

Map<Long, Application> applicationsById = findAndValidateApplications(decision, candidatesByKey.values());
Map<Long, Commit> commitsById = findAndValidateCommits(repositoryIds, candidatesByKey.values());
List<DecisionCommitMatchCandidate> validCandidates = candidatesByKey.values().stream()
.filter(candidate -> candidate.resolvedCommitId() != null)
.toList();

// 새 추천 스냅샷을 저장하기 전에 기존 추천 결과를 통째로 교체
applicationCommitsRepository.deleteByDecisionId(decision.getId());
decisionCommitsRepository.deleteByDecisionId(decision.getId());

if (validCandidates.isEmpty()) {
decisionRepository.updateReliabilityScore(decision.getId(), null);
return;
}

Map<Long, DecisionCommits> decisionCommitsByCommitId = new LinkedHashMap<>();
for (DecisionCommitMatchCandidate candidate : candidatesByKey.values()) {
for (DecisionCommitMatchCandidate candidate : validCandidates) {
Application application = applicationsById.get(candidate.resolvedApplicationId());
Commit commit = commitsById.get(candidate.resolvedCommitId());

Expand Down Expand Up @@ -298,9 +308,18 @@ private Map<Long, Commit> findAndValidateCommits(List<Long> repositoryIds,
Map<Long, Commit> commitsById = new LinkedHashMap<>();

for (DecisionCommitMatchCandidate candidate : candidates) {
Commit commit = resolveCommit(candidate, repositoryIdSet);
candidate.resolveCommitId(commit.getId());
commitsById.put(commit.getId(), commit);
try {
Commit commit = resolveCommit(candidate, repositoryIdSet);
candidate.resolveCommitId(commit.getId());
commitsById.put(commit.getId(), commit);
} catch (ErrorHandler exception) {
log.warn("추천 커밋 후보를 찾을 수 없어 제외한다: repositoryId={}, commitHash={}, commitId={}, errorCode={}, message={}",
candidate.repositoryId(),
candidate.commitHash(),
candidate.commitId(),
exception.getErrorReason().getCode(),
exception.getErrorReason().getMessage());
}
}

return commitsById;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,18 @@ public class CommitAnalysis extends BaseEntity {
@Column(name = "summary", columnDefinition = "TEXT")
private String summary;

@Column(name = "embedding_ready", nullable = false)
private boolean embeddingReady;

public static CommitAnalysis create(Commit commit) {
CommitAnalysis commitAnalysis = new CommitAnalysis();
commitAnalysis.commit = commit;
commitAnalysis.embeddingReady = false;
return commitAnalysis;
}

public void updateSummary(String summary) {
public void updateSummary(String summary, boolean embeddingReady) {
this.summary = summary;
this.embeddingReady = embeddingReady;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ Set<String> findExistingHashes(
FROM Commit c
LEFT JOIN c.commitAnalysis ca
WHERE c.repository.id = :repositoryId
AND ca.id IS NULL
AND (ca.id IS NULL OR ca.embeddingReady = false)
""")
List<Commit> findUnanalyzedCommitsByRepositoryId(@Param("repositoryId") Long repositoryId);
List<Commit> findEmbeddingNotReadyCommitsByRepositoryId(@Param("repositoryId") Long repositoryId);

@Query("""
SELECT c.id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,19 +272,19 @@ private void syncCommits(GHRepository ghRepository, Repository repository, Local
log.info("새로 동기화할 커밋이 없습니다: {}", ghRepository.getFullName());
}

List<Commit> unanalyzedCommits = commitRepository.findUnanalyzedCommitsByRepositoryId(repository.getId());
List<Commit> analyzeTargets = mergeAnalyzeTargets(savedCommits, unanalyzedCommits);
List<Commit> embeddingNotReadyCommits = commitRepository.findEmbeddingNotReadyCommitsByRepositoryId(repository.getId());
List<Commit> analyzeTargets = mergeAnalyzeTargets(savedCommits, embeddingNotReadyCommits);

if (!analyzeTargets.isEmpty()) {
triggerCommitAnalyzeRunsAfterCommit(ghRepository, repository, analyzeTargets);
log.info("커밋 분석 요청 대상 {}개 확인: {}", analyzeTargets.size(), ghRepository.getFullName());
}
}

private List<Commit> mergeAnalyzeTargets(List<Commit> savedCommits, List<Commit> unanalyzedCommits) {
private List<Commit> mergeAnalyzeTargets(List<Commit> savedCommits, List<Commit> embeddingNotReadyCommits) {
Map<Long, Commit> analyzeTargets = new LinkedHashMap<>();
savedCommits.forEach(commit -> analyzeTargets.put(commit.getId(), commit));
unanalyzedCommits.forEach(commit -> analyzeTargets.putIfAbsent(commit.getId(), commit));
embeddingNotReadyCommits.forEach(commit -> analyzeTargets.putIfAbsent(commit.getId(), commit));
return new ArrayList<>(analyzeTargets.values());
}

Expand Down Expand Up @@ -371,7 +371,7 @@ private void createCommitAnalyzeRun(GHRepository ghRepository, Repository reposi
}

/**
* FastAPI 분석 run 상태를 폴링해 summary가 준비된 최종 결과를 가져옵니다.
* FastAPI 분석 run 상태를 폴링해 summary와 embedding이 준비된 최종 결과를 가져옵니다.
*/
private JsonNode pollCommitAnalyzeRun(String runId) {
for (int attempt = 1; attempt <= 120; attempt++) {
Expand All @@ -387,6 +387,7 @@ private JsonNode pollCommitAnalyzeRun(String runId) {
String phase = readText(runResult, "phase");
String error = readText(runResult, "error");
String summary = readNestedText(runResult, "result", "summary");
boolean embeddingReady = isCommitEmbeddingReady(runResult);

if (isFailed(status, phase)) {
throw new FastApiException(
Expand All @@ -395,7 +396,7 @@ private JsonNode pollCommitAnalyzeRun(String runId) {
);
}

if (summary != null && !summary.isBlank()) {
if (summary != null && !summary.isBlank() && embeddingReady) {
return runResult;
}

Expand All @@ -409,21 +410,22 @@ private JsonNode pollCommitAnalyzeRun(String runId) {
}

/**
* 완료된 분석 결과에서 summary를 upsert 저장합니다.
* 완료된 분석 결과에서 summary와 embedding 준비 상태를 upsert 저장합니다.
*/
private void saveCommitAnalysis(Commit commit, JsonNode runResult) {
String summary = readNestedText(runResult, "result", "summary");
boolean embeddingReady = isCommitEmbeddingReady(runResult);

if (summary == null || summary.isBlank()) {
if (summary == null || summary.isBlank() || !embeddingReady) {
throw new FastApiException(FastApiErrorCode.FAST_API_RESPONSE_EMPTY);
}

CommitAnalysis commitAnalysis = commitAnalysisRepository.findByCommitId(commit.getId())
.orElseGet(() -> CommitAnalysis.create(commit));
commitAnalysis.updateSummary(summary);
commitAnalysis.updateSummary(summary, embeddingReady);
commitAnalysisRepository.save(commitAnalysis);

log.info("커밋 분석 저장 완료: commitHash={}", commit.getHash());
log.info("커밋 분석 저장 완료: commitHash={}, embeddingReady={}", commit.getHash(), embeddingReady);
}

/**
Expand All @@ -433,6 +435,34 @@ private boolean isFailed(String status, String phase) {
return "failed".equalsIgnoreCase(status) || "failed".equalsIgnoreCase(phase);
}

/**
* FastAPI 커밋 분석 결과의 embedding 준비 여부를 확인합니다.
*/
private boolean isCommitEmbeddingReady(JsonNode runResult) {
String phase = readText(runResult, "phase");
if ("embedding_ready".equalsIgnoreCase(phase)) {
return true;
}

Boolean embeddingReady = readBoolean(runResult, "embedding_ready");
if (embeddingReady != null) {
return embeddingReady;
}

Boolean camelEmbeddingReady = readBoolean(runResult, "embeddingReady");
if (camelEmbeddingReady != null) {
return camelEmbeddingReady;
}

Boolean nestedEmbeddingReady = readNestedBoolean(runResult, "result", "embedding_ready");
if (nestedEmbeddingReady != null) {
return nestedEmbeddingReady;
}

Boolean nestedCamelEmbeddingReady = readNestedBoolean(runResult, "result", "embeddingReady");
return Boolean.TRUE.equals(nestedCamelEmbeddingReady);
}

/**
* 커밋 분석 실패가 재시도 가능한 일시적 오류인지 확인합니다.
*/
Expand Down Expand Up @@ -505,6 +535,37 @@ private String readNestedText(JsonNode node, String parentFieldName, String chil
return child != null ? child.asText(null) : null;
}

/**
* JSON 필드의 Boolean 값을 안전하게 읽습니다.
*/
private Boolean readBoolean(JsonNode node, String fieldName) {
JsonNode value = node != null ? node.get(fieldName) : null;
if (value == null || value.isNull()) {
return null;
}
if (value.isBoolean()) {
return value.asBoolean();
}
if (value.isTextual()) {
String text = value.asText();
if ("true".equalsIgnoreCase(text)) {
return true;
}
if ("false".equalsIgnoreCase(text)) {
return false;
}
}
return null;
}

/**
* 중첩 JSON 필드의 Boolean 값을 안전하게 읽습니다.
*/
private Boolean readNestedBoolean(JsonNode node, String parentFieldName, String childFieldName) {
JsonNode parent = node != null ? node.get(parentFieldName) : null;
return readBoolean(parent, childFieldName);
}

/**
* FastAPI 응답의 result가 비어 있으면 예외를 던집니다.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import com.whylog.server.global.external.fast.dto.response.TranscribeApplicationRunResponse;
import com.whylog.server.global.external.fast.exception.FastApiErrorCode;
import com.whylog.server.global.external.fast.exception.FastApiException;
import com.whylog.server.global.apiPayload.code.ErrorReasonDTO;
import com.whylog.server.global.apiPayload.exception.GeneralException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
Expand Down Expand Up @@ -387,6 +389,10 @@ private void matchApplicationCommitsSafely(Meeting meeting, SavedApplications sa
decisionCommitMatchService.matchApplicationCommits(savedApplications.decisionId());
log.info("적용사항-커밋 추천 매칭 완료: meetingId={}, decisionId={}",
meeting.getId(), savedApplications.decisionId());
} catch (GeneralException exception) {
ErrorReasonDTO reason = exception.getErrorReason();
log.error("적용사항-커밋 추천 매칭 실패: meetingId={}, decisionId={}, errorCode={}, message={}",
meeting.getId(), savedApplications.decisionId(), reason.getCode(), reason.getMessage(), exception);
} catch (Exception exception) {
log.error("적용사항-커밋 추천 매칭 실패: meetingId={}, decisionId={}",
meeting.getId(), savedApplications.decisionId(), exception);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@

import com.whylog.server.global.apiPayload.code.BaseErrorCode;
import com.whylog.server.global.apiPayload.code.ErrorReasonDTO;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {

private BaseErrorCode code;

public GeneralException(BaseErrorCode code) {
super(code.getReason().getMessage());
this.code = code;
}

public ErrorReasonDTO getErrorReason() {
return this.code.getReason();
}

public ErrorReasonDTO getErrorReasonHttpStatus(){
return this.code.getReasonHttpStatus();
}
}
}
Loading