merge: integrate JPA production capability
# Conflicts: # .github/ci-gate-matrix.yml # .github/scripts/verify-gate-matrix.sh # .github/workflows/ci-quality-gates.yml # src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java
This commit is contained in:
@@ -98,6 +98,8 @@ failure translation contract 다. 여기서는 SPI 구조와 fallback 근거만
|
||||
기여한다. vendor 별 row(`40P01`, `25P03`, `57014` 등 PostgreSQL)는 `adapter-persistence-postgresql`
|
||||
가 추가 `SqlStateErrorMapping` 빈으로 기여한다.
|
||||
|
||||
- 서로 다른 contributor가 같은 exact SQLState를 등록하면 code가 같더라도 startup construction을
|
||||
실패시킨다. last-writer-wins merge는 mapping ownership drift를 숨기므로 허용하지 않는다.
|
||||
- `08*` connection-class prefix → `DB_UNAVAILABLE` 규칙은 맵 엔트리가 아니라 translator 가 직접
|
||||
처리한다. 따라서 core 매핑 맵에는 `08*` 가 없다.
|
||||
- **Fallback:** 기여된 어떤 row 에도 없는 SQLState — 또는 cause chain 에
|
||||
@@ -205,6 +207,120 @@ retention 보다 오래된 PUBLISHED row 를 주기적으로 비워 테이블
|
||||
cadence 측정 필요). retention 한 값은 reaper-local 이라 `@Value` 로 받지만, canonical 6-property
|
||||
문서는 app-bootstrap `OutboxSettings` / `application.yml` 에 있다.
|
||||
|
||||
## JPA production capability candidate
|
||||
|
||||
`src/config/jpa/readiness-cards.yaml`이 15개 capability와 7개 독립 schema stream의
|
||||
machine-readable SSOT다. `selected` base card와 `implemented-candidate` reliability card를
|
||||
구분하며, 실제 PostgreSQL 테스트 통과만으로 immutable 운영 evidence가 필요한 R2를 주장하지
|
||||
않는다.
|
||||
|
||||
독립 Flyway stream은 broad `classpath:db/migration`으로 함께 실행하지 않는다. 각 stream은
|
||||
자기 location/history table을 사용하고 non-empty schema adoption 때 version 0 baseline을 명시한
|
||||
뒤 V1부터 실행한다.
|
||||
|
||||
| Capability | Location | History table | 상태 |
|
||||
|---|---|---|---|
|
||||
| core/adoption | `db/migration/jpa/core` | `flyway_jpa_core_history` | selected candidate |
|
||||
| idempotency V2 | `db/migration/jpa/idempotency` | `flyway_jpa_idempotency_history` | implemented-candidate |
|
||||
| outbox storage V2 | `db/migration/jpa/outbox-storage` | `flyway_jpa_outbox_storage_history` | implemented-candidate |
|
||||
| polling delivery V2 | `db/migration/jpa/outbox-polling` | `flyway_jpa_outbox_polling_history` | implemented-candidate |
|
||||
| inbox V1 | `db/migration/jpa/inbox` | `flyway_jpa_inbox_history` | implemented-candidate |
|
||||
|
||||
### owner-safe idempotency V2
|
||||
|
||||
`PostgreSqlOwnerSafeIdempotencyStore`는 row lock을 얻은 뒤 `clock_timestamp()`를 평가한다.
|
||||
claim takeover와 start/renew/complete/fail/release는 scope/state/owner/attempt/claim operation/
|
||||
state revision을 SQL predicate로 다시 검증한다. expired `CLAIMED`만 takeover하며 expired
|
||||
`EXECUTING`은 `ABANDONED`로 닫고 reconciliation을 요구한다. raw client key는 저장하지 않고
|
||||
versioned HMAC scope digest만 쓴다.
|
||||
|
||||
### immutable outbox storage와 polling delivery V2
|
||||
|
||||
`PostgreSqlImmutableOutboxAppendAdapter`는 publication control을 `FOR SHARE`로 잠근 상태에서
|
||||
compact global identity guard와 partitioned immutable envelope를 같은 business transaction에
|
||||
기록한다. cutover는 control `FOR UPDATE`와 충돌하므로 시작된 append를 추월하지 못하며, target
|
||||
authority 활성화 뒤 legacy V1 writer trigger가 실패한다.
|
||||
|
||||
polling mode일 때 database trigger가 initial `outbox_delivery_v2` row를 같은 transaction에
|
||||
생성한다. `PostgreSqlPollingDeliveryAdapter`는 bounded `FOR UPDATE SKIP LOCKED` claim,
|
||||
aggregate version/ordinal strict-order head gate, owner/token/attempt/version/epoch completion
|
||||
CAS를 사용한다. broker 호출은 transaction 밖이고 duplicate publish 가능성은 stable event ID로
|
||||
consumer inbox에서 처리한다.
|
||||
|
||||
### same-store inbox
|
||||
|
||||
`PostgreSqlSameStoreInboxAdapter`의 transactional claim은 business mutation/outgoing outbox/
|
||||
completion과 caller의 한 primary write transaction에 참여한다. received lease expiry는 takeover할
|
||||
수 있지만 processing lease expiry는 blind retry하지 않고 recovery-required terminal state로
|
||||
보낸다. broker ACK는 commit 이후에만 실행한다.
|
||||
|
||||
### 실제 PostgreSQL task
|
||||
|
||||
base 6개 task 외에 다음 candidate task가 Docker 부재 시 skip이 아니라 실패하도록 등록돼 있다.
|
||||
|
||||
```text
|
||||
postgresqlIdempotencyIntegrationTest
|
||||
postgresqlOutboxStorageIntegrationTest
|
||||
postgresqlOutboxPollingIntegrationTest
|
||||
postgresqlInboxIntegrationTest
|
||||
```
|
||||
|
||||
### evidence manifest와 R2 gate
|
||||
|
||||
`readiness-cards.yaml`의 `evidence.scenarios`와 `evidence.task-claims`가 required evidence를 실제
|
||||
JUnit selector/Gradle task에 연결한다. `verifyJpaReadinessRegistryContract`는 unknown claim,
|
||||
duplicate selector와 다른 card task 차용을 mutation test로 거절한다.
|
||||
|
||||
```bash
|
||||
./gradlew :adapter:outbound:persistence-jpa:verifyJpaCandidateEvidence --console=plain
|
||||
```
|
||||
|
||||
위 task는 active card 11개의 producer를 실행하고 JUnit XML에서 exact selector와
|
||||
executed/skipped/failure/error 수를 읽는다. 각 manifest는 source revision/dirty digest,
|
||||
prerequisite manifest ID, PostgreSQL image digest, pgjdbc/Hibernate/Flyway version, topology와
|
||||
migration/dispatch metadata를 담고 다음 위치에 canonical JSON SHA-256 이름으로 생성된다.
|
||||
|
||||
```text
|
||||
build/jpa-evidence/manifests/<card-id>/<sha256>.json
|
||||
```
|
||||
|
||||
후보 검증은 zero-skip, schema, content hash와 prerequisite link가 맞으면 성공하지만
|
||||
`attainedReadiness=R1`을 유지한다. 로컬 후보 lane은 다음 E2/E3 동작을 실제 PostgreSQL에서
|
||||
검증한다.
|
||||
|
||||
- bounded pool saturation과 shutdown 뒤 connection 거부
|
||||
- runtime/migration role 분리, trusted namespace, TLS `verify-full`의 정상·hostname mismatch·
|
||||
untrusted CA·expired certificate 경로
|
||||
- persistence failure의 HTTP/log/span redaction
|
||||
- fresh/legacy adoption, interrupted migration forward recovery, N/N-1 additive rolling shape
|
||||
- serialization/deadlock/lock/statement timeout, pool exhaustion, commit transport 단절
|
||||
- idempotency/outbox/inbox 독립 stream의 disabled/first-enable/disable/re-enable/interrupted
|
||||
lifecycle
|
||||
|
||||
각 manifest는 그래도 candidate profile, dirty source와 아직 R2가 아닌 prerequisite를
|
||||
`readinessBlockers`에 보존하므로 후보 통과를 R2로 오인할 수 없다.
|
||||
|
||||
실제 aggregation gate는 별도 명령이다.
|
||||
|
||||
```bash
|
||||
./gradlew \
|
||||
:adapter:outbound:persistence-jpa:verifyJpaPrimaryFoundationEvidence \
|
||||
-PjpaEvidenceProfile=r2 \
|
||||
--console=plain
|
||||
```
|
||||
|
||||
이 task는 clean revision, `JPA_EVIDENCE_CI_JOB`,
|
||||
`JPA_EVIDENCE_ARTIFACT_LOCATION`, immutable PostgreSQL image digest, 모든 required evidence와
|
||||
R2 prerequisite DAG가 있어야만 성공한다. `.github/workflows/ci-quality-gates.yml`의 candidate
|
||||
job은 PR에서 zero-skip manifest를 보존하고, `.github/workflows/jpa-r2-evidence.yml`은 명시적으로
|
||||
실행하는 production-profile lane이다. 로컬 dirty worktree 또는 unpublished 실행은
|
||||
`worktree-is-dirty`/CI provenance blocker를 보고 실패하는 것이 정식 동작이다. R2 승격은 clean
|
||||
revision에서 workflow를 실행하고 보존된 manifest artifact를 검토한 뒤에만 가능하다.
|
||||
|
||||
stream migration이 중단되면 history/registry/object 상태를 먼저 확인하고 기존 migration을
|
||||
임의 수정하거나 history를 바로 `repair`하지 않는다. 장애를 수정한 forward migration으로
|
||||
복구하는 운영 절차는 `docs/runbooks/migration-failed.md`를 따른다.
|
||||
|
||||
## lock — 분산 락
|
||||
|
||||
provider 선택표(flag → bean → registry)의 SSOT 는 CLAUDE.md(또는 app-bootstrap 와이어링)다.
|
||||
|
||||
Reference in New Issue
Block a user