# 주제: 재개(resume)된 리드라이브가 "옮기지 못한" 메시지를 건너뛴다.
#       그리고 그 사실을 가리는 것은, 실제 불변식을 재현할 수 없는 테스트 대역이다.
# revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
# severity: P1

# ---- 코드 ----
# RedriveService.java:105-120
#   List<MessageId> moved = new ArrayList<>();
#   int failed = 0;
#   int completed = resumeFrom;
#   try {
#     // Everything before resumeFrom was moved and settled by the previous attempt. Re-publishing
#     // it is not a retry, it is a duplicate.
#     for (MessageId messageId :
#         candidates.subList(Math.min(resumeFrom, candidates.size()), candidates.size())) {
#       if (attempt(messageId, request)) { moved.add(messageId); } else { failed++; }
#       completed++;                       <-- 성공/실패 양쪽에서 증가
#       checkpoint.accept(completed);
#     }
#   }
#
# RedriveService.java:100
#   List<MessageId> candidates = source.peek(request.source(), request.batchSize());
#   -> 매 시도마다 원본(DLQ)에서 새로 peek 한다.
#
# RedriveService.java:142-156  attempt(...)
#   성공한 것만 source.settle(...) 로 DLQ 에서 제거된다. 실패한 것은 DLQ 에 남는다.

# ---- 주석의 전제가 성립하지 않는다 ----
# 주석: "Everything before resumeFrom was moved and settled by the previous attempt."
# 그러나 `completed` 는 moved + failed 다(:118 이 두 분기 모두에서 실행). 즉 resumeFrom 은
# "옮긴 개수" 가 아니라 "시도한 개수" 다. 실패분은 settle 되지 않아 DLQ 에 남아 있고,
# 다음 시도의 peek 결과에는 그대로 포함된다.
# subList(resumeFrom, …) 는 그 목록의 앞에서부터 resumeFrom 개를 버리므로,
# 버려지는 것은 "이미 옮긴 것" 이 아니라 "아직 옮기지 못한 것" 이다.

# ---- 구체적 실패 시나리오 ----
# DLQ = [m1, m2, m3, m4, m5], batchSize=50
# 1차 시도 (resumeFrom=0):
#   peek -> [m1,m2,m3,m4,m5]
#   m1 CONFIRMED -> settle -> DLQ 에서 제거.  completed=1, checkpoint(1)
#   m2 미확인    -> failed++.  DLQ 에 남음.   completed=2, checkpoint(2)
#   프로세스 사망.  저널 itemsCompleted = 2
# 2차 시도:
#   journal.begin -> lease.resumeFrom = 2
#   peek -> [m2,m3,m4,m5]        (m1 은 settle 되어 사라짐)
#   subList(min(2,4), 4) = [m4, m5]
#   -> m2(실패했던 것)와 m3(아예 시도되지 않은 것)이 **영구히 건너뛰어진다**
#   m4, m5 성공.  RedriveReport(candidates=4, moved=2, failed=0)
#   journal.complete(lease, 2+0, now)  -> 상태 COMPLETED, 승인 소진
# 결과: 운영자에게는 성공으로 보이고, m2·m3 는 DLQ 에 남으며, 어떤 기록도 그 둘을 지목하지 않는다.
#      승인은 소진되었으므로 재실행은 APPROVAL_ALREADY_EXECUTED 로 거절된다.

# ---- 플랫폼은 이것을 감지할 수단을 이미 갖고 있다 ----
# RedriveResult.isFullyAccounted()  (messaging-admin-api/RedriveResult.java:48-50)
#   return moved + stillParked == candidates;
#   javadoc: "An unaccounted message is a bug, not a partial success: it was neither republished
#             nor left parked, which means the redrive lost track of it."
# 위 시나리오에서 2 + 0 == 4 -> false. 정확히 이 결함을 잡는 술어다.
# 그러나 isFullyAccounted() 의 프로덕션 호출부는 0건이다(EVD-302). 아무도 묻지 않는다.

# ---- 왜 테스트가 잡지 못하는가 ----
# command: sed -n "183,201p" .../RedriveResumptionTest.java
  private static final class RecordingSource implements RedriveService.RedriveSource {
    private List<MessageId> staged = List.of();
    private final List<MessageId> settled = new ArrayList<>();
    private void stage(List<MessageId> ids) { staged = List.copyOf(ids); }
    @Override
    public List<MessageId> peek(DestinationName destination, int batchSize) {
      return staged;                      <-- 항상 같은 전체 목록
    }
    @Override
    public void settle(DestinationName destination, MessageId messageId) {
      settled.add(messageId);             <-- staged 에서 제거하지 않는다
    }
  }
# => 이 대역은 "정산된 메시지는 DLQ 에서 사라진다" 는 실제 불변식을 재현하지 못한다.
#    따라서 재개 시 peek 결과가 줄어드는 상황이 테스트 세계에 존재하지 않는다.

# ---- 테스트 5건이 덮는 조합 ----
#   oneThrowingMessageDoesNotAbandonTheRest      resumeFrom 없음, 실패 1건
#   theAuditRecordIsWrittenEvenOnFailure         resumeFrom 없음, 전건 실패
#   aResumedPassDoesNotRepublishWhatWasAlreadyMoved  resumeFrom=3, **전건 성공**
#   progressIsReportedAfterEachItem              resumeFrom=0, 전건 성공
#   resumingPastTheEndOfTheBatch                 resumeFrom=10, 전건 성공
# => 실패와 재개를 **동시에** 넣는 테스트가 없다. 결함은 그 교집합에서만 나타난다.

# 참고: DefaultMessagingAdminService:203
#   journal.complete(lease, report.moved() + report.failed(), clock.get());
#   -> itemsCompleted 가 moved+failed 라는 점은 저장 측에서도 일관된다.
#      즉 이것은 오타가 아니라 "시도 개수" 라는 일관된 선택이며,
#      그 선택이 subList 인덱스로 재사용되는 지점에서만 틀린다.
