From 54db79a1cc3813f163124aa32fce931a6e6da162 Mon Sep 17 00:00:00 2001 From: yoo20370 Date: Mon, 29 Dec 2025 23:27:41 +0900 Subject: [PATCH 01/10] =?UTF-8?q?fix(Bid)=20:=20Outbox=20updatedAt=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=88=84=EB=9D=BD=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/outbox/OutboxJpaRepositoryCustomImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java b/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java index d25e39a5..3ef6c62a 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java @@ -7,6 +7,7 @@ import com.smore.bidcompetition.domain.status.EventStatus; import com.smore.bidcompetition.infrastructure.persistence.entity.QOutboxEntity; import jakarta.persistence.EntityManager; +import java.time.LocalDateTime; import java.util.Collection; import java.util.List; import lombok.RequiredArgsConstructor; @@ -50,6 +51,7 @@ public int claim(Long outboxId, EventStatus eventStatus) { long updated = queryFactory .update(outboxEntity) .set(outboxEntity.eventStatus, eventStatus) + .set(outboxEntity.updatedAt, LocalDateTime.now()) .where( outboxEntity.id.eq(outboxId), outboxEntity.eventStatus.eq(EventStatus.PENDING) @@ -67,6 +69,7 @@ public int markSent(Long outboxId, EventStatus eventStatus) { long updated = queryFactory .update(outboxEntity) .set(outboxEntity.eventStatus, eventStatus) + .set(outboxEntity.updatedAt, LocalDateTime.now()) .where( outboxEntity.id.eq(outboxId), outboxEntity.eventStatus.eq(EventStatus.PROCESSING) @@ -85,6 +88,7 @@ public int markRetry(Long outboxId, EventStatus eventStatus) { .update(outboxEntity) .set(outboxEntity.eventStatus, eventStatus) .set(outboxEntity.retryCount, outboxEntity.retryCount.add(1)) + .set(outboxEntity.updatedAt, LocalDateTime.now()) .where( outboxEntity.id.eq(outboxId), outboxEntity.eventStatus.eq(EventStatus.PROCESSING) @@ -102,6 +106,7 @@ public int markFail(Long outboxId, EventStatus eventStatus, Integer maxRetryCoun long updated = queryFactory .update(outboxEntity) .set(outboxEntity.eventStatus, eventStatus) + .set(outboxEntity.updatedAt, LocalDateTime.now()) .where( outboxEntity.id.eq(outboxId), outboxEntity.eventStatus.eq(EventStatus.PROCESSING), From 7890a0cd2b5442e51bfbbc8be1969703e9d67d30 Mon Sep 17 00:00:00 2001 From: yoo20370 Date: Mon, 29 Dec 2025 23:33:26 +0900 Subject: [PATCH 02/10] =?UTF-8?q?fix(Order)=20:=20Outbox=20updatedAt=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=88=84=EB=9D=BD=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/outbox/OutboxJpaRepositoryCustomImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java b/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java index 9b78699c..ce22e9ce 100644 --- a/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java +++ b/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java @@ -7,6 +7,7 @@ import com.smore.order.domain.status.EventStatus; import jakarta.persistence.EntityManager; +import java.time.LocalDateTime; import java.util.Collection; import java.util.List; import lombok.RequiredArgsConstructor; @@ -50,6 +51,7 @@ public int claim(Long outboxId, EventStatus eventStatus) { long updated = queryFactory .update(outboxEntity) .set(outboxEntity.eventStatus, eventStatus) + .set(outboxEntity.updatedAt, LocalDateTime.now()) .where( outboxEntity.id.eq(outboxId), outboxEntity.eventStatus.eq(EventStatus.PENDING) @@ -67,6 +69,7 @@ public int markSent(Long outboxId, EventStatus eventStatus) { long updated = queryFactory .update(outboxEntity) .set(outboxEntity.eventStatus, eventStatus) + .set(outboxEntity.updatedAt, LocalDateTime.now()) .where( outboxEntity.id.eq(outboxId), outboxEntity.eventStatus.eq(EventStatus.PROCESSING) @@ -85,6 +88,7 @@ public int markRetry(Long outboxId, EventStatus eventStatus) { .update(outboxEntity) .set(outboxEntity.eventStatus, eventStatus) .set(outboxEntity.retryCount, outboxEntity.retryCount.add(1)) + .set(outboxEntity.updatedAt, LocalDateTime.now()) .where( outboxEntity.id.eq(outboxId), outboxEntity.eventStatus.eq(EventStatus.PROCESSING) @@ -102,6 +106,7 @@ public int markFail(Long outboxId, EventStatus eventStatus, Integer maxRetryCoun long updated = queryFactory .update(outboxEntity) .set(outboxEntity.eventStatus, eventStatus) + .set(outboxEntity.updatedAt, LocalDateTime.now()) .where( outboxEntity.id.eq(outboxId), outboxEntity.eventStatus.eq(EventStatus.PROCESSING), From cb6822eb84dae9b529a52e5501ac364a7107ae68 Mon Sep 17 00:00:00 2001 From: yoo20370 Date: Mon, 29 Dec 2025 23:36:12 +0900 Subject: [PATCH 03/10] =?UTF-8?q?fix(Bid)=20:=20processing=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=98=81=EA=B5=AC=20=EA=B3=A0=EC=B0=A9=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20(=EB=A7=8C=EB=A3=8C=20?= =?UTF-8?q?=ED=9A=8C=EC=88=98=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/repository/OutboxRepository.java | 6 ++++++ .../presentation/scheduler/OutboxScheduler.java | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/application/repository/OutboxRepository.java b/bidcompetition/src/main/java/com/smore/bidcompetition/application/repository/OutboxRepository.java index 7a0db99b..78b2320d 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/application/repository/OutboxRepository.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/application/repository/OutboxRepository.java @@ -2,7 +2,9 @@ import com.smore.bidcompetition.domain.status.EventStatus; import com.smore.bidcompetition.domain.model.Outbox; +import java.time.LocalDateTime; import java.util.Collection; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -12,6 +14,8 @@ public interface OutboxRepository { Page findPendingIds(Collection states, Pageable pageable); + List findExpiredProcessingIds(LocalDateTime expiredAt); + Outbox save(Outbox outbox); int claim(Long outboxId, EventStatus eventStatus); @@ -22,4 +26,6 @@ public interface OutboxRepository { int markFail(Long outboxId, EventStatus eventStatus, Integer maxRetryCount); + int bulkResetExpiredProcessingToReady(List ids); + } diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/presentation/scheduler/OutboxScheduler.java b/bidcompetition/src/main/java/com/smore/bidcompetition/presentation/scheduler/OutboxScheduler.java index 5a704645..05d86e64 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/presentation/scheduler/OutboxScheduler.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/presentation/scheduler/OutboxScheduler.java @@ -4,6 +4,8 @@ import com.smore.bidcompetition.application.repository.OutboxRepository; import com.smore.bidcompetition.application.service.OutboxProcessor; import com.smore.bidcompetition.domain.status.EventStatus; +import java.time.LocalDateTime; +import java.util.List; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -49,4 +51,17 @@ public void outboxTasks() { page++; } } + + @Scheduled(fixedDelay = 60000) + public void recoverExpiredProcessing() { + LocalDateTime expiredAt = LocalDateTime.now().minusMinutes(2); + + List expiredProcessingIds = outboxRepository.findExpiredProcessingIds(expiredAt); + + if (expiredProcessingIds.isEmpty()) { + return; + } + + int updated = outboxRepository.bulkResetExpiredProcessingToReady(expiredProcessingIds); + } } From 969cb7976d07c089e4138c1e58669eb91c4089f2 Mon Sep 17 00:00:00 2001 From: yoo20370 Date: Mon, 29 Dec 2025 23:37:26 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat(Bid)=20:=20Outbox=20PROCESSING=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=A7=8C=EB=A3=8C=20=ED=9A=8C=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20find,=20bulkUpdate=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../outbox/OutboxJpaRepositoryCustom.java | 6 ++++ .../outbox/OutboxJpaRepositoryCustomImpl.java | 32 +++++++++++++++++++ .../outbox/OutboxRepositoryImpl.java | 12 +++++++ 3 files changed, 50 insertions(+) diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustom.java b/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustom.java index 209d571c..8f78abd7 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustom.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustom.java @@ -1,7 +1,9 @@ package com.smore.bidcompetition.infrastructure.persistence.repository.outbox; import com.smore.bidcompetition.domain.status.EventStatus; +import java.time.LocalDateTime; import java.util.Collection; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -9,6 +11,8 @@ public interface OutboxJpaRepositoryCustom { Page findPendingIds(Collection states, Pageable pageable); + List findExpiredProcessingIds(LocalDateTime expiredAt); + int claim(Long outboxId, EventStatus eventStatus); int markSent(Long outboxId, EventStatus eventStatus); @@ -17,4 +21,6 @@ public interface OutboxJpaRepositoryCustom { int markFail(Long outboxId, EventStatus eventStatus, Integer maxRetryCount); + int bulkResetExpiredProcessingToReady(List ids); + } diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java b/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java index 3ef6c62a..eb1a8239 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java @@ -45,6 +45,19 @@ public Page findPendingIds(Collection states, Pageable pageab return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } + @Override + public List findExpiredProcessingIds(LocalDateTime expiredAt) { + + return queryFactory + .select(outboxEntity.id) + .from(outboxEntity) + .where( + outboxEntity.eventStatus.eq(EventStatus.PROCESSING), + outboxEntity.updatedAt.loe(expiredAt) + ) + .fetch(); + } + @Override public int claim(Long outboxId, EventStatus eventStatus) { @@ -120,4 +133,23 @@ public int markFail(Long outboxId, EventStatus eventStatus, Integer maxRetryCoun return (int) updated; } + @Override + public int bulkResetExpiredProcessingToReady(List ids) { + long updated = queryFactory + .update(outboxEntity) + .set(outboxEntity.eventStatus, EventStatus.PENDING) + .set(outboxEntity.retryCount, outboxEntity.retryCount.add(1)) + .set(outboxEntity.updatedAt, LocalDateTime.now()) + .where( + outboxEntity.id.in(ids), + outboxEntity.eventStatus.eq(EventStatus.PROCESSING) + ) + .execute(); + + em.flush(); + em.clear(); + + return (int) updated; + } + } diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxRepositoryImpl.java b/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxRepositoryImpl.java index b687a478..03bd5ce6 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxRepositoryImpl.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/persistence/repository/outbox/OutboxRepositoryImpl.java @@ -9,7 +9,9 @@ import com.smore.bidcompetition.infrastructure.persistence.exception.CreateOutboxFailException; import com.smore.bidcompetition.infrastructure.persistence.exception.NotFoundOutboxException; import com.smore.bidcompetition.infrastructure.persistence.mapper.OutboxMapper; +import java.time.LocalDateTime; import java.util.Collection; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -55,6 +57,11 @@ public Page findPendingIds(Collection states, Pageable pageab return outboxJpaRepository.findPendingIds(states, pageable); } + @Override + public List findExpiredProcessingIds(LocalDateTime expiredAt) { + return outboxJpaRepository.findExpiredProcessingIds(expiredAt); + } + @Override public Outbox save(Outbox outbox) { @@ -125,4 +132,9 @@ public int markFail(Long outboxId, EventStatus eventStatus, Integer maxRetryCoun return outboxJpaRepository.markFail(outboxId, eventStatus, maxRetryCount); } + + @Override + public int bulkResetExpiredProcessingToReady(List ids) { + return outboxJpaRepository.bulkResetExpiredProcessingToReady(ids); + } } From 9a2a5074327372e2df3a19a996bf6fe45d1bceed Mon Sep 17 00:00:00 2001 From: yoo20370 Date: Mon, 29 Dec 2025 23:42:29 +0900 Subject: [PATCH 05/10] =?UTF-8?q?fix(Order)=20:=20processing=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=98=81=EA=B5=AC=20=EA=B3=A0=EC=B0=A9=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20(=EB=A7=8C=EB=A3=8C=20?= =?UTF-8?q?=ED=9A=8C=EC=88=98=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/repository/OutboxRepository.java | 6 ++++++ .../presentation/scheduler/OutboxScheduler.java | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/order/src/main/java/com/smore/order/application/repository/OutboxRepository.java b/order/src/main/java/com/smore/order/application/repository/OutboxRepository.java index c7e9c24c..d55065c3 100644 --- a/order/src/main/java/com/smore/order/application/repository/OutboxRepository.java +++ b/order/src/main/java/com/smore/order/application/repository/OutboxRepository.java @@ -2,7 +2,9 @@ import com.smore.order.domain.model.Outbox; import com.smore.order.domain.status.EventStatus; +import java.time.LocalDateTime; import java.util.Collection; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -12,6 +14,8 @@ public interface OutboxRepository { Page findPendingIds(Collection states, Pageable pageable); + List findExpiredProcessingIds(LocalDateTime expiredAt); + Outbox save(Outbox outbox); int claim(Long outboxId, EventStatus eventStatus); @@ -22,4 +26,6 @@ public interface OutboxRepository { int markFail(Long outboxId, EventStatus eventStatus, Integer maxRetryCount); + int bulkResetExpiredProcessingToReady(List ids); + } diff --git a/order/src/main/java/com/smore/order/presentation/scheduler/OutboxScheduler.java b/order/src/main/java/com/smore/order/presentation/scheduler/OutboxScheduler.java index 2c76db72..27bb0460 100644 --- a/order/src/main/java/com/smore/order/presentation/scheduler/OutboxScheduler.java +++ b/order/src/main/java/com/smore/order/presentation/scheduler/OutboxScheduler.java @@ -4,6 +4,8 @@ import com.smore.order.application.service.OutboxProcessor; import com.smore.order.domain.status.EventStatus; +import java.time.LocalDateTime; +import java.util.List; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -50,4 +52,17 @@ public void outboxTasks() { page++; } } + + @Scheduled(fixedDelay = 60000) + public void recoverExpiredProcessing() { + LocalDateTime expiredAt = LocalDateTime.now().minusMinutes(2); + + List expiredProcessingIds = outboxRepository.findExpiredProcessingIds(expiredAt); + + if (expiredProcessingIds.isEmpty()) { + return; + } + + int updated = outboxRepository.bulkResetExpiredProcessingToReady(expiredProcessingIds); + } } From b719a93bf996771a4d88927763b28cc6a9df1fb1 Mon Sep 17 00:00:00 2001 From: yoo20370 Date: Mon, 29 Dec 2025 23:43:12 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat(Order)=20:=20Outbox=20PROCESSING=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=A7=8C=EB=A3=8C=20=ED=9A=8C=EC=88=98?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20find,=20bulkUpade=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../outbox/OutboxJpaRepositoryCustom.java | 6 ++++ .../outbox/OutboxJpaRepositoryCustomImpl.java | 32 +++++++++++++++++++ .../outbox/OutboxRepositoryImpl.java | 12 +++++++ 3 files changed, 50 insertions(+) diff --git a/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustom.java b/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustom.java index 356a3bf5..0eaae4b3 100644 --- a/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustom.java +++ b/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustom.java @@ -1,7 +1,9 @@ package com.smore.order.infrastructure.persistence.repository.outbox; import com.smore.order.domain.status.EventStatus; +import java.time.LocalDateTime; import java.util.Collection; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -9,6 +11,8 @@ public interface OutboxJpaRepositoryCustom { Page findPendingIds(Collection states, Pageable pageable); + List findExpiredProcessingIds(LocalDateTime expiredAt); + int claim(Long outboxId, EventStatus eventStatus); int markSent(Long outboxId, EventStatus eventStatus); @@ -17,4 +21,6 @@ public interface OutboxJpaRepositoryCustom { int markFail(Long outboxId, EventStatus eventStatus, Integer maxRetryCount); + int bulkResetExpiredProcessingToReady(List ids); + } diff --git a/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java b/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java index ce22e9ce..d837b07a 100644 --- a/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java +++ b/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxJpaRepositoryCustomImpl.java @@ -45,6 +45,19 @@ public Page findPendingIds(Collection states, Pageable pageab return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne); } + @Override + public List findExpiredProcessingIds(LocalDateTime expiredAt) { + + return queryFactory + .select(outboxEntity.id) + .from(outboxEntity) + .where( + outboxEntity.eventStatus.eq(EventStatus.PROCESSING), + outboxEntity.updatedAt.loe(expiredAt) + ) + .fetch(); + } + @Override public int claim(Long outboxId, EventStatus eventStatus) { @@ -120,4 +133,23 @@ public int markFail(Long outboxId, EventStatus eventStatus, Integer maxRetryCoun return (int) updated; } + @Override + public int bulkResetExpiredProcessingToReady(List ids) { + long updated = queryFactory + .update(outboxEntity) + .set(outboxEntity.eventStatus, EventStatus.PENDING) + .set(outboxEntity.retryCount, outboxEntity.retryCount.add(1)) + .set(outboxEntity.updatedAt, LocalDateTime.now()) + .where( + outboxEntity.id.in(ids), + outboxEntity.eventStatus.eq(EventStatus.PROCESSING) + ) + .execute(); + + em.flush(); + em.clear(); + + return (int) updated; + } + } diff --git a/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxRepositoryImpl.java b/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxRepositoryImpl.java index 0c8c07b7..e38d3040 100644 --- a/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxRepositoryImpl.java +++ b/order/src/main/java/com/smore/order/infrastructure/persistence/repository/outbox/OutboxRepositoryImpl.java @@ -8,7 +8,9 @@ import com.smore.order.infrastructure.persistence.exception.CreateOutboxFailException; import com.smore.order.infrastructure.persistence.exception.NotFoundOutboxException; import com.smore.order.infrastructure.persistence.mapper.OutboxMapper; +import java.time.LocalDateTime; import java.util.Collection; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -54,6 +56,11 @@ public Page findPendingIds(Collection states, Pageable pageab return outboxJpaRepository.findPendingIds(states, pageable); } + @Override + public List findExpiredProcessingIds(LocalDateTime expiredAt) { + return outboxJpaRepository.findExpiredProcessingIds(expiredAt); + } + @Override public Outbox save(Outbox outbox) { @@ -124,4 +131,9 @@ public int markFail(Long outboxId, EventStatus eventStatus, Integer maxRetryCoun return outboxJpaRepository.markFail(outboxId, eventStatus, maxRetryCount); } + + @Override + public int bulkResetExpiredProcessingToReady(List ids) { + return outboxJpaRepository.bulkResetExpiredProcessingToReady(ids); + } } From f3c129752d014fd46cc7dd983a0776e43a58216d Mon Sep 17 00:00:00 2001 From: yoo20370 Date: Tue, 30 Dec 2025 02:40:00 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat(Bid)=20:=20Outbox=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=A0=20EvnetType=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20SAVE=5FSTOCK,=20DELETE=5FSTOCK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/smore/bidcompetition/domain/status/EventType.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/domain/status/EventType.java b/bidcompetition/src/main/java/com/smore/bidcompetition/domain/status/EventType.java index 02218ea7..e7b63b41 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/domain/status/EventType.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/domain/status/EventType.java @@ -4,7 +4,9 @@ public enum EventType { BID_WINNER_SELECTED("경쟁 승리자 선정"), PRODUCT_INVENTORY_ADJUSTED("환불"), BID_RESULT_FINALIZED("경쟁 결과 최종 확정"), - BID_INVENTORY_CONFIRM_TIMEOUT("재고 확보 실패") + BID_INVENTORY_CONFIRM_TIMEOUT("재고 확보 실패"), + SAVE_STOCK("재고 저장"), + DELETE_STOCK("재고 클리어") ; private final String description; From 200ee18297e9527cee1fc905b6e5ce73937b0c7c Mon Sep 17 00:00:00 2001 From: yoo20370 Date: Tue, 30 Dec 2025 02:41:39 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat(Bid)=20:=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=EC=97=90=20=EC=B4=88=EA=B8=B0=20=EC=9E=AC=EA=B3=A0?= =?UTF-8?q?=EB=A5=BC=20=EB=93=B1=EB=A1=9D=ED=95=98=EB=8A=94=20OutboxHandle?= =?UTF-8?q?r=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20Factory=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EC=97=90=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../factory/OutboxHandlerFactory.java | 5 ++ .../application/handler/SaveStockHandler.java | 70 +++++++++++++++++++ .../service/BidCompetitionService.java | 27 +++---- 3 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 bidcompetition/src/main/java/com/smore/bidcompetition/application/handler/SaveStockHandler.java diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/application/factory/OutboxHandlerFactory.java b/bidcompetition/src/main/java/com/smore/bidcompetition/application/factory/OutboxHandlerFactory.java index cac5b0d1..f70fc580 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/application/factory/OutboxHandlerFactory.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/application/factory/OutboxHandlerFactory.java @@ -1,11 +1,14 @@ package com.smore.bidcompetition.application.factory; import com.smore.bidcompetition.application.handler.BidResultFinalizedHandler; +import com.smore.bidcompetition.application.handler.ClearStockHandler; import com.smore.bidcompetition.application.handler.InventoryConfirmTimeout; import com.smore.bidcompetition.application.handler.ProductInventoryAdjustedHandler; import com.smore.bidcompetition.application.handler.OutboxHandler; +import com.smore.bidcompetition.application.handler.SaveStockHandler; import com.smore.bidcompetition.application.handler.WinnerCreatedHandler; import com.smore.bidcompetition.domain.model.Outbox; +import com.smore.bidcompetition.infrastructure.redis.StockRedisService; import io.micrometer.tracing.propagation.Propagator; import io.micrometer.tracing.Tracer; import lombok.RequiredArgsConstructor; @@ -32,6 +35,7 @@ public class OutboxHandlerFactory { private final KafkaTemplate kafkaTemplate; private final Tracer tracer; private final Propagator propagator; + private final StockRedisService stockRedisService; public OutboxHandler from(Outbox outbox) { return switch (outbox.getEventType()) { @@ -39,6 +43,7 @@ public OutboxHandler from(Outbox outbox) { case PRODUCT_INVENTORY_ADJUSTED -> new ProductInventoryAdjustedHandler(tracer, propagator, productInventoryAdjustedTopic, kafkaTemplate, outbox); case BID_RESULT_FINALIZED -> new BidResultFinalizedHandler(tracer, propagator, bidResultFinalizedTopic, kafkaTemplate, outbox); case BID_INVENTORY_CONFIRM_TIMEOUT -> new InventoryConfirmTimeout(tracer, propagator, bidInventoryConfirmTimeoutTopic, kafkaTemplate, outbox); + case SAVE_STOCK -> new SaveStockHandler(tracer, propagator, stockRedisService, outbox); default -> throw new IllegalArgumentException( "지원되지 않은 이벤트입니다." + outbox.getEventType() ); diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/application/handler/SaveStockHandler.java b/bidcompetition/src/main/java/com/smore/bidcompetition/application/handler/SaveStockHandler.java new file mode 100644 index 00000000..9ae4cee5 --- /dev/null +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/application/handler/SaveStockHandler.java @@ -0,0 +1,70 @@ +package com.smore.bidcompetition.application.handler; + +import com.smore.bidcompetition.domain.model.Outbox; +import com.smore.bidcompetition.domain.status.OutboxResult; +import com.smore.bidcompetition.infrastructure.redis.StockRedisService; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.propagation.Propagator; +import lombok.extern.slf4j.Slf4j; + +@Slf4j(topic = "SaveStockHandler") +public class SaveStockHandler implements OutboxHandler{ + + private final StockRedisService stockRedisService; + private final Outbox outbox; + private final Tracer tracer; + private final Propagator propagator; + + public SaveStockHandler(Tracer tracer, Propagator propagator, + StockRedisService stockRedisService, Outbox outbox) { + this.tracer = tracer; + this.propagator = propagator; + this.stockRedisService = stockRedisService; + this.outbox = outbox; + } + + @Override + public OutboxResult execute() { + + Span newSpan = restoreAndStartSpan(); + + try (Tracer.SpanInScope ws = tracer.withSpan(newSpan)) { + try { + long setResult = stockRedisService.setStock(outbox.getAggregateId(), Integer.parseInt(outbox.getPayload())); + + if (setResult == -1L) { + log.error("재고 초기화 실패: bidId={}, stock={}",outbox.getAggregateId(), Integer.parseInt(outbox.getPayload())); + return OutboxResult.FAIL; + } else if (setResult == 0L) { + log.info("이미 재고 키가 존재합니다. bidId={}", outbox.getAggregateId(), Integer.parseInt(outbox.getPayload())); + } else { + log.info("재고 초기화 완료: bidId={}, stock={}", outbox.getAggregateId(), Integer.parseInt(outbox.getPayload())); + } + } catch (Exception e) { + log.error("재고 초기화 중 예외 발생. bidId={}", outbox.getAggregateId(), e); + return OutboxResult.FAIL; + } + return OutboxResult.SUCCESS; + } finally { + newSpan.end(); + } + } + + private Span restoreAndStartSpan() { + Span.Builder spanBuilder = propagator.extract(outbox, (carrier, key) -> { + if ("X-B3-TraceId".equalsIgnoreCase(key)) { + return carrier.getTraceId(); + } + if ("X-B3-SpanId".equalsIgnoreCase(key)) { + return carrier.getSpanId(); + } + return null; + }); + + Span newSpan = spanBuilder + .name("redis-saved-stock") + .start(); + return newSpan; + } +} diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/application/service/BidCompetitionService.java b/bidcompetition/src/main/java/com/smore/bidcompetition/application/service/BidCompetitionService.java index fb72a2bb..97a989bc 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/application/service/BidCompetitionService.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/application/service/BidCompetitionService.java @@ -89,19 +89,22 @@ public void createBid(BidCreateCommand command) { BidCompetition saved = bidCompetitionRepository.save(newBid); - try { - long setResult = stockRedisService.setStock(saved.getId(), saved.getTotalQuantity()); - - if (setResult == -1L) { - log.error("재고 초기화 실패: bidId={}, stock={}", saved.getId(), saved.getTotalQuantity()); - } else if (setResult == 0L) { - log.info("이미 재고 키가 존재합니다. bidId={}", saved.getId()); - } else { - log.info("재고 초기화 완료: bidId={}, stock={}", saved.getId(), setResult); - } - } catch (Exception e) { - log.error("재고 초기화 중 예외 발생. bidId={}", saved.getId(), e); + Outbox outbox = Outbox.create( + AggregateType.BID, + saved.getId(), + EventType.SAVE_STOCK, + UUID.randomUUID(), + String.valueOf(saved.getTotalQuantity()) + ); + + if (tracer.currentSpan() != null) { + outbox.attachTracing( + tracer.currentSpan().context().traceId(), + tracer.currentSpan().context().spanId() + ); } + + outboxRepository.save(outbox); } @Transactional From 9cdf0ff4438de2ef4e61b50d36ce27248b106c72 Mon Sep 17 00:00:00 2001 From: yoo20370 Date: Tue, 30 Dec 2025 02:42:47 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat(Bid)=20:=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=EC=97=90=EC=84=9C=20=EC=9E=AC=EA=B3=A0=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=98=EB=8A=94=20StockRedisService?= =?UTF-8?q?=EC=9D=98=20deleteStock()=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../infrastructure/redis/StockRedisService.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/redis/StockRedisService.java b/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/redis/StockRedisService.java index f3b98cc4..58058e48 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/redis/StockRedisService.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/infrastructure/redis/StockRedisService.java @@ -74,4 +74,9 @@ public long setStock(UUID bidId, int stockQuantity) { String.valueOf(stockQuantity) ); } + + public boolean deleteStock(UUID bidId) { + Boolean deleted = redis.delete(keys.stockKey(bidId)); + return Boolean.TRUE.equals(deleted); + } } From 4bc771baba2ddaba301136071590742e3d567292 Mon Sep 17 00:00:00 2001 From: yoo20370 Date: Tue, 30 Dec 2025 02:43:39 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat(Bid)=20:=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=EC=97=90=EC=84=9C=20=EC=9E=AC=EA=B3=A0=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EC=A0=9C=EA=B1=B0=ED=95=98=EB=8A=94=20Out?= =?UTF-8?q?boxHandler=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20Factory=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EC=97=90=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../factory/OutboxHandlerFactory.java | 1 + .../handler/ClearStockHandler.java | 66 +++++++++++++++++++ .../application/service/BidEndFinalizer.java | 17 +++++ 3 files changed, 84 insertions(+) create mode 100644 bidcompetition/src/main/java/com/smore/bidcompetition/application/handler/ClearStockHandler.java diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/application/factory/OutboxHandlerFactory.java b/bidcompetition/src/main/java/com/smore/bidcompetition/application/factory/OutboxHandlerFactory.java index f70fc580..59fa7c7a 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/application/factory/OutboxHandlerFactory.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/application/factory/OutboxHandlerFactory.java @@ -44,6 +44,7 @@ public OutboxHandler from(Outbox outbox) { case BID_RESULT_FINALIZED -> new BidResultFinalizedHandler(tracer, propagator, bidResultFinalizedTopic, kafkaTemplate, outbox); case BID_INVENTORY_CONFIRM_TIMEOUT -> new InventoryConfirmTimeout(tracer, propagator, bidInventoryConfirmTimeoutTopic, kafkaTemplate, outbox); case SAVE_STOCK -> new SaveStockHandler(tracer, propagator, stockRedisService, outbox); + case DELETE_STOCK -> new ClearStockHandler(tracer, propagator, stockRedisService, outbox); default -> throw new IllegalArgumentException( "지원되지 않은 이벤트입니다." + outbox.getEventType() ); diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/application/handler/ClearStockHandler.java b/bidcompetition/src/main/java/com/smore/bidcompetition/application/handler/ClearStockHandler.java new file mode 100644 index 00000000..b5220238 --- /dev/null +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/application/handler/ClearStockHandler.java @@ -0,0 +1,66 @@ +package com.smore.bidcompetition.application.handler; + +import com.smore.bidcompetition.domain.model.Outbox; +import com.smore.bidcompetition.domain.status.OutboxResult; +import com.smore.bidcompetition.infrastructure.redis.StockRedisService; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.propagation.Propagator; +import lombok.extern.slf4j.Slf4j; + +@Slf4j(topic = "ClearStockHandler") +public class ClearStockHandler implements OutboxHandler{ + + private final StockRedisService stockRedisService; + private final Outbox outbox; + private final Tracer tracer; + private final Propagator propagator; + + public ClearStockHandler(Tracer tracer, Propagator propagator, + StockRedisService stockRedisService, Outbox outbox) { + this.tracer = tracer; + this.propagator = propagator; + this.stockRedisService = stockRedisService; + this.outbox = outbox; + } + + @Override + public OutboxResult execute() { + + Span newSpan = restoreAndStartSpan(); + + try (Tracer.SpanInScope ws = tracer.withSpan(newSpan)) { + try { + boolean deleted = stockRedisService.deleteStock(outbox.getAggregateId()); + if (!deleted) { + log.info("stock 키가 없어 삭제할 게 없습니다. bidId={}", outbox.getAggregateId()); + } else { + log.info("stock 키 삭제 완료. bidId={}", outbox.getAggregateId()); + } + return OutboxResult.SUCCESS; + } catch (Exception e) { + log.error("stock 키 삭제 중 예외. bidId={}", outbox.getAggregateId(), e); + return OutboxResult.FAIL; + } + } finally { + newSpan.end(); + } + } + + private Span restoreAndStartSpan() { + Span.Builder spanBuilder = propagator.extract(outbox, (carrier, key) -> { + if ("X-B3-TraceId".equalsIgnoreCase(key)) { + return carrier.getTraceId(); + } + if ("X-B3-SpanId".equalsIgnoreCase(key)) { + return carrier.getSpanId(); + } + return null; + }); + + Span newSpan = spanBuilder + .name("redis-clear-stock") + .start(); + return newSpan; + } +} diff --git a/bidcompetition/src/main/java/com/smore/bidcompetition/application/service/BidEndFinalizer.java b/bidcompetition/src/main/java/com/smore/bidcompetition/application/service/BidEndFinalizer.java index d7bd29e9..c86c7a09 100644 --- a/bidcompetition/src/main/java/com/smore/bidcompetition/application/service/BidEndFinalizer.java +++ b/bidcompetition/src/main/java/com/smore/bidcompetition/application/service/BidEndFinalizer.java @@ -76,6 +76,23 @@ public void finalizeBid(UUID bidId, LocalDateTime now) { } outboxRepository.save(outbox); + + Outbox redisOutbox = Outbox.create( + AggregateType.BID, + bid.getId(), + EventType.DELETE_STOCK, + UUID.randomUUID(), + "" + ); + + if (tracer.currentSpan() != null) { + redisOutbox.attachTracing( + tracer.currentSpan().context().traceId(), + tracer.currentSpan().context().spanId() + ); + } + + outboxRepository.save(redisOutbox); } private String makePayload(BidEvent event) {