삭제하고 삽입하는 사이
이날은 “삭제하고 넣는 사이에 실패하면 데이터가 사라진다”는 교훈을 정리한 날이다.
2단계 작업을 원자적 연산으로 바꿨다.
그리고 이전에 내린 결론이 틀렸다는 것도 깨달았다.
SQLite WAL 복사는 위험하지 않았다.
법원경매 데이터 파이프라인에 삭제 후 삽입 패턴이 있었다.
권리 정보를 갱신할 때, 기존 데이터를 지우고 새 데이터를 넣는다.
2개 요청이다.
근데 중간에 실패하면?
지웠는데 넣기 전에 연결이 끊기면?
데이터가 영원히 사라진다.
기존 데이터도 없고 새 데이터도 없다.
이건 데이터 손실 버그다.
2단계 작업이 원자적이지 않으면, 중간 실패가 영구 손실로 이어진다.
원자적 연산으로 바꿨다
해결은 삭제와 삽입을 하나의 원자적 연산으로 만드는 거였다.
PL/PostgreSQL 함수 함수로 묶었다.
새 데이터가 커밋될 때만 기존 데이터가 삭제된다.
중간에 실패하면 기존 데이터가 유지된다.
이게 핵심이다.
“지우고 넣는다”는 2단계가 아니라, “교환한다”는 1단계로 만들어야 한다.
교환은 원자적이다.
중간 상태가 없다.
에러를 삼키는 패턴도 고쳤다
이날 데이터 손실의 다른 원인도 발견했다.
에러를 삼키는 패턴이었다.
Promise.all에서 자식 요청이 실패해도 조용히 무시된다.
사진 업로드가 실패해도 항상 성공으로 보고된다.
전송 실패가 무시된다.
에러를 삼키면 “성공했다”는 거짓 보고가 나온다.
실제로는 데이터가 누락되었는데, 아무도 모른다.
에러는 삼키는 게 아니라 전파해야 한다.
내 결론이 틀렸다: SQLite WAL 복사
이전에 SQLite WAL 모드 데이터베이스를 cp로 복사하면 위험하다는 결론을 내렸었다.
“복사 중에 쓰기가 끼면 데이터가 깨진다.
반드시 백업 API를 써야 한다.” 틀렸다.
프레임별 체크섬이 있다
SQLite WAL은 프레임별 체크섬을 가지고 있다.
복사 중에 쓰기가 끼어들면, 손상된 프레임의 체크섬이 맞지 않는다.
그러면 열 때 마지막 유효 커밋으로 복구된다.
cp -R로 디렉토리 전체를 복사하면 “오래되었지만 유효한” 복사본이 나온다.
최근 커밋이 누락될 수는 있지만 깨지지는 않는다.
결론을 뒤집었다
“cp는 위험하다, 백업 API만 안전하다”가 아니었다.
둘 다
유효하다.
차이가 있을 뿐이다.
cp -R은 오래되었지만 유효한 복사본을 만든다.
최근 커밋이 누락될 수 있다.
빠르다.
백업 API는 경쟁 조건 없이 일관된 단일 파일을 만든다.
가장 안전하다.
느리다.
목적에 따라 선택하면 된다.
“cp는 무조건 위험”이라는 결론이 틀렸다.
체크섬 복구 메커니즘을 몰라서 내린 잘못된 결론이었다.
모델이 1429번 반복했다
GLM-5.2 모델이 한 턴에서 같은 도구 호출을 1429번 반복했다.
스키마 검증 에러가 났는데, 모델이 같은 호출을 계속한 거다.
외부에서 중단하기 전까지 멈추지 않았다.
다른 모델은 44턴에 걸쳐 31킬로토큰을 소비하며 같은 패턴을 반복했다.
존재하지 않는 파일 경로를 반복적으로 읽으려 했다.
모델 수준 복구 불가 상태
이런 반복은 모델 수준에서 복구 불가능하다.
확장 가드로도 잡을 수 없다.
외부 개입이 필요하다.
하네스가 중단하거나, 사람이 멈추거나. 모델이 동일한 도구 호출을 반복하면, 그건 “재시도”가 아니라 “무한 루프”다.
재시도는 맥락을 바꿔서 다시 시도하는 거다.
맥락을 안 바꾸면 같은 결과가 나온다.
같은 결과에 같은 행동을 반복하는 건 정의상 무한 루프다.
마무리
이날의 핵심은 두 가지였다.
첫째, 2단계 작업을 원자적으로 만들어라. 삭제하고 삽입하는 사이에 실패하면 데이터가 사라진다.
교환을 하나의 연산으로 묶어야 한다.
그리고 에러를 삼키지 마라. 삼킨 에러는 거짓 성공을 만든다.
둘째, 결론을 뒤집을 수 있어야 한다.
SQLite WAL 복사가 위험하다는 결론을 내렸다.
근데 프레임별 체크섬을 확인해보니 틀렸다.
체크섬 복구 메커니즘이 있었다.
결론을 고집하지 않고 새 증거로 뒤집었다.
결론은 영구적이지 않다.
새 증거가 나오면 갱신해야 한다.
고집은 학습의 적이다.