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>
50 lines
2.5 KiB
Plaintext
50 lines
2.5 KiB
Plaintext
# 다이제스트가 덮던 다섯 성분과 그때 같아지던 쌍
|
|
* <p>It omitted several. {@code transition|operationId|ownerToken|attempt|stateRevision} covers who
|
|
* and when, and nothing about what: a {@code FAIL} recorded with a retryable disposition and one
|
|
* recorded as abandoned produced identical digests, as did two completions carrying different
|
|
* responses or different retention. A replay comparing digests concluded "same transition" for two
|
|
* transitions whose whole difference was the part that mattered.
|
|
*
|
|
* <p>Length-framed, because every component is variable-width text and at least one — the owner
|
|
* token — is not this platform's to constrain. Joining with a delimiter means two different
|
|
* component lists can render to one string.
|
|
*/
|
|
|
|
# 지금 각 호출부가 실제로 넘기는 의미 인자
|
|
181: String startDigest = transitionDigest("START", operationId, owner);
|
|
220: transitionDigest("RENEW", operationId, owner, Long.toString(processingLeaseTtl.toMillis()));
|
|
365: String releaseDigest = transitionDigest("RELEASE", operationId, owner);
|
|
transitionDigest(
|
|
"COMPLETE",
|
|
operationId,
|
|
owner,
|
|
responseDigest,
|
|
Long.toString(replayTtl.toMillis())),
|
|
response.payload(),
|
|
String transitionKind =
|
|
disposition == IdempotencyFailureDisposition.NO_EFFECT_RETRYABLE
|
|
? "FAIL_RETRYABLE"
|
|
: "FAIL_ABANDONED";
|
|
String failDigest =
|
|
transitionDigest(
|
|
transitionKind,
|
|
operationId,
|
|
owner,
|
|
disposition.name(),
|
|
Long.toString(retention.toMillis()));
|
|
|
|
# 이어 붙일 때 각 성분 앞에 적는 길이와, 해시가 실제로 먹는 바이트
|
|
private static void write(StringBuilder out, String value) {
|
|
out.append(value.length()).append(':').append(value);
|
|
79: MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)));
|
|
30: static final int VERSION = 2;
|
|
59: write(framed, "v" + VERSION);
|
|
|
|
# 그런데 완료 재생 분기는 이 다이제스트를 비교하지 않는다
|
|
if (row.state() == IdempotencyState.COMPLETED
|
|
&& "COMPLETE".equals(row.lastTransitionKind())
|
|
&& operationId.value().equals(row.lastTransitionOperationId())) {
|
|
return responseDigest.equals(row.responseDigest())
|
|
? IdempotencyCompleteOutcome.ALREADY_COMPLETED_SAME_RESULT
|
|
: IdempotencyCompleteOutcome.RESPONSE_CONFLICT;
|