Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/analysis-finding-a05-f032.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 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>
2026-09-04 22:51:59 +09:00

272 lines
19 KiB
Plaintext

# 야간 워크플로가 이 레인에 대해 적는 것
jpa-nightly.yml:122 - name: Verify pool saturation and REQUIRES_NEW connection behaviour
jpa-nightly.yml:123 working-directory: src
jpa-nightly.yml:124 # A behaviour contract, not a measurement. This step used to switch assertions off with an
jpa-nightly.yml:125 # explicit property and call the result a certification, so the only threshold it ever
jpa-nightly.yml:126 # asserted was that thresholds were not being asserted. What
jpa-nightly.yml:127 # it checks now — that REQUIRES_NEW needs two connections per concurrent thread, that a
jpa-nightly.yml:128 # saturated pool reports its pending count, that a caller waits rather than proceeding
jpa-nightly.yml:129 # without a connection — is true on any runner, so there is nothing to switch off.
jpa-nightly.yml:130 run: >-
jpa-nightly.yml:131 ./gradlew
jpa-nightly.yml:132 :adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest
jpa-nightly.yml:133 --no-daemon
jpa-nightly.yml:134 --stacktrace
# 그 레인이 도는 소스 세트와 그 안의 시험 파일
persistence-jpa/build.gradle:281 // What the lane genuinely verifies is a behaviour contract: a REQUIRES_NEW depth of one needs two
persistence-jpa/build.gradle:282 // connections per concurrent thread, a saturated pool reports its pending count, and a caller waits
persistence-jpa/build.gradle:283 // rather than proceeding without a connection. Those are true on any machine, so they need no flag
persistence-jpa/build.gradle:284 // — and this name does not promise a number nobody measured. A real performance gate needs a
persistence-jpa/build.gradle:285 // dedicated runner, warmup and sample counts, and recorded thresholds; when that exists it belongs
persistence-jpa/build.gradle:286 // in a lane of its own rather than behind a boolean on this one.
persistence-jpa/build.gradle:287 def jpaPlatformPoolContractTest = tasks.register('jpaPlatformPoolContractTest', Test) {
persistence-jpa/build.gradle:288 group = 'verification'
persistence-jpa/build.gradle:289 description = 'Verifies Hikari pool and REQUIRES_NEW connection behaviour (design §38).'
persistence-jpa/build.gradle:290 testClassesDirs = sourceSets.jpaPlatformPerformanceTest.output.classesDirs
persistence-jpa/build.gradle:291 classpath = sourceSets.jpaPlatformPerformanceTest.runtimeClasspath
persistence-jpa/build.gradle:292 useJUnitPlatform()
persistence-jpa/build.gradle:293 failOnNoDiscoveredTests = true
persistence-jpa/build.gradle:294 outputs.upToDateWhen { false }
persistence-jpa/build.gradle:295 jvmArgs('-Duser.timezone=UTC')
persistence-jpa/build.gradle:296 }
persistence-jpa/build.gradle:297
persistence-jpa/build.gradle:298 // The JPA release gate (design §41). Aggregates every lane whose absence would let one of the
persistence-jpa/build.gradle:299 // documented gates in docs/jpa/support-matrix.md pass unverified.
persistence-jpa/build.gradle:300 tasks.register('jpaPlatformReleaseGate') {
persistence-jpa/build.gradle:301 group = 'verification'
persistence-jpa/build.gradle:302 description = 'Runs every JPA platform lane required for a release (design §41).'
persistence-jpa/build.gradle:303 dependsOn tasks.named('test')
persistence-jpa/build.gradle:304 dependsOn jpaPlatformContractTest
persistence-jpa/build.gradle:305 dependsOn jpaPlatformMigrationTest
persistence-jpa/build.gradle:306 dependsOn jpaPlatformFailureTest
persistence-jpa/build.gradle:307 dependsOn jpaPlatformQueryPlanTest
persistence-jpa/build.gradle:308 dependsOn jpaPlatformSecurityTest
persistence-jpa/build.gradle:309 dependsOn jpaPlatformPoolContractTest
persistence-jpa/build.gradle:310 }
HikariPoolSaturationContractTest.java
PoolPressureContractTest.java
RequiresNewPoolPressureContractTest.java
# 세 시험 파일의 시험과 단언 전수
--- HikariPoolSaturationContractTest.java
:3 import static org.assertj.core.api.Assertions.assertThat;
:4 import static org.assertj.core.api.Assertions.assertThatThrownBy;
:52 void saturatedPoolFailsWithinItsTimeout() throws SQLException {
:61 assertThatThrownBy(pool::getConnection).isInstanceOf(SQLException.class);
:64 assertThat(waited)
:77 void releasingAConnectionLetsTheNextCallerThrough() throws SQLException {
:84 assertThat(third.isValid(1)).isTrue();
:92 void measurementReportsPoolState() throws SQLException {
:97 new PoolMeasurement(
:100 bean.getThreadsAwaitingConnection(),
:103 assertThat(measurement.active()).isEqualTo(1);
:104 assertThat(measurement.total()).isGreaterThanOrEqualTo(1);
:105 assertThat(held.isValid(1)).isTrue();
--- PoolPressureContractTest.java
:3 import static org.assertj.core.api.Assertions.assertThat;
:28 void reportsPendingAndAcquireLatencyTogether() {
:29 var measurement = new PoolMeasurement(4, 2, 3, Duration.ofMillis(80));
:31 assertThat(measurement.pending()).isEqualTo(3);
:32 assertThat(measurement.acquireLatency()).isEqualTo(Duration.ofMillis(80));
:33 assertThat(measurement.total()).isEqualTo(6);
:34 assertThat(measurement.saturated()).isTrue();
:39 void requiresNewNeedsTwoConnectionsPerThread() {
:45 assertThat(required)
--- RequiresNewPoolPressureContractTest.java
:3 import static org.assertj.core.api.Assertions.assertThat;
:4 import static org.assertj.core.api.Assertions.assertThatThrownBy;
:45 void poolOfOneCannotServeAnInnerTransaction() throws SQLException {
:50 assertThatThrownBy(pool::getConnection)
:61 void poolOfTwoServesTheSameNesting() throws SQLException {
:68 assertThat(inner.isValid(1)).isTrue();
:69 assertThat(inner).isNotSameAs(outer);
:80 void sizingRuleMatchesTheObservedRequirement() {
:86 assertThat(required)
# 대기 수를 단언하는 자리
measurement.pending() 를 단언하는 줄 : 1 개
그 줄이 있는 시험의 입력 :
PoolPressureContractTest.java:26 @Test
PoolPressureContractTest.java:27 @DisplayName("pending count and acquire latency are reported together")
PoolPressureContractTest.java:28 void reportsPendingAndAcquireLatencyTogether() {
PoolPressureContractTest.java:29 var measurement = new PoolMeasurement(4, 2, 3, Duration.ofMillis(80));
PoolPressureContractTest.java:30
PoolPressureContractTest.java:31 assertThat(measurement.pending()).isEqualTo(3);
PoolPressureContractTest.java:32 assertThat(measurement.acquireLatency()).isEqualTo(Duration.ofMillis(80));
PoolPressureContractTest.java:33 assertThat(measurement.total()).isEqualTo(6);
PoolPressureContractTest.java:34 assertThat(measurement.saturated()).isTrue();
PoolPressureContractTest.java:35 }
PoolPressureContractTest.java:36
PoolPressureContractTest.java:37 @Test
PoolPressureContractTest.java:38 @DisplayName("a REQUIRES_NEW depth of one needs two connections per concurrent thread")
PoolPressureContractTest.java:39 void requiresNewNeedsTwoConnectionsPerThread() {
PoolPressureContractTest.java:40 int concurrentThreads = 8;
PoolPressureContractTest.java:41 int maxRequiresNewDepth = 1;
PoolPressureContractTest.java:42
PoolPressureContractTest.java:43 int required = concurrentThreads * (1 + maxRequiresNewDepth) + 1;
PoolPressureContractTest.java:44
PoolPressureContractTest.java:45 assertThat(required)
PoolPressureContractTest.java:46 .as("this is a property of REQUIRES_NEW, not of the machine, so it needs no opt-in")
PoolPressureContractTest.java:47 .isEqualTo(17);
포화를 다루는 세 시험 전문 :
HikariPoolSaturationContractTest.java:30 class HikariPoolSaturationContractTest {
HikariPoolSaturationContractTest.java:31
HikariPoolSaturationContractTest.java:32 private static final int POOL_SIZE = 2;
HikariPoolSaturationContractTest.java:33 private static final Duration ACQUIRE_TIMEOUT = Duration.ofMillis(500);
HikariPoolSaturationContractTest.java:34
HikariPoolSaturationContractTest.java:50 @Test
HikariPoolSaturationContractTest.java:51 @DisplayName("a saturated pool fails within its acquisition timeout")
HikariPoolSaturationContractTest.java:52 void saturatedPoolFailsWithinItsTimeout() throws SQLException {
HikariPoolSaturationContractTest.java:53 try (HikariDataSource pool = pool()) {
HikariPoolSaturationContractTest.java:54 List<Connection> held = new ArrayList<>();
HikariPoolSaturationContractTest.java:55 try {
HikariPoolSaturationContractTest.java:56 for (int index = 0; index < POOL_SIZE; index++) {
HikariPoolSaturationContractTest.java:57 held.add(pool.getConnection());
HikariPoolSaturationContractTest.java:58 }
HikariPoolSaturationContractTest.java:59
HikariPoolSaturationContractTest.java:60 Instant startedAt = Instant.now();
HikariPoolSaturationContractTest.java:61 assertThatThrownBy(pool::getConnection).isInstanceOf(SQLException.class);
HikariPoolSaturationContractTest.java:62 Duration waited = Duration.between(startedAt, Instant.now());
HikariPoolSaturationContractTest.java:63
HikariPoolSaturationContractTest.java:64 assertThat(waited)
HikariPoolSaturationContractTest.java:65 .as("an unbounded wait turns exhaustion into requests that never return")
HikariPoolSaturationContractTest.java:66 .isLessThan(ACQUIRE_TIMEOUT.plusSeconds(2));
HikariPoolSaturationContractTest.java:67 } finally {
HikariPoolSaturationContractTest.java:68 for (Connection connection : held) {
HikariPoolSaturationContractTest.java:69 connection.close();
HikariPoolSaturationContractTest.java:70 }
HikariPoolSaturationContractTest.java:71 }
HikariPoolSaturationContractTest.java:72 }
HikariPoolSaturationContractTest.java:73 }
HikariPoolSaturationContractTest.java:74
HikariPoolSaturationContractTest.java:75 @Test
HikariPoolSaturationContractTest.java:76 @DisplayName("releasing a connection lets the next caller through")
HikariPoolSaturationContractTest.java:77 void releasingAConnectionLetsTheNextCallerThrough() throws SQLException {
HikariPoolSaturationContractTest.java:78 try (HikariDataSource pool = pool()) {
HikariPoolSaturationContractTest.java:79 Connection first = pool.getConnection();
HikariPoolSaturationContractTest.java:80 Connection second = pool.getConnection();
HikariPoolSaturationContractTest.java:81 first.close();
HikariPoolSaturationContractTest.java:82
HikariPoolSaturationContractTest.java:83 try (Connection third = pool.getConnection()) {
HikariPoolSaturationContractTest.java:84 assertThat(third.isValid(1)).isTrue();
HikariPoolSaturationContractTest.java:85 }
HikariPoolSaturationContractTest.java:86 second.close();
HikariPoolSaturationContractTest.java:87 }
HikariPoolSaturationContractTest.java:88 }
HikariPoolSaturationContractTest.java:89
HikariPoolSaturationContractTest.java:90 @Test
HikariPoolSaturationContractTest.java:91 @DisplayName("the measurement reports what the pool is actually doing")
HikariPoolSaturationContractTest.java:92 void measurementReportsPoolState() throws SQLException {
HikariPoolSaturationContractTest.java:93 try (HikariDataSource pool = pool()) {
HikariPoolSaturationContractTest.java:94 try (Connection held = pool.getConnection()) {
HikariPoolSaturationContractTest.java:95 var bean = pool.getHikariPoolMXBean();
HikariPoolSaturationContractTest.java:96 var measurement =
HikariPoolSaturationContractTest.java:97 new PoolMeasurement(
HikariPoolSaturationContractTest.java:98 bean.getActiveConnections(),
HikariPoolSaturationContractTest.java:99 bean.getIdleConnections(),
HikariPoolSaturationContractTest.java:100 bean.getThreadsAwaitingConnection(),
HikariPoolSaturationContractTest.java:101 Duration.ZERO);
HikariPoolSaturationContractTest.java:102
HikariPoolSaturationContractTest.java:103 assertThat(measurement.active()).isEqualTo(1);
HikariPoolSaturationContractTest.java:104 assertThat(measurement.total()).isGreaterThanOrEqualTo(1);
HikariPoolSaturationContractTest.java:105 assertThat(held.isValid(1)).isTrue();
HikariPoolSaturationContractTest.java:106 }
HikariPoolSaturationContractTest.java:107 }
HikariPoolSaturationContractTest.java:108 }
HikariPoolSaturationContractTest.java:109
HikariPoolSaturationContractTest.java:110 private static HikariDataSource pool() {
HikariPoolSaturationContractTest.java:111 HikariConfig config = new HikariConfig();
HikariPoolSaturationContractTest.java:112 config.setJdbcUrl(container.getJdbcUrl());
HikariPoolSaturationContractTest.java:113 config.setUsername(container.getUsername());
HikariPoolSaturationContractTest.java:114 config.setPassword(container.getPassword());
HikariPoolSaturationContractTest.java:115 config.setMaximumPoolSize(POOL_SIZE);
HikariPoolSaturationContractTest.java:116 config.setConnectionTimeout(ACQUIRE_TIMEOUT.toMillis());
HikariPoolSaturationContractTest.java:117 return new HikariDataSource(config);
HikariPoolSaturationContractTest.java:118 }
# 나머지 두 주장에는 실제 단언이 있다
RequiresNewPoolPressureContractTest.java:43 @Test
RequiresNewPoolPressureContractTest.java:44 @DisplayName("a pool of one deadlocks the moment an inner transaction is needed")
RequiresNewPoolPressureContractTest.java:45 void poolOfOneCannotServeAnInnerTransaction() throws SQLException {
RequiresNewPoolPressureContractTest.java:46 try (HikariDataSource pool = pool(1)) {
RequiresNewPoolPressureContractTest.java:47 try (Connection outer = pool.getConnection()) {
RequiresNewPoolPressureContractTest.java:48 outer.setAutoCommit(false);
RequiresNewPoolPressureContractTest.java:49
RequiresNewPoolPressureContractTest.java:50 assertThatThrownBy(pool::getConnection)
RequiresNewPoolPressureContractTest.java:51 .as("the outer transaction still holds its connection while the inner one is opened")
RequiresNewPoolPressureContractTest.java:52 .isInstanceOf(SQLException.class);
RequiresNewPoolPressureContractTest.java:53
RequiresNewPoolPressureContractTest.java:54 outer.rollback();
RequiresNewPoolPressureContractTest.java:55 }
RequiresNewPoolPressureContractTest.java:56 }
RequiresNewPoolPressureContractTest.java:57 }
RequiresNewPoolPressureContractTest.java:58
RequiresNewPoolPressureContractTest.java:59 @Test
RequiresNewPoolPressureContractTest.java:60 @DisplayName("a pool of two serves the same nesting")
RequiresNewPoolPressureContractTest.java:61 void poolOfTwoServesTheSameNesting() throws SQLException {
RequiresNewPoolPressureContractTest.java:62 try (HikariDataSource pool = pool(2)) {
RequiresNewPoolPressureContractTest.java:63 try (Connection outer = pool.getConnection()) {
RequiresNewPoolPressureContractTest.java:64 outer.setAutoCommit(false);
RequiresNewPoolPressureContractTest.java:65
RequiresNewPoolPressureContractTest.java:66 try (Connection inner = pool.getConnection()) {
RequiresNewPoolPressureContractTest.java:67 inner.setAutoCommit(false);
RequiresNewPoolPressureContractTest.java:68 assertThat(inner.isValid(1)).isTrue();
RequiresNewPoolPressureContractTest.java:69 assertThat(inner).isNotSameAs(outer);
RequiresNewPoolPressureContractTest.java:70 inner.commit();
RequiresNewPoolPressureContractTest.java:71 }
RequiresNewPoolPressureContractTest.java:72
RequiresNewPoolPressureContractTest.java:73 outer.rollback();
RequiresNewPoolPressureContractTest.java:74 }
RequiresNewPoolPressureContractTest.java:75 }
RequiresNewPoolPressureContractTest.java:76 }
RequiresNewPoolPressureContractTest.java:77
RequiresNewPoolPressureContractTest.java:78 @Test
RequiresNewPoolPressureContractTest.java:79 @DisplayName("the sizing rule matches the observed requirement")
RequiresNewPoolPressureContractTest.java:80 void sizingRuleMatchesTheObservedRequirement() {
RequiresNewPoolPressureContractTest.java:81 int concurrentThreads = 1;
RequiresNewPoolPressureContractTest.java:82 int maxRequiresNewDepth = 1;
RequiresNewPoolPressureContractTest.java:83
RequiresNewPoolPressureContractTest.java:84 int required = concurrentThreads * (1 + maxRequiresNewDepth) + 1;
RequiresNewPoolPressureContractTest.java:85
RequiresNewPoolPressureContractTest.java:86 assertThat(required)
RequiresNewPoolPressureContractTest.java:87 .as("one thread at depth one needs two connections; the rule adds headroom")
RequiresNewPoolPressureContractTest.java:88 .isEqualTo(3);
RequiresNewPoolPressureContractTest.java:89 }
# PoolMeasurement 는 어디서 온 타입인가
세 시험이 그것을 어디서 import 하는가 :
HikariPoolSaturationContractTest.java:8 import dev.caskeleton.adapter.outbound.persistence.testkit.pool.PoolMeasurement;
PoolPressureContractTest.java:5 import dev.caskeleton.adapter.outbound.persistence.testkit.pool.PoolMeasurement;
PoolMeasurement.java:6 /**
PoolMeasurement.java:7 * A reading of connection pool pressure (design §38).
PoolMeasurement.java:8 *
PoolMeasurement.java:9 * <p>Pending count and acquire latency are recorded together because either alone is misleading. A
PoolMeasurement.java:10 * pool can show zero pending at the instant it is sampled while callers routinely wait; a high
PoolMeasurement.java:11 * acquire latency with no pending count gives no idea how many callers are affected.
PoolMeasurement.java:12 */
PoolMeasurement.java:13 public record PoolMeasurement(int active, int idle, int pending, Duration acquireLatency) {
PoolMeasurement.java:14
PoolMeasurement.java:15 public PoolMeasurement {
PoolMeasurement.java:16 Objects.requireNonNull(acquireLatency, "acquireLatency");
PoolMeasurement.java:17 if (active < 0 || idle < 0 || pending < 0) {
PoolMeasurement.java:18 throw new IllegalArgumentException("pool counters must not be negative");
PoolMeasurement.java:19 }
PoolMeasurement.java:20 if (acquireLatency.isNegative()) {
PoolMeasurement.java:21 throw new IllegalArgumentException("acquire latency must not be negative");
PoolMeasurement.java:22 }
PoolMeasurement.java:23 }
PoolMeasurement.java:24
PoolMeasurement.java:25 /** How many connections the pool currently holds. */
PoolMeasurement.java:26 public int total() {
PoolMeasurement.java:27 return active + idle;
PoolMeasurement.java:28 }
PoolMeasurement.java:29
PoolMeasurement.java:30 /** Whether callers are waiting for a connection. */
PoolMeasurement.java:31 public boolean saturated() {
PoolMeasurement.java:32 return pending > 0;
PoolMeasurement.java:33 }
PoolMeasurement.java:34 }
프로덕션에서 대기 수를 읽는 자리 : 0 개
[대조] 그 검색이 훑은 main 파일 : 4714 개