docs(charge): 충전 이벤트 보너스 지급 안정화 계획을 추가한다
This commit is contained in:
170
docs/plan-task/20260518_충전이벤트보너스지급안정화.md
Normal file
170
docs/plan-task/20260518_충전이벤트보너스지급안정화.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# 충전 이벤트 보너스 지급 안정화 작업 계획
|
||||
|
||||
## 목적
|
||||
- 충전 완료 후 백그라운드로 지급되는 충전 이벤트 보너스가 누락되거나 중복 지급되지 않도록 한다.
|
||||
- 결제 완료 처리, 이벤트 보너스 지급, 재시도, 운영 확인이 모두 DB 기준으로 추적 가능하도록 한다.
|
||||
|
||||
## 현재 확인된 문제
|
||||
- `ChargeSpringEventListener.applyChargeEvent`가 `@Async @TransactionalEventListener`로 동작해 결제 완료 트랜잭션 커밋 후 별도 스레드에서 보너스를 지급한다.
|
||||
- 비동기 이벤트 처리 실패가 원 결제 API 응답에 드러나지 않아, 결제는 성공했지만 이벤트 보너스만 누락될 수 있다.
|
||||
- `ChargeEventRepository.getPaymentCount(member, method, startDate, endDate)`가 `startDate`, `endDate`를 파라미터로 받지만 실제 쿼리 조건에 사용하지 않는다.
|
||||
- `Member.charge(...)`는 단순 필드 증가(`+=`)인데 원본 충전 완료와 이벤트 지급 흐름 모두 회원 row lock 없이 실행되면 동시 충전 시 잔액 유실이 발생할 수 있다.
|
||||
- 원본 충전 건(`sourceChargeId`) 기준으로 이벤트 보너스 지급 여부를 고유하게 보장하는 DB 제약이 없다.
|
||||
|
||||
## 추가 캔 수 기록 여부 판단
|
||||
- `charge_event_job`에는 `additional_can`을 저장하는 것이 좋다.
|
||||
- 이유는 다음과 같다.
|
||||
- 작업 생성 시점의 이벤트 조건을 스냅샷으로 보존할 수 있다.
|
||||
- 재시도 시점에 관리자가 `addPercent`, 이벤트 제목, 기간을 수정해도 최초 충전 완료 당시 계산된 보너스 캔을 그대로 지급할 수 있다.
|
||||
- 운영자가 실패 작업을 확인할 때 “얼마를 지급해야 했는지”를 별도 재계산 없이 알 수 있다.
|
||||
- 재시도 worker가 이벤트 설정을 다시 읽어 계산하는 과정에서 발생할 수 있는 불일치를 줄인다.
|
||||
- 따라서 `source_charge_id`, `member_id`, `charge_event_id`, `job_type`, `additional_can`, `method_snapshot`을 함께 저장한다.
|
||||
|
||||
## 확정 운영 정책
|
||||
- worker 실행 주기는 5분으로 한다.
|
||||
- worker batch size는 30건으로 한다.
|
||||
- worker backoff는 5분, 10분, 15분 순서로 적용한다.
|
||||
- 최대 재시도 횟수는 3회로 한다.
|
||||
- 3회 재시도 후에도 실패하면 `FAILED`로 전환하고 자동 재시도를 중단한다.
|
||||
- `DONE`, `PROCESSING` 상태를 제외한 작업을 확인할 수 있는 관리자 API를 추가한다.
|
||||
- `FAILED` 작업을 다시 재시도할 수 있는 관리자 API를 추가한다.
|
||||
- worker는 활성 충전 이벤트가 없으면 `charge_event_job` 조회를 수행하지 않고 종료한다.
|
||||
- 충전 완료 처리(`verify`) 시 충전 이벤트가 있으면 `charge_event_job`을 항상 기록한다.
|
||||
- 원본 충전 완료 처리에서도 `MemberRepository.findByIdForUpdate(...)`로 회원 row를 잠근 뒤 `member.charge(...)`를 수행한다.
|
||||
- 충전 완료 처리(`verify`) 시 `charge_event_job` 기록 후 이벤트 보너스를 즉시 지급한다.
|
||||
- 즉시 지급 중인 작업은 `PROCESSING` 상태로 선점해 worker가 실행하지 않도록 한다.
|
||||
- 즉시 지급에 성공하면 `DONE`, 실패하면 `PENDING`과 `next_retry_at = now + 5분`으로 전환해 worker 재시도 대상으로 둔다.
|
||||
- 관리자 API는 기존 관리자 컨트롤러 관례에 맞춰 `/admin/...` 경로, `@PreAuthorize("hasRole('ADMIN')")`, `ApiResponse.ok(...)` 응답을 사용한다.
|
||||
|
||||
## DB 설계
|
||||
|
||||
### `charge_event_job`
|
||||
- 충전 완료 후 이벤트 보너스 지급 작업을 영속화하는 테이블이다.
|
||||
- `idempotency_key`에 unique key를 걸어 같은 원본 충전 건의 같은 이벤트 보너스 작업이 중복 생성되지 않도록 한다.
|
||||
- `status`, `retry_count`, `next_retry_at`, `last_error`로 실패 작업을 재시도하고 운영자가 확인할 수 있게 한다.
|
||||
- 현재 DDL에는 boolean 컬럼이 필요하지 않아 포함하지 않는다. 추후 boolean 컬럼을 추가할 경우 MySQL 기준 `TINYINT(1)`로 작성한다.
|
||||
|
||||
```sql
|
||||
CREATE TABLE charge_event_job
|
||||
(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'PK',
|
||||
source_charge_id BIGINT NOT NULL COMMENT '이벤트 보너스 지급의 기준이 되는 원본 충전 ID(charge.id)',
|
||||
result_charge_id BIGINT NULL COMMENT '이벤트 보너스로 생성된 Charge ID(charge.id)',
|
||||
member_id BIGINT NOT NULL COMMENT '이벤트 보너스를 지급받을 회원 ID(member.id)',
|
||||
charge_event_id BIGINT NULL COMMENT '적용된 충전 이벤트 ID(charge_event.id), 첫 충전 이벤트처럼 별도 이벤트 row가 없으면 NULL',
|
||||
job_type VARCHAR(30) NOT NULL COMMENT '작업 유형(FIRST_CHARGE, ACTIVE_CHARGE_EVENT)',
|
||||
idempotency_key VARCHAR(100) NOT NULL COMMENT '중복 작업 방지 키(예: charge-event:{sourceChargeId}:{jobType}:{chargeEventId 또는 none})',
|
||||
additional_can INT NOT NULL COMMENT '추가 지급할 보너스 캔 수',
|
||||
payment_gateway VARCHAR(30) NOT NULL COMMENT '원본 충전의 결제 게이트웨이(PG, PAYVERSE, GOOGLE_IAP, APPLE_IAP 등)',
|
||||
container VARCHAR(10) NOT NULL COMMENT '회원 캔 잔액 반영 대상(pg, aos, ios)',
|
||||
method_snapshot VARCHAR(100) NOT NULL COMMENT '이벤트 보너스 Charge.payment.method에 기록할 지급 사유 스냅샷',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '작업 상태(PENDING, PROCESSING, DONE, FAILED)',
|
||||
retry_count INT NOT NULL DEFAULT 0 COMMENT '재시도 횟수',
|
||||
next_retry_at TIMESTAMP NULL COMMENT '다음 재시도 가능 시각',
|
||||
processing_started_at TIMESTAMP NULL COMMENT 'PROCESSING 상태로 선점한 시각',
|
||||
processed_at TIMESTAMP NULL COMMENT '지급 성공 처리 시각',
|
||||
last_error TEXT NULL COMMENT '마지막 실패 사유',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_charge_event_job_idempotency_key (idempotency_key),
|
||||
KEY idx_charge_event_job_status_next_retry_at (status, next_retry_at),
|
||||
KEY idx_charge_event_job_source_charge_id (source_charge_id),
|
||||
KEY idx_charge_event_job_result_charge_id (result_charge_id),
|
||||
KEY idx_charge_event_job_member_id (member_id),
|
||||
KEY idx_charge_event_job_charge_event_id (charge_event_id),
|
||||
CONSTRAINT fk_charge_event_job_source_charge_id
|
||||
FOREIGN KEY (source_charge_id) REFERENCES charge (id),
|
||||
CONSTRAINT fk_charge_event_job_result_charge_id
|
||||
FOREIGN KEY (result_charge_id) REFERENCES charge (id),
|
||||
CONSTRAINT fk_charge_event_job_member_id
|
||||
FOREIGN KEY (member_id) REFERENCES member (id),
|
||||
CONSTRAINT fk_charge_event_job_charge_event_id
|
||||
FOREIGN KEY (charge_event_id) REFERENCES charge_event (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='충전 이벤트 보너스 지급 작업';
|
||||
```
|
||||
|
||||
## 구현 항목
|
||||
- [x] `ChargeEventRepository.getPaymentCount(...)` 쿼리 수정
|
||||
- [x] `charge.createdAt`이 이벤트 `startDate`, `endDate` 사이인지 조건에 추가한다.
|
||||
- [x] 이벤트 보너스 지급 건만 세도록 `charge.status.eq(ChargeStatus.EVENT)` 조건을 추가한다.
|
||||
- [x] `fetch().count()` 대신 DB count 쿼리를 사용한다.
|
||||
|
||||
- [x] 결제 완료 처리 idempotency 강화
|
||||
- [x] `ChargeRepository`에 원본 `Charge` row를 `PESSIMISTIC_WRITE`로 조회하는 메서드를 추가한다.
|
||||
- [x] `payverseWebhook`, `payverseVerify`, `verify`, `verifyHecto`, `appleVerify`, `processGoogleIap`에서 `PaymentStatus.REQUEST -> COMPLETE` 전이일 때만 회원 잔액 반영과 이벤트 작업 생성을 수행한다.
|
||||
- [x] 각 완료 경로는 `MemberRepository.findByIdForUpdate(...)`로 회원 row를 잠근 뒤 원본 충전의 `member.charge(...)`를 호출한다.
|
||||
- [x] 이미 `COMPLETE`인 경우 원 결제와 이벤트 작업을 다시 만들지 않고 완료 응답만 반환한다.
|
||||
|
||||
- [x] 회원 잔액 업데이트 직렬화
|
||||
- [x] 원본 충전 완료 시 `MemberRepository.findByIdForUpdate(...)`로 회원 row를 잠근 뒤 `member.charge(...)`를 호출한다.
|
||||
- [x] 이벤트 보너스 즉시 지급 시에도 같은 방식으로 회원 row를 잠근 뒤 보너스 캔을 반영한다.
|
||||
- [x] 이벤트 보너스 worker 재시도에서도 같은 방식으로 회원 row를 잠근 뒤 보너스 캔을 반영한다.
|
||||
|
||||
- [x] `charge_event_job` 기반 이벤트 보너스 작업 생성
|
||||
- [x] 충전 완료 트랜잭션 안에서 이벤트 적용 대상인지 판단하고 `charge_event_job` row를 항상 생성한다.
|
||||
- [x] 즉시 지급을 시작하는 작업은 `PROCESSING`으로 저장해 worker가 조회하지 않도록 한다.
|
||||
- [x] `additional_can`, `method_snapshot`, `payment_gateway`, `container`는 작업 생성 시점 값으로 저장한다.
|
||||
- [x] `idempotency_key` unique key 충돌 시 이미 생성된 작업으로 보고 중복 생성하지 않는다.
|
||||
- [x] 즉시 지급 성공 시 `DONE`, `processed_at`, `result_charge_id`를 기록한다.
|
||||
- [x] 즉시 지급 실패 시 `PENDING`, `next_retry_at = now + 5분`, `last_error`를 기록한다.
|
||||
|
||||
- [x] 이벤트 보너스 worker 구현
|
||||
- [x] 기존 `TranslationJobWorker`의 `@Scheduled`, 상태 선점, retry/backoff 구조를 참고한다.
|
||||
- [x] `@Scheduled(fixedDelayString = "\${sodalive.charge-event-job.fixed-delay-ms:300000}")` 방식으로 5분 주기를 기본값으로 둔다.
|
||||
- [x] worker는 한 번에 최대 30건만 처리한다.
|
||||
- [x] worker 시작 시 활성 충전 이벤트가 없으면 `charge_event_job` 조회 없이 즉시 종료한다.
|
||||
- [x] `PENDING` 작업 중 `next_retry_at IS NULL OR next_retry_at <= now`인 작업을 조회한다.
|
||||
- [x] 작업을 `PROCESSING`으로 선점한 뒤 별도 트랜잭션에서 보너스 `Charge(status = EVENT)` 생성과 `member.charge(...)`를 수행한다.
|
||||
- [x] 성공하면 `DONE`으로 변경하고 `processed_at`을 기록한다.
|
||||
- [x] 1회 재시도 실패 시 `retry_count = 1`, `next_retry_at = now + 10분`, `last_error`를 기록하고 `PENDING`으로 되돌린다.
|
||||
- [x] 2회 재시도 실패 시 `retry_count = 2`, `next_retry_at = now + 15분`, `last_error`를 기록하고 `PENDING`으로 되돌린다.
|
||||
- [x] 3회 재시도 실패 시 `retry_count = 3`, `status = FAILED`, `last_error`를 기록하고 자동 재시도를 중단한다.
|
||||
|
||||
- [x] 이벤트 작업 관리자 API 추가
|
||||
- [x] `AdminChargeEventJobController`와 `AdminChargeEventJobService`를 추가한다.
|
||||
- [x] 컨트롤러는 `@RequestMapping("/admin/charge/event-jobs")`와 `@PreAuthorize("hasRole('ADMIN')")`를 사용한다.
|
||||
- [x] API 응답은 기존 관리자 API 관례에 맞춰 `ApiResponse.ok(...)`를 사용한다.
|
||||
- [x] `GET /admin/charge/event-jobs`로 `DONE`, `PROCESSING` 상태를 제외한 작업 목록을 조회한다.
|
||||
- [x] 조회 대상에는 재시도 대기 중인 `PENDING` 작업과 운영 확인이 필요한 `FAILED` 작업을 포함한다.
|
||||
- [x] 응답에는 `id`, `sourceChargeId`, `resultChargeId`, `memberId`, `chargeEventId`, `jobType`, `additionalCan`, `status`, `retryCount`, `nextRetryAt`, `lastError`, `createdAt`, `updatedAt`을 포함한다.
|
||||
- [x] `POST /admin/charge/event-jobs/{jobId}/retry`로 `FAILED` 작업을 재시도 대상으로 변경한다.
|
||||
- [x] 재시도 API는 `FAILED` 상태 작업만 `PENDING`으로 변경하고 `next_retry_at`을 현재 시각으로 갱신한다.
|
||||
- [x] 재시도 API는 `DONE`, `PROCESSING`, `PENDING` 상태 작업에 대해서는 상태를 변경하지 않는다.
|
||||
|
||||
- [x] 기존 `@Async @TransactionalEventListener` 역할 정리
|
||||
- [x] 기존 `ChargeSpringEventListener`가 직접 `ChargeEventService.applyChargeEvent(...)`를 호출해 보너스를 지급하는 구조는 제거한다.
|
||||
- [x] 즉시 지급은 충전 완료 처리(`verify`) 흐름에서 생성한 `charge_event_job`을 기준으로 `ChargeEventJobService`가 수행한다.
|
||||
- [x] `ChargeSpringEventListener`는 제거하거나, 남기더라도 보너스 지급을 직접 수행하지 않는다.
|
||||
- [x] 보너스 지급은 즉시 지급과 worker 재시도 모두 DB에 저장된 `charge_event_job`을 기준으로 처리한다.
|
||||
|
||||
- [ ] 테스트 항목
|
||||
- [ ] 같은 `source_charge_id`로 이벤트 작업 생성이 중복 요청되어도 `charge_event_job`이 1건만 생성되는지 검증한다.
|
||||
- [ ] 충전 완료 처리에서 `charge_event_job` 생성 후 즉시 지급 성공 시 `DONE`으로 기록되는지 검증한다.
|
||||
- [ ] 즉시 지급 중인 `PROCESSING` 작업을 worker가 처리하지 않는지 검증한다.
|
||||
- [ ] 즉시 지급 실패 시 `PENDING`, `next_retry_at = now + 5분`으로 전환되는지 검증한다.
|
||||
- [ ] worker 실패 시 `retry_count`, `next_retry_at`, `last_error`가 갱신되는지 검증한다.
|
||||
- [ ] worker가 30건까지만 처리하는지 검증한다.
|
||||
- [ ] 활성 충전 이벤트가 없으면 worker가 `charge_event_job` 조회 없이 종료하는지 검증한다.
|
||||
- [ ] worker 재시도 성공 시 보너스 `Charge(status = EVENT)`와 회원 보너스 캔이 1회만 반영되는지 검증한다.
|
||||
- [ ] worker가 3회 재시도 실패 후 `FAILED`로 전환하고 자동 재시도를 중단하는지 검증한다.
|
||||
- [ ] 이벤트 작업 조회 관리자 API가 `PENDING`, `FAILED` 작업을 반환하고 `DONE`, `PROCESSING` 작업을 제외하는지 검증한다.
|
||||
- [ ] `FAILED` 재시도 관리자 API가 작업을 `PENDING`으로 되돌리는지 검증한다.
|
||||
- [ ] 동시 충전 요청에서 회원 잔액이 유실되지 않는지 검증한다.
|
||||
- [ ] `getPaymentCount(...)`가 이벤트 기간 밖의 과거 보너스 지급 건을 세지 않는지 검증한다.
|
||||
|
||||
## 검증 항목
|
||||
- [x] `./gradlew test`
|
||||
- [x] `./gradlew ktlintCheck`
|
||||
- [ ] 스테이징에서 결제 완료 후 `charge_event_job` 생성 확인
|
||||
- [ ] 스테이징에서 결제 완료 후 즉시 지급 성공 시 worker가 해당 작업을 중복 처리하지 않는지 확인
|
||||
- [ ] 스테이징에서 worker 실패 강제 후 재시도 성공 확인
|
||||
- [ ] 스테이징에서 3회 실패 후 `FAILED` 전환 및 관리자 API 재시도 확인
|
||||
- [ ] 같은 결제 검증 요청을 중복 호출해도 원 결제와 이벤트 보너스가 중복 지급되지 않는지 확인
|
||||
|
||||
## 검증 로그
|
||||
- [ ] 문서 작성 검증: 계획 문서에 수정 대상 4가지, MySQL DDL, 컬럼 COMMENT, `TIMESTAMP`, `TINYINT(1)` 규칙 반영 여부를 확인했다.
|
||||
- [x] 구현 검증: global worktree(`/Users/klaus/.config/superpowers/worktrees/sodalive/charge-event-job-stabilization`)에서 `charge_event_job` 엔티티/리포지토리/서비스/worker/관리자 API를 추가하고, 결제 완료 경로가 `ChargeEventJobService.createAndProcessImmediate(...)`를 호출하도록 변경했다.
|
||||
- [x] 테스트 검증: `./gradlew test`를 실행해 전체 테스트 통과를 확인했다.
|
||||
- [x] 린트 검증: `./gradlew ktlintCheck`를 실행해 ktlint 통과를 확인했다.
|
||||
- [x] LSP 검증: Kotlin LSP 서버가 설정되어 있지 않아 `lsp_diagnostics`는 실행 불가했고, 대신 Gradle 컴파일/테스트와 ktlint로 검증했다.
|
||||
107
docs/prd/20260518_충전이벤트보너스지급안정화_prd.md
Normal file
107
docs/prd/20260518_충전이벤트보너스지급안정화_prd.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# PRD: 충전 이벤트 보너스 지급 안정화
|
||||
|
||||
## 1. Overview
|
||||
- 충전 완료 후 지급되는 충전 이벤트 보너스가 누락되거나 중복 지급되지 않도록 안정화한다.
|
||||
- 상세 구현 방향은 `docs/plan-task/20260518_충전이벤트보너스지급안정화.md`를 기준으로 한다.
|
||||
- 이 PRD는 충전 이벤트 보너스 지급 작업의 재시도, worker, 관리자 API 운영 정책을 확정한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. Problem
|
||||
- 현재 충전 이벤트 보너스 지급은 결제 완료 흐름과 분리된 비동기 이벤트에서 처리된다.
|
||||
- 비동기 처리 실패가 발생하면 원 결제는 성공했지만 이벤트 보너스만 누락될 수 있다.
|
||||
- 실패 작업을 DB에 남기고, 정해진 주기와 횟수 안에서 재시도하며, 최종 실패 건은 관리자 API로 확인/재시도할 수 있어야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Goals
|
||||
- 이벤트 보너스 지급 실패가 DB에 남고 재시도 가능해야 한다.
|
||||
- 중복 결제 검증 또는 worker 재시도에도 보너스가 중복 지급되지 않아야 한다.
|
||||
- 원본 충전 완료 처리에서도 회원 잔액 변경은 회원 row lock을 잡은 뒤 수행해 동시 충전 시 잔액 유실을 방지해야 한다.
|
||||
- 최대 재시도 후에도 실패한 작업은 운영자가 원인과 지급 대상, 지급할 캔 수를 확인할 수 있어야 한다.
|
||||
- 충전 완료 검증 시 이벤트 작업을 기록하고 즉시 지급을 시도하되, 즉시 수행 중인 작업을 worker가 중복 처리하지 않아야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 4. Non-Goals
|
||||
- 결제 PG 검증 방식 자체를 교체하지 않는다.
|
||||
- 충전 상품 가격, 이벤트 보너스율, 첫 충전 이벤트율을 변경하지 않는다.
|
||||
- 관리자 화면 신규 개발은 포함하지 않는다. 단, 실패 작업 조회/재시도용 관리자 API는 포함한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. Core Features
|
||||
|
||||
### Feature A. 충전 이벤트 보너스 지급 작업 추적
|
||||
|
||||
#### Requirements
|
||||
- 충전 완료 후 이벤트 보너스 지급 대상이면 DB 작업으로 추적한다.
|
||||
- 지급 대상, 원본 충전, 추가 지급 캔 수, 처리 상태, 실패 사유를 확인할 수 있어야 한다.
|
||||
- 원본 충전 완료 처리(`verify`, `verifyHecto`, `appleVerify`, `payverseVerify`, `payverseWebhook`, `processGoogleIap`)는 회원 row lock 안에서 `PaymentStatus.REQUEST -> COMPLETE` 전이와 `member.charge(...)`를 수행한다.
|
||||
- 충전 완료 처리(`verify`) 시 `charge_event_job`을 기록하고, 같은 흐름에서 이벤트 보너스 지급을 즉시 시도한다.
|
||||
- 즉시 지급 중인 작업은 worker가 처리하지 않도록 `PROCESSING` 상태로 선점한다.
|
||||
- 상세 테이블 구조와 구현 항목은 plan-task 문서를 따른다.
|
||||
|
||||
#### Edge Cases
|
||||
- 같은 결제 검증 요청이 여러 번 들어와도 보너스 작업은 중복 생성되지 않아야 한다.
|
||||
- worker가 같은 작업을 재시도해도 보너스는 1회만 지급되어야 한다.
|
||||
- 이벤트 설정이 재시도 시점에 변경되어도 최초 작업 생성 시점의 추가 캔 수를 유지해야 한다.
|
||||
- 즉시 지급 실패 후 worker가 재시도할 수 있도록 작업은 `PENDING`과 `next_retry_at`으로 전환되어야 한다.
|
||||
|
||||
---
|
||||
|
||||
### Feature B. 이벤트 보너스 worker 재시도
|
||||
|
||||
#### Requirements
|
||||
- worker 실행 주기는 5분으로 한다.
|
||||
- worker batch size는 30건으로 한다.
|
||||
- worker는 활성 충전 이벤트가 없으면 `charge_event_job` 조회를 수행하지 않고 종료한다.
|
||||
- worker는 `PENDING` 상태이면서 `next_retry_at IS NULL OR next_retry_at <= now`인 작업만 처리한다.
|
||||
- backoff 정책은 5분, 10분, 15분 순서로 적용한다.
|
||||
- 최대 재시도 횟수는 3회로 한다.
|
||||
- 3회 재시도 후에도 실패하면 `FAILED`로 전환하고 자동 재시도를 중단한다.
|
||||
|
||||
#### Edge Cases
|
||||
- 같은 시간에 다른 scheduler/worker가 실행되어도 worker는 최대 30건만 처리한다.
|
||||
- 이미 `PROCESSING`, `DONE`, `FAILED`인 작업은 자동 worker가 처리하지 않는다.
|
||||
- `FAILED` 작업은 관리자 재시도 API를 통해서만 다시 재시도 대상으로 전환한다.
|
||||
|
||||
---
|
||||
|
||||
### Feature C. 이벤트 작업 관리자 API
|
||||
|
||||
#### Requirements
|
||||
- `DONE`, `PROCESSING` 상태를 제외한 `charge_event_job` 목록을 조회할 수 있는 관리자 API를 추가한다.
|
||||
- 조회 API는 재시도 대기 중인 `PENDING` 작업과 운영 확인이 필요한 `FAILED` 작업을 함께 보여준다.
|
||||
- `FAILED` 상태의 단일 작업을 재시도 대상으로 되돌리는 관리자 API를 추가한다.
|
||||
- 재시도 API는 대상 작업을 `PENDING`으로 변경하고 `next_retry_at`을 즉시 실행 가능하도록 갱신한다.
|
||||
- 재시도 API는 이미 `DONE` 또는 `PROCESSING`인 작업을 변경하지 않아야 한다.
|
||||
|
||||
#### Edge Cases
|
||||
- 관리자가 같은 실패 작업 재시도 API를 여러 번 호출해도 중복 지급되지 않아야 한다.
|
||||
- 이미 성공 처리된 작업은 재시도 API에서 거부되어야 한다.
|
||||
- 즉시 지급 중인 `PROCESSING` 작업과 지급 완료된 `DONE` 작업은 관리자 조회 목록에 노출하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 7. Technical Constraints
|
||||
- DB는 MySQL을 기준으로 한다.
|
||||
- 날짜/시간 컬럼은 `TIMESTAMP`를 사용한다.
|
||||
- boolean 컬럼이 필요해질 경우 `TINYINT(1)`을 사용한다.
|
||||
- 각 DB 컬럼에는 `COMMENT`를 작성한다.
|
||||
- 구현 계획과 DDL은 `docs/plan-task/20260518_충전이벤트보너스지급안정화.md`를 기준으로 관리한다.
|
||||
|
||||
---
|
||||
|
||||
## 8. Metrics
|
||||
- 결제 완료 후 이벤트 보너스 지급 작업 생성 성공률
|
||||
- 이벤트 보너스 지급 성공률
|
||||
- 재시도 후 성공한 작업 수
|
||||
- 최종 `FAILED` 상태로 남은 작업 수
|
||||
- 중복 지급 방지 위반 건수
|
||||
- 관리자 API로 재시도된 작업 수
|
||||
|
||||
---
|
||||
|
||||
## 9. Related Documents
|
||||
- `docs/plan-task/20260518_충전이벤트보너스지급안정화.md`
|
||||
Reference in New Issue
Block a user