수동 백업 중단 뒤 100행 체크포인트와 같은 ID로 재개하기

이 글의 목차 8개 항목

문제와 원인

수동 백업은 D1을 100행씩 읽어도 모든 페이지를 한 생성 호출 안에서 처리했다. cursor·파트 목록·검증 위치가 메모리에만 남아 중간 PUT이 실패하면 진행이 사라졌고, 재시도는 새 UUID로 백업을 만들었다.

수정 전 코드

createExternalBackup

const id = crypto.randomUUID();
const generatedAt = now();

scanStableTable

while (true) {
  const query = stablePageQuery(table, cursor);
  const page = await allRows(query.sql, query.values);
  if (!page.length) break;
  for (const row of page) validateRowShape(table, row);
  for (const rows of backupPartGroups(page)) {
    await visit(rows, partIndex, rowStart);
    partIndex += 1;
    rowStart += rows.length;
  }
  cursor = page.at(-1) ?? null;
  if (page.length < BACKUP_PAGE_ROWS) break;
}

수정 후 코드

advanceManualExternalBackup

backup_snapshots.payload에 collect → verify → publish 단계, 테이블 위치, cursor, 파트 목록, 검증 위치와 revision을 저장했다. 수집·원본 재검증은 호출당 최대 1페이지·100행을 처리하고 페이지 완료 뒤 진행을 저장한다.

const MANUAL_BACKUP_PAGES_PER_REQUEST = 1;

진행 갱신은 이전 payload와 유효한 lease가 일치할 때만 허용한다.

const result = await execute(`UPDATE backup_snapshots
  SET payload = ?, counts = ?, byte_size = ?
  WHERE id = ? AND status = 'creating' AND payload = ?
    AND EXISTS (SELECT 1 FROM maintenance_runs WHERE task = 'backup_create'
      AND lease_owner = ? AND julianday(lease_until) > julianday('now'))`, [
  serialized, JSON.stringify(next.counts), next.dataByteSize,
  backupId, row!.payload, leaseOwner,
]);
if (Number(result.meta.changes ?? 0) !== 1) {
  throw new Error("백업 진행 상태가 다른 요청과 충돌했습니다.");
}

파트 키에도 UUID를 붙여 이전 요청의 늦은 PUT이 다음 요청의 파트를 덮어쓰는 일을 막는다. 실패 요청은 자신이 만든 미저장 객체를 정리한다. 전체 수집 뒤 원본을 다시 읽어 행 수·본문 SHA·파트 순서를 비교하고 매니페스트 검증을 마친 뒤 completed를 게시한다. 수동 단계 lease는 30분에서 3분으로 조정했다.

검증 결과

Node.js·SQLite의 로컬 메모리 DB와 D1·R2 대역에서 수정 전후 원본 백업 함수를 버전별 1회 시나리오로 실행했다. 기본 설정 2행·합성 설정 510행, 총 512행을 넣고 두 번째 파트 PUT을 한 번 실패시킨 뒤 재시도·다운로드·SHA 검증을 수행했다.

지표수정 전수정 후
중단 후 저장된 수집 진행0행100행
중단 직후 스냅샷 상태failedcreating
재시도 완료까지 생성된 스냅샷2개1개
재시도 백업 ID새 ID같은 ID
최종 다운로드 행 수512행512행
다운로드 SHA 검증 오류0개0개
실패를 포함한 생성·진행 함수 호출2회15회

로컬 Swagger UI의 1280×720 CSS px·DPR 2 화면에서도 새 합성 D1·R2 대역의 같은 시나리오를 1회 HTTP 요청으로 실행했다. HTTP 200 응답에서 체크포인트 0→100행, 스냅샷 2→1개, 같은 ID 재개, 최종 512행·SHA 오류 0개를 얻었다. 표의 2회·15회는 페이지 처리와 단계 전환을 포함한 내부 함수 호출 수다.

수정 전에는 중단 뒤 체크포인트가 0행·상태가 failed였다. 재시도까지 스냅샷 2개가 생성됐고 백업 ID도 달라졌다.

수정 전 백업 응답: 진행 0행·failed·스냅샷 2개·새 ID원본 보기 (새 창)

수정 전 백업 응답: 진행 0행·failed·스냅샷 2개·새 ID

100%원본 보기 (새 창)

확대한 이미지는 상하좌우로 이동할 수 있습니다. 키보드는 이미지 영역에 초점을 두고 방향키를 사용하세요. Esc로 닫습니다.

수정 후에는 100행 체크포인트·creating 상태를 유지해 같은 ID로 재개했다. 스냅샷 1개로 512행을 다운로드했고 SHA 오류는 0개였다.

수정 후 백업 응답: 진행 100행·같은 ID·스냅샷 1개·완료 512행원본 보기 (새 창)

수정 후 백업 응답: 진행 100행·같은 ID·스냅샷 1개·완료 512행

100%원본 보기 (새 창)

확대한 이미지는 상하좌우로 이동할 수 있습니다. 키보드는 이미지 영역에 초점을 두고 방향키를 사용하세요. Esc로 닫습니다.

재개 조건

조건처리
체크포인트 저장 후 다음 파트 PUT 실패저장된 100행·cursor를 유지하고 같은 ID에서 해당 페이지 재시도
같은 ID의 완료 결과 재조회기존 완료 결과 반환
다른 관리자·백업 유형·포함 범위최초 작업과 일치할 때 재개
소스·스키마 버전 또는 원본 데이터 변경계약 검사·재검증에서 완료 게시 거부
늦게 끝난 이전 단계lease·이전 payload 불일치 시 갱신 거부

재개 경로에는 backupId를 전달한다. ID 없는 기존 단일 생성 경로도 남아 있으며, 페이지당 100행 제한은 행 크기·R2 파트 수 제한과 함께 적용한다.