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>
215 lines
17 KiB
Plaintext
215 lines
17 KiB
Plaintext
# 이 타입이 존재하는 이유
|
|
BoundScopedOperations.java:12 /**
|
|
BoundScopedOperations.java:13 * {@link ScopedMongoOperations} over one physical collection.
|
|
BoundScopedOperations.java:14 *
|
|
BoundScopedOperations.java:15 * <p>Every method passes the collection this scope was built with. That is the whole implementation
|
|
BoundScopedOperations.java:16 * and the whole point: a caller cannot supply a different one, so the registered collection profile
|
|
BoundScopedOperations.java:17 * and any tenant boundary derived from it hold without the caller having to cooperate.
|
|
BoundScopedOperations.java:18 *
|
|
BoundScopedOperations.java:19 * <p>Every query-shaped method also carries the operation's deadline as {@code maxTimeMS}, and that
|
|
BoundScopedOperations.java:20 * is the difference between a deadline and a report about one. The blocking executor could only
|
|
BoundScopedOperations.java:21 * measure elapsed time after the callback returned — a Java callback cannot be interrupted mid
|
|
BoundScopedOperations.java:22 * driver call — so an operation that ran past its budget was detected, never stopped. Sent to the
|
|
BoundScopedOperations.java:23 * server, the same number ends the work.
|
|
BoundScopedOperations.java:24 *
|
|
BoundScopedOperations.java:25 * <p>It applies to the methods that take a {@link Query} or an {@link Aggregation}, because those
|
|
BoundScopedOperations.java:26 * are the ones the server can cut short. {@code insert} carries no query to attach it to.
|
|
BoundScopedOperations.java:27 */
|
|
그 데드라인이 실제로 붙는 자리 :
|
|
BoundScopedOperations.java:19 * <p>Every query-shaped method also carries the operation's deadline as {@code maxTimeMS}, and that
|
|
BoundScopedOperations.java:53 return query.maxTimeMsec(timeout.toMillis());
|
|
BoundScopedOperations.java:98 .maxTime(timeout)
|
|
|
|
BoundScopedOperations.java:48 * <p>Mutates the caller's query rather than copying it, which is what Spring Data's own fluent
|
|
BoundScopedOperations.java:49 * API does; a copy would silently drop any hint, collation or read preference the caller set.
|
|
BoundScopedOperations.java:50 */
|
|
BoundScopedOperations.java:51 private Query bounded(Query query) {
|
|
BoundScopedOperations.java:52 Objects.requireNonNull(query, "query");
|
|
BoundScopedOperations.java:53 return query.maxTimeMsec(timeout.toMillis());
|
|
BoundScopedOperations.java:54 }
|
|
BoundScopedOperations.java:55
|
|
BoundScopedOperations.java:56 @Override
|
|
|
|
BoundScopedOperations.java:92
|
|
BoundScopedOperations.java:93 @Override
|
|
BoundScopedOperations.java:94 public <T> List<T> aggregate(Aggregation aggregation, Class<T> resultType) {
|
|
BoundScopedOperations.java:95 Aggregation bounded =
|
|
BoundScopedOperations.java:96 aggregation.withOptions(
|
|
BoundScopedOperations.java:97 org.springframework.data.mongodb.core.aggregation.AggregationOptions.builder()
|
|
BoundScopedOperations.java:98 .maxTime(timeout)
|
|
BoundScopedOperations.java:99 .build());
|
|
BoundScopedOperations.java:100 return operations.aggregate(bounded, collection, resultType).getMappedResults();
|
|
BoundScopedOperations.java:101 }
|
|
BoundScopedOperations.java:102 }
|
|
|
|
# 그 타입에 닿는 유일한 문
|
|
MongoCollectionAccess.java:24
|
|
MongoCollectionAccess.java:25 /**
|
|
MongoCollectionAccess.java:26 * The operations this callback may perform.
|
|
MongoCollectionAccess.java:27 *
|
|
MongoCollectionAccess.java:28 * <p>Scoped: no method takes a collection name, so the check {@link #collection(String)} performs
|
|
MongoCollectionAccess.java:29 * cannot be bypassed by not calling it. The previous {@code MongoOperations} accessor could reach
|
|
MongoCollectionAccess.java:30 * any collection in the database.
|
|
MongoCollectionAccess.java:31 */
|
|
MongoCollectionAccess.java:32 ScopedMongoOperations scoped();
|
|
MongoCollectionAccess.java:33 }
|
|
|
|
MongoPlatformCollectionAccess.java:5 /**
|
|
MongoPlatformCollectionAccess.java:6 * Raw access, for the platform's own executors only.
|
|
MongoPlatformCollectionAccess.java:7 *
|
|
MongoPlatformCollectionAccess.java:8 * <p>The bulk, atomic, aggregation and geospatial executors build driver-level commands and need
|
|
MongoPlatformCollectionAccess.java:9 * the unrestricted template. They are inside the platform and are the code that installs the
|
|
MongoPlatformCollectionAccess.java:10 * guardrails, which is a different position from a caller's callback — so the raw handle is a type
|
|
MongoPlatformCollectionAccess.java:11 * they ask for by name rather than something every callback receives.
|
|
MongoPlatformCollectionAccess.java:12 *
|
|
MongoPlatformCollectionAccess.java:13 * <p>Keeping it out of {@link MongoCollectionAccess} is the point. An interface that offers both a
|
|
MongoPlatformCollectionAccess.java:14 * scoped API and an escape hatch offers an escape hatch.
|
|
MongoPlatformCollectionAccess.java:15 */
|
|
MongoPlatformCollectionAccess.java:16 public interface MongoPlatformCollectionAccess extends MongoCollectionAccess {
|
|
MongoPlatformCollectionAccess.java:17
|
|
MongoPlatformCollectionAccess.java:18 /** The template bound to the operation's consistency profile, with no collection scoping. */
|
|
MongoPlatformCollectionAccess.java:19 MongoOperations rawOperations();
|
|
MongoPlatformCollectionAccess.java:20 }
|
|
|
|
# 두 접근자를 부르는 자리 전수
|
|
저장소 전체에서 scoped() 를 부르는 줄 : 0 개
|
|
저장소 전체에서 rawOperations() 를 부르는 줄 : 7 개
|
|
두 이름이 나오는 자리 전부 (선언 포함) :
|
|
main · SpringMongoGeospatialOperations.java:58 return access.rawOperations().find(near, documentType, access.collection());
|
|
main · SpringMongoGeospatialOperations.java:82 return access.rawOperations().find(within, documentType, access.collection());
|
|
main · DefaultMongoImperativeExecutor.java:184 public ScopedMongoOperations scoped() {
|
|
main · DefaultMongoImperativeExecutor.java:192 public MongoOperations rawOperations() {
|
|
main · MongoCollectionAccess.java:32 ScopedMongoOperations scoped();
|
|
main · MongoPlatformCollectionAccess.java:19 MongoOperations rawOperations();
|
|
main · MongoAtomicOperationsTemplate.java:84 access.rawOperations(),
|
|
main · MongoAtomicOperationsTemplate.java:92 access.rawOperations(),
|
|
main · MongoBulkExecutor.java:69 access.rawOperations().bulkOps(plan.mode().toSpringMode(), access.collection());
|
|
main · DefaultReactiveMongoExecutor.java:218 public ReactiveScopedMongoOperations scoped() {
|
|
main · ReactiveMongoCollectionAccess.java:31 ReactiveScopedMongoOperations scoped();
|
|
test · MongoRawAccessBoundaryTest.java:18 * <p>{@code MongoPlatformCollectionAccess.rawOperations()} and {@code
|
|
test · MongoRawAccessBoundaryTest.java:37 List.of(".rawOperations()", ".executeInternal(");
|
|
[대조] 그 검색이 훑은 파일 : 6444 개
|
|
|
|
# rawOperations 가 무엇을 돌려주는가
|
|
DefaultMongoImperativeExecutor.java:180 return physicalCollection;
|
|
DefaultMongoImperativeExecutor.java:181 }
|
|
DefaultMongoImperativeExecutor.java:182
|
|
DefaultMongoImperativeExecutor.java:183 @Override
|
|
DefaultMongoImperativeExecutor.java:184 public ScopedMongoOperations scoped() {
|
|
DefaultMongoImperativeExecutor.java:185 // The deadline travels with the scope. The executor can only measure elapsed time after the
|
|
DefaultMongoImperativeExecutor.java:186 // callback returns; the scoped operations send the same number to the server as maxTimeMS,
|
|
DefaultMongoImperativeExecutor.java:187 // where it actually ends the work.
|
|
DefaultMongoImperativeExecutor.java:188 return new BoundScopedOperations(physicalCollection, operations, timeout);
|
|
DefaultMongoImperativeExecutor.java:189 }
|
|
DefaultMongoImperativeExecutor.java:190
|
|
DefaultMongoImperativeExecutor.java:191 @Override
|
|
DefaultMongoImperativeExecutor.java:192 public MongoOperations rawOperations() {
|
|
DefaultMongoImperativeExecutor.java:193 return operations;
|
|
DefaultMongoImperativeExecutor.java:194 }
|
|
DefaultMongoImperativeExecutor.java:195 }
|
|
DefaultMongoImperativeExecutor.java:196 }
|
|
|
|
# 서버에 데드라인을 실제로 붙이는 다른 경로들
|
|
PolicyAwareMongoAggregationExecutor.java:88
|
|
PolicyAwareMongoAggregationExecutor.java:89 private static MongoOperationBudget budgetFor(
|
|
PolicyAwareMongoAggregationExecutor.java:90 MongoOperationContext context, MongoOperationBudget registered) {
|
|
PolicyAwareMongoAggregationExecutor.java:91 // The context's timeout is the caller's deadline; it may tighten maxTimeMS but never extend it.
|
|
PolicyAwareMongoAggregationExecutor.java:92 long contextMillis = Math.max(1L, context.timeout().toMillis());
|
|
PolicyAwareMongoAggregationExecutor.java:93 return new MongoOperationBudget(
|
|
PolicyAwareMongoAggregationExecutor.java:94 registered.maxResults(),
|
|
PolicyAwareMongoAggregationExecutor.java:95 registered.maxResultBytes(),
|
|
PolicyAwareMongoAggregationExecutor.java:96 Math.min(registered.maxTimeMillis(), contextMillis),
|
|
PolicyAwareMongoAggregationExecutor.java:97 registered.cursorBatchSize());
|
|
PolicyAwareMongoAggregationExecutor.java:98 }
|
|
PolicyAwareMongoAggregationExecutor.java:99
|
|
PolicyAwareMongoAggregationExecutor.java:100 private static AggregationOptions optionsFor(
|
|
PolicyAwareMongoAggregationExecutor.java:101 MongoAggregationProfile profile, MongoOperationBudget budget) {
|
|
PolicyAwareMongoAggregationExecutor.java:102 AggregationOptions.Builder options =
|
|
PolicyAwareMongoAggregationExecutor.java:103 AggregationOptions.builder()
|
|
PolicyAwareMongoAggregationExecutor.java:104 .allowDiskUse(profile.allowDiskUse())
|
|
PolicyAwareMongoAggregationExecutor.java:105 .cursorBatchSize(budget.cursorBatchSize())
|
|
PolicyAwareMongoAggregationExecutor.java:106 .maxTime(Duration.ofMillis(budget.maxTimeMillis()));
|
|
PolicyAwareMongoAggregationExecutor.java:107 return profile.strictMapping() ? options.strictMapping().build() : options.build();
|
|
PolicyAwareMongoAggregationExecutor.java:108 }
|
|
maxTimeMillis 를 부르는 자리 전부 (에러 메시지 포함) :
|
|
PolicyAwareMongoAggregationExecutor.java:96 Math.min(registered.maxTimeMillis(), contextMillis),
|
|
PolicyAwareMongoAggregationExecutor.java:106 .maxTime(Duration.ofMillis(budget.maxTimeMillis()));
|
|
PolicyAwareMongoQueryBuilder.java:200 query.maxTimeMsec(budget.maxTimeMillis());
|
|
MongoBudgetEnforcer.java:36 + requested.maxTimeMillis()
|
|
MongoBudgetEnforcer.java:44 + registered.maxTimeMillis()
|
|
MongoReactiveCursorPublisher.java:58 .maxTimeMsec(budget.maxTimeMillis())
|
|
context.timeout() 이나 narrowedTo 가 나오는 자리 전부 :
|
|
PolicyAwareMongoAggregationExecutor.java:72 enforcer.narrow(budget, budget.narrowedTo(budgetFor(context, budget)));
|
|
PolicyAwareMongoAggregationExecutor.java:92 long contextMillis = Math.max(1L, context.timeout().toMillis());
|
|
DefaultMongoImperativeExecutor.java:83 ScopedAccess access = new ScopedAccess(physicalCollection, operations, context.timeout());
|
|
DefaultMongoImperativeExecutor.java:93 if (elapsed.compareTo(context.timeout()) > 0) {
|
|
DefaultMongoImperativeExecutor.java:103 + context.timeout().toMillis()
|
|
MongoBudgetEnforcer.java:57 return registered.narrowedTo(requested);
|
|
MongoOperationBudget.java:43 public MongoOperationBudget narrowedTo(MongoOperationBudget other) {
|
|
DefaultReactiveMongoExecutor.java:76 .timeout(context.timeout())
|
|
DefaultReactiveMongoExecutor.java:108 .timeout(context.timeout())
|
|
DefaultReactiveMongoExecutor.java:154 context.operationName(), operationType, context.timeout()),
|
|
|
|
# 그 다섯 자리를 가진 세 실행기가 조립되는가
|
|
MongoPlatformAutoConfiguration.java:154 .MongoAtomicOperationsTemplate(executor, policies);
|
|
MongoPlatformAutoConfiguration.java:169 return new dev.caskeleton.adapter.outbound.mongo.imperative.bulk.MongoBulkExecutor(
|
|
SpringMongoGeospatialOperations.java:31 public SpringMongoGeospatialOperations(
|
|
MongoAtomicOperationsTemplate.java:27 public MongoAtomicOperationsTemplate(
|
|
MongoBulkExecutor.java:43 public MongoBulkExecutor(
|
|
그 셋이 자기 안에서 데드라인을 따로 거는가 :
|
|
SpringMongoGeospatialOperations.java maxTime 이나 timeout 이 나오는 줄 : 0 개
|
|
MongoAtomicOperationsTemplate.java maxTime 이나 timeout 이 나오는 줄 : 0 개
|
|
MongoBulkExecutor.java maxTime 이나 timeout 이 나오는 줄 : 0 개
|
|
[대조] 같은 검색을 BoundScopedOperations 에 걸면 : 8 줄
|
|
|
|
# 저장소가 그 손잡이를 어떻게 지키는가
|
|
MongoRawAccessBoundaryTest.java:15 /**
|
|
MongoRawAccessBoundaryTest.java:16 * The Mongo platform's raw escape hatches stay inside the platform.
|
|
MongoRawAccessBoundaryTest.java:17 *
|
|
MongoRawAccessBoundaryTest.java:18 * <p>{@code MongoPlatformCollectionAccess.rawOperations()} and {@code
|
|
MongoRawAccessBoundaryTest.java:19 * DefaultMongoImperativeExecutor.executeInternal(...)} hand out Spring Data's unbounded {@code
|
|
MongoRawAccessBoundaryTest.java:20 * MongoOperations} — no collection binding, no consistency template, no deadline, no result budget.
|
|
MongoRawAccessBoundaryTest.java:21 * The scoped API exists precisely so a caller cannot reach those, and the only thing stopping one
|
|
MongoRawAccessBoundaryTest.java:22 * was a javadoc sentence saying "not public API for callers".
|
|
MongoRawAccessBoundaryTest.java:23 *
|
|
MongoRawAccessBoundaryTest.java:24 * <p>Both are genuinely needed *inside* the platform: the geospatial, atomic and bulk operations
|
|
MongoRawAccessBoundaryTest.java:25 * are built on them. So the rule is not "delete them" but "nobody outside may call them", which is
|
|
MongoRawAccessBoundaryTest.java:26 * a boundary rather than a visibility problem — the callers live in sibling packages, so Java's
|
|
MongoRawAccessBoundaryTest.java:27 * package-private cannot express it.
|
|
MongoRawAccessBoundaryTest.java:28 *
|
|
MongoRawAccessBoundaryTest.java:29 * <p>Checked from the composition root because that is the only place that sees both the platform
|
|
MongoRawAccessBoundaryTest.java:30 * and everything that consumes it. A rule living inside the Mongo leaf could only ever prove the
|
|
MongoRawAccessBoundaryTest.java:31 * leaf does not violate itself.
|
|
MongoRawAccessBoundaryTest.java:32 */
|
|
MongoRawAccessBoundaryTest.java:35 /** The escape hatches, by the text a caller would have to write to use one. */
|
|
MongoRawAccessBoundaryTest.java:36 private static final List<String> ESCAPE_HATCHES =
|
|
MongoRawAccessBoundaryTest.java:37 List.of(".rawOperations()", ".executeInternal(");
|
|
MongoRawAccessBoundaryTest.java:38
|
|
MongoRawAccessBoundaryTest.java:39 /** Where they may be used: the Mongo adapter's own production sources. */
|
|
MongoRawAccessBoundaryTest.java:40 private static final String PLATFORM_SOURCE =
|
|
MongoRawAccessBoundaryTest.java:41 "/src/adapter/outbound/persistence-mongo/src/main/java/";
|
|
|
|
# 세 실행기가 대신 하는 것
|
|
DefaultMongoImperativeExecutor.java:85 long startedAt = System.nanoTime();
|
|
DefaultMongoImperativeExecutor.java:86 try (MongoOperationObservation observation = observer.start(context, operationType)) {
|
|
DefaultMongoImperativeExecutor.java:87 try {
|
|
DefaultMongoImperativeExecutor.java:88 T value = callback.doInMongo(access);
|
|
DefaultMongoImperativeExecutor.java:89 Duration elapsed = Duration.ofNanos(System.nanoTime() - startedAt);
|
|
DefaultMongoImperativeExecutor.java:90 // A read is not a confirmed write. Recording every successful FIND as WRITE_CONFIRMED made
|
|
DefaultMongoImperativeExecutor.java:91 // the metric that answers "are writes being acknowledged" count reads as well.
|
|
DefaultMongoImperativeExecutor.java:92 MongoExecutionOutcome outcome = MongoCompletion.successOutcomeFor(operationType);
|
|
DefaultMongoImperativeExecutor.java:93 if (elapsed.compareTo(context.timeout()) > 0) {
|
|
DefaultMongoImperativeExecutor.java:94 // The context declares a deadline on every operation and nothing compared anything to it.
|
|
DefaultMongoImperativeExecutor.java:95 // A callback cannot be interrupted mid-driver-call, so this cannot cut the work short —
|
|
DefaultMongoImperativeExecutor.java:96 // but reporting a breach beats reporting success for work that ran past its budget.
|
|
DefaultMongoImperativeExecutor.java:97 observation.success(outcome);
|
|
DefaultMongoImperativeExecutor.java:98 throw MongoOperationRejectedException.of(
|
|
DefaultMongoImperativeExecutor.java:99 context.operationName().value(),
|
|
DefaultMongoImperativeExecutor.java:100 "the operation ran for "
|
|
DefaultMongoImperativeExecutor.java:101 + elapsed.toMillis()
|
|
DefaultMongoImperativeExecutor.java:102 + "ms against a declared timeout of "
|
|
DefaultMongoImperativeExecutor.java:103 + context.timeout().toMillis()
|
|
DefaultMongoImperativeExecutor.java:104 + "ms");
|
|
DefaultMongoImperativeExecutor.java:105 }
|
|
DefaultMongoImperativeExecutor.java:106 observation.success(outcome);
|