-
Notifications
You must be signed in to change notification settings - Fork 1
[FIX] 행사 종료 시, 출석 체크가 자동으로 종료되지 않는 오류를 수정합니다. #250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
d6a2394
2636968
d673d49
105748a
8a12680
562c283
a78e08d
440bb47
67854ca
4b9bd99
fdf24f7
245b944
3b767f5
fedeb83
61be00e
a965482
eb9c78e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,23 +1,90 @@ | ||||||
| package com.blackcompany.eeos.program.application.service; | ||||||
|
|
||||||
| import com.blackcompany.eeos.common.utils.DateConverter; | ||||||
| import com.blackcompany.eeos.program.application.model.ProgramAttendMode; | ||||||
| import com.blackcompany.eeos.program.application.model.ProgramModel; | ||||||
| import com.blackcompany.eeos.program.application.support.DelayedQueue; | ||||||
| import com.blackcompany.eeos.program.application.usecase.ProgramQuitUsecase; | ||||||
| import com.blackcompany.eeos.program.persistence.ProgramRepository; | ||||||
| import com.blackcompany.eeos.program.persistence.RedisDelayedQueue; | ||||||
| import java.time.Instant; | ||||||
| import com.blackcompany.eeos.target.application.model.AttendStatus; | ||||||
| import com.blackcompany.eeos.target.persistence.AttendRepository; | ||||||
| import java.time.LocalDate; | ||||||
| import java.util.HashSet; | ||||||
| import java.util.Set; | ||||||
| import java.util.stream.Collectors; | ||||||
| import lombok.RequiredArgsConstructor; | ||||||
| import lombok.extern.slf4j.Slf4j; | ||||||
| import org.springframework.scheduling.annotation.Scheduled; | ||||||
| import org.springframework.stereotype.Service; | ||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||
|
|
||||||
| @Slf4j | ||||||
| @Service | ||||||
| @RequiredArgsConstructor | ||||||
| public class ProgramQuitService implements ProgramQuitUsecase { | ||||||
|
|
||||||
| private final RedisDelayedQueue redisDelayedQueue; | ||||||
| private final DelayedQueue delayedQueue; | ||||||
| private final AttendRepository attendRepository; | ||||||
| private final ProgramRepository programRepository; | ||||||
| private final String KEY = "quit_program_reservation"; | ||||||
|
|
||||||
| @Override | ||||||
| public void pushQuitAttendJob(ProgramModel model) { | ||||||
| long delayedTime = model.getProgramDate().getTime() - Instant.now().toEpochMilli(); | ||||||
| redisDelayedQueue.addTask(model.getId(), delayedTime); | ||||||
| public void reserveQuitProgram(ProgramModel model) { | ||||||
| // programDate 를 score 로 사용 | ||||||
| long programDate = model.getProgramDate().getTime() / 1000; | ||||||
|
|
||||||
| delayedQueue.addTask(KEY, model.getId(), programDate); | ||||||
| } | ||||||
|
|
||||||
| @Transactional | ||||||
| @Scheduled(cron = "0 0 0 * * *") | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3. 스케줄링을 사용할 경우, 두 가지 방식이 가능할 것 같아요.
GPT에게 물어보니, 데이터가 많을 경우에는 큐 방식이 더 적절하다고 추천하더라고요. 😆 |
||||||
| public void quitAttend() { | ||||||
| log.info("출석 체크 자동 종료 시작"); | ||||||
| try { | ||||||
| long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime(); | ||||||
|
|
||||||
|
Comment on lines
+44
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DateConverter 사용 시 주의가 필요합니다. 이전 리뷰에서 논의된 바와 같이, DateConverter.toEpochSecond()는 Timestamp 객체를 반환하므로 .getTime()을 호출하여 long 값을 얻는 것이 맞습니다. 다만, 변환된 시간이 초 단위인지 밀리초 단위인지 일관성을 유지해야 합니다. reserveQuitProgram 메서드에서는 밀리초 값을 1000으로 나누어 초 단위로 변환하고 있는데, 여기서는 그대로 밀리초 값을 사용하고 있습니다. 두 메서드 간의 일관성이 필요합니다. - long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime();
+ long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime() / 1000;📝 Committable suggestion
Suggested change
|
||||||
| Set<Long> jobs = getReadyTasks(programDate); | ||||||
|
|
||||||
| Set<Long> completedIds = doQuit(jobs); | ||||||
|
|
||||||
| removeCompleteTask(completedIds); | ||||||
|
|
||||||
| } catch (Exception e) { | ||||||
| log.error("행사 자동 종료 중 에러가 발생하였습니다. {}", e.getMessage()); | ||||||
| throw e; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private Set<Long> doQuit(Set<Long> programIds) { | ||||||
| if (!programIds.isEmpty()) { | ||||||
| Set<Long> completedIds = new HashSet<>(); | ||||||
|
|
||||||
| for (Long id : programIds) { | ||||||
| log.info("출석 체크 자동 종료 (programId : {})", id); | ||||||
|
|
||||||
| programRepository.changeAttendMode(id, ProgramAttendMode.END); | ||||||
| attendRepository.updateAttendStatusByProgramId( | ||||||
| id, AttendStatus.NONRESPONSE, AttendStatus.ABSENT); | ||||||
|
|
||||||
| completedIds.add(id); | ||||||
| } | ||||||
|
|
||||||
| return completedIds; | ||||||
| } | ||||||
|
|
||||||
| log.info("종료할 행사가 존재하지 않습니다."); | ||||||
| return new HashSet<>(); | ||||||
| } | ||||||
|
|
||||||
| private Set<Long> getReadyTasks(long programDate) { | ||||||
kssumin marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| Set<Long> jobs = | ||||||
| delayedQueue.getReadyTasks(KEY, (double) programDate).stream() | ||||||
| .map(id -> Long.parseLong(id.toString())) | ||||||
| .collect(Collectors.toSet()); | ||||||
| return jobs; | ||||||
| } | ||||||
|
|
||||||
| private void removeCompleteTask(Set<Long> programIds) { | ||||||
| delayedQueue.removeByValue(KEY, programIds); | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package com.blackcompany.eeos.program.application.support; | ||
|
|
||
| import java.util.Set; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class DelayedQueue { | ||
|
|
||
| private final RedisTemplate<String, Object> redisTemplate; | ||
|
|
||
| public void addTask(String key, Object value, double score) { | ||
| redisTemplate.opsForZSet().add(key, value, score); | ||
| } | ||
|
|
||
| public Set<Object> getReadyTasks(String key, double score) { | ||
| Set<Object> tasks = redisTemplate.opsForZSet().rangeByScore(key, 0, score); | ||
|
|
||
| return tasks; | ||
| } | ||
|
|
||
| public void removeByScore(String key, double score) { | ||
| redisTemplate.opsForZSet().removeRangeByScore(key, 0, score); | ||
| } | ||
|
|
||
| public void removeByValue(String key, Object... value) { | ||
| redisTemplate.opsForZSet().remove(key, value); | ||
| } | ||
| } | ||
rlajm1203 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,13 +6,11 @@ | |
| import com.blackcompany.eeos.target.persistence.AttendRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.context.event.EventListener; | ||
| import org.springframework.scheduling.annotation.Async; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Propagation; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import org.springframework.transaction.event.TransactionPhase; | ||
| import org.springframework.transaction.event.TransactionalEventListener; | ||
| import org.springframework.transaction.support.TransactionSynchronizationManager; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
|
|
@@ -23,21 +21,21 @@ public class EndAttendModeEventListener { | |
| private final ProgramRepository programRepository; | ||
|
|
||
| @Async | ||
| @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) | ||
| @Transactional(propagation = Propagation.REQUIRES_NEW) | ||
| public void handleDeletedProgram(EndAttendModeEvent event) { | ||
| log.info( | ||
| "출석 체크 종료 Transaction committed: {}", | ||
| TransactionSynchronizationManager.isActualTransactionActive()); | ||
| @EventListener(EndAttendModeEvent.class) | ||
| public void handle(EndAttendModeEvent event) { | ||
|
||
| log.info("출석 체크 자동 종료 시작"); | ||
|
|
||
| for (Long id : event.getProgramIds()) { | ||
| programRepository.changeAttendMode(id, ProgramAttendMode.END); | ||
| attendRepository.updateAttendStatusByProgramId( | ||
| id, AttendStatus.NONRESPONSE, AttendStatus.ABSENT); | ||
| if (!event.getProgramIds().isEmpty()) { | ||
| for (Long id : event.getProgramIds()) { | ||
| log.info("출석 체크 자동 종료 (programId : {})", id); | ||
| programRepository.changeAttendMode(id, ProgramAttendMode.END); | ||
| attendRepository.updateAttendStatusByProgramId( | ||
| id, AttendStatus.NONRESPONSE, AttendStatus.ABSENT); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (event.getProgramIds().isEmpty()) { | ||
| log.info("종료할 프로그램이 없습니다."); | ||
| } | ||
| log.info("종료할 프로그램이 없습니다."); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
p3.
현재 구조에서는 행사 자동 종료 중 하나에서 에러가 발생하면, 상위에서 트랜잭션이 관리되기 때문에 모든 행사의 자동 종료가 중단될 가능성이 있을 것 같아요.
하지만 한 프로그램의 자동 종료가 실패했다고 해서 다른 프로그램까지 롤백되거나 시도조차 못 할 이유는 없다고 생각해요.
그래서 개별 단위로 트랜잭션을 관리하는 방식도 좋은 대안이 될 것 같아요!