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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
43bccd08a8
commit
b2963105a8
+95
@@ -0,0 +1,95 @@
|
||||
# 청구 경로는 행을 잠근 뒤 데이터베이스 시각을 읽고 만료를 먼저 본다
|
||||
127: IdempotencyRecordRow row =
|
||||
128: rows.findForUpdate(request.scope().digest()).orElseThrow(this::indeterminateClaim);
|
||||
129: Instant dbNow = rows.databaseNow();
|
||||
130: if (inserted == 1) {
|
||||
131: return acquired(row);
|
||||
132: }
|
||||
133:
|
||||
134: if (isExpiredCompleted(row, dbNow)) {
|
||||
135: return resetClaim(request, row);
|
||||
136: }
|
||||
137: if (!row.requestHash().equals(request.requestFingerprint().hex())) {
|
||||
138: return new IdempotencyClaimOutcome.FingerprintMismatch();
|
||||
139: }
|
||||
140: if (row.state() == IdempotencyState.COMPLETED && row.replayUntil() != null) {
|
||||
141: return new IdempotencyClaimOutcome.CompletedReplay(
|
||||
142: new StoredResponse(row.responsePayload()), row.replayUntil());
|
||||
143: }
|
||||
586: private static boolean isExpiredCompleted(IdempotencyRecordRow row, Instant dbNow) {
|
||||
587- return row.state() == IdempotencyState.COMPLETED
|
||||
588- && row.replayUntil() != null
|
||||
589- && !dbNow.isBefore(row.replayUntil());
|
||||
590- }
|
||||
# claim 본문에서 databaseNow 를 부르는 줄: 1
|
||||
|
||||
# 조회 경로 전문
|
||||
393: public IdempotencyInspection inspect(IdempotencyInspectionRequest request) {
|
||||
394: Objects.requireNonNull(request, "request");
|
||||
395: Optional<IdempotencyRecordRow> found = rows.find(request.scope().digest());
|
||||
396: if (found.isEmpty()) {
|
||||
397: return IdempotencyInspection.outcome(IdempotencyInspectionOutcome.ABSENT);
|
||||
398: }
|
||||
399: IdempotencyRecordRow row = found.get();
|
||||
400: if (!row.requestHash().equals(request.requestFingerprint().hex())) {
|
||||
401: return IdempotencyInspection.outcome(IdempotencyInspectionOutcome.FINGERPRINT_MISMATCH);
|
||||
402: }
|
||||
403: boolean sameAttempt = sameClaimAttempt(row, request.claimAttempt());
|
||||
404: if (row.state() == IdempotencyState.COMPLETED && row.responsePayload() != null) {
|
||||
405: return new IdempotencyInspection(
|
||||
406: IdempotencyInspectionOutcome.COMPLETED_REPLAY,
|
||||
407: Optional.empty(),
|
||||
408: Optional.empty(),
|
||||
409: Optional.of(new StoredResponse(row.responsePayload())),
|
||||
410: Optional.ofNullable(row.replayUntil()));
|
||||
411: }
|
||||
412: if (sameAttempt && row.state() == IdempotencyState.CLAIMED) {
|
||||
413: return inspectionWithOwner(IdempotencyInspectionOutcome.CLAIMED_SAME_OPERATION, row);
|
||||
414: }
|
||||
415: if (sameAttempt && row.state() == IdempotencyState.EXECUTING) {
|
||||
416: return inspectionWithOwner(IdempotencyInspectionOutcome.EXECUTING_SAME_OPERATION, row);
|
||||
417: }
|
||||
418: if (row.ownerToken().equals(request.claimAttempt().ownerToken()) && !sameAttempt) {
|
||||
419: return IdempotencyInspection.outcome(IdempotencyInspectionOutcome.OPERATION_CONFLICT);
|
||||
420: }
|
||||
421: return switch (row.state()) {
|
||||
422: case FAILED_RETRYABLE ->
|
||||
423: IdempotencyInspection.outcome(IdempotencyInspectionOutcome.FAILED_RETRYABLE);
|
||||
424: case ABANDONED -> IdempotencyInspection.outcome(IdempotencyInspectionOutcome.ABANDONED);
|
||||
425: default -> IdempotencyInspection.outcome(IdempotencyInspectionOutcome.IN_PROGRESS_OTHER);
|
||||
426: };
|
||||
427: }
|
||||
# inspect 본문에서 databaseNow 를 부르는 줄: 0
|
||||
# inspect 본문이 replayUntil 로 하는 일
|
||||
18: Optional.ofNullable(row.replayUntil()));
|
||||
# 만료 비교는 파일에 한 곳뿐이고, 그것을 부르는 곳
|
||||
134: if (isExpiredCompleted(row, dbNow)) {
|
||||
586: private static boolean isExpiredCompleted(IdempotencyRecordRow row, Instant dbNow) {
|
||||
# 조회가 쓰는 조회 SQL 에 시간 술어가 있는가
|
||||
|
||||
# 조회 결과를 받는 세 자리
|
||||
146: case COMPLETED_REPLAY -> codec.deserialize(requireResponse(inspection).payload());
|
||||
147- case FINGERPRINT_MISMATCH ->
|
||||
148- throw IdempotencyRequestMismatchException.forScopeDigest(diagnostic(request.scope()));
|
||||
149- case IN_PROGRESS_OTHER ->
|
||||
--
|
||||
192: case COMPLETED_REPLAY -> codec.deserialize(requireResponse(inspection).payload());
|
||||
193- case UNAVAILABLE -> throw new IdempotencyUnavailableException();
|
||||
194- default -> throw recovery("execution start is indeterminate");
|
||||
195- };
|
||||
--
|
||||
261: case COMPLETED_REPLAY -> {
|
||||
262- StoredResponse stored = requireResponse(inspection);
|
||||
263- if (stored.payload().equals(codec.serialize(result))) {
|
||||
264- yield result;
|
||||
|
||||
# 출하 통합 시험의 만료 시험이 무엇을 만료시키는가
|
||||
130: void expiredClaimCanBeTakenOverButTheStaleOwnerCannotStart() {
|
||||
164: void expiredExecutingRecordRequiresReconciliationAndIsNeverBlindlyTakenOver() {
|
||||
138: Duration.ofMillis(25))))
|
||||
171: store.claim(request(original, Duration.ofMillis(25))))
|
||||
366: private static IdempotencyClaimRequest request(
|
||||
367- IdempotencyClaimAttempt attempt, Duration processingLease) {
|
||||
368- return new IdempotencyClaimRequest(
|
||||
369- SCOPE, FINGERPRINT, attempt, processingLease, Duration.ofHours(24), "json.v1", 2);
|
||||
grep: adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRecordRows.java: No such file or directory
|
||||
Reference in New Issue
Block a user