diff --git a/eeos/src/main/java/com/blackcompany/eeos/program/application/model/ProgramModel.java b/eeos/src/main/java/com/blackcompany/eeos/program/application/model/ProgramModel.java index 6f3cc5a6..81214374 100644 --- a/eeos/src/main/java/com/blackcompany/eeos/program/application/model/ProgramModel.java +++ b/eeos/src/main/java/com/blackcompany/eeos/program/application/model/ProgramModel.java @@ -40,7 +40,7 @@ public class ProgramModel implements AbstractModel { private Long id; private String title; private String content; - private Timestamp programDate; + private Timestamp programDate; // programDate 는 행사 종료 시간 private String eventStatus; private ProgramCategory programCategory; private String githubUrl; diff --git a/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java b/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java index 567795f2..f65e628f 100644 --- a/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java +++ b/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java @@ -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(); + + delayedQueue.addTask(KEY, model.getId(), programDate); + } + + @Transactional + @Scheduled(cron = "0 0 0 * * *") + public void quitAttend() { + log.info("출석 체크 자동 종료 시작"); + try { + long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime(); + + Set jobs = getReadyTasks(programDate); + + Set completedIds = doQuit(jobs); + + removeCompleteTask(completedIds); + + } catch (Exception e) { + log.error("행사 자동 종료 중 에러가 발생하였습니다. {}", e.getMessage()); + throw e; + } + } + + private Set doQuit(Set programIds) { + if (!programIds.isEmpty()) { + Set 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 getReadyTasks(long programDate) { + Set jobs = + delayedQueue.getReadyTasks(KEY, (double) programDate).stream() + .map(id -> Long.parseLong(id.toString())) + .collect(Collectors.toSet()); + return jobs; + } + + private void removeCompleteTask(Set programIds) { + delayedQueue.removeByValue(KEY, programIds); } } diff --git a/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramService.java b/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramService.java index 138185c0..61807211 100644 --- a/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramService.java +++ b/eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramService.java @@ -90,7 +90,7 @@ public CommandProgramResponse create(final Long memberId, final CreateProgramReq attendTargetService.save(saveId, request.getMembers()); presentTeamUsecase.save(saveId, request.getTeamIds()); - quitUsecase.pushQuitAttendJob(model.toBuilder().id(saveId).build()); + quitUsecase.reserveQuitProgram(model.toBuilder().id(saveId).build()); return responseConverter.from(saveId); } diff --git a/eeos/src/main/java/com/blackcompany/eeos/program/application/support/DelayedQueue.java b/eeos/src/main/java/com/blackcompany/eeos/program/application/support/DelayedQueue.java new file mode 100644 index 00000000..3556eaac --- /dev/null +++ b/eeos/src/main/java/com/blackcompany/eeos/program/application/support/DelayedQueue.java @@ -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 redisTemplate; + + public void addTask(String key, Object value, double score) { + redisTemplate.opsForZSet().add(key, value, score); + } + + public Set getReadyTasks(String key, double score) { + Set 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); + } +} diff --git a/eeos/src/main/java/com/blackcompany/eeos/program/application/support/ProgramAttendScheduler.java b/eeos/src/main/java/com/blackcompany/eeos/program/application/support/ProgramAttendScheduler.java deleted file mode 100644 index 1b0edaa6..00000000 --- a/eeos/src/main/java/com/blackcompany/eeos/program/application/support/ProgramAttendScheduler.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.blackcompany.eeos.program.application.support; - -import com.blackcompany.eeos.program.persistence.RedisDelayedQueue; -import com.blackcompany.eeos.target.application.event.EndAttendModeEvent; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -@Component -@RequiredArgsConstructor -public class ProgramAttendScheduler { - - private final RedisDelayedQueue redisDelayedQueue; - private final ApplicationEventPublisher eventPublisher; - - @Transactional - @Scheduled(cron = "0 0 0 * * *") - public void quitAttend() { - Set jobs = redisDelayedQueue.getReadyTasks(); - eventPublisher.publishEvent(EndAttendModeEvent.of(jobs)); - } -} diff --git a/eeos/src/main/java/com/blackcompany/eeos/program/application/usecase/ProgramQuitUsecase.java b/eeos/src/main/java/com/blackcompany/eeos/program/application/usecase/ProgramQuitUsecase.java index 1738c0f6..d0150311 100644 --- a/eeos/src/main/java/com/blackcompany/eeos/program/application/usecase/ProgramQuitUsecase.java +++ b/eeos/src/main/java/com/blackcompany/eeos/program/application/usecase/ProgramQuitUsecase.java @@ -4,5 +4,5 @@ public interface ProgramQuitUsecase { - void pushQuitAttendJob(ProgramModel model); + void reserveQuitProgram(ProgramModel model); } diff --git a/eeos/src/main/java/com/blackcompany/eeos/program/persistence/RedisDelayedQueue.java b/eeos/src/main/java/com/blackcompany/eeos/program/persistence/RedisDelayedQueue.java deleted file mode 100644 index 823ddf13..00000000 --- a/eeos/src/main/java/com/blackcompany/eeos/program/persistence/RedisDelayedQueue.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.blackcompany.eeos.program.persistence; - -import java.util.Set; -import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class RedisDelayedQueue { - - private final RedisTemplate redisTemplate; - private final String QUEUE_KEY = "REDIS_QUEUE_KEY"; - - public void addTask(Long programId, long delayInSeconds) { - long executionTime = System.currentTimeMillis() + (delayInSeconds * 1000); - redisTemplate.opsForZSet().add(QUEUE_KEY, programId, executionTime); - } - - public Set getReadyTasks() { - long now = System.currentTimeMillis(); - Set tasks = - redisTemplate.opsForZSet().rangeByScore(QUEUE_KEY, 0, now).stream() - .map(e -> (Long) e) - .collect(Collectors.toSet()); - - if (tasks != null && !tasks.isEmpty()) { - redisTemplate.opsForZSet().removeRangeByScore(QUEUE_KEY, 0, now); - } - - return tasks; - } -} diff --git a/eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEvent.java b/eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEvent.java deleted file mode 100644 index acb75d99..00000000 --- a/eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEvent.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.blackcompany.eeos.target.application.event; - -import java.util.Set; -import lombok.Getter; - -@Getter -public class EndAttendModeEvent { - - private final Set programIds; - - private EndAttendModeEvent(Set programIds) { - this.programIds = programIds; - } - - public static EndAttendModeEvent of(Set programIds) { - return new EndAttendModeEvent(programIds); - } -} diff --git a/eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEventListener.java b/eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEventListener.java deleted file mode 100644 index 25e1a340..00000000 --- a/eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEventListener.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.blackcompany.eeos.target.application.event; - -import com.blackcompany.eeos.program.application.model.ProgramAttendMode; -import com.blackcompany.eeos.program.persistence.ProgramRepository; -import com.blackcompany.eeos.target.application.model.AttendStatus; -import com.blackcompany.eeos.target.persistence.AttendRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -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 -@Slf4j -public class EndAttendModeEventListener { - - private final AttendRepository attendRepository; - 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()); - - for (Long id : event.getProgramIds()) { - programRepository.changeAttendMode(id, ProgramAttendMode.END); - attendRepository.updateAttendStatusByProgramId( - id, AttendStatus.NONRESPONSE, AttendStatus.ABSENT); - } - - if (event.getProgramIds().isEmpty()) { - log.info("종료할 프로그램이 없습니다."); - } - } -}