132 lines
7.5 KiB
Markdown
132 lines
7.5 KiB
Markdown
---
|
|
id:
|
|
kind: CASE
|
|
slug: commit-ambiguity-is-not-only-sqlstate-08
|
|
title: pg_terminate_backend 가 57P01 로 도착하고 커밋 레코드는 이미 WAL 에 있었다
|
|
topic: commit-ambiguity-as-a-result
|
|
topicName: 커밋 모호성 — 「모른다」를 결과로 유지하기
|
|
project: clean-architecture-backend-template
|
|
status: 게시 전
|
|
studio: ""
|
|
lastVerifiedOn: 2026-08-29
|
|
source:
|
|
- final/document.md#4-1
|
|
- final/document.md#a05
|
|
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
|
evidence:
|
|
- ../../../final/evidence/raw/050-jpa-commit-ambiguity-probe.txt
|
|
- ../../../final/evidence/raw/115-integration-lane-original-verification.txt
|
|
---
|
|
|
|
# pg_terminate_backend 가 57P01 로 도착하고 커밋 레코드는 이미 WAL 에 있었다
|
|
|
|
커밋 모호성을 SQLSTATE class 08 로만 정의한 규칙이, 서버가 자기 종료를 알리는 57P01 앞에서 성립하지 않았다. 그 코드가 도착하는 시점에 커밋 레코드는 이미 WAL 에 있을 수 있다.
|
|
|
|
## 관계
|
|
|
|
- **트랜잭션 결과 대수 — 다섯 변형이 각각 답하는 질문**
|
|
이 사례의 결론이 어느 결과 변형으로 들어가는지를 정의한다.
|
|
- **모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다**
|
|
이 사례가 그 세 번째 결과를 필요로 하는 이유다.
|
|
- **completion-unknown 은 자동으로도 수동으로도 재시도하지 않는다**
|
|
이 분류가 만들어 내는 예외를 어떻게 다룰지 정한 결정이다.
|
|
- **커밋 모호성 레인이 HEAD 에서도 통과하는지는 실행이 아니라 드리프트 0 으로 답했다**
|
|
이 사례를 고정하는 계약 레인에 남은 질문이다.
|
|
|
|
## 문제
|
|
|
|
설계는 커밋 모호성을 SQLSTATE class 08, 즉 연결 예외로 정의했다. 그 정의는 클라이언트가 자기 연결에서 무슨 일을 겪었는지를 기준으로 삼는다.
|
|
|
|
서버가 스스로 종료를 알리는 경우가 그 정의 밖으로 나간다. 커밋이 진행 중인 백엔드를 pg_terminate_backend 로 끊으면 클라이언트는 57P01 을 받는다. 이 코드는 class 08 이 아니다. 클라이언트의 연결 시도가 아니라 서버가 자기 종료를 알린 것이어서 계열이 다르다.
|
|
|
|
커밋 입장에서 결과는 같고 오히려 더 나쁘다. class 08 만 모호성으로 보는 규칙은 이 실패를 평범한 실패로 분류하고, 평범한 실패는 재시도된다. 커밋됐을 수도 있는 쓰기를 다시 실행하는 경로가 여기서 열린다.
|
|
|
|
## 결론
|
|
|
|
규칙이 57P01 과 57P02 와 57P03 까지 넓어졌다.
|
|
|
|
CommitFailureClassifier 가 모호성으로 보는 상태는 네 가지다.
|
|
|
|
40003 : statement completion unknown
|
|
08 로 시작하는 상태 : connection exception
|
|
57P01 57P02 57P03 : 서버가 자기 종료를 알린 상태
|
|
transport 수준 단절
|
|
|
|
세 상태를 함께 넣은 근거는 코드 주석에 남아 있다. 57P01 은 종료된 백엔드나 fast shutdown 이나 failover 가 보고하고, 그것이 도착할 때 커밋 레코드는 이미 WAL 에 있을 수 있다. 57P02 는 crash shutdown, 57P03 은 지금 접속할 수 없음이며, 서버가 진행 중인 커밋을 어떻게 처리했든 클라이언트가 그것을 알지 못했다는 점에서 셋이 같다.
|
|
|
|
넓어진 규칙이 다시 좁아지지 못하도록 CommitAmbiguityContractTest 가 SQLSTATE 를 직접 단언한다.
|
|
|
|
반대 방향의 비용도 같은 자료에 적혀 있다. 커밋 단계의 모든 연결 오류를 모호성으로 표시하면 평범한 풀 고갈과 서버 재시작이 조정 큐로 밀려든다. 그러면 운영자는 그 큐를 읽지 않고 비우는 습관을 배우고, 중요했던 한 건이 나머지와 같이 지워진다. 규칙은 넓혀도 좁혀도 해롭고, 그래서 커밋 단계와 드라이버의 침묵이 동시에 성립할 때로 조건이 좁혀져 있다.
|
|
|
|
## 검증 환경
|
|
|
|
OpenJDK : 21
|
|
Gradle : 9.0.0
|
|
Spring Boot : 4.0.8
|
|
데이터베이스 : PostgreSQL, Testcontainers 로 기동
|
|
레인 리비전 : a24ece9cf797f7ea647e33bf846b115208ed1ba5
|
|
레인 실행 시각 : 2026-08-29T14:46:23+00:00
|
|
|
|
## 재현 조건
|
|
|
|
1. 커밋이 진행 중인 트랜잭션을 만든다.
|
|
2. 그 백엔드를 pg_terminate_backend 로 끊는다.
|
|
3. 클라이언트가 받은 SQLSTATE 와 그 시점의 커밋 레코드 상태를 확인한다.
|
|
4. CommitFailureClassifier 가 그 상태를 TransactionCompletionUnknownException 과 retryable=false 로 번역하는지 본다.
|
|
|
|
이 절차는 CommitAmbiguityContractTest 가 수행하고, 그 클래스는 jpaPlatformFailureTest 레인에 있다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
## 규칙이 처음 적혀 있던 형태
|
|
|
|
설계 문서는 커밋 모호성을 한 문장으로 정의했다.
|
|
|
|
> 커밋 모호성은 SQLSTATE class 08 뿐이다.
|
|
|
|
class 08 은 connection exception 계열이고, 클라이언트가 연결을 잃었다는 사실을 말한다. 커밋 응답을 받지 못한 상황을 여기에 대응시키는 것은 처음에는 맞아 보인다.
|
|
|
|
## 레인이 보여 준 SQLSTATE
|
|
|
|
컨테이너 레인은 커밋이 진행 중인 백엔드를 `pg_terminate_backend` 로 끊는다. 클라이언트가 받은 것은 `08006` 이 아니라 `57P01` 이었다.
|
|
|
|
`57P01` 은 `admin_shutdown` 이고, 서버가 자기 종료를 알린 것이다. PostgreSQL 은 이 상황을 클라이언트 연결 문제로 보고하지 않는데, 커밋을 요청한 쪽에서 보면 두 상황은 구별되지 않는다 — 응답이 오지 않았고, 서버가 무엇을 했는지 알 수 없다.
|
|
|
|
그리고 `57P01` 이 도착할 때 커밋 레코드는 이미 WAL(write-ahead log, 미리 쓰기 로그)에 기록돼 있을 수 있다. 종료 신호와 커밋 기록 사이에 순서 보장이 없으므로 둘 중 어느 쪽이 먼저였는지 클라이언트는 알지 못한다.
|
|
|
|
## 넓힌 규칙과 그것을 고정하는 단언
|
|
|
|
현재 규칙은 네 갈래다.
|
|
|
|
| 상태 | 무엇을 뜻하나 |
|
|
|---|---|
|
|
| `40003` | statement completion unknown |
|
|
| `08*` | connection exception |
|
|
| `57P01` · `57P02` · `57P03` | 서버가 자기 종료를 알렸다 |
|
|
| transport 단절 | 드라이버가 응답을 받지 못했다 |
|
|
|
|
`CommitAmbiguityContractTest` 는 이 목록을 문장으로 서술하지 않고 SQLSTATE 값을 직접 단언한다. 규칙을 좁히는 변경은 그 단언을 깨뜨려야만 들어올 수 있다.
|
|
|
|
## 프로브가 보여 준 경계
|
|
|
|
같은 사이클에서 돌린 프로브는 다른 것을 보여 준다. 가짜 `PlatformTransactionManager` 의 커밋이 `08006` 을 던지게 하고 현재의 벤더 번역기를 태우면 결과는 이렇다.
|
|
|
|
```text
|
|
type=ConnectionUnavailableException
|
|
category=CONNECTION_UNAVAILABLE
|
|
completionUnknown=false
|
|
retryable=false
|
|
```
|
|
|
|
`08006` 이 그 자체로 completion-unknown 을 만들지 않는다. 판정에는 관측된 단계가 함께 필요하고, 이 프로브는 실제 소켓 유실을 흉내 내지 않으며 데이터베이스가 실제로 커밋했음을 증명하지도 않는다. 프로브 파일의 관측 경계에 그 한계가 적혀 있다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
레인은 `a24ece9c` 에서 돌았다. 다섯 태그 레인을 `--rerun-tasks` 로 실행해 51 클래스 244 tests 가 0 failures 로 끝났고 `jpaPlatformFailureTest` 는 그중 2 클래스 6 tests 였다. HEAD 인 `21234e38` 에서는 다시 돌리지 않았고, 두 리비전 사이에 persistence-jpa 경로의 변경 파일이 0 이라는 측정으로 대신했다.
|
|
|
|
`57P01` 관측이 PostgreSQL 16 과 17 과 18 에서 같게 나오는지도 재지 않았다. 레인의 매트릭스는 실행 한 번에 major 하나만 고르는데 이 사이클은 한 번만 돌렸다.
|
|
|
|
<!-- body:end -->
|