Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/311-outbox-cleanup-unbounded-confirmed.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

69 lines
3.6 KiB
Plaintext

# 주제: EVD-294(bounded purge 미호출)의 구현 측 확인 —
# 프로덕션은 무제한 DELETE 한 방을 쏘고, 그것을 가리는 것은 스크립트 대역이다
# revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
# severity: P1 (EVD-294 와 동일 항목의 구현 측 근거)
# ---- 구현: 두 오버로드 ----
# JdbcOutboxRepository.java:486-518 bounded
# public int purgePublishedBefore(Instant publishedBefore, int limit) {
# if (limit < 1) throw new IllegalArgumentException("a bounded purge deletes at least one row per call");
# // The CTE picks a bounded set of ids with SKIP LOCKED and deletes exactly those. An unbounded
# // DELETE holds locks and writes WAL in proportion to the whole backlog, which stalls the relay
# // and the business writes behind retention.
# WITH expired AS (
# SELECT message_id FROM messaging_outbox
# WHERE status = 'PUBLISHED' AND published_at < ?
# ORDER BY published_at LIMIT ? FOR UPDATE SKIP LOCKED)
# DELETE FROM messaging_outbox o USING expired e WHERE o.message_id = e.message_id
#
# JdbcOutboxRepository.java:519-533 unbounded
# public int purgePublishedBefore(Instant publishedBefore) {
# "DELETE FROM messaging_outbox WHERE status = 'PUBLISHED' AND published_at < ?"
# }
# ---- 호출자는 무제한 쪽을 부른다 ----
# OutboxCleanupJob.java:44-57
# int removed = 0;
# for (int batch = 0; batch < maxBatches; batch++) {
# int deleted = outbox.purgePublishedBefore(cutoff); <-- 무제한 오버로드
# removed += deleted;
# if (deleted == 0) break;
# }
# => 1회차가 전체를 지우고, 2회차가 0을 반환해 break. maxBatches 는 실질적으로 죽은 값이다.
# ---- 배선 ----
# MessagingReliabilityAutoConfiguration.java:140-141
# public OutboxCleanupJob outboxCleanupJob(OutboxRepository outbox, OutboxProperties properties) {
# return new OutboxCleanupJob(outbox, properties, 20);
# }
# MessagingReliabilityAutoConfiguration.java:169-170
# return new InboxCleanupJob(inbox, policy, 20);
# => 둘 다 빈이며 maxBatches=20 하드코딩. 둘 다 무제한 오버로드를 부른다.
# ---- 왜 테스트가 잡지 못하는가 ----
# OutboxOperationsTest.java:120-134 RecordingRepository
# @Override
# public int purgePublishedBefore(Instant publishedBefore, int limit) {
# return Math.min(purgePublishedBefore(publishedBefore), limit); <-- 전부 지우고 숫자만 깎는다
# }
# @Override
# public int purgePublishedBefore(Instant publishedBefore) {
# cutoffs.add(publishedBefore);
# return pass < deletions.size() ? deletions.get(pass++) : 0; <-- 스크립트
# }
#
# OutboxOperationsTest.java:157-165
# void cleanupDeletesInBoundedBatchesRatherThanOneLongStatement() {
# RecordingRepository repository = new RecordingRepository(List.of(1000, 1000, 250));
# int removed = new OutboxCleanupJob(repository, OutboxProperties.defaults(), 10).runOnce(NOW);
# assertThat(removed).isEqualTo(2250);
# assertThat(repository.cutoffs).hasSize(4);
# }
# => "여러 번 나눠 지운다" 는 관측은 전적으로 대역의 스크립트(1000,1000,250,0)가 만든 것이다.
# 실제 JdbcOutboxRepository 를 넣으면 1회차에 전체가 지워지고 cutoffs 는 2가 된다.
#
# 실 DB 테스트도 무제한 쪽만 부른다:
# OutboxPostgresIT.java:202 int removed = repository.purgePublishedBefore(NOW.plusSeconds(1));
# => bounded 오버로드는 전 저장소에서 호출부 0건이라는 EVD-294 의 판정이 구현 측에서도 확인된다.
# (이 리프의 대역 2개 + inbox 대역 3개 + 포트 선언 2개 + 구현 2개 = 9개 등장, 호출 0)