Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/an-active-transaction-check-that-asked-the-wrong-question.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

150 lines
12 KiB
Plaintext

# 가드가 자기 존재 이유로 적는 것
IdempotencyCapabilityGuard.java:9 /**
IdempotencyCapabilityGuard.java:10 * The preconditions a mutation must meet before any SQL runs.
IdempotencyCapabilityGuard.java:11 *
IdempotencyCapabilityGuard.java:12 * <p>Its own type because these are three separate questions — is the schema sanctioned, is there a
IdempotencyCapabilityGuard.java:13 * transaction, is it <em>this</em> store's transaction — and the store answered the third one
IdempotencyCapabilityGuard.java:14 * wrongly. It checked that the thread had an active read-write transaction, which is true whenever
IdempotencyCapabilityGuard.java:15 * any transaction is open on any data source; the sibling outbox and inbox adapters check {@code
IdempotencyCapabilityGuard.java:16 * hasResource(dataSource)}, which is the question that actually matters.
IdempotencyCapabilityGuard.java:17 *
IdempotencyCapabilityGuard.java:18 * <p>The difference shows up in an application with two data sources: a mutation issued inside the
IdempotencyCapabilityGuard.java:19 * <em>other</em> one's transaction passed the old check, ran against this store's connection
IdempotencyCapabilityGuard.java:20 * outside any transaction, and committed independently of the work it was supposed to be atomic
IdempotencyCapabilityGuard.java:21 * with.
IdempotencyCapabilityGuard.java:22 */
IdempotencyCapabilityGuard.java:23 final class IdempotencyCapabilityGuard {
# 그 데이터소스 필드는 어떻게 들어오는가
IdempotencyCapabilityGuard.java:28 private final JdbcOperations jdbc;
IdempotencyCapabilityGuard.java:29 private final DataSource dataSource;
IdempotencyCapabilityGuard.java:30 private final String activeCapabilitySql;
IdempotencyCapabilityGuard.java:31
IdempotencyCapabilityGuard.java:32 private volatile boolean postgreSqlConfirmed;
IdempotencyCapabilityGuard.java:33
IdempotencyCapabilityGuard.java:34 IdempotencyCapabilityGuard(
IdempotencyCapabilityGuard.java:35 JdbcOperations jdbc, DataSource dataSource, String activeCapabilitySql) {
IdempotencyCapabilityGuard.java:36 this.jdbc = Objects.requireNonNull(jdbc, "jdbc");
IdempotencyCapabilityGuard.java:37 this.dataSource = dataSource;
IdempotencyCapabilityGuard.java:38 this.activeCapabilitySql = Objects.requireNonNull(activeCapabilitySql, "activeCapabilitySql");
IdempotencyCapabilityGuard.java:39 }
# 세 번째 질문을 지금 어떻게 묻는가
IdempotencyCapabilityGuard.java:87 /**
IdempotencyCapabilityGuard.java:88 * Fails closed unless this store's own data source is enlisted in a read-write transaction.
IdempotencyCapabilityGuard.java:89 *
IdempotencyCapabilityGuard.java:90 * @throws IllegalStateException naming which of the three conditions failed
IdempotencyCapabilityGuard.java:91 */
IdempotencyCapabilityGuard.java:92 void requirePrimaryWriteTransaction() {
IdempotencyCapabilityGuard.java:93 if (!TransactionSynchronizationManager.isActualTransactionActive()) {
IdempotencyCapabilityGuard.java:94 throw new IllegalStateException(
IdempotencyCapabilityGuard.java:95 "owner-safe idempotency mutation requires an active primary transaction");
IdempotencyCapabilityGuard.java:96 }
IdempotencyCapabilityGuard.java:97 if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
IdempotencyCapabilityGuard.java:98 throw new IllegalStateException(
IdempotencyCapabilityGuard.java:99 "owner-safe idempotency mutation requires a read-write transaction");
IdempotencyCapabilityGuard.java:100 }
IdempotencyCapabilityGuard.java:101 if (dataSource != null && !TransactionSynchronizationManager.hasResource(dataSource)) {
IdempotencyCapabilityGuard.java:102 // The exact check the outbox and inbox adapters already make. Without it, a transaction on
IdempotencyCapabilityGuard.java:103 // another data source satisfied the guard while this store's work committed on its own.
IdempotencyCapabilityGuard.java:104 throw new IllegalStateException(
IdempotencyCapabilityGuard.java:105 "owner-safe idempotency mutation requires a transaction bound to this store's data"
IdempotencyCapabilityGuard.java:106 + " source; the active transaction belongs to another one");
IdempotencyCapabilityGuard.java:107 }
IdempotencyCapabilityGuard.java:108 }
IdempotencyCapabilityGuard.java:109 }
# 그 필드 값을 정하는 곳
PostgreSqlOwnerSafeIdempotencyStore.java:84 public PostgreSqlOwnerSafeIdempotencyStore(JdbcOperations jdbc) {
PostgreSqlOwnerSafeIdempotencyStore.java:85 this(jdbc, new SecureRandom());
PostgreSqlOwnerSafeIdempotencyStore.java:86 }
PostgreSqlOwnerSafeIdempotencyStore.java:87
PostgreSqlOwnerSafeIdempotencyStore.java:88 PostgreSqlOwnerSafeIdempotencyStore(JdbcOperations jdbc, SecureRandom secureRandom) {
PostgreSqlOwnerSafeIdempotencyStore.java:89 Objects.requireNonNull(jdbc, "jdbc");
PostgreSqlOwnerSafeIdempotencyStore.java:90 this.secureRandom = Objects.requireNonNull(secureRandom, "secureRandom");
PostgreSqlOwnerSafeIdempotencyStore.java:91 this.guard = new IdempotencyCapabilityGuard(jdbc, dataSourceOf(jdbc), ACTIVE_CAPABILITY_SQL);
PostgreSqlOwnerSafeIdempotencyStore.java:92 this.rows = new IdempotencyRowMapper(jdbc);
PostgreSqlOwnerSafeIdempotencyStore.java:93 this.claims = new IdempotencyClaimGateway(jdbc);
PostgreSqlOwnerSafeIdempotencyStore.java:94 this.transitions = new IdempotencyTransitionGateway(jdbc);
PostgreSqlOwnerSafeIdempotencyStore.java:95 }
PostgreSqlOwnerSafeIdempotencyStore.java:96
PostgreSqlOwnerSafeIdempotencyStore.java:97 /**
PostgreSqlOwnerSafeIdempotencyStore.java:98 * The data source behind this store's operations, when it can be determined.
PostgreSqlOwnerSafeIdempotencyStore.java:99 *
PostgreSqlOwnerSafeIdempotencyStore.java:100 * <p>{@code JdbcTemplate} knows its own; a hand-rolled {@code JdbcOperations} may not. The guard
PostgreSqlOwnerSafeIdempotencyStore.java:101 * treats an unknown data source as "cannot check", which is weaker than the outbox adapter's
PostgreSqlOwnerSafeIdempotencyStore.java:102 * exact check and still stronger than what was here before — but it is the honest answer when the
PostgreSqlOwnerSafeIdempotencyStore.java:103 * collaborator genuinely cannot be identified.
PostgreSqlOwnerSafeIdempotencyStore.java:104 */
PostgreSqlOwnerSafeIdempotencyStore.java:105 private static javax.sql.DataSource dataSourceOf(JdbcOperations jdbc) {
PostgreSqlOwnerSafeIdempotencyStore.java:106 return jdbc instanceof org.springframework.jdbc.core.JdbcTemplate template
PostgreSqlOwnerSafeIdempotencyStore.java:107 ? template.getDataSource()
PostgreSqlOwnerSafeIdempotencyStore.java:108 : null;
PostgreSqlOwnerSafeIdempotencyStore.java:109 }
# 이 저장소를 만드는 자리 전부와 각각이 넘기는 것
postgresqlIntegrationTest · PostgreSqlIdempotencyIntegrationTest.java:61 store = new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
test · OwnerSafeIdempotencyPreconditionTest.java:47 new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
main · PostgreSqlIdempotencyProviderConfig.java:60 return new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
그 자리들이 넘기는 JdbcOperations 의 출처 :
PostgreSqlIdempotencyProviderConfig.java:55 * @param jdbc the primary {@code JdbcOperations}; the store's transaction guard identifies its
PostgreSqlIdempotencyProviderConfig.java:56 * data source from it so a transaction opened on a different one cannot pass
PostgreSqlIdempotencyProviderConfig.java:57 */
PostgreSqlIdempotencyProviderConfig.java:58 @Bean
PostgreSqlIdempotencyProviderConfig.java:59 IdempotencyStorePortV2 postgreSqlOwnerSafeIdempotencyStore(JdbcOperations jdbc) {
PostgreSqlIdempotencyProviderConfig.java:60 return new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
PostgreSqlIdempotencyProviderConfig.java:61 }
PostgreSqlIdempotencyProviderConfig.java:62
OwnerSafeIdempotencyPreconditionTest.java:44
OwnerSafeIdempotencyPreconditionTest.java:45 private final JdbcOperations jdbc = mock(JdbcOperations.class);
OwnerSafeIdempotencyPreconditionTest.java:46 private final PostgreSqlOwnerSafeIdempotencyStore store =
OwnerSafeIdempotencyPreconditionTest.java:47 new PostgreSqlOwnerSafeIdempotencyStore(jdbc);
OwnerSafeIdempotencyPreconditionTest.java:48
저장소에 JdbcOperations 구현이나 JdbcTemplate 하위 클래스가 있는가 :
0 건
# 세 번째 검사를 덮는 시험이 있는가
IdempotencyCapabilityGuard.java:106: + " source; the active transaction belongs to another one");
전제 시험이 단언하는 것 :
OwnerSafeIdempotencyPreconditionTest.java:50 void clearTransactionState() {
OwnerSafeIdempotencyPreconditionTest.java:57 void aClaimWithNoTransactionIsRefused() {
OwnerSafeIdempotencyPreconditionTest.java:67 void aClaimAgainstAnotherVendorIsRefused() throws Exception {
OwnerSafeIdempotencyPreconditionTest.java:81 void aClaimAgainstAnUnsanctionedStreamIsRefused() throws Exception {
# 가드를 부르는 자리
main · IdempotencyCapabilityGuard.java:55 void requireActiveCapability() {
main · IdempotencyCapabilityGuard.java:92 void requirePrimaryWriteTransaction() {
main · PostgreSqlOwnerSafeIdempotencyStore.java:122 requirePrimaryWriteTransaction();
main · PostgreSqlOwnerSafeIdempotencyStore.java:123 requireActiveCapability();
main · PostgreSqlOwnerSafeIdempotencyStore.java:176 requirePrimaryWriteTransaction();
main · PostgreSqlOwnerSafeIdempotencyStore.java:211 requirePrimaryWriteTransaction();
main · PostgreSqlOwnerSafeIdempotencyStore.java:255 requirePrimaryWriteTransaction();
main · PostgreSqlOwnerSafeIdempotencyStore.java:303 requirePrimaryWriteTransaction();
main · PostgreSqlOwnerSafeIdempotencyStore.java:360 requirePrimaryWriteTransaction();
main · PostgreSqlOwnerSafeIdempotencyStore.java:502 private void requireActiveCapability() {
main · PostgreSqlOwnerSafeIdempotencyStore.java:503 guard.requireActiveCapability();
main · PostgreSqlOwnerSafeIdempotencyStore.java:506 private void requirePrimaryWriteTransaction() {
main · PostgreSqlOwnerSafeIdempotencyStore.java:507 guard.requirePrimaryWriteTransaction();
main · PostgreSqlSameStoreInboxAdapter.java:227 requireActiveCapability();
main · PostgreSqlSameStoreInboxAdapter.java:494 private void requireActiveCapability() {
main · PostgreSqlImmutableOutboxAppendAdapter.java:166 requireActiveCapability();
main · PostgreSqlImmutableOutboxAppendAdapter.java:312 private void requireActiveCapability() {
# 형제 어댑터는 같은 질문을 어떻게 묻고 데이터소스를 어떻게 받는가
PostgreSqlImmutableOutboxAppendAdapter :
PostgreSqlImmutableOutboxAppendAdapter.java:149 private final DataSource dataSource;
PostgreSqlImmutableOutboxAppendAdapter.java:153 public PostgreSqlImmutableOutboxAppendAdapter(DataSource dataSource) {
PostgreSqlImmutableOutboxAppendAdapter.java:158 this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
PostgreSqlImmutableOutboxAppendAdapter.java:320 private void requireSameResourcePrimaryWriteTransaction() {
PostgreSqlImmutableOutboxAppendAdapter.java:321 if (!TransactionSynchronizationManager.isActualTransactionActive()) {
PostgreSqlImmutableOutboxAppendAdapter.java:325 if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
PostgreSqlImmutableOutboxAppendAdapter.java:328 if (!TransactionSynchronizationManager.hasResource(dataSource)) {
PostgreSqlSameStoreInboxAdapter :
PostgreSqlSameStoreInboxAdapter.java:199 private final DataSource dataSource;
PostgreSqlSameStoreInboxAdapter.java:204 public PostgreSqlSameStoreInboxAdapter(DataSource dataSource) {
PostgreSqlSameStoreInboxAdapter.java:210 this.dataSource = Objects.requireNonNull(dataSource, "dataSource");
PostgreSqlSameStoreInboxAdapter.java:502 private void requireSameResourcePrimaryWriteTransaction() {
PostgreSqlSameStoreInboxAdapter.java:503 if (!TransactionSynchronizationManager.isActualTransactionActive()
PostgreSqlSameStoreInboxAdapter.java:504 || TransactionSynchronizationManager.isCurrentTransactionReadOnly()
PostgreSqlSameStoreInboxAdapter.java:505 || !TransactionSynchronizationManager.hasResource(dataSource)) {