Files

7.5 KiB

id, kind, slug, title, topic, topicName, project, status, studio, lastVerifiedOn, source, sourceRevision, evidence
id kind slug title topic topicName project status studio lastVerifiedOn source sourceRevision evidence
CASE commit-ambiguity-is-not-only-sqlstate-08 pg_terminate_backend 가 57P01 로 도착하고 커밋 레코드는 이미 WAL 에 있었다 commit-ambiguity-as-a-result 커밋 모호성 — 「모른다」를 결과로 유지하기 clean-architecture-backend-template 게시 전 2026-08-29
final/document.md#4-1
final/document.md#a05
21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
../../../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 레인에 있다.

본문

규칙이 처음 적혀 있던 형태

설계 문서는 커밋 모호성을 한 문장으로 정의했다.

커밋 모호성은 SQLSTATE class 08 뿐이다.

class 08 은 connection exception 계열이고, 클라이언트가 연결을 잃었다는 사실을 말한다. 커밋 응답을 받지 못한 상황을 여기에 대응시키는 것은 처음에는 맞아 보인다.

레인이 보여 준 SQLSTATE

컨테이너 레인은 커밋이 진행 중인 백엔드를 pg_terminate_backend 로 끊는다. 클라이언트가 받은 것은 08006 이 아니라 57P01 이었다.

57P01admin_shutdown 이고, 서버가 자기 종료를 알린 것이다. PostgreSQL 은 이 상황을 클라이언트 연결 문제로 보고하지 않는데, 커밋을 요청한 쪽에서 보면 두 상황은 구별되지 않는다 — 응답이 오지 않았고, 서버가 무엇을 했는지 알 수 없다.

그리고 57P01 이 도착할 때 커밋 레코드는 이미 WAL(write-ahead log, 미리 쓰기 로그)에 기록돼 있을 수 있다. 종료 신호와 커밋 기록 사이에 순서 보장이 없으므로 둘 중 어느 쪽이 먼저였는지 클라이언트는 알지 못한다.

넓힌 규칙과 그것을 고정하는 단언

현재 규칙은 네 갈래다.

상태 무엇을 뜻하나
40003 statement completion unknown
08* connection exception
57P01 · 57P02 · 57P03 서버가 자기 종료를 알렸다
transport 단절 드라이버가 응답을 받지 못했다

CommitAmbiguityContractTest 는 이 목록을 문장으로 서술하지 않고 SQLSTATE 값을 직접 단언한다. 규칙을 좁히는 변경은 그 단언을 깨뜨려야만 들어올 수 있다.

프로브가 보여 준 경계

같은 사이클에서 돌린 프로브는 다른 것을 보여 준다. 가짜 PlatformTransactionManager 의 커밋이 08006 을 던지게 하고 현재의 벤더 번역기를 태우면 결과는 이렇다.

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 하나만 고르는데 이 사이클은 한 번만 돌렸다.