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>
239 lines
17 KiB
Plaintext
239 lines
17 KiB
Plaintext
# 루트 예외가 세우는 두 규칙
|
|
MongoPersistenceException.java:6 /**
|
|
MongoPersistenceException.java:7 * Root of the provider-stable MongoDB error hierarchy (design §15).
|
|
MongoPersistenceException.java:8 *
|
|
MongoPersistenceException.java:9 * <p>Two rules shape this class. First, the message is derived from {@link MongoFailureContext},
|
|
MongoPersistenceException.java:10 * which can only hold bounded, data-free values — so no caller can accidentally put a document or a
|
|
MongoPersistenceException.java:11 * query parameter into an exception message. Second, no constructor accepts a {@link Throwable}
|
|
MongoPersistenceException.java:12 * cause: attaching the driver exception would re-expose everything the failure context deliberately
|
|
MongoPersistenceException.java:13 * dropped, through {@code getCause()} and through every stack trace printer. The driver's
|
|
MongoPersistenceException.java:14 * information survives as the error labels and server code already carried by the context.
|
|
MongoPersistenceException.java:15 */
|
|
MongoPersistenceException.java:16 public abstract class MongoPersistenceException extends RuntimeException {
|
|
MongoPersistenceException.java:17
|
|
MongoPersistenceException.java:18 @Serial private static final long serialVersionUID = 1L;
|
|
MongoPersistenceException.java:19
|
|
MongoPersistenceException.java:20 private final transient MongoFailureContext failureContext;
|
|
MongoPersistenceException.java:21
|
|
MongoPersistenceException.java:22 protected MongoPersistenceException(String summary, MongoFailureContext failureContext) {
|
|
MongoPersistenceException.java:23 super(
|
|
MongoPersistenceException.java:24 summary + " [" + Objects.requireNonNull(failureContext, "failureContext").describe() + "]");
|
|
MongoPersistenceException.java:25 this.failureContext = failureContext;
|
|
MongoPersistenceException.java:26 }
|
|
MongoPersistenceException.java:27
|
|
MongoPersistenceException.java:28 /** The bounded metadata for this failure. Never contains document or query data. */
|
|
MongoPersistenceException.java:29 public MongoFailureContext failureContext() {
|
|
MongoPersistenceException.java:30 return failureContext;
|
|
MongoPersistenceException.java:31 }
|
|
|
|
# 그 규칙이 적용되는 범위
|
|
MongoPersistenceException 을 상속하는 main 타입 : 22 개
|
|
MongoBulkPartialFailureException.java
|
|
MongoChangeHistoryLostException.java
|
|
MongoConnectionException.java
|
|
MongoCursorException.java
|
|
MongoDataSchemaUnsupportedException.java
|
|
MongoDocumentNotFoundException.java
|
|
MongoDocumentTooLargeException.java
|
|
MongoDuplicateKeyException.java
|
|
MongoEncryptionException.java
|
|
MongoOperationRejectedException.java
|
|
MongoOptimisticConflictException.java
|
|
MongoReadConcernException.java
|
|
MongoResumeException.java
|
|
MongoSchemaValidationException.java
|
|
MongoServerSelectionException.java
|
|
MongoShardRoutingException.java
|
|
MongoTimeoutException.java
|
|
MongoTransactionCommitUnknownException.java
|
|
MongoTransactionTransientException.java
|
|
MongoUnclassifiedFailureException.java
|
|
MongoWriteConcernException.java
|
|
MongoWriteConflictException.java
|
|
|
|
# 이 계층에서 원인을 받거나 설정하는 자리 전수
|
|
Throwable 을 인자로 받는 생성자 줄 : 1 개
|
|
initCause 를 부르는 줄 : 1 개
|
|
MongoTimeoutException.java:25 public MongoTimeoutException(MongoFailureContext failureContext, Throwable cause) {
|
|
MongoTimeoutException.java:27 initCause(cause);
|
|
[대조] 그 검색이 훑은 파일 : 351 개
|
|
[대조] 옆 계층 persistence-jpa 에서 같은 검색 : 19 줄
|
|
|
|
# 그 하나의 타입
|
|
MongoTimeoutException.java:5 /**
|
|
MongoTimeoutException.java:6 * The operation exceeded its deadline (design §15).
|
|
MongoTimeoutException.java:7 *
|
|
MongoTimeoutException.java:8 * <p>A timeout says when the client stopped waiting, not what the server did. A write that timed
|
|
MongoTimeoutException.java:9 * out after being sent is {@code WRITE_RESULT_UNKNOWN} and must be reconciled; only a timeout that
|
|
MongoTimeoutException.java:10 * fired before the command left is safe to replay.
|
|
MongoTimeoutException.java:11 */
|
|
MongoTimeoutException.java:12 public final class MongoTimeoutException extends MongoPersistenceException {
|
|
MongoTimeoutException.java:13
|
|
MongoTimeoutException.java:14 @Serial private static final long serialVersionUID = 1L;
|
|
MongoTimeoutException.java:15
|
|
MongoTimeoutException.java:16 public MongoTimeoutException(MongoFailureContext failureContext) {
|
|
MongoTimeoutException.java:17 super("the MongoDB operation exceeded its deadline", failureContext);
|
|
MongoTimeoutException.java:18 }
|
|
MongoTimeoutException.java:19
|
|
MongoTimeoutException.java:20 /**
|
|
MongoTimeoutException.java:21 * Creates the exception from a client-side timeout.
|
|
MongoTimeoutException.java:22 *
|
|
MongoTimeoutException.java:23 * @param cause the underlying timeout, kept so a reactive stack trace still names its source
|
|
MongoTimeoutException.java:24 */
|
|
MongoTimeoutException.java:25 public MongoTimeoutException(MongoFailureContext failureContext, Throwable cause) {
|
|
MongoTimeoutException.java:26 super("the MongoDB operation exceeded its deadline", failureContext);
|
|
MongoTimeoutException.java:27 initCause(cause);
|
|
MongoTimeoutException.java:28 }
|
|
MongoTimeoutException.java:29 }
|
|
|
|
# 2 인자 생성자를 부르는 자리
|
|
MongoTimeoutException.java:16 public MongoTimeoutException(MongoFailureContext failureContext) {
|
|
MongoTimeoutException.java:25 public MongoTimeoutException(MongoFailureContext failureContext, Throwable cause) {
|
|
DefaultMongoFailureTranslator.java:104 case TIMEOUT -> new MongoTimeoutException(failureContext);
|
|
DefaultReactiveMongoExecutor.java:152 new dev.caskeleton.adapter.outbound.mongo.api.error.MongoTimeoutException(
|
|
|
|
DefaultReactiveMongoExecutor.java:138 MongoOperationObservation observation,
|
|
DefaultReactiveMongoExecutor.java:139 Throwable failure) {
|
|
DefaultReactiveMongoExecutor.java:140 if (failure instanceof MongoPersistenceException alreadyTranslated) {
|
|
DefaultReactiveMongoExecutor.java:141 if (observation != null) {
|
|
DefaultReactiveMongoExecutor.java:142 observation.failure(alreadyTranslated.failureContext());
|
|
DefaultReactiveMongoExecutor.java:143 }
|
|
DefaultReactiveMongoExecutor.java:144 return alreadyTranslated;
|
|
DefaultReactiveMongoExecutor.java:145 }
|
|
DefaultReactiveMongoExecutor.java:146 if (failure instanceof java.util.concurrent.TimeoutException reactorTimeout) {
|
|
DefaultReactiveMongoExecutor.java:147 // Reactor's own timeout, not the driver's. It used to escape as a raw
|
|
DefaultReactiveMongoExecutor.java:148 // java.util.concurrent.TimeoutException — a type no caller of this platform is told about,
|
|
DefaultReactiveMongoExecutor.java:149 // and one that carries no operation, no outcome and no observation. The failure was also
|
|
DefaultReactiveMongoExecutor.java:150 // never recorded, because this method returned before reaching the observer.
|
|
DefaultReactiveMongoExecutor.java:151 MongoPersistenceException timedOut =
|
|
DefaultReactiveMongoExecutor.java:152 new dev.caskeleton.adapter.outbound.mongo.api.error.MongoTimeoutException(
|
|
DefaultReactiveMongoExecutor.java:153 dev.caskeleton.adapter.outbound.mongo.api.error.MongoFailureContext.timedOut(
|
|
DefaultReactiveMongoExecutor.java:154 context.operationName(), operationType, context.timeout()),
|
|
DefaultReactiveMongoExecutor.java:155 reactorTimeout);
|
|
DefaultReactiveMongoExecutor.java:156 if (observation != null) {
|
|
DefaultReactiveMongoExecutor.java:157 observation.failure(timedOut.failureContext());
|
|
DefaultReactiveMongoExecutor.java:158 }
|
|
DefaultReactiveMongoExecutor.java:159 return timedOut;
|
|
DefaultReactiveMongoExecutor.java:160 }
|
|
DefaultReactiveMongoExecutor.java:161 MongoException driverFailure = asDriverFailure(failure);
|
|
DefaultReactiveMongoExecutor.java:162 if (driverFailure == null) {
|
|
DefaultReactiveMongoExecutor.java:163 return failure;
|
|
DefaultReactiveMongoExecutor.java:164 }
|
|
DefaultReactiveMongoExecutor.java:165 MongoPersistenceException translated =
|
|
DefaultReactiveMongoExecutor.java:166 translator.translate(
|
|
DefaultReactiveMongoExecutor.java:167 context, operationType, Duration.ZERO, MongoDriverFailureView.from(driverFailure), 1);
|
|
DefaultReactiveMongoExecutor.java:168 if (observation != null) {
|
|
DefaultReactiveMongoExecutor.java:169 observation.failure(translated.failureContext().withTraceId(observation.traceId()));
|
|
DefaultReactiveMongoExecutor.java:170 }
|
|
DefaultReactiveMongoExecutor.java:171 return translated;
|
|
DefaultReactiveMongoExecutor.java:172 }
|
|
|
|
# 생성자를 지나지 않고 드라이버 예외가 붙는 자리
|
|
이 계층에서 addSuppressed 를 부르는 줄 : 3 개
|
|
SpringMongoTransactionSessionFactory.java:88 startFailed.addSuppressed(closeFailed);
|
|
SpringMongoTransactionSessionFactory.java:194 inFlight.addSuppressed(failure);
|
|
SpringMongoTransactionSessionFactory.java:200 cleanupFailure.addSuppressed(failure);
|
|
|
|
SpringMongoTransactionSessionFactory.java:159 public void abort() {
|
|
SpringMongoTransactionSessionFactory.java:160 if (!committed && session.hasActiveTransaction()) {
|
|
SpringMongoTransactionSessionFactory.java:161 session.abortTransaction();
|
|
SpringMongoTransactionSessionFactory.java:162 }
|
|
SpringMongoTransactionSessionFactory.java:163 }
|
|
SpringMongoTransactionSessionFactory.java:164
|
|
SpringMongoTransactionSessionFactory.java:165 @Override
|
|
SpringMongoTransactionSessionFactory.java:166 public void close() {
|
|
SpringMongoTransactionSessionFactory.java:167 try {
|
|
SpringMongoTransactionSessionFactory.java:168 abort();
|
|
SpringMongoTransactionSessionFactory.java:169 } catch (RuntimeException abortFailed) {
|
|
SpringMongoTransactionSessionFactory.java:170 recordCleanupFailure(abortFailed);
|
|
SpringMongoTransactionSessionFactory.java:171 } finally {
|
|
SpringMongoTransactionSessionFactory.java:172 try {
|
|
SpringMongoTransactionSessionFactory.java:173 session.close();
|
|
SpringMongoTransactionSessionFactory.java:174 } catch (RuntimeException closeFailed) {
|
|
SpringMongoTransactionSessionFactory.java:175 recordCleanupFailure(closeFailed);
|
|
SpringMongoTransactionSessionFactory.java:176 }
|
|
|
|
SpringMongoTransactionSessionFactory.java:185 /**
|
|
SpringMongoTransactionSessionFactory.java:186 * Keeps a cleanup failure without letting it become the outcome.
|
|
SpringMongoTransactionSessionFactory.java:187 *
|
|
SpringMongoTransactionSessionFactory.java:188 * <p>Attached to the failure already in flight when there is one, so the stack the caller sees
|
|
SpringMongoTransactionSessionFactory.java:189 * still names what actually went wrong, with the failed abort alongside it rather than in place
|
|
SpringMongoTransactionSessionFactory.java:190 * of it.
|
|
SpringMongoTransactionSessionFactory.java:191 */
|
|
SpringMongoTransactionSessionFactory.java:192 private void recordCleanupFailure(RuntimeException failure) {
|
|
SpringMongoTransactionSessionFactory.java:193 if (inFlight != null) {
|
|
SpringMongoTransactionSessionFactory.java:194 inFlight.addSuppressed(failure);
|
|
SpringMongoTransactionSessionFactory.java:195 return;
|
|
SpringMongoTransactionSessionFactory.java:196 }
|
|
SpringMongoTransactionSessionFactory.java:197 if (cleanupFailure == null) {
|
|
SpringMongoTransactionSessionFactory.java:198 cleanupFailure = failure;
|
|
SpringMongoTransactionSessionFactory.java:199 } else {
|
|
|
|
SpringMongoTransactionSessionFactory.java:212 private RuntimeException translate(Throwable failure, MongoFailurePhase phase) {
|
|
SpringMongoTransactionSessionFactory.java:213 RuntimeException translated = classify(failure, phase);
|
|
SpringMongoTransactionSessionFactory.java:214 inFlight = translated;
|
|
SpringMongoTransactionSessionFactory.java:215 return translated;
|
|
SpringMongoTransactionSessionFactory.java:216 }
|
|
SpringMongoTransactionSessionFactory.java:217
|
|
SpringMongoTransactionSessionFactory.java:218 private RuntimeException translateCommit(Throwable failure) {
|
|
SpringMongoTransactionSessionFactory.java:219 RuntimeException translated = classify(failure, MongoFailurePhase.TRANSACTION_COMMIT);
|
|
SpringMongoTransactionSessionFactory.java:220 inFlight = translated;
|
|
SpringMongoTransactionSessionFactory.java:221 return translated;
|
|
SpringMongoTransactionSessionFactory.java:222 }
|
|
SpringMongoTransactionSessionFactory.java:223
|
|
SpringMongoTransactionSessionFactory.java:224 private RuntimeException classify(Throwable failure, MongoFailurePhase phase) {
|
|
SpringMongoTransactionSessionFactory.java:225 MongoDriverFailureView view = MongoFailureExtractor.viewOf(failure).orElse(null);
|
|
SpringMongoTransactionSessionFactory.java:226 if (view == null) {
|
|
SpringMongoTransactionSessionFactory.java:227 // Not a Mongo failure, or a cause chain that cannot be walked. Terminal is the fail-closed
|
|
SpringMongoTransactionSessionFactory.java:228 // verdict when the alternative is replaying work.
|
|
SpringMongoTransactionSessionFactory.java:229 return failure instanceof RuntimeException runtime
|
|
SpringMongoTransactionSessionFactory.java:230 ? runtime
|
|
SpringMongoTransactionSessionFactory.java:231 : new IllegalStateException("unclassifiable Mongo failure", failure);
|
|
SpringMongoTransactionSessionFactory.java:232 }
|
|
SpringMongoTransactionSessionFactory.java:233 // The phase is passed through: the same socket failure is a body replay before the commit
|
|
SpringMongoTransactionSessionFactory.java:234 // and a commit-unknown during it, and the classifier could not tell them apart.
|
|
SpringMongoTransactionSessionFactory.java:235 MongoRetryScope scope =
|
|
SpringMongoTransactionSessionFactory.java:236 classifier
|
|
SpringMongoTransactionSessionFactory.java:237 .classify(
|
|
SpringMongoTransactionSessionFactory.java:238 phase == MongoFailurePhase.TRANSACTION_COMMIT
|
|
SpringMongoTransactionSessionFactory.java:239 ? MongoOperationType.TRANSACTION_COMMIT
|
|
SpringMongoTransactionSessionFactory.java:240 : MongoOperationType.UPDATE,
|
|
SpringMongoTransactionSessionFactory.java:241 phase,
|
|
SpringMongoTransactionSessionFactory.java:242 view)
|
|
SpringMongoTransactionSessionFactory.java:243 .retryScope();
|
|
SpringMongoTransactionSessionFactory.java:244 String serverCode = view.hasServerCode() ? Integer.toString(view.serverCode()) : "";
|
|
SpringMongoTransactionSessionFactory.java:245 // The context is derived from the scope, not built first and then labelled. Building a
|
|
SpringMongoTransactionSessionFactory.java:246 // commit-unknown context and wrapping it in whichever exception the classifier chose is how
|
|
SpringMongoTransactionSessionFactory.java:247 // a "replay the body" exception came to carry "unknown commit, not retryable, ambiguous".
|
|
SpringMongoTransactionSessionFactory.java:248 if (scope == MongoRetryScope.COMMIT_ONLY) {
|
|
SpringMongoTransactionSessionFactory.java:249 return new MongoTransactionCommitUnknownException(
|
|
SpringMongoTransactionSessionFactory.java:250 MongoFailureContext.commitUnknown(SESSION_OPERATION, serverCode, Duration.ZERO),
|
|
SpringMongoTransactionSessionFactory.java:251 "read the transaction record, version or idempotency key");
|
|
SpringMongoTransactionSessionFactory.java:252 }
|
|
SpringMongoTransactionSessionFactory.java:253 if (scope == MongoRetryScope.WHOLE_TRANSACTION) {
|
|
SpringMongoTransactionSessionFactory.java:254 return new MongoTransactionTransientException(
|
|
SpringMongoTransactionSessionFactory.java:255 MongoFailureContext.transientTransaction(SESSION_OPERATION, serverCode, Duration.ZERO));
|
|
SpringMongoTransactionSessionFactory.java:256 }
|
|
|
|
# 그 규칙을 검사하는 시험
|
|
이 계층의 시험에서 getCause 를 단언하는 줄 : 1 개
|
|
MongoFailureContextTest.java:65 assertThat(exception.getCause()).isNull();
|
|
[대조] test 소스 세트에서는 : 9 줄
|
|
[대조] 시험 소스 세트를 모두 훑으면 : 9 줄
|
|
[대조] 이 계층의 하위 타입 가운데 final 로 닫힌 것 : 22 개
|
|
|
|
MongoFailureContextTest.java:56
|
|
MongoFailureContextTest.java:57 @Test
|
|
MongoFailureContextTest.java:58 void exceptionsDoNotExposeADriverCause() {
|
|
MongoFailureContextTest.java:59 MongoPersistenceException exception =
|
|
MongoFailureContextTest.java:60 new MongoTransactionCommitUnknownException(
|
|
MongoFailureContextTest.java:61 MongoFailureContext.commitUnknown(
|
|
MongoFailureContextTest.java:62 new MongoOperationName("order.reserve"), "251", Duration.ZERO),
|
|
MongoFailureContextTest.java:63 "read the transaction record");
|
|
MongoFailureContextTest.java:64
|
|
MongoFailureContextTest.java:65 assertThat(exception.getCause()).isNull();
|
|
MongoFailureContextTest.java:66 assertThat(exception.category()).isEqualTo(MongoFailureCategory.TRANSACTION_COMMIT_UNKNOWN);
|
|
MongoFailureContextTest.java:67 }
|
|
MongoTimeoutException 이 나오는 시험 줄 : 0 개
|
|
[대조] MongoTransactionCommitUnknownException 은 : 4 줄
|