# 주제: messaging-admin-runtime 의 공개 API·의존 선언에서 확인된 것들 # revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 # ---- (1) public 인터페이스가 package-private 타입을 반환한다 ---- # DefaultMessagingAdminService.java:242-256 # /** How many messages a redrive would move, and how many already failed one. */ # record RedriveEstimate(int candidates, int alreadyRedriven) {} <-- 수식어 없음 = package-private # # /** Estimates a redrive's candidates. */ # @FunctionalInterface # public interface RedriveEstimator { <-- public # RedriveEstimate estimate(RedriveRequest request); <-- package-private 반환 타입 # } # # DefaultMessagingAdminService 는 public final class 이고 그 생성자는 # public DefaultMessagingAdminService(…, ReplayEstimator, RedriveEstimator, …) (:78-88) # 로 두 estimator 를 외부에서 받는다. 그런데 RedriveEstimator 를 구현하려면 # 반환 타입 RedriveEstimate 를 이름으로 써야 하고, 그 타입은 패키지 밖에서 접근할 수 없다. # => public 생성자가 요구하는 public 인터페이스를 패키지 밖에서 구현할 수 없다. # (컴파일은 된다. Java 는 이 조합을 막지 않는다.) # 대조: 같은 파일의 ReplayEstimator 는 long 을 반환하므로 외부 구현이 가능하다(:229-240). # command: git grep -n "RedriveEstimate" -- src # DefaultMessagingAdminService.java:163, 243, 255 — 전부 같은 파일. 외부 구현 시도 자체가 없다. # ---- (2) 감사 싱크 인터페이스가 중복이다 ---- # RedriveService.java:199-209 # /** Records privileged operations. */ # @FunctionalInterface # public interface AuditSink { # void record(dev.caskeleton.messaging.observation.MessagingAuditEvent event); # } # messaging-observability/.../MessagingAuditSink.java:18-25 # public interface MessagingAuditSink { # void record(MessagingAuditEvent event); # } # => 시그니처가 동일하고 이벤트 타입도 같다. admin-runtime 은 이미 messaging-observability 를 # 의존하며 MessagingAuditEvent 를 그 모듈에서 import 한다(RedriveService.java:126). # 즉 플랫폼 표준 싱크를 쓸 수 있는데 중첩 인터페이스를 새로 선언했다. # # 부수 효과 2개: # (a) ReplayService 가 형제 서비스의 중첩 타입에 의존한다. # ReplayService.java:28 private final RedriveService.AuditSink audit; # ReplayService.java:38 public ReplayService(…, RedriveService.AuditSink audit) # (b) MessagingAuditSink 는 InMemory 구현을 이미 제공한다(:33-55). RedriveResumptionTest 는 # RecordingAudit 를 다시 만든다(:203-210). # # 그리고 계약이 하나 유실된다: MessagingAuditSink javadoc:15-16 # "Every record has already passed {@link MessagingRedactor}, so an audit trail proves who did # what without becoming a second copy of the payload." # RedriveService.AuditSink 에는 그런 서술이 없고, RedriveService:125-136 은 # request.source().value() 와 details 를 레닥션 없이 그대로 넣는다. # ---- (3) 선언된 의존 6개 중 3개가 import 0건 ---- # command: git grep -h "^import dev.caskeleton.messaging." -- src/messaging/messaging-admin-runtime/src/main | wc -l messaging-core-api import dev.caskeleton.messaging.api = 6건 O messaging-admin-api import dev.caskeleton.messaging.admin = 45건 O messaging-observability import dev.caskeleton.messaging.observation = 1건 O messaging-policy import dev.caskeleton.messaging.policy = 0건 X messaging-transport-spi import dev.caskeleton.messaging.transport = 0건 X messaging-security import dev.caskeleton.messaging.security = 0건 X # build.gradle 은 6개 전부 api 로 선언한다. # ---- (4) 저널의 단조성(monotonic itemsCompleted)이 인터페이스 계약에 없다 ---- # DefaultMessagingAdminService.java:146, 200 # journal.fail(lease, lease.resumeFrom(), failureCodeOf(failure), clock.get()); # -> 이번 시도에서 checkpoint 로 올린 값이 아니라, 시도 **시작 시점**의 값을 넘긴다. # AdminOperationJournal.fail javadoc (messaging-admin-api/AdminOperationJournal.java:65-73) # "Marks the operation failed at its current checkpoint, leaving it resumable." # "@param itemsCompleted how many items are durably done" # -> 파라미터를 문자 그대로 저장하라는 서술이다. # # 두 구현 모두 최댓값으로 clamp 한다: # InMemoryAdminOperationJournal.java:128,147,167 Math.max(current.itemsCompleted(), itemsCompleted) # JdbcAdminOperationJournal CHECKPOINT/SETTLE SQL SET items_completed = GREATEST(items_completed, ?) # => 진행 상황이 0으로 되돌아가지 않는 것은 두 구현이 각각 clamp 하기 때문이며, # 인터페이스가 요구해서가 아니다. 파라미터를 문자 그대로 저장하는 세 번째 구현은 # 유일한 호출자와 결합했을 때 체크포인트를 잃는다. # ---- (5) DestructiveMessagingAdmin.Approved 가 검증되지 않은 승인을 담는다 ---- # DestructiveMessagingAdmin.java:23-38 # record Approved( # DestructiveOperation operation, # DestinationName destination, # AdminApproval approval, <-- public 생성자를 가진 평범한 record # long estimatedMessagesAffected) { … } # 생성자는 null/음수만 검사한다. 다음을 검사하지 않는다: # - approval 이 이 operation 을 인가하는가 # - approval 이 이 destination 을 인가하는가 # - estimatedMessagesAffected 가 승인 상한 이하인가 # - 계획 다이제스트 결합 (필드 자체가 없다) # # 대조군: messaging-admin-api 의 ApprovedReplayPlan/ApprovedRedrivePlan 은 # VerifiedApproval 을 담고 생성자에서 4가지를 검사한다. # 그리고 VerifiedApproval javadoc:9-13 이 정확히 이 형태를 과거 결함으로 기록한다: # "The approved-plan types used to hold a plain AdminApproval record with a public constructor, # so 'this plan was approved' was a claim the caller made about itself. Any code that could # reach the execute method could write new AdminApproval("TICKET-1", "someone", now, later) # and the platform believed it." # => 그 수정이 REPLAY/REDRIVE(복구 가능한 작업)에는 적용되었고, # PURGE/DELETE_DESTINATION/OFFSET_RESET(복구 불가능한 작업)에는 적용되지 않았다. # 현재 구현체가 0건이므로 실행되는 결함은 아니다(EVD-307). 그러나 운영자 도구를 쓰는 순간의 모양이다. # ---- (6) guard 우회를 dryRun 파라미터로 표현한다 ---- # ReplayService.java:58-64 # boolean needsApproval = !request.isolatedConsumerGroup(); # guard.authorize(DestructiveOperation.REPLAY, request.destination(), approval, # request.dryRun() || !needsApproval, <-- dryRun 자리 # now); # DestructiveOperationGuard.authorize 의 dryRun 파라미터는 :54-56 에서 무조건 return 한다. # 격리 컨슈머그룹 리플레이가 승인 불필요라는 판단 자체는 ReplayService.java:16-19 에 근거가 있다. # 다만 그 판단이 "dryRun=true 로 넘긴다" 로 구현되어, 감사 로그(:77)에는 # approval.map(VerifiedApproval::ticket).orElse("isolated") # 로 남고 guard 쪽에는 dry run 과 구별되지 않는다.