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>
78 lines
4.6 KiB
Plaintext
78 lines
4.6 KiB
Plaintext
# 주제: Debezium 커넥터 설정이 두 곳에 있고, 운영자가 실제로 배포하는 쪽이 수정 이전 버전이다
|
|
# revision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
# severity: P2
|
|
|
|
# ---- 표현 1: Java ----
|
|
# DebeziumOutboxEventRouter.java:35-85
|
|
# public static final String KEY_COLUMN = "routing_key";
|
|
# public Map<String, String> connectorConfiguration(String topicPrefix) {
|
|
# configuration.put("transforms.outbox.table.field.event.key", KEY_COLUMN); // routing_key
|
|
# configuration.put("transforms.outbox.route.by.field", ROUTE_BY_COLUMN); // destination
|
|
# configuration.put("transforms.outbox.route.topic.replacement", topicPrefix + "${routedByValue}");
|
|
# configuration.put("transforms.outbox.table.fields.additional.placement", <15개>);
|
|
# }
|
|
#
|
|
# 클래스 javadoc:21-26 이 고친 결함을 명시한다:
|
|
# "The message key and the topic are two different questions and used to be answered by one
|
|
# column. `destination` routed the topic *and* became the key, so every message on a topic
|
|
# carried the same key, landed on one partition, and made keyed ordering a claim with no
|
|
# mechanism behind it. The key now comes from `routing_key` …"
|
|
|
|
# ---- 표현 2: 배포되는 리소스 ----
|
|
# src/main/resources/debezium/outbox-event-router.properties:28
|
|
# transforms.outbox.table.field.event.key=destination <-- 수정 이전 값
|
|
# :33
|
|
# transforms.outbox.route.topic.replacement=${routedByValue} <-- 프리픽스 없음
|
|
# :30
|
|
# transforms.outbox.table.field.event.timestamp=created_at <-- Java 에는 없는 키
|
|
# :37
|
|
# transforms.outbox.table.fields.additional.placement=
|
|
# message_type:header:msg.type,
|
|
# schema_version:header:msg.schema-version,
|
|
# content_type:header:msg.content-type,
|
|
# message_id:header:msg.id <-- 4개
|
|
|
|
# ---- 측정 ----
|
|
# command: (Java) sed -n "66,83p" DebeziumOutboxEventRouter.java | grep -c "header:"
|
|
# 15
|
|
# command: (properties) grep "additional.placement" …properties | tr "," "\n" | grep -c "header:"
|
|
# 4
|
|
# command: grep "event.key" …properties
|
|
# transforms.outbox.table.field.event.key=destination
|
|
|
|
# properties 에 없는 11개 헤더: created_at(PRODUCED_AT), destination(ORIGIN_DESTINATION),
|
|
# producer, occurred_at, correlation_id, causation_id, tenant, partition_key, ordering_key,
|
|
# traceparent, tracestate, baggage
|
|
# (= V4__messaging_outbox_canonical_metadata.sql 이 추가한 정경 메타데이터 전부)
|
|
# V4 주석:6-9 "Everything else the envelope carries … had nowhere to go, so it was either lost
|
|
# when the relay rebuilt the envelope or smuggled through the header map"
|
|
# -> 그 손실을 막으려고 컬럼을 만들었는데, CDC 배포 파일은 그 컬럼들을 헤더로 옮기지 않는다.
|
|
|
|
# ---- 두 표현을 잇는 것이 없다 ----
|
|
# command: git grep -rn "outbox-event-router" -- src
|
|
# exit: 1 (출력 없음)
|
|
# => properties 파일을 읽는 코드도, 그 내용을 Java 설정과 대조하는 테스트도 없다.
|
|
|
|
# ---- 테스트는 Java 쪽만 검증하며, 그 테스트는 의도적으로 견고하게 작성되어 있다 ----
|
|
# DebeziumOutboxRecordMapperTest.java:154-162
|
|
# @Test
|
|
# void theRoutedKeyIsNotTheTopicName() {
|
|
# // Literals, not the class's own constants: comparing a configuration value against the
|
|
# // constant that produced it asserts that the router agrees with itself, which it always will.
|
|
# assertThat(new DebeziumOutboxEventRouter().connectorConfiguration("prod."))
|
|
# .as("keying by destination puts every message on a topic onto one partition")
|
|
# .containsEntry("transforms.outbox.table.field.event.key", "routing_key")
|
|
# .containsEntry("transforms.outbox.route.by.field", "destination");
|
|
# }
|
|
# => 리터럴 대조까지 하는 테스트가 Java 를 지키고 있고, 운영자가 배포하는 파일은 아무도 지키지 않는다.
|
|
|
|
# ---- 부수 발견: aggregateIdAsPartitionKey 는 커넥터에 도달할 수 없다 ----
|
|
# DebeziumOutboxProfile 은 aggregateIdAsPartitionKey 필드를 갖는다(:20).
|
|
# DebeziumOutboxRecordMapper 는 그것으로 분기한다(:70-78):
|
|
# if (!aggregateIdAsPartitionKey) { return Optional.empty(); }
|
|
# 그러나 connectorConfiguration(String topicPrefix) 는 프로필을 받지 않으며(:55),
|
|
# event.key 를 항상 routing_key 로 고정한다(:60).
|
|
# => 플래그가 false 여도 실제 Debezium 은 키를 붙인다. 모델(mapper)은 "키 없음" 을 예측한다.
|
|
# mapper 의 존재 이유가 "Produces what Debezium's Event Router will emit"(:11) 인데,
|
|
# 기본값(polling() -> false)에서 그 예측이 커넥터 설정과 불일치한다.
|