file-storage 크래시 안전성 로드맵 (GlueSQL #2016→#2017)

NOTE

file-storage 백엔드의 크래시 안전성을 개선하는 작업을 “row 파일 하나” → “statement 전체” → “스토리지 전체(마이그레이션)” 순으로 확장해가는 로드맵. PR #2016에서 시작해 Issue #2017로 이어졌고, 설계 방향을 잡을 때 다른 기여자가 연 Issue #2004에 메인테이너(panarch)가 남긴 설계 원칙을 그대로 가져와 적용했다. 각 단계에서 “왜 여기까지만 하고 다음으로 넘겼는가”의 근거를 정리한다.

📌 개념

1단계 — #2016: 파일 하나 단위의 원자성 (진행 중)

insert_data/append_data가 row 하나를 File::create + write_all로 직접 덮어쓰던 걸, 크레이트 안에 이미 있던 “임시 파일 + fsync + rename” 패턴(write_file_atomically)으로 교체. 자세한 설계는 file-storage 원자적 쓰기 설계와 모듈 분리(#2016) 참고.

  • 범위: row 파일 1개를 쓰는 동안의 크래시만 방지. delete_data는 unlink 자체가 원자적이라 스코프 밖.
  • 명시적으로 미룬 것 두 가지 (PR 본문에 known limitation으로 적어둠):
    1. 백업 rename 직후~최종 rename 직전 사이 크래시 시 남는 .tmp-*/.bak-* 파일의 시작 시 자동 복구 — 별도 후속 PR로 트래킹.
    2. statement(한 번의 insert_data 호출) 전체의 원자성 — row가 여러 개일 때 일부만 쓰인 채 크래시하는 문제는 이 PR의 스코프 밖. → 이게 그대로 2017이 됨.
  • 현재 상태(2026-09-07 기준): 오픈 상태, CodeRabbit 자동 리뷰는 액셔너블 코멘트 없음, 다만 사람 리뷰(panarch/devgony/ever0de/zmrdltl에게 요청됨)는 아직 없고 브랜치가 main과 드리프트됨 → rebase 후 리뷰 대기가 다음 행동.

2단계 — #2017: statement 단위의 원자성 (설계 확정, 메인테이너 피드백 대기)

멀티로우 INSERT 하나가 row N개를 순차적으로 쓰다가 중간에 크래시하면, 일부 row만 존재하는 반쪽짜리 상태가 남는 문제.

최초 제안(WAL/저널 방식): statement 시작 전 저널에 pending write를 기록 → row 적용(#2016의 write_file_atomically 재사용) → 성공 시 저널 항목 삭제 → 시작 시 미완료 항목이 있으면 replay 또는 discard.

설계를 바꾼 이유: 같은 크레이트 안의 같은 성격 문제(마이그레이션 중 크래시, #2004)에 메인테이너 panarch가 이미 답을 내놓은 상태였다. 거기서 명시적으로 “per-row resume state를 유지하는 저널”보다 “완성본을 스테이징 위치에 먼저 만들고, 완전히 끝난 뒤에만 원본을 교체” 하는 4단계(Lock → Build → Cut over → Clean up) 구조를 선호한다고 밝혔다. 저널 방식을 그대로 밀어붙이면 나중에 “왜 여기는 저널, 저기는 스테이징이냐”는 일관성 지적을 받을 가능성이 컸다.

최종 설계(스테이징+무조건 replay로 축소 적용):

  1. Build: statement의 row 전체를 <table>/.pending-<uuid>.ron 하나에 리스트로 직렬화해서 write_all + fsync 1번으로 씀. 파일 하나 쓰기라 torn 상태 자체가 없음 — 이 파일이 존재하는 순간 statement 전체가 durable.
  2. Apply: pending 파일을 다시 읽어서 각 row를 실제 경로에 write_file_atomically(#2016 재사용)로 반영.
  3. Clean up: 전부 반영되면 pending 파일 삭제(단일 unlink, 원자적).
  4. Recovery: 재시작 시 남은 .pending-*.ron을 발견하면, 내용이 이미 fsync로 보장돼 있고 write_file_atomically의 덮어쓰기가 멱등이므로 묻지도 따지지도 않고 전부 재적용(discard 분기 없음 — 항상 앞으로만 수렴).

이 설계는 원래 제안의 “row당 fsync 1번 → statement당 fsync 1번” 배치 목표도 그대로 satisfy한다(Build 단계의 fsync 1번이 곧 배치임). 이슈 본문은 2026-09-07에 이 방향으로 수정 완료(Issue #2017).

이슈를 먼저 연 이유: 새 영속 아티팩트(pending 파일)와 복구 프로토콜이 들어가는 설계 판단이라, file-storage가 “단순한 사람이 읽을 수 있는 백엔드”로 남아야 하는지(강한 트랜잭션이 필요하면 redb-storage를 쓰면 된다는 2004의 논의) 메인테이너 합의가 먼저 필요.

3단계 이후 — #2017 완료 후 남는 작업들

순서작업트래킹왜 지금 안 하는가
3-a.tmp-*/.bak-* 잔여 파일의 시작 시 복구2016/2017 본문에 “별도 후속 PR”로 명시#2016·#2017 모두 “row/statement 쓰기 자체를 안전하게 만드는 것”에 집중, 복구 스캔 로직은 독립적으로 검증 가능한 별개 단위 작업
3-bcsv-storage/json-storage에 동일 패턴 확장#2017 Scope/non-goals에 명시독립된 크레이트라 각자 쓰기 경로가 다름 — file-storage에서 패턴이 검증된 뒤 이식하는 게 순서상 안전
3-c여러 statement에 걸친 트랜잭션/롤백 시맨틱#2017 Scope/non-goals에 명시(비목표)이건 redb-storage가 이미 강한 트랜잭션 보장으로 커버하는 영역(#2004 논의) — file-storage에 중복 구현할 이유가 약함
3-d(병행)#2004: 마이그레이션 전체의 원자성jun02160이 진행 예고 (2026-08-22 코멘트)직접 담당하는 이슈는 아니지만, 2017과 같은 스테이징/replay 설계 언어를 쓰므로 진행 상황을 계속 주시 — 완료되면 #2017 리뷰 시 “이미 검증된 패턴의 확장”이라는 근거로 재인용 가능

🔗 참고

관련 문서