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
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ jobs:
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
set -euo pipefail

echo "=========================================="
echo "Backend Deployment Started"
echo "Time: $(date)"
Expand Down
9 changes: 5 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
services:
git-ranker-api:
image: ${DOCKERHUB_USERNAME:?DOCKERHUB_USERNAME is required}/git-ranker:${IMAGE_TAG:-latest}
build: .
container_name: git-ranker-api
restart: unless-stopped
Expand All @@ -10,7 +11,7 @@ services:
SPRING_PROFILES_ACTIVE: prod
DB_URL: jdbc:mysql://git-ranker-db:3306/${DB_NAME}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
GITHUB_REDIRECT_URI: ${GITHUB_REDIRECT_URI}
Expand Down Expand Up @@ -45,7 +46,7 @@ services:
environment:
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
TZ: Asia/Seoul
ports:
Expand Down Expand Up @@ -135,7 +136,7 @@ services:
container_name: git-ranker-grafana
restart: unless-stopped
ports:
- "3001:3000"
- "127.0.0.1:3001:3000"
volumes:
- ./datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
- grafana_data:/var/lib/grafana
Expand All @@ -144,7 +145,7 @@ services:
- ./alerting:/etc/grafana/provisioning/alerting
environment:
GF_SECURITY_ADMIN_USER: ${GF_SECURITY_ADMIN_USER}
GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD}
GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD:?GF_SECURITY_ADMIN_PASSWORD is required}
DISCORD_WEBHOOK_URL: ${DISCORD_WEBHOOK_URL}
GF_SERVER_DOMAIN: ${GF_SERVER_DOMAIN}
GF_SERVER_ROOT_URL: ${GF_SERVER_ROOT_URL}
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/com/gitranker/api/batch/dto/ScoredUserUpdate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.gitranker.api.batch.dto;

import com.gitranker.api.domain.user.User;
import com.gitranker.api.domain.user.vo.ActivityStatistics;

import java.time.LocalDate;

/**
* 프로세서가 계산한 점수/활동 통계를 writer로 넘기는 carrier.
* DB 쓰기는 writer가 청크 트랜잭션 안에서 수행하고, 프로세서는 순수 계산 결과만 담는다.
*/
public record ScoredUserUpdate(
User user,
ActivityStatistics stats,
ActivityStatistics diff,
LocalDate date
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
import com.gitranker.api.batch.listener.BatchProgressListener;
import com.gitranker.api.batch.listener.GitHubCostListener;
import com.gitranker.api.batch.listener.UserScoreCalculationSkipListener;
import com.gitranker.api.batch.dto.ScoredUserUpdate;
import com.gitranker.api.batch.processor.ScoreRecalculationProcessor;
import com.gitranker.api.batch.reader.UserItemReader;
import com.gitranker.api.batch.tasklet.RankingRecalculationTasklet;
import com.gitranker.api.batch.writer.UserItemWriter;
import com.gitranker.api.domain.user.User;
import com.gitranker.api.global.error.exception.GitHubApiNonRetryableException;
import com.gitranker.api.global.error.exception.GitHubApiRetryableException;
import com.gitranker.api.global.error.exception.GitHubRateLimitException;
import com.gitranker.api.global.error.exception.GitHubRateLimitExhaustedException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
Expand Down Expand Up @@ -53,12 +56,15 @@ public Job dailyScoreRecalculationJob() {
@Bean
public Step scoreRecalculationStep() {
return new StepBuilder("scoreRecalculationStep", jobRepository)
.<User, User>chunk(chunkSize, transactionManager)
.<User, ScoredUserUpdate>chunk(chunkSize, transactionManager)
.reader(userItemReader.createReader(chunkSize))
.processor(scoreRecalculationProcessor)
.writer(userItemWriter)
.faultTolerant()
.processorNonTransactional()
.retry(GitHubApiRetryableException.class)
.noRetry(GitHubRateLimitException.class)
.noRetry(GitHubRateLimitExhaustedException.class)
.retryLimit(3)
.backOffPolicy(new ExponentialBackOffPolicy())
.skip(GitHubApiNonRetryableException.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.gitranker.api.batch.processor;

import com.gitranker.api.batch.dto.ScoredUserUpdate;
import com.gitranker.api.batch.strategy.ActivityUpdateContext;
import com.gitranker.api.batch.strategy.ActivityUpdateStrategy;
import com.gitranker.api.batch.strategy.FullActivityUpdateStrategy;
import com.gitranker.api.batch.strategy.IncrementalActivityUpdateStrategy;
import com.gitranker.api.domain.log.ActivityLog;
import com.gitranker.api.domain.log.ActivityLogRepository;
import com.gitranker.api.domain.log.ActivityLogService;
import com.gitranker.api.domain.user.User;
import com.gitranker.api.domain.user.vo.ActivityStatistics;
import com.gitranker.api.domain.user.vo.Score;
Expand All @@ -27,16 +27,15 @@
@Slf4j
@Component
@RequiredArgsConstructor
public class ScoreRecalculationProcessor implements ItemProcessor<User, User> {
public class ScoreRecalculationProcessor implements ItemProcessor<User, ScoredUserUpdate> {

private final ActivityLogRepository activityLogRepository;
private final ActivityLogService activityLogService;
private final IncrementalActivityUpdateStrategy incrementalStrategy;
private final FullActivityUpdateStrategy fullStrategy;
private final GitHubActivityService gitHubActivityService;

@Override
public User process(User user) {
public ScoredUserUpdate process(User user) {
try {
return recalculateScore(user);
} catch (GitHubApiNonRetryableException e) {
Expand All @@ -51,7 +50,7 @@ public User process(User user) {
}
}

private User recalculateScore(User user) {
private ScoredUserUpdate recalculateScore(User user) {
int oldScore = user.getTotalScore();
int currentYear = LocalDate.now().getYear();

Expand All @@ -62,15 +61,14 @@ private User recalculateScore(User user) {
user.updateScore(newScore);

ActivityStatistics diffStats = updateStats.calculateDiff(previousStats);
activityLogService.saveActivityLog(user, updateStats, diffStats, LocalDate.now());

log.debug("점수 갱신 완료 - 사용자: {}, 변동: {}",
LogSanitizer.maskUsername(user.getUsername()), newScore.differenceFrom(Score.of(oldScore)));

return user;
return new ScoredUserUpdate(user, updateStats, diffStats, LocalDate.now());
}

private User handleUsernameChanged(User user) {
private ScoredUserUpdate handleUsernameChanged(User user) {
String oldUsername = user.getUsername();

GitHubNodeUserResponse response = gitHubActivityService.fetchUserByNodeId(user.getNodeId());
Expand Down
28 changes: 24 additions & 4 deletions src/main/java/com/gitranker/api/batch/writer/UserItemWriter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.gitranker.api.batch.writer;

import com.gitranker.api.batch.dto.ScoredUserUpdate;
import com.gitranker.api.domain.log.ActivityLogService;
import com.gitranker.api.domain.user.User;
import com.gitranker.api.domain.user.UserRepository;
import com.gitranker.api.global.error.exception.BusinessException;
Expand All @@ -10,22 +12,40 @@
import org.springframework.batch.item.ItemWriter;
import org.springframework.stereotype.Component;

import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class UserItemWriter implements ItemWriter<User> {
public class UserItemWriter implements ItemWriter<ScoredUserUpdate> {

private final UserRepository userRepository;
private final ActivityLogService activityLogService;

@Override
public void write(Chunk<? extends User> chunk) throws Exception {
public void write(Chunk<? extends ScoredUserUpdate> chunk) throws Exception {
try {
userRepository.saveAll(chunk.getItems());
List<User> users = chunk.getItems().stream()
.map(ScoredUserUpdate::user)
.toList();
userRepository.saveAll(users);

for (ScoredUserUpdate update : chunk.getItems()) {
upsertActivityLog(update);
}

log.debug("배치 Chunk 저장 완료 - Size: {}", chunk.getItems().size());
log.debug("배치 Chunk 저장 완료 - Size: {}", users.size());

} catch (Exception e) {
throw new BusinessException(ErrorType.BATCH_STEP_FAILED, "DB 저장 실패");
}
}

private void upsertActivityLog(ScoredUserUpdate update) {
activityLogService.findByDate(update.user(), update.date())
.ifPresentOrElse(
existingLog -> activityLogService.updateActivityLog(existingLog, update.stats(), update.diff()),
() -> activityLogService.saveActivityLog(update.user(), update.stats(), update.diff(), update.date())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.gitranker.api.infrastructure.github.dto.GitHubAllActivitiesResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
Expand All @@ -38,7 +39,17 @@ public RegisterUserResponse register(OAuthAttributes attributes) {
Optional<User> existingUser = userRepository.findByNodeId(attributes.nodeId());

return existingUser.map(user -> handleExistingUser(user, attributes))
.orElseGet(() -> handleNewUser(attributes));
.orElseGet(() -> registerNewUserOrFallback(attributes));
}

private RegisterUserResponse registerNewUserOrFallback(OAuthAttributes attributes) {
try {
return handleNewUser(attributes);
} catch (DataIntegrityViolationException e) {
User concurrentlyCreatedUser = userRepository.findByNodeId(attributes.nodeId())
.orElseThrow(() -> e);
return handleExistingUser(concurrentlyCreatedUser, attributes);
}
}

private RegisterUserResponse handleNewUser(OAuthAttributes attributes) {
Expand Down
Loading
Loading