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>
69 lines
3.6 KiB
Plaintext
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)
|