콘텐츠로 이동

CUBRID Recovery Manager — 코드 수준 심층 분석

이 문서의 위치: 상위 수준 분석 문서인 cubrid-recovery-manager.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 모든 분기와 필드를 코드 수준에서 추적한다. 각 장은 독립적으로 읽을 수 있지만, 순서대로 읽으면 커널 내부에서 크래시된 데이터베이스가 재시작되는 전 과정을 따라갈 수 있다.

목차:

Ch제목상태
1자료구조 지도✅
2재시작 진입점과 로그 페이지 접근✅
3분석 패스 드라이버✅
4분석 레코드 디스패치와 트랜잭션 테이블 재구성✅
5분석 단계의 Sysop·Postpone 어노테이션 기록✅
6Redo 패스 드라이버와 동기 적용✅
7병렬 Redo 인프라✅
8원자적 시스템 오퍼레이션 중단 및 Postpone 완료✅
9Undo 패스와 보상 레코드✅
10RV_fun 디스패치 테이블✅
11특수 경로✅

이론적 배경은 동반 문서 cubrid-recovery-manager.md(“복구 디스패치 테이블”, “Redo 패스 — 템플릿 기반 현대적 디스패치”)에서 다룬다. 이 챕터는 복구 관련 자료구조의 모든 필드와 구조체 간 포인터 관계를 구체적으로 짚어낸다.

1.1 개요 — 누가 누구를 가리키는가

섹션 제목: “1.1 개요 — 누가 누구를 가리키는가”
flowchart TB
  LD["LOG_DATA\nrcvindex / vpid / offset"]
  RVF["RV_fun[rcvindex]\n(struct rvfun)"]
  CTX["log_rv_redo_context"]
  RCV["LOG_RCV"]
  RECINFO["log_rv_redo_rec_info<T>"]
  FUNC["undofun / redofun"]

  LD -->|"선택"| RVF
  RVF --> FUNC
  RECINFO -->|"타입별 헤더 복사"| RCV
  CTX -->|"압축 해제 버퍼 → rcv.data 공급"| RCV
  FUNC -->|"&rcv를 인자로 호출"| RCV

Figure 1-1 — rcvindex가 RV_fun 항목을 선택하고, redo context가 페이로드를 압축 해제해 선택된 함수가 받는 LOG_RCV에 채워 넣는다.

undo, redo, compensate, run-postpone 함수는 모두 int (*)(THREAD_ENTRY *, LOG_RCV *) 시그니처를 공유한다. LOG_RCV는 이 모든 경로를 하나로 묶는 좁은 허리(narrow waist)다.

// log_rcv -- src/transaction/recovery.h
struct log_rcv
{ /* Recovery information */
MVCCID mvcc_id = MVCCID_NULL; /* mvcc id */
PAGE_PTR pgptr = nullptr; /* Page to recover. Page should not be free by recovery functions,
* however it should be set dirty whenever is needed */
// ... condensed: PGLENGTH offset; int length ...
const char *data = nullptr; /* Replacement data. Pointer becomes invalid once the recovery
* of the data is finished */ /* <- borrowed, see invariant below */
LOG_LSA reference_lsa = NULL_LSA; /* Next LSA used by compensate/postpone. */
// ... condensed: default ctor; copy/move ctors and both assignments deleted ...
};
필드역할존재 이유
mvcc_idMVCC 클래스 레코드의 MVCCID, 그 외는 MVCCID_NULL레코드 헤더 필드; log_rv_get_log_rec_mvccid MVCC 특수화에서만 설정
pgptr복구 대상 페이지, 드라이버가 fix; 논리적 레코드의 경우 nullptrfix/unfix는 드라이버에서 중앙 처리
offsetpgptr 내 오프셋 또는 슬롯 ID, LOG_DATA.offset에서 유래물리적 복구는 page+offset 주소 방식
lengthdata의 바이트 길이종단자 없는 raw 버퍼
dataredo 교체 이미지 또는 undo before-imageLOG_ZIP 버퍼나 로그 페이지를 가리키는 포인터 — 수명 규칙은 아래 참조
reference_lsacompensate: undo 시점의 트랜잭션 undo_nxlsa — undo 체인이 재개할 다음 LSA로, log_sysop_end_logical_compensate에 전달됨; log_rollback_record(런타임 롤백)와 log_rv_undo_record(재시작 undo) 모두 채움. Run-postpone: 원본 postpone 레코드의 LSA, log_execute_run_postpone가 채움보상 / run-postpone 레코드를 직접 추가하는 수동 논리 함수(1.7)의 앵커

불변 조건 (빌린 데이터의 수명). rcv.data와 rcv.pgptr은 빌림(loan)이다. data는 압축 해제 버퍼(m_redo_zip.log_data) 또는 로그 페이지를 가리키고, pgptr은 반환 후 호출자가 unfix한다. 강제 수단: 네 가지 복사/이동 연산이 모두 삭제됨; log_rv_redo_record_sync는 레코드마다 스택 로컬 LOG_RCV를 새로 생성하고, scope_exit으로 pgptr을 unfix한다. rcv->data를 보관하면 다음 레코드의 압축 해제 시 재생(replay) 내용이 조용히 깨진다.

1.3 rvfun과 RV_fun[] 디스패치 테이블

섹션 제목: “1.3 rvfun과 RV_fun[] 디스패치 테이블”

rvfun(recovery.h)은 fun_t = int (*)(THREAD_ENTRY *, LOG_RCV *), dump_fun_t = void (*)(FILE *, int, void *), 그리고 여섯 개 필드를 묶는다. extern struct rvfun RV_fun[]은 recovery.c에서 초기화된다.

필드역할존재 이유
recv_index항목 자신의 인덱스 복사본 (/* For verification */)rv_check_rvfuns가 디버그 시작 시 RV_fun[i].recv_index == i를 assert
recv_string이름, 예: "RVDK_FORMAT"rv_rcvindex_string을 통한 trace/dump 출력
undofunundo/롤백에서 적용됨 — compensate 레코드의 redo에서도 호출: log_rv_get_fun<LOG_REC_COMPENSATE>가 undofun을 반환(// yes, undo)CLR의 redo는 곧 원래 undo
redofunredo 패스, run-postpone, HA 복제 적용에서 호출전진 이미지(forward image) 적용자
dump_undofun / dump_redofun페이로드 출력 도구, 없으면 NULL로그 덤프 도구 전용

rv_rcvindex_string은 분기 없이 동작한다(return RV_fun[rcvindex].recv_string;). rv_check_rvfuns는 초기화 순서 오류만 디버그 빌드 시작 실패로 전환한다(er_set + assert (false)); 범위를 벗어난 인자(예: RV_NOT_DEFINED 999)는 별도로 방어하지 않는다 — 호출자가 유효한 인덱스를 전달해야 한다.

1.4 LOG_RCVINDEX — 계열별 인덱스 공간

섹션 제목: “1.4 LOG_RCVINDEX — 계열별 인덱스 공간”

불변 조건 (추가 전용 번호 부여). 인덱스는 로그 레코드 내부에 영속되므로 번호를 바꾸면 이전 데이터베이스에서 잘못된 함수가 재생된다. enum 헤더 주석은 경고한다: “NEW ENTRIES SHOULD BE ADDED AT THE BOTTON OF THE FILE … to AVOID OLD DATABASES TO BE RECOVERED UNDER OLD FILE” — 따라서 RVPGBUF_SET_TDE_ALGORITHM (127)이 형제 항목(120–123)과 멀리 떨어져 있다. RV_LAST_LOGID = RVHF_LOB_REMOVE_DIR (129)가 상한을 표시하고, RV_NOT_DEFINED = 999는 “rcvindex 없음” 센티널이다.

계열범위서브시스템
RVDK_*0–9disk manager
RVFL_*10–32, 128file manager
RVHF_*33–53, 126, 129heap
RVOVF_*54–57overflow records
RVEH_*58–65extendible hash
RVBT_*66–91, 124–125b-tree, 논리 키 집합 포함 (1.7)
RVCT_*92–96catalog pages
RVLOG_*97논리 redo noop 마커
RVREPL_*98–103replication, HA 적용자 전용
RVVAC_*104–117vacuum
RVES_*118external storage (LOB)
RVLOC_*119locator classname dummy
RVPGBUF_*120–123, 127page buffer

redo 패스(Ch 6)와 각 병렬 redo 적용자(Ch 7)는 각자 log_rv_redo_context(log_recovery_redo.hpp) 하나를 소유한다.

필드역할존재 이유
m_readerLOG_CS_SAFE_READER로 생성된 log_reader 커서적용자마다 독립적인 로그 위치
m_redo_zipredo 페이로드의 압축 해제 대상; 그 log_data가 rcv.data가 됨출력이 복구 함수 호출보다 오래 유지되어야 함
m_undo_zipdiff undoredo 레코드의 undo 절반 압축 해제 대상LOG_DIFF_UNDOREDO_DATA는 redo를 undo에 대한 XOR diff로 저장
m_end_redo_lsaconst 상한; 이 LSA 이상의 레코드는 redo하지 않음패스 시작 전에 redo 지평(horizon)을 고정
m_reader_fetch_page_modeset_lsa_and_fetch_page의 const fetch 모드; NORMAL은 pageid가 바뀔 때만 재fetch(do_fetch_page = FORCE || m_lsa.pageid != lsa.pageid)유일한 생성자 호출(redo 패스, log_recovery.c)은 NORMAL을 전달; FORCE는 향후 재사용을 위해 미사용 상태로 보존(log_reader.hpp 주석)

기본 생성자는 삭제됨; 인자 두 개짜리 생성자는 두 버퍼를 LOGAREA_SIZE로 사전 확장; 이동과 양방향 대입은 삭제됨. 허용된 유일한 복사 생성자는 (o.m_end_redo_lsa, o.m_reader_fetch_page_mode)로 위임하므로, 두 const 파라미터만 살아남고 각 병렬 redo 워커는 신선한 버퍼와 reader를 갖게 된다.

각 적용 레코드는 log_rv_redo_rec_info<T>다. (log_lsa, LOG_RECTYPE, const T &) 생성자를 제외한 모든 특수 멤버가 삭제됨 — 한 번 생성 시 완전히 초기화되며, 재할당되지 않는다.

필드역할존재 이유
m_start_lsa레코드 헤더의 LSA적용 후 페이지에 찍힘(pgbuf_set_lsa); 아래 검사의 키
m_typeLOG_RECTYPElog_rv_get_log_rec_redo_data의 LOG_DIFF_UNDOREDO_DATA XOR diff 분기를 결정
m_logrec타입별 바디 T의 값 복사본 — LOG_REC_{UNDOREDO, MVCC_UNDOREDO, REDO, MVCC_REDO, RUN_POSTPONE, COMPENSATE} 중 하나reader가 앞으로 이동할 수 있게 해방; log_rv_get_log_rec_* 특수화들이 vpid/mvccid/length/offset을 추출

불변 조건 (페이지 단위 LSA 순서, 디버그 빌드). 한 페이지에 대한 redo는 스레드를 넘어서도 로그 순서대로 적용되어야 한다; vpid_lsa_consistency_check(NDEBUG에서는 컴파일 제외)가 그 필요 조건을 확인한다.

// vpid_lsa_consistency_check::check -- src/transaction/log_recovery_redo.cpp
std::lock_guard<std::mutex> lck (mtx);
const vpid_key_t key {a_vpid.volid, a_vpid.pageid};
const auto map_it = consistency_check_map.find (key);
if (map_it != consistency_check_map.cend ())
{
assert ((*map_it).second < a_log_lsa); /* <- later applies must beat the stored LSA */
}
consistency_check_map.emplace (key, a_log_lsa); /* <- emplace never overwrites an existing key */
필드역할존재 이유
mtxcheck와 cleanup을 직렬화맵(log_Gl_recovery_redo_consistency_check 전역)이 모든 적용자에서 접근됨
consistency_check_map페이지 단위 기준선 — vpid_log_lsa_map_t는 vpid_key_t = (volid, pageid)를 해당 페이지에 처음 적용된 LSA에 매핑; emplace는 기존 키를 덮어쓰지 않으므로 기준선이 전진하지 않음assert는 이후의 모든 적용이 기준선 LSA보다 높은 LSA를 가져야 함을 요구 — 완전한 쌍별 단조성(pairwise monotonicity)보다 약한 조건이지만(나중 두 적용의 순서 뒤바뀜은 통과), 첫 번째 적용보다 오래된 이미지는 잡아낼 수 있음

cleanup()은 패스 종료 후 맵을 지운다; log_rv_redo_record_sync는 log_Gl.rcv_phase != LOG_RESTARTED인 동안에만 참조한다.

LOG_RCV_TDES (log_tdes의 rcv 필드, log_impl.h)는 분석 패스의 발견 사항을 이후 패스(Ch 4, 5, 8)로 전달한다. 다섯 개의 LSA로 구성된다.

필드역할존재 이유
sysop_start_postpone_lsa크래시 시점에 진행 중이던 LOG_SYSOP_START_POSTPONE의 LSAsysop postpone 단계의 재개 앵커 (Ch 8)
tran_start_postpone_lsa트랜잭션 수준 postpone이 시작된 위치”커밋됨, postpone 대기 중”을 단순 active와 구분
atomic_sysop_start_lsa중단된 atomic 파일 연산(file_perm_alloc / file_perm_dealloc)의 시작 LSApostpone 실행 전에 완전히 완료되거나 롤백되어야 함 (Ch 8)
analysis_last_aborted_sysop_lsa분석 중 abort된 마지막 sysop의 끝 LSA (“논리 redo 연산 복구를 위해”)논리 redo가 abort된 범위에 재진입하면 안 됨
analysis_last_aborted_sysop_start_lsa해당 sysop의 시작 LSA범위의 반대쪽 끝

LOG_RECVPHASE (log_impl.h), 전역 모드 스위치 log_Gl.rcv_phase는 복구 외부에서도 광범위하게 참조된다(페이지 버퍼 fix 규칙, 위의 검사 등): LOG_RESTARTED (복구 완료), LOG_RECOVERY_ANALYSIS_PHASE (Ch 3–5), LOG_RECOVERY_REDO_PHASE (Ch 6–7), LOG_RECOVERY_UNDO_PHASE (Ch 9), LOG_RECOVERY_FINISH_2PC_PHASE (Ch 11).

체크포인트 스냅샷 레코드 (log_record.hpp): 고정 크기 LOG_REC_CHKPT 헤더, 뒤이어 ntrans개 트랜잭션 항목, 그다음 ntops개 sysop 항목이 따라온다.

log_rec_chkpt 필드역할존재 이유
redo_lsa”페이지 버퍼 내 dirty 데이터 페이지의 가장 오래된 LSA” (소스 주석)redo 패스 하한 — fuzzy checkpoint 계약
ntrans뒤따르는 트랜잭션 항목 수가변 크기 레코드
ntops트랜잭션 배열 뒤의 sysop 항목 수동일

LOG_INFO_CHKPT_TRANS는 같은 이름의 살아있는 log_tdes 필드를 스냅샷한다; 분석 패스는 항목마다 TDES를 재생성한 뒤 로그 테일에서 수정한다 (Ch 4).

필드역할존재 이유
isloose_end체크포인트 시점의 loose-end 플래그2PC/클라이언트 loose end 표시
trid트랜잭션 식별자TDES 재생성의 키
state체크포인트 시점의 TRAN_STATEloose-end 분류 초기값
head_lsa트랜잭션의 첫 번째 로그 레코드역방향 체인의 경계
tail_lsa체크포인트 시점의 마지막 레코드분석 스캔이 여기서 재개
undo_nxlsaundo 중 CLR이 기록된 경우 다음 undo 대상 레코드이미 보상된 작업을 롤백이 건너뜀
posp_nxlsa첫 번째 postpone 레코드postpone 실행 시작 위치
savept_lsa마지막 세이브포인트세이브포인트 체인 헤드, 부분 롤백
tail_topresult_lsa마지막 부분 abort/commit중첩 sysop 해소
start_postpone_lsapostpone 도중이라면 start-postpone 주소해당 트랜잭션은 undo가 아닌 postpone을 완료해야 함
user_name클라이언트 이름 (char[LOG_USERNAME_MAX])TDES에 복원

LOG_INFO_CHKPT_SYSOP는 LOG_RCV_TDES의 두 영속 sysop 앵커를 스냅샷한다. 나머지 세 LOG_RCV_TDES LSA는 여기에 포함되지 않는다: analysis_last_aborted_* 쌍은 현재 분석 실행의 산물이라 영속화되지 않고, tran_start_postpone_lsa는 트랜잭션 항목의 LOG_INFO_CHKPT_TRANS.start_postpone_lsa로 전달된다.

필드역할존재 이유
trid두 LSA를 복원할 대상 트랜잭션의 TDES트랜잭션 배열과 병렬이 아닌, 트랜잭션 기준 키
sysop_start_postpone_lsa저장된 rcv.sysop_start_postpone_lsasysop 상태가 체크포인트 이전부터 존재할 수 있음
atomic_sysop_start_lsa저장된 rcv.atomic_sysop_start_lsa동일, 중단된 atomic 파일 연산

LOG_ZIP (log_compress.h), 쓰기 경로와 (1.5) redo context의 압축 워크스페이스로, log_data를 소유한다(log_zip_free_data로 해제). 네 가지 복사/이동 연산은 모두 삭제됨 — 멤버별 복사는 이중 해제(double-free)를 유발한다.

필드역할존재 이유
data_lengthlog_data 내 현재 유효 바이트 수log_unzip 후 rcv.length에 전달되는 길이
buf_size할당된 용량log_zip_realloc_if_needed가 확장; 레코드 간에 유지됨
log_data소유된 버퍼 (“used as data buffer”)rcv.data가 빌려 쓰는 저장소 — 1.2의 수명 규칙

저장된 길이는 최상위 비트로 압축 여부를 표시한다: MAKE_ZIP_LEN(l)은 0x80000000을 설정하고, ZIP_CHECK(l)이 검사하며, GET_ZIP_LEN(l)이 제거한다.

분류 매크로. LOG_RCVINDEX에 대한 순수한 OR 조합 네 가지로 구성된다; 각 분기는 열거된 인덱스 그대로다.

// RCV_IS_BTREE_LOGICAL_LOG -- src/transaction/recovery.h
#define RCV_IS_BTREE_LOGICAL_LOG(idx) \
((idx) == RVBT_DELETE_OBJECT_PHYSICAL \
|| (idx) == RVBT_MVCC_DELETE_OBJECT \
|| (idx) == RVBT_MVCC_INSERT_OBJECT \
|| (idx) == RVBT_NON_MVCC_INSERT_OBJECT \
|| (idx) == RVBT_MARK_DELETED \
|| (idx) == RVBT_DELETE_OBJECT_POSTPONE \
|| (idx) == RVBT_MVCC_INSERT_OBJECT_UNQ \
|| (idx) == RVBT_MVCC_NOTIFY_VACUUM \
|| (idx) == RVBT_ONLINE_INDEX_UNDO_TRAN_DELETE \
|| (idx) == RVBT_ONLINE_INDEX_UNDO_TRAN_INSERT)

이 열 개 연산은 페이지 이미지가 아닌 키 값으로 기록되므로, undo는 트리를 재탐색하며 하나의 고정 페이지에 대해서는 실행되지 않는다.

RCV_IS_LOGICAL_COMPENSATE_MANUAL은 btree 집합에 정확히 여섯 개를 더한 것이다: RVFL_ALLOC, RVFL_USER_PAGE_MARK_DELETE, RVPGBUF_DEALLOC, RVFL_TRACKER_HEAP_REUSE, RVHF_LOB_REMOVE_DIR, RVFL_TRACKER_UNREGISTER. 이들의 undofun은 rcv.reference_lsa로 보상 레코드를 직접 추가하므로, 롤백 드라이버가 자동으로 LOG_COMPENSATE를 추가해서는 안 된다 — 재크래시 시 이중 undo가 발생한다. RCV_IS_LOGICAL_RUN_POSTPONE_MANUAL은 정확히 네 개와 일치한다: RVFL_DEALLOC, RVHF_MARK_DELETED, RVHF_LOB_REMOVE_DIR, RVBT_DELETE_OBJECT_POSTPONE. postpone 액션으로서 이들의 redofun은 표준 LOG_RUN_POSTPONE가 아닌 LOG_SYSOP_END_LOGICAL_RUN_POSTPONE로 마무리된다 (Ch 8). RVHF_LOB_REMOVE_DIR과 RVBT_DELETE_OBJECT_POSTPONE는 두 집합 모두에 속한다.

RCV_IS_LOGICAL_LOG (vpid, idx)는 마스터 테스트이자 주소를 검사하는 유일한 매크로다: ((vpid)->volid == NULL_VOLID) || ((vpid)->pageid == NULL_PAGEID) 조건이 참이면 인덱스에 무관하게 논리적으로 단락(short-circuit)된다; 이후 RCV_IS_BTREE_LOGICAL_LOG (idx); 그다음 열한 개 인덱스: RVBT_MVCC_INCREMENTS_UPD, RVPGBUF_FLUSH_PAGE, RVFL_DESTROY, RVFL_ALLOC, RVFL_DEALLOC, RVVAC_NOTIFY_DROPPED_FILE, RVPGBUF_DEALLOC, RVES_NOTIFY_VACUUM, RVHF_MARK_DELETED, RVFL_TRACKER_HEAP_REUSE, RVFL_TRACKER_UNREGISTER. 새 논리 인덱스가 여기에 빠지면 복구가 존재하지 않는 페이지를 fix하려 하다 — 버그가 배포된 한참 후에 롤백 도중 fix 오류로 나타난다.

flowchart TD
  A["레코드 vpid + rcvindex"] --> B{"volid 또는 pageid가 NULL?"}
  B -- 예 --> L["논리적: undofun은 pgptr = nullptr를 받음"]
  B -- 아니오 --> C{"RCV_IS_BTREE_LOGICAL_LOG?"}
  C -- 예 --> L
  C -- 아니오 --> D{"열한 개 인덱스 중 하나?"}
  D -- 예 --> L
  D -- 아니오 --> P["물리적: 드라이버가 페이지를 fix하고 pgptr 전달"]

Figure 1-2 — undo/롤백 드라이버가 평가하는 RCV_IS_LOGICAL_LOG.

  1. LOG_RCV는 유일한 호출 규약이다; data와 pgptr은 빌림(loan)이므로 네 가지 복사/이동 연산이 모두 삭제된다.
  2. RV_fun[]은 추가 전용(append-only) LOG_RCVINDEX로 인덱싱된다; 디버그 시작 시 rv_check_rvfuns는 순서 오류만 잡고 범위 초과 조회는 방어하지 않는다.
  3. Compensate 레코드는 undofun으로 redo된다(log_rv_get_fun<LOG_REC_COMPENSATE>): CLR의 redo는 원래 undo를 다시 수행하는 것이다.
  4. log_rv_redo_context (reader + 두 개의 zip 버퍼 + 동결된 m_end_redo_lsa; 복사 시 신선한 버퍼 재생성; NORMAL fetch 모드만 사용)는 불변 log_rv_redo_rec_info<T> 스냅샷을 공급한다; 디버그 전용 vpid_lsa_consistency_check는 페이지 단위 모든 후속 적용이 첫 번째 적용 LSA보다 높아야 함을 assert한다 — 로그 순서의 필요 조건이지, 완전한 쌍별 단조성은 아니다.
  5. 분석 상태 = LOG_RCV_TDES (다섯 개 LSA), LOG_REC_CHKPT + LOG_INFO_CHKPT_TRANS + LOG_INFO_CHKPT_SYSOP에서 초기화됨 (두 개의 sysop 앵커만 영속; 트랜잭션 수준 postpone은 트랜잭션 항목에 전달됨), LOG_RECVPHASE로 게이트됨.
  6. RCV_IS_* 매크로는 물리적 vs 논리적, 자동 vs 수동을 구분한다; RCV_IS_LOGICAL_LOG에서 누락된 새 논리 인덱스는 기능이 배포된 훨씬 뒤에 롤백을 깨뜨린다.

Chapter 2: 재시작 진입점과 로그 페이지 접근

섹션 제목: “Chapter 2: 재시작 진입점과 로그 페이지 접근”

서버 시작 시 리커버리를 누가 주도하는지, 체크포인트 앵커를 어떻게 찾고 미디어 크래시 시 어떻게 다운그레이드하는지, 그리고 각 패스(3장, 6장, 9장)가 로그 페이지를 물리적으로 어떻게 읽는지를 다룬다. 이론적 배경은 companion 문서 cubrid-recovery-manager.md를 참고하라.

2.1 log_recovery — 재시작 오케스트레이터, 분기별 분석

섹션 제목: “2.1 log_recovery — 재시작 오케스트레이터, 분기별 분석”

log_recovery(log_recovery.c)의 호출자는 log_initialize_internal 하나이며, init_emergency == false && (log_Gl.hdr.is_shutdown == false || ismedia_crash == true) 조건을 만족할 때만 진입한다(restoredb는 ismedia_crash를 전달하고, 긴급 시작은 리커버리를 건너뛴다). 호출자는 로그 CS를 쓰기 모드로 보유한 상태여야 한다(assert (LOG_CS_OWN_WRITE_MODE)).

// log_recovery -- src/transaction/log_recovery.c
/* ... condensed: branch 1 -- NULL LOG_FIND_TDES is er_set + logpb_fatal_error, return ... */
rcv_tdes->state = TRAN_RECOVERY; /* <- the recovery "transaction" */
if (LOG_HAS_LOGGING_BEEN_IGNORED ())
{ /* <- branch 2: fatal, then clear the flag */
/* ... condensed ... */
}
/* ... condensed ... */
LSA_COPY (&rcv_lsa, &log_Gl.hdr.chkpt_lsa);
if (ismedia_crash != false)
{ /* <- branch 3a: downgrade anchor */
(void) fileio_map_mounted (thread_p,
(bool (*)(THREAD_ENTRY *, VOLID, void *)) log_rv_find_checkpoint, &rcv_lsa);
}
/* ... condensed: else, branch 3b -- if (stopat != NULL) *stopat = -1 ... */
vacuum_notify_server_crashed (&rcv_lsa);

분기 1과 2는 logpb_fatal_error로 종료된다. 분기 2는 LOG_HAS_LOGGING_BEEN_IGNORED()(log_impl.h)가 log_Gl.hdr.has_logging_been_skipped를 감지할 때 발동하며, 로깅이 건너뛰어진 상태에서 크래시가 발생하면 복구가 불가능하다(ER_LOG_CORRUPTED_DB_DUE_CRASH_NOLOGGING). 분기 3a: 복원된 볼륨이 헤더 체크포인트보다 오래되었을 수 있으므로, log_rv_find_checkpoint를 모든 볼륨에 걸쳐 순회하면서 LSA_ISNULL (rcv_lsa) || LSA_LT (&chkpt_lsa, rcv_lsa) 조건이 맞으면 해당 볼륨의 disk_get_checkpoint LSA를 rcv_lsa에 복사한다. 반환값은 항상 true여서 전 볼륨을 방문하고, 가장 오래된 체크포인트가 최종 앵커가 된다. vacuum_notify_server_crashed는 rcv_lsa를 vacuum_Data.recovery_lsa에 복사해, 분석 단계에서 MVCC op 레코드를 찾지 못했을 때 vacuum의 역방향 스캔 시작점으로 쓰인다.

불변 조건 — 분석 시작 LSA는 영구 볼륨에 기록된 체크포인트보다 새로울 수 없다. 볼륨 헤더는 마지막 flush 시점의 체크포인트 LSA를 저장하며, 재플레이는 반드시 그 시점 이전부터 시작해야 한다. 그렇지 않으면 복원된 볼륨이 받지 못한 업데이트를 redo가 건너뛰게 된다. Figure 2-1은 이후 흐름을 정리한다.

flowchart TD
  A["분석 Ch 3"] --> B["logpb_fetch_start_append_page<br/>오류: fatal 종료"]
  B --> C{did_incom_recovery}
  C -->|false| D["EOF back_lsa 기반으로 LOG_RESET_PREV_LSA"] --> G
  C -->|true| G["LOG_DUMMY_CRASH_RECOVERY 추가<br/>rcv_phase_lsa = tail_lsa"]
  G --> H["REDO Ch 6, UNDO Ch 9<br/>log_system_tdes::rv_final"] --> K{did_incom_recovery}
  K -->|true| L["log_recovery_notpartof_volumes"] --> N
  K -->|false| N["TRAN_ACTIVE, logtb_set_num_loose_end_trans"]
  N --> O{2PC 미완료 항목}
  O -->|yes| P["FINISH_2PC: log_2pc_recovery Ch 11"] --> R
  O -->|no| R["logpb_decache_archive_info<br/>CS 해제, logpb_checkpoint, CS 재진입"]
  R --> S["전체 flush + 헤더, locator_initialize,<br/>heap_classrepr_restart_cache — 각 오류: fatal"]

Figure 2-1: 앵커 확정 이후 log_recovery의 흐름.

섹션 2.5는 어펜드 포인트 재설정을 다루며, log_append_empty_record가 LOG_DUMMY_CRASH_RECOVERY를 기록하면 그 LSA가 log_Gl.rcv_phase_lsa가 되어 undo(9장)의 크래시 경계 기준점이 된다. stopat 컷오프가 설정되면 did_incom_recovery가 세워지고(3장), 이후 log_recovery_notpartof_volumes가 복원 시점 이후에 생성된 볼륨을 제거한다. 마무리 단계에서는 logpb_checkpoint 호출을 전후로 로그 CS에서 잠깐 빠져나왔다가 재진입하고, 더티 페이지와 헤더를 flush하며, 카탈로그 트래커와 클래스 표현을 다시 캐싱한다 — 여기서도 두 개의 fatal 분기가 있다. 호출자는 이후 LOG_RESTARTED를 설정한다.

log_Gl.rcv_phase(enum log_recvphase, log_impl.h)는 전역 리커버리 모드를 나타내며, LOG_ISRESTARTED()는 LOG_RESTARTED 상태인지를 검사한다. 이 값은 위의 패스들이 끝난 뒤 호출자가 설정한다. logpb_copy_page는 !LOG_ISRESTARTED()일 때만 리커버리 캐시를 채우고, 물리 리더는 LOG_RESTARTED 상태에서만 디버그 체크섬 검사를 수행한다 — 리커버리 중에 발생하는 torn tail은 논리적으로 복구된다(섹션 2.7).

2.3 logpb_fetch_page — 단일 물리 읽기 진입점

섹션 제목: “2.3 logpb_fetch_page — 단일 물리 읽기 진입점”

logpb_fetch_page(log_page_buffer.c)는 enum log_cs_access_mode(log_impl.h)를 인자로 받는다. 분석과 undo 스캔의 고전적 방식은 LOG_CS_FORCE_USE로 호출한다(이 패스들은 log_recovery가 보유한 로그 CS 안에서 실행된다). 반면 redo 머신러리의 log_reader는 LOG_CS_SAFE_READER를 전달하여 위치 기반 fetch가 CS를 잡지 않도록 한다(섹션 2.6).

// logpb_fetch_page -- src/transaction/log_page_buffer.c
if (LSA_LE (&append_lsa, req_lsa) /* <- case 1: page beyond flushed area */
|| LSA_LE (&append_prev_lsa, req_lsa)) /* <- case 2: page may hold a temp EOL */
{
LOG_CS_ENTER (thread_p);
/* ... condensed ... */
if (LSA_LE (&log_Gl.hdr.append_lsa, req_lsa)) /* retry with mutex */
{
logpb_prior_lsa_append_all_list (thread_p); /* <- drain prior list to buffers */
}
LOG_CS_EXIT (thread_p);
}
rv = logpb_copy_page (thread_p, req_lsa->pageid, access_mode, log_pgptr);
/* ... condensed: rv != NO_ERROR is the only error exit ... */

앞단 게이트는 인메모리 prior-LSA 리스트를 페이지 버퍼로 드레인하여, 어펜드 포인트 근처에서 읽는 리더가 낡은 tail을 보지 않도록 보장한다. 이후 logpb_copy_page는 네 가지 경로로 분기한다: LOGPB_HEADER_PAGE_ID 요청은 캐시된 header_buffer에서 처리하고(캐시 미스 시 파일 읽기), 버퍼 인덱스가 범위를 벗어나면 ER_LOG_PAGE_CORRUPTED를 발생시키며, 버퍼 히트 시에는 memcpy 후 log_bufptr->pageid를 재검증한다 — safe-reader 모드는 읽기 CS를 잡지 않으므로 이 재검증이 lock-free 유효성 검사 역할을 한다. 나머지 경우는 logpb_read_page_from_file로 떨어지며, !LOG_ISRESTARTED() 동안은 페이지를 전진 방향으로만 캐싱한다.

2.4 활성 로그 대 아카이브: logpb_read_page_from_file

섹션 제목: “2.4 활성 로그 대 아카이브: logpb_read_page_from_file”

페이지 ID가 아카이브에 속하는지는 LOGPB_IS_ARCHIVE_PAGE(pageid)로 판단한다 — 헤더 페이지가 아니면서 LOGPB_NEXT_ARCHIVE_PAGE_ID(log_Gl.hdr.nxarv_pageid)보다 작을 때이다. logpb_is_page_in_archive가 이를 래핑한다. LOG_CS_SAFE_READER 모드는 읽기 CS를 직접 잡고(log_csect_entered 설정), 다른 모드는 assert (LOG_CS_OWN)로 이미 CS를 보유하고 있음을 확인한다. CS는 아카이브 집합을 보호한다 — 읽기 도중 아카이브가 새로 생성되면 logpb_to_physical_pageid가 stale해지는 버그가 실제로 있었으며, 코드 주석에도 기록되어 있다.

// logpb_read_page_from_file -- src/transaction/log_page_buffer.c
bool fetch_from_archive = logpb_is_page_in_archive (pageid);
if (fetch_from_archive)
{
bool is_archive_page_in_active_log = (pageid + LOGPB_ACTIVE_NPAGES) > log_Gl.hdr.append_lsa.pageid;
bool dont_fetch_archive_from_active = !LOG_ISRESTARTED () || log_Gl.hdr.was_active_log_reset;
if (is_archive_page_in_active_log && !dont_fetch_archive_from_active)
{
fetch_from_archive = false; /* <- slot not yet lapped in circular active file */
}
}

단축 경로의 원리: 활성 로그 파일은 LOGPB_ACTIVE_NPAGES(= log_Gl.hdr.npages) 슬롯의 원형 구조이므로, 아카이빙된 페이지라도 해당 슬롯이 다시 사용되기 전까지는 활성 파일에서 읽을 수 있다 — 단, 리커버리 중이거나 활성 로그가 리셋된 경우에는 비활성화된다. 이때의 활성 tail은 크래시가 만들어낸 상태 그대로이기 때문이다.

나머지 경로: 아카이브 fetch(logpb_fetch_from_archive)가 NULL을 반환하면 goto error로 처리한다. 활성 파일 fetch는 logpb_to_physical_pageid로 슬롯을 매핑한 뒤 fileio_read를 호출하며(ER_LOG_READ, goto error), 셀프 ID 검사에서 hdr.logical_pageid == pageid이면 정상이고 이후 암호화된 경우 tde_decrypt_log_page를 수행한다(아카이브는 logpb_fetch_from_archive 내부에서 복호화). == pageid + LOGPB_ACTIVE_NPAGES이면 검사 사이에 랩어라운드가 발생한 것이므로 아카이브에서 재시도하고, 그 외는 ER_LOG_PAGE_CORRUPTED로 처리한다. 두 종료 경로 모두 log_csect_entered가 세워져 있으면 CS를 해제하며, 디버그 체크섬은 LOG_RESTARTED 상태에서만 수행된다.

불변 조건 — 모든 로그 페이지는 자신을 식별한다. 활성 파일 읽기는 hdr.logical_pageid가 요청한 pageid와 일치할 때만 유효하다. 유일하게 허용되는 불일치는 정확히 한 바퀴 랩어라운드된 경우(pageid + LOGPB_ACTIVE_NPAGES)이며, 이 검사가 없으면 랩어라운드된 슬롯이 이전 페이지로 재플레이될 것이다.

2.5 logpb_fetch_start_append_page — 어펜드 포인트 재설정

섹션 제목: “2.5 logpb_fetch_start_append_page — 어펜드 포인트 재설정”

분석과 redo 사이에 로그는 다시 쓰기 가능 상태가 되어야 한다. 네 가지 분기가 있다: 비어 있는 로그(append_lsa offset 0, pageid 0 — 디버그 빌드: PRM_ID_FIRST_LOG_PAGEID)는 logpb_locate_page에 OLD_PAGE 대신 NEW_PAGE를 요청하고, 남아 있는 log_Gl.append.log_pgptr는 버리며(logpb_invalid_all_append_pages), logpb_locate_page가 NULL을 반환하면 유일한 오류 종료(ER_FAILED, log_recovery에서 fatal 처리)이고, 성공 시에는 set_nxio_lsa(log_Gl.hdr.append_lsa)를 기록하고 해당 페이지를 flush_info->toflush에 추가하여 배열이 가득 차면 flush한다(logpb_flush_pages_direct).

2.6 log_reader — redo 머신러리를 위한 C++ fetch 래퍼

섹션 제목: “2.6 log_reader — redo 머신러리를 위한 C++ fetch 래퍼”

현대적인 redo 경로(6장, 7장)는 log_reader(log_reader.hpp, final 클래스, 헤더 전용)를 사용한다. 형제 파일인 log_reader.cpp는 낡은 파일로 CMakeLists에 포함되지 않는다.

필드역할존재 이유
m_thread_entry지연 캐시된 THREAD_ENTRY *단일 스레드 계약, 매 사용 시 assert
m_lsa읽기 위치; 초기값 NULL_LSAfetch pageid, 페이지 내 offset, 메모이제이션을 결정
m_cs_accesslogpb_fetch_page에 전달되는 모드; 기본값 LOG_CS_FORCE_USECS 보유 패스 vs CS 없는 리더 (LETS 포트 잔재)
m_page생성자에서 m_area_buffer에 정렬된 log_page *전용 fetch 목적지 — 공유 버퍼 락 불필요
m_area_bufferchar [IO_MAX_PAGE_SIZE + DOUBLE_ALIGNMENT]인라인 힙 불필요 저장소; 복사된 워커는 각자 보유

set_lsa_and_fetch_page는 do_fetch_page { fetch_page_mode == fetch_mode::FORCE || m_lsa.pageid != lsa.pageid }를 계산하고, m_lsa = lsa를 할당한 뒤 해당 조건이 참일 때만 fetch(logpb_fetch_page(.., m_cs_access, m_page), 실패 시 fatal)를 수행한다: NORMAL 모드는 현재 페이지를 메모이제이션하고, FORCE 모드는 항상 재fetch한다. align, add_align, advance_when_does_not_fit, copy_from_log은 고전적인 LOG_READ_ALIGN 패밀리와 logpb_copy_from_log(동일 헤더 하단)에 위임하며, 페이지 경계에서 재fetch한다 — 단, m_cs_access를 전달하는 것은 fetch_page(set_lsa_and_fetch_page 및 skip 경유)뿐이며, 위임 멤버들은 패밀리 기본값인 LOG_CS_FORCE_USE를 사용하므로 safe reader라도 레코드 중간 페이지 경계에서 잠깐 읽기 CS를 잡는다.

소유 애그리게이트 log_rv_redo_context(log_recovery_redo.hpp):

필드역할존재 이유
m_readerlog_reader { LOG_CS_SAFE_READER }컨텍스트별 전용 리더; CS 없는 위치 기반 fetch
m_redo_zip, m_undo_zipLOG_ZIP 스크래치 버퍼레코드 간 재사용되는 압축 해제 대상 (섹션 2.8)
m_end_redo_lsaconst LOG_LSA redo 종료 경계워커가 전역 변수 접근 없이 레코드 LSA를 비교
m_reader_fetch_page_modeconst log_reader::fetch_modeNORMAL은 페이지 메모이제이션; FORCE는 재사용 목적 유지

동기 redo 드라이버는 fetch_mode::NORMAL로 생성하고, 복사 생성자는 메인 생성자를 재실행하여 각 병렬 워커(7장)가 독립적인 버퍼를 갖도록 한다. 생성자는 두 zip 버퍼를 LOGAREA_SIZE로 미리 할당하며, 소멸자는 해제한다(log_zip_free_data).

2.7 불완전하게 아카이빙된 레코드를 위한 NULL_OFFSET 규약

섹션 제목: “2.7 불완전하게 아카이빙된 레코드를 위한 NULL_OFFSET 규약”

NULL_OFFSET은 (-1)이다(storage_common.h). 아카이버가 마지막 레코드가 다음 페이지로 이어지는 활성 페이지를 복사할 때, 그 연속 부분을 가리키는 LSA의 offset이 NULL_OFFSET을 가질 수 있다: 레코드 완성이 아카이빙 이후에 이루어졌기 때문이다. 분석, redo, log_startof_nxrec 같은 워커를 포함한 모든 전진 스캔은 역참조 전에 이를 복구해야 한다:

// log_recovery_analysis (record loop) -- src/transaction/log_recovery.c
if (lsa.offset == NULL_OFFSET)
{
lsa.offset = log_page_p->hdr.offset; /* <- page's first record offset */
if (lsa.offset == NULL_OFFSET)
{
/* Continue with next pageid */
if (logpb_is_page_in_archive (log_lsa.pageid))
{
lsa.pageid = log_lsa.pageid + 1; /* <- archive: keep walking */
}
else
{
lsa.pageid = NULL_PAGEID; /* <- active: stop scan */
}
continue;
}
}

hdr.offset 자체도 NULL_OFFSET인 페이지는 레코드 시작점이 없는 순수 연속 페이지다 — 아카이브에서는 다음 페이지로 진행하고, 활성 로그에서는 스캔을 종료한다. 분석용 스크래치 페이지는 hdr.offset = NULL_OFFSET으로 초기화된다.

2.8 각 패스가 인스턴스화하는 LOG_ZIP 할당 헬퍼

섹션 제목: “2.8 각 패스가 인스턴스화하는 LOG_ZIP 할당 헬퍼”

LOG_ZIP(struct log_zip, log_compress.h)은 grow-only 버퍼다:

필드역할존재 이유
data_length현재 저장된 바이트 수소비자는 정확히 이 크기만큼 읽는다; 용량은 더 클 수 있다
buf_sizelog_data의 용량grow-only 검사 기준; LZ4 최악 케이스에 맞춰 크기 결정
log_data버퍼(char *)레코드 간 재사용; 첫 크기 설정 전까지 nullptr

log_zip_realloc_if_needed(log_zip, new_size)(log_compress.c)는 new_size > 0 && new_size > log_zip.buf_size일 때만 LOG_ZIP_BUF_SIZE(LZ4, new_size)로 확장한다(실패 시 ER_OUT_OF_VIRTUAL_MEMORY). 두 번째 검사인 new_size > 0 && log_zip.log_data == nullptr는 필드를 초기화하고 false를 반환하며(호출자가 fatal 처리), true는 성공 또는 확장 불필요를 의미한다. log_zip_alloc은 구조체를 malloc + 초기화하고 동일한 방식으로 크기를 설정한다(실패 시 nullptr 반환, husk 해제). log_zip_free는 log_zip_free_data를 실행한 뒤 구조체를 해제한다. redo 컨텍스트는 두 zip 버퍼를 미리 할당하고(섹션 2.6), undo 패스는 log_zip_alloc(LOGAREA_SIZE)로 undo_unzip_ptr를 할당하여 log_recovery_undo의 모든 종료 경로에서 해제한다. 공유 소비자 log_rv_get_unzip_log_data는 ZIP_CHECK(length)로 압축 여부를 구분하여 log_zip_realloc_if_needed 이후 log_unzip 또는 memcpy를 선택한다.

  1. log_recovery는 이미 쓰기 모드로 획득된 로그 CS 아래에서 TRAN_RECOVERY 시스템 트랜잭션으로 실행된다. 호출자는 오직 log_initialize_internal뿐이며, 긴급 시작은 리커버리를 건너뛴다.
  2. 분석 앵커는 log_Gl.hdr.chkpt_lsa이며, 미디어 크래시 시 log_rv_find_checkpoint가 찾은 볼륨별 최고참 체크포인트로 다운그레이드된다.
  3. 분석과 redo 사이에 어펜드 포인트가 재설정되고 LOG_DUMMY_CRASH_RECOVERY가 추가된다. 그 LSA(log_Gl.rcv_phase_lsa)가 undo의 크래시 경계 기준점이 된다.
  4. 고전적인 분석·undo 스캔은 보유 중인 로그 CS 아래에서 LOG_CS_FORCE_USE로 fetch하고, log_reader는 위치 기반 fetch에 LOG_CS_SAFE_READER를 전달한다. 단, 페이지 경계를 넘는 헬퍼는 여전히 LOG_CS_FORCE_USE를 기본값으로 사용하여 읽기 CS를 잠깐 잡는다.
  5. logpb_read_page_from_file은 LOGPB_IS_ARCHIVE_PAGE를 기준으로 활성 로그와 아카이브를 분리한다. 셀프 ID 불일치 중 유일하게 허용되는 케이스는 한 바퀴 랩어라운드된 경우(pageid + LOGPB_ACTIVE_NPAGES)이며, 아카이빙되었지만 활성 파일에서도 읽을 수 있는 단축 경로는 리커버리 중에 비활성화된다.
  6. NULL_OFFSET(-1)은 불완전하게 아카이빙된 레코드의 LSA를 표시한다. 모든 전진 스캔은 hdr.offset으로 복구하며, 아카이브에서는 한 페이지 전진하고 활성 로그에서는 스캔을 종료한다.

log_recovery_analysis는 체크포인트 앵커에서 시작해 손상되거나 잘린 로그를 순방향으로 순회하며 redo 범위를 계산한다. 구조는 페이지 페치(fetch) 외부 루프와 레코드 스텝 내부 루프의 2단 중첩이다. 레코드 의미론은 log_rv_analysis_record(4–5장)가 담당하고, 드라이버는 커서 전진·손상 방어·잘림-또는-치명 분기·redo 범위 갱신을 책임진다. ARIES 이론적 배경은 cubrid-recovery-manager.md의 복구 단계 절을, 페이지 페치 메커니즘은 2장을 참고하라.

3.1 진입점, 출력값, 드라이버 상태

섹션 제목: “3.1 진입점, 출력값, 드라이버 상태”

log_recovery는 앵커를 결정한다 — 일반 재시작에서는 log_Gl.hdr.chkpt_lsa, 미디어 크래시(media crash)에서는 데이터 볼륨 헤더들 가운데 가장 오래된 체크포인트(log_rv_find_checkpoint) — 를 start_lsa로 넘기면서, is_media_crash(잘림 vs 치명, 3.2)와 stop_at(restoredb -d 경계, 3.7)을 함께 전달한다. 출력: start_redo_lsa(앵커; 4장이 이전으로 당길 수 있음), end_redo_lsa(불변 조건 3-B), did_incom_recovery(잘림 발생; log_recovery가 EOF 백링크 수정 생략), num_redo_log_records(3.8).

log_recovery_analysis의 핵심 드라이버 지역 변수:

지역 변수역할이유
lsa다음 레코드; NULL이면 두 루프 모두 종료단일 종료 조건
log_lsa현재 레코드, log_page_p에 로드된 페이지lsa가 디스패치 전에 먼저 전진 (3.6)
prev_lsa마지막 정상 레코드resetlog 목표 위치
prev_prev_lsaresetlog의 new_prev_lsaprev_lsa를 추적; 첫 페치 페이지가 깨진 경우에만 NULL
first_corrupted_rec_lsa첫 번째 전체-0xff 4 KB 블록레코드 단위 컷오프 (3.5)
last_checked_page_id이미 체크섬 검증을 마친 페이지페이지당 한 번만 탐색 (3.3)
is_log_page_broken페치 실패 또는 레코드 꼬리 누락잘림-또는-치명 분기 (3.2)
is_log_page_corrupted읽기는 됐으나 체크섬 실패부분 플러시 (3.5); 종단 조건 (불변 조건 3-C)
null_blockLOG_PAGE_INIT_VALUE(0xff, log_common_impl.h)로 채워진 4 KB티어(tear) 탐지용 memcmp 피연산자
checkpoint_lsaLOG_END_CHKPT 디스패치(4장)에서 설정2PC 꼬리 재독 (3.8)
may_use_checkpoint / may_need_synch_checkpoint_2pc디스패치 플래그 (4장)두 번째 플래그가 2PC 꼬리를 활성화
last_at_time드라이버에서는 -1을 유지*stop_at으로의 에코는 현재 비활성 (3.7)

초기화 단계에서는 start_lsa를 lsa, start_redo_lsa, end_redo_lsa, prev_lsa에 복사하며 — 검증 전까지의 퇴화된 redo 범위 — 나머지는 모두 null 또는 0으로 설정한다.

3.2 외부 루프: is_log_page_broken 분기

섹션 제목: “3.2 외부 루프: is_log_page_broken 분기”

외부 루프는 매 반복마다 커서 위치의 페이지를 logpb_fetch_page로 가져온다. 플러시된 로그 끝을 넘어감, 아카이브 누락, TDE 복호화 오류 등 모든 실패가 is_log_page_broken을 세우며, 내부 루프의 꼬리 누락 break(3.4) 역시 마찬가지다. 하나의 분기가 “broken”의 의미를 결정한다.

미디어 크래시 경로: 잘림 후 수락 — 복원 지점 이후의 로그는 정상적으로 존재하지 않을 수 있다. last_at_time을 *stop_at으로 에코하고, 마지막 레코드 소유자 tdes의 tail_lsa/undo_nxlsa를 log_rec->prev_tranlsa로 되돌려 undo가 잘린 레코드를 따라가지 않도록 한다. 그다음 prev_lsa 페이지를 재페치(실패한 페치가 페이지를 덮어씌웠으므로; 재실패 시 치명)하고, log_recovery_resetlog (thread_p, &prev_lsa, &prev_prev_lsa)로 prev_lsa를 새 추가(append) 지점으로 만든다(11장). *did_incom_recovery를 세우고, MVCC 테이블을 초기화한 뒤 반환 — 2PC 꼬리(3.8)는 건너뛴다. 일반 크래시 경로: 치명 — 단순 크래시 후에는 eof_lsa까지의 모든 페이지가 읽힐 수 있어야 한다. er_errid ()가 ER_TDE_CIPHER_IS_NOT_LOADED이면 메시지는 TDE를 명시한다: 페이지 자체는 온전하지만 복호화 불가.

불변 조건 3-B (redo 범위 정직성). 반환 시, [start_redo_lsa, end_redo_lsa) 내 모든 레코드는 읽을 수 있고 구조적으로 완전하다; 경계 자체는 마지막으로 완전히 탐색된 레코드이거나, redo가 해당 위치를 읽기 전에 재초기화된 위치이다. 정상 종료: 마지막 디스패치된 레코드. 잘림(3.6 8단계): prev_lsa로 되돌림. 깨진 레코드 탐색(3.4): 의도적으로 깨진 레코드 위치로 전진 — prev_lsa와 동일 — 하여 resetlog가 해당 위치를 새 추가 지점으로 삼고, redo 실행 전에 LOG_DUMMY_CRASH_RECOVERY로 덮어쓴다. 위반 시: redo(6장)가 반쪽짜리 본문을 적용하게 된다.

flowchart TD
  A["lsa 위치의 페이지 페치"] --> B{"broken?"}
  B -- 아니오 --> C["내부 루프 3.3-3.6"] --> D{"lsa null?"}
  D -- 아니오 --> A
  D -- 예 --> E["2PC 꼬리; reset_start_mvccid"]
  B -- "예, 미디어 크래시" --> F["prev_lsa에서 resetlog; did_incom_recovery; 반환"]
  B -- "예, 일반 크래시" --> G["치명 (TDE 또는 일반)"]

Figure 3-1 — log_recovery_analysis의 외부 루프.

3.3 내부 루프 진입: NULL_OFFSET 복구와 손상 탐색

섹션 제목: “3.3 내부 루프 진입: NULL_OFFSET 복구와 손상 탐색”

내부 루프는 커서가 페치된 페이지에 머무는 동안 실행된다: while (!LSA_ISNULL (&lsa) && lsa.pageid == log_lsa.pageid). 레코드 접근 전에 두 가지 준비 단계가 선행된다.

NULL_OFFSET 복구. 불완전한 상태로 아카이브된 레코드는 연속 부분의 오프셋을 알 수 없게 만든다: 커서가 (pageid, NULL_OFFSET)으로 도착하면 이 페이지에서 시작하는 첫 번째 헤더인 log_page_p->hdr.offset으로 재앵커한다. 그 값도 NULL_OFFSET이면(이 페이지에는 연속 바이트만 존재): 아카이브 페이지라면 lsa.pageid = log_lsa.pageid + 1로 설정해 레코드 중간을 계속 걷고, 활성 페이지라면 lsa.pageid = NULL_PAGEID로 스캔을 종료한다. 두 경우 모두 continue.

페이지 단위 손상 탐색. last_checked_page_id로 가드되어 페이지당 한 번만 실행한다. logpb_page_check_corruption은 logpb_page_has_valid_checksum(CRC32 대 hdr.checksum)을 래핑한다; 헬퍼 오류는 치명적이다. 손상된 아카이브 페이지는 치명적(/* Should not happen. */ — 아카이브는 한 번만 기록됨); 손상된 활성 페이지는 부분 페이지 플러시를 의미한다. logpb_page_get_first_null_block_lsa가 티어 위치를 찾는다: null_block과 memcmp 결과가 같은 첫 번째 4 KB 블록이 (hdr.logical_pageid, i * block_size)를 산출하며, 비영(nonzero)일 때는 sizeof (LOG_HDRPAGE)를 빼는데 — LSA 오프셋은 area[]를 인덱싱하고 raw 페이지는 그보다 앞서 시작하기 때문이다.

어떤 블록도 매치되지 않으면(손상됐지만 모든 블록에 데이터가 있음), first_corrupted_rec_lsa는 NULL로 유지된다: 3.5의 컷오프와 그 안전망(!is_log_page_corrupted로 게이팅됨)은 건너뛰고, 불변 조건 3-C의 페이지-전진 금지와 EOF 정지만 적용된다.

3.4 다중 페이지 레코드: log_is_page_of_record_broken

섹션 제목: “3.4 다중 페이지 레코드: log_is_page_of_record_broken”

log_rec = LOG_GET_LOG_RECORD_HEADER (log_page_p, &log_lsa) 이후, 미디어 크래시 경로에서 추가 탐색을 한 번 더 수행한다 — 헤더가 마지막으로 복원된 페이지에 있고 본문이 복원되지 않은 페이지로 넘어가는 상황이 가능하기 때문이다:

// log_is_page_of_record_broken -- src/transaction/log_recovery.c
LSA_COPY (&fwd_log_lsa, &log_rec_header->forw_lsa);
/* TODO - Do we need to handle NULL fwd_log_lsa? */
if (!LSA_ISNULL (&fwd_log_lsa))
{
if (LSA_GE (log_lsa, &fwd_log_lsa)
|| (!LSA_ISNULL (&log_Gl.hdr.eof_lsa) && LSA_GT (&fwd_log_lsa, &log_Gl.hdr.eof_lsa)))
{
is_log_page_broken = true; /* <- forw_lsa is nonsense */
}
else
{
if (fwd_log_lsa.pageid != log_lsa->pageid
&& (fwd_log_lsa.offset != 0 || fwd_log_lsa.pageid > log_lsa->pageid + 1))
{
// ... condensed: record spans pages -- probe-fetch fwd_log_lsa page;
// failure -> broken ...
}
}
}

분기별 설명: (1) forw_lsa NULL — 판단 포기; 3.5 안전망이 대신 판단한다(TODO 주석이 이 공백을 인정). (2) forw_lsa가 현재 레코드 뒤에 있지 않거나, eof_lsa를 넘어섬 — 헤더 자체가 쓰레기: broken(eof_lsa는 NULL-guard됨: 활성 볼륨 없이 복원하면 분석 중에만 복구됨). (3) forw_lsa가 다음 페이지에서 비영 오프셋에 있거나, 두 페이지 이상 앞서 있음 — 본문이 해당 페이지까지 확실히 뻗음: 탐색 페치 수행; 실패 시 꼬리 누락, 성공 시 정상. 제외된 경우 — 다음 페이지 오프셋 0의 다음 레코드 — 는 아무것도 증명하지 못하므로 페치를 소비하지 않는다.

broken 판정이 나면 내부 루프는 end_redo_lsa = lsa를 복사하고, prev_lsa와 prev_prev_lsa를 그 위치로 설정한 뒤, 디버그 트레이스 후 break한다 — resetlog는 3.2에서 수행되고, 이때 prev_lsa는 깨진 레코드 자체다: resetlog는 그 위치에서 잘라 이전의 모든 것을 보존한다.

체크섬에 실패한 페이지는 레코드 단위로 해당 레코드가 티어 영역 이전에 있는지를 판단한다. 판정을 넓히는 두 가지 안전망이 먼저 적용된다(is_log_page_corrupted가 false인 동안에만): (1) end-of-log 누락 — 활성 로그에서 LOG_END_OF_LOG가 아닌 레코드의 forw_lsa가 NULL인 것은 불가능하다(모든 체인은 EOF 레코드로 끝남): 페이지를 손상으로 선언하고 null-block 스캔의 컷오프로 삼는다. (2) null 블록을 가로지르는 본문 — forw_lsa가 페이지 내에 머문다면, 레코드 시작과 forw_lsa - 1을 블록 인덱스로 매핑한다((offset + sizeof (LOG_HDRPAGE)) / block_size); 인덱스가 다르고 종료 블록이 null_block과 같으면, 본문이 완전히 플러시되지 않은 것: 컷오프를 레코드 자체로 설정한다.

컷오프가 non-NULL이면 세 가지 결과가 있다. 티어보다 엄격히 뒤에 있는 레코드는 이전의 정상 레코드에서 스캔을 종료한다:

// log_recovery_analysis -- src/transaction/log_recovery.c
if (LSA_GT (&log_lsa, &first_corrupted_rec_lsa))
{
LOG_RESET_APPEND_LSA (end_redo_lsa); /* <- starts past the tear */
LSA_SET_NULL (&lsa);
break;
}

else 분기는 log_lsa == first_corrupted_rec_lsa일 때, forw_lsa가 티어를 넘어 가리킬 때, 또는 헤더의 DB_ALIGN 정렬된 끝이 LOGAREA_SIZE를 초과하거나 티어를 지날 때 레코드를 손상으로 표시한다; 그다음 LOG_RESET_APPEND_LSA (&log_lsa) — 첫 번째 피해 레코드의 위치 자체가 새 추가 지점 — 가 lsa를 null로 만들고 break한다. 티어보다 확실히 앞에 있는 레코드는 정상적으로 처리된다.

불변 조건 3-C (손상은 페이지 단위 종단 조건). is_log_page_corrupted가 true가 된 이후, 커서는 절대로 다른 페이지로 전진하지 않는다. 전진 후 null 처리(손상됨, LOG_END_OF_LOG 아님, lsa.pageid != log_lsa.pageid → LSA_SET_NULL)와 LOG_END_OF_LOG 디스패치 후 정지로 강제된다. 이전 로그 랩(wrap)에서 재사용된 페이지에는 유효해 보이는 오래된 레코드가 남아 있을 수 있다; 그것을 따라가면 이전 에포크(epoch)를 다시 재생하게 된다.

3.6 커서 전진: 나머지 모든 분기

섹션 제목: “3.6 커서 전진: 나머지 모든 분기”

내부 루프 본문의 나머지 단계, 순서대로:

  1. end_redo_lsa = lsa; lsa = log_rec->forw_lsa — 범위 끝이 디스패치 전에 먼저 이동한다.
  2. 손상된 페이지의 페이지-전진 금지 (불변 조건 3-C).
  3. 아카이브 null-forward 수정: 아카이브 페이지에서 lsa가 NULL이면 → log_lsa.pageid + 1 — 불완전한 아카이빙이며 로그 끝이 아님.
  4. 루프 가드. lsa가 역방향 또는 제자리(lsa.pageid < log_lsa.pageid, 또는 같은 페이지에서 lsa.offset <= log_lsa.offset): “loop in the log” 디버그 트레이스, logpb_fatal_error, 그다음 LSA_SET_NULL (&lsa); break;.
  5. 누락된 EOF 복구. lsa NULL, log_rtype != LOG_END_OF_LOG, 아직 잘림 없음: 추가 LSA를 end_redo_lsa에 파킹; log_startof_nxrec으로 다음 레코드 시작을 찾으면 그리로 전진하고, 버퍼 내 log_rec->forw_lsa를 패치한 뒤 페이지를 플러시(logpb_write_page_to_disk) — 물리적 복구. 어느 경우든 log_Gl.hdr.next_trid = tran_id.
  6. Redo 카운팅. *num_redo_log_records가 열두 가지 redo 유형을 셈 — LOG_REDO_DATA, LOG_UNDOREDO_DATA, LOG_DIFF_UNDOREDO_DATA, 세 가지 LOG_MVCC_* 대응 타입, LOG_DBEXTERN_REDO_DATA, LOG_RUN_POSTPONE, LOG_COMPENSATE, LOG_2PC_PREPARE, LOG_2PC_START, LOG_2PC_RECV_ACK; 나머지는 모두 조용한 default.
  7. 디스패치. log_rv_analysis_record가 트랜잭션 상태를 재건한다(4장); LOG_END_OF_LOG case는 log_rv_analysis_log_end(3.8).
  8. 디스패치 후 잘림. *did_incom_recovery 상승(3.7): end_redo_lsa = prev_lsa — 트리거 레코드는 redo에서 제외; lsa null 처리 후 break.
  9. 자기 루프 assert. LSA_EQ (end_redo_lsa, &lsa) — 커서가 움직이지 않음: assert_release, NULL 커서로 스캔 중단.
  10. 손상된 페이지 + LOG_END_OF_LOG → 정지 (불변 조건 3-C의 후반부).
  11. prev_lsa = end_redo_lsa; prev_prev_lsa = prev_lsa; — resetlog 앵커가 끝점보다 한 레코드 뒤를 추적한다.
  12. Page-id 역채움. 앞으로 가리키는 (pageid, NULL_OFFSET)에 오래된 더 작은 pageid가 있으면 → 현재 페이지로 수정 (3.3의 복구와 쌍을 이룸).

불변 조건 3-A (단조 커서). 매 반복마다 커서 (pageid, offset)이 엄격하게 증가한다. 4단계와 9단계가 스캔을 종단시키는 방식으로 강제한다. 위반 시: 분석이 무한 루프에 빠진다.

3.7 시점 지정 정지: stop_at와 LOG_REC_DONETIME

섹션 제목: “3.7 시점 지정 정지: stop_at와 LOG_REC_DONETIME”

stop_at은 log_recovery에서 온다 — 일반 재시작에서는 -1(제한 없음), 미디어 크래시에서는 restoredb -d 타임스탬프. 드라이버 자체는 커밋 시각을 직접 읽지 않는다 — log_rv_analysis_complete가 LOG_COMMIT / LOG_ABORT를 처리하며(log_rv_analysis_commit_with_postpone도 LOG_REC_START_POSTPONE의 at_time에 동일 검사를 적용한다). 헤더 뒤 LOG_REC_DONETIME 페이로드를 읽어서, *stop_at != (time_t) (-1)이고 difftime (*stop_at, last_at_time) < 0 — 정지 지점 이후에 찍힌 첫 번째 완료 레코드 — 이면 페이지 커서를 null로 만들고, log_recovery_resetlog (thread_p, &record_header_lsa, prev_lsa)를 호출해 이 커밋 직전에서 로그를 자르며, *did_incom_recovery를 세운다; 3.6의 8단계가 해당 레코드를 제외하고 스캔을 종료한다. 이 last_at_time은 지역 변수이며, 3.2에서 *stop_at으로 에코되는 드라이버의 사본은 -1을 유지 — 현재 비활성.

3.8 log_rv_analysis_log_end, 2PC 재독 꼬리, 그리고 출력

섹션 제목: “3.8 log_rv_analysis_log_end, 2PC 재독 꼬리, 그리고 출력”

드라이버 스토리에 속하는 유일한 디스패치 case는 로그의 깔끔한 종료인 log_rv_analysis_log_end다 — logpb_is_page_in_archive로 한 번 분기한다. 활성 로그에서는 EOF 자체의 위치가 log_Gl.hdr.append_lsa가 된다(LOG_RESET_APPEND_LSA (log_lsa) — 새 추가가 EOF 레코드를 덮어씀), next_trid는 소유자로부터 복원되고, 커서는 EOF의 NULL forw_lsa를 취한다 — 두 루프 모두 종료된다(누락된 EOF 복구는 LOG_END_OF_LOG를 면제함). 아카이브 페이지의 EOF는 아카이빙 컷 이전의 구식 잔재다: 헤더는 건드리지 않고 NULL forward가 3.6 3단계를 통과하며 스캔이 계속된다.

2PC 재독 꼬리. 디스패치된 레코드 중에 may_need_synch_checkpoint_2pc를 세운 것이 있으면(체크포인트 시점에 2PC 상태인 트랜잭션을 나열하는 LOG_REC_CHKPT — 4장), 드라이버는 외부 루프 이후 체크포인트 레코드를 재독한다: (1) checkpoint_lsa에서 logpb_fetch_page; 실패 시 치명; (2) chkpt.ntrans 항목으로 이루어진 LOG_INFO_CHKPT_TRANS 배열, log_lsa.offset + size < LOGAREA_SIZE이면 in-page 읽기, 아니면 malloc + logpb_copy_from_log(malloc 실패: 치명); (3) 각 chkpt_trans[i].trid를 logtb_find_tran_index로 resolve; log_2pc_recovery_analysis_info는 아직 LOG_ISTRAN_2PC인 tdes에 대해서만 실행. 3.2의 미디어 크래시 경로는 이 꼬리 이전에 반환 — 잘린 복원에서는 2PC 재구성을 건너뛴다.

log_recovery는 그다음 범위와 카운트에서 ER_LOG_RECOVERY_REDO_STARTED를 방출한다; log_cnt_pages_containing_lsa는 *to_lsa == *from_lsa이면 0을, 아니면 포함적인 to_lsa->pageid - from_lsa->pageid + 1을 반환한다. 앵커 이후에 살아남은 것이 아무것도 없으면, end_redo_lsa는 초기화 값인 start_redo_lsa와 동일한 채로 — 카운트는 정직하게 0이다.

  1. log_recovery_analysis는 페이지 페치 외부 루프와 레코드 스텝 내부 루프의 2단 구조다; 손상 판단은 드라이버가, 레코드 의미론은 log_rv_analysis_record(4–5장)가 담당한다.
  2. 페이지 broken 판정은 is_media_crash로 분기한다: 백업 복원은 prev_lsa에서 log_recovery_resetlog로 잘리고 did_incom_recovery를 세우며, 일반 재시작은 치명적 오류다(ER_TDE_CIPHER_IS_NOT_LOADED는 “TDE 키를 로드하라”는 뜻).
  3. 부분 페이지 플러시는 페이지당 한 번의 CRC 검사로 감지하며; 티어는 첫 번째 전체-0xff 4 KB 블록이고, first_corrupted_rec_lsa가 세 가지 레코드 단위 결과로 스캔을 컷오프한다. 손상된 페이지는 종단 조건이다(불변 조건 3-C).
  4. log_is_page_of_record_broken(미디어 크래시 전용)은 forw_lsa의 타당성을 검증하고 다중 페이지 레코드의 마지막 페이지를 탐색 페치한다; 꼬리가 누락되면 end_redo_lsa와 prev_lsa를 깨진 레코드 위에 파킹하여 resetlog가 그 위치에서 자른다.
  5. redo 범위는 정직하다(불변 조건 3-B): end_redo_lsa 앞의 모든 것은 읽을 수 있고 완전하며; 경계는 완전히 탐색됐거나, redo가 해당 위치를 읽기 전에 재초기화된다.
  6. 시점 지정 복원은 log_rv_analysis_complete(LOG_REC_DONETIME)에 있고 드라이버에는 없다; 분석은 읽기 전용도 아니다 — 누락된 LOG_END_OF_LOG는 log_startof_nxrec, forw_lsa 패치, 페이지 플러시로 물리적 복구된다.

Chapter 4: 분석 레코드 디스패치와 트랜잭션 테이블 재구성

섹션 제목: “Chapter 4: 분석 레코드 디스패치와 트랜잭션 테이블 재구성”

Chapter 3의 드라이버는 읽어 들인 모든 LOG_RECORD_HEADER를 log_rv_analysis_record로 전달한다. 이 챕터에서는 각 분기가 트랜잭션 테이블 상태를 어떻게 재구성하는지, 그리고 append point, next TRANID, MVCCID horizon 같은 전역 카운터가 어떻게 함께 갱신되는지를 추적한다. postpone/sysop 분기는 Chapter 5에서, ARIES 이론은 cubrid-recovery-manager.md에서 다룬다.

4.1 log_rv_analysis_record의 dispatch switch

섹션 제목: “4.1 log_rv_analysis_record의 dispatch switch”

log_rv_analysis_record는 순수한 역다중화기(demultiplexer)다: switch (log_type) 하나만 있을 뿐 자체 로직은 없고, 포인터 파라미터는 모두 Chapter 3 드라이버 루프의 상태에 속한다. 모든 LOG_RECTYPE은 정확히 하나의 분기로 떨어진다:

레코드 타입핸들러테이블에 미치는 효과
LOG_UNDOREDO_DATA, LOG_DIFF_UNDOREDO_DATA, LOG_UNDO_DATA, LOG_REDO_DATA, 그리고 그것들의 LOG_MVCC_* 쌍 네 개, LOG_DBEXTERN_REDO_DATAlog_rv_analysis_undo_redotail_lsa + undo_nxlsa 전진 (4.3)
LOG_SAVEPOINTlog_rv_analysis_save_point위와 동일, savept_lsa 추가 (4.3)
LOG_COMPENSATElog_rv_analysis_compensate이미 undo된 작업을 건너뛰도록 undo_nxlsa 재설정 (4.3)
LOG_COMMIT, LOG_ABORTlog_rv_analysis_completetran index 해제, 또는 분석 조기 종료 (4.4)
LOG_2PC_* 7종log_rv_analysis_2pc_* 7개 분기2PC tdes->state 스탬핑 (4.5)
LOG_START_CHKPT / LOG_END_CHKPTlog_rv_analysis_start_checkpoint / _end_checkpointmay_use_checkpoint 활성화 (4.7) / 스냅샷 병합 (4.8)
LOG_DUMMY_HEAD_POSTPONE, LOG_POSTPONE, LOG_RUN_POSTPONE, LOG_COMMIT_WITH_POSTPONE (+_OBSOLETE), LOG_SYSOP_START_POSTPONE, LOG_SYSOP_END, LOG_SYSOP_ATOMIC_START대응하는 log_rv_analysis_* postpone/sysop 분기Chapter 5; commit-with-postpone의 조기 종료 분기는 4.4와 동일
LOG_END_OF_LOGlog_rv_analysis_log_endappend point + next_trid 재설정 (4.10)
LOG_DUMMY_CRASH_RECOVERY, LOG_REPLICATION_DATA, LOG_REPLICATION_STATEMENT, LOG_DUMMY_HA_SERVER_STATE, LOG_DUMMY_OVF_RECORD, LOG_DUMMY_GENERIC, LOG_SUPPLEMENTAL_INFO없음 — 단순 break테이블 효과 없음
LOG_SMALLER_LOGREC_TYPE, LOG_LARGER_LOGREC_TYPE, default없음er_set (ER_LOG_PAGE_CORRUPTED) + assert (false) — “로그가 손상되었을 가능성이 높음”

반환 코드는 버려진다 — 대부분 (void) 캐스트로 처리되고, sysop-end 및 checkpoint 분기는 단순히 무시한다. 거의 모든 실패는 logpb_fatal_error를 호출해 recovery를 종료한다. 유일한 예외는 end-checkpoint의 sysop 재읽기(4.8 7단계)로, debug 빌드에서는 assert하고 release 빌드에서는 오류를 삼킨다.

4.2 logtb_rv_find_allocate_tran_index — 지연(lazy) TDES 할당자

섹션 제목: “4.2 logtb_rv_find_allocate_tran_index — 지연(lazy) TDES 할당자”

거의 모든 분기는 여기서 시작한다(log_tran_table.c): tran_id를 TDES로 매핑하며, 처음 발견할 때 할당한다. 세 가지 분기가 있다: B1 — logtb_is_system_worker_tranid (trid)가 log_system_tdes::rv_get_or_alloc_tdes로 단락(short-circuit)하여 시스템 워커를 테이블 밖에 유지한다. B2 — logtb_find_tran_index 미스: logtb_allocate_tran_index (..., TRAN_UNACTIVE_UNILATERALLY_ABORTED, ...) 후 LSA_COPY (&tdes->head_lsa, log_lsa) 실행; 할당 실패는 logpb_fatal_error + return NULL. B3 — 히트: LOG_FIND_TDES.

불변 조건 — 추정 abort(presumed abort). 분석 중 생성된 모든 TDES는 TRAN_UNACTIVE_UNILATERALLY_ABORTED 상태로 태어나며, head_lsa는 최초 발견 시점이다. 이후 완료 레코드(4.4, 제거)나 2PC/postpone 레코드(상태 업그레이드)만이 이 판정을 바꿀 수 있다. 다른 초기 상태를 부여하면 undo 패스(Chapter 9)가 패자(loser) 트랜잭션을 건너뛰어 그 변경이 디스크에 남는다.

4.3 단순 분기 — undo_redo, save_point, compensate

섹션 제목: “4.3 단순 분기 — undo_redo, save_point, compensate”

log_rv_analysis_undo_redo는 아홉 가지 데이터 변경 타입 전체를 처리한다. 비정상 경로는 하나뿐: TDES가 NULL이면 logpb_fatal_error, return ER_FAILED. 그 외에는 LSA_COPY (&tdes->tail_lsa, log_lsa) 후 LSA_COPY (&tdes->undo_nxlsa, &tdes->tail_lsa): tail_lsa는 가장 최근 레코드, undo_nxlsa는 undo가 역방향으로 걷기 시작하는 위치다. 평범한 데이터 레코드에서는 두 값이 일치한다. log_rv_analysis_save_point는 재시작 후 부분 롤백을 위해 LSA_COPY (&tdes->savept_lsa, &tdes->tail_lsa)를 추가한다.

log_rv_analysis_compensate는 LOG_COMPENSATE — 어떤 업데이트가 이미 undo되었다는 증거인 CLR(Compensation Log Record) — 를 처리하며, undo_nxlsa가 tail_lsa와 달라지는 유일한 분기다. 할당자 + NULL-fatal 분기 이후, LOG_REC_COMPENSATE 본문까지 전진(LOG_READ_ADD_ALIGN, LOG_READ_ADVANCE_WHEN_DOESNT_FIT)한 뒤 복사를 하나만 실행한다 — LSA_COPY (&tdes->undo_nxlsa, &compensate->undo_nxlsa) — 이때 tail_lsa는 전진하지 않는다. 복사된 포인터는 보상된 업데이트 이전에 위치하므로 undo는 CLR 자체에서 다시 시작하지 않는다: ARIES의 “이미 undo한 것은 다시 undo하지 않는다(never-undo-an-undo)” 규칙을 포인터 재설정만으로 구현한 것이다.

4.4 log_rv_analysis_complete — commit/abort 마무리

섹션 제목: “4.4 log_rv_analysis_complete — commit/abort 마무리”

LOG_COMMIT과 LOG_ABORT는 log_rv_analysis_complete를 공유한다 — 테이블 상태를 제거하는 유일한 분기이며, 두 개의 조기 종료 분기 중 하나다(나머지는 Chapter 5의 log_rv_analysis_commit_with_postpone로, 동일한 stop_at/resetlog 분기를 가진다). 네 가지 분기:

// log_rv_analysis_complete -- src/transaction/log_recovery.c
tran_index = logtb_find_tran_index (thread_p, tran_id); /* <- find, never allocate */
// ... condensed: B1 -- if not media crash, goto end; else read LOG_REC_DONETIME -> last_at_time ...
if (stop_at != NULL && *stop_at != (time_t) (-1) && difftime (*stop_at, last_at_time) < 0)
{ /* B2: completion is newer than --until-time */
log_lsa->pageid = NULL_PAGEID;
log_recovery_resetlog (thread_p, &record_header_lsa, prev_lsa);
*did_incom_recovery = true;
return NO_ERROR; /* <- index NOT freed: tran stays a loser */
}
end:
// ... condensed: B3 -- if tran_index != NULL_TRAN_INDEX, logtb_free_tran_index ...
return NO_ERROR; /* B4: never seen before -> nothing to drop */

두 가지 비대칭이 있다: 이 함수는 찾을 뿐 할당하지 않는다 — 윈도우 이전에 완료된 트랜잭션은 no-op(B4)이다. 그리고 B2는 index를 유지한다 — commit 레코드에서 로그를 잘라내면 해당 트랜잭션이 소급하여 진행 중인 것으로 처리되어, undo가 롤백한다: 이것이 타임스탬프 시점 복원(restore-to-timestamp)이다.

4.5 7개의 2PC 분기 — 상태 전이 표

섹션 제목: “4.5 7개의 2PC 분기 — 상태 전이 표”

구조적으로 동일하다: TDES 할당(NULL: logpb_fatal_error, ER_FAILED), tdes->state 덮어쓰기, tail_lsa 전진. 어느 분기도 undo_nxlsa를 건드리지 않는다. 차이는 스탬핑하는 상태뿐이다:

레코드 타입핸들러스탬핑되는 tdes->state
LOG_2PC_PREPARElog_rv_analysis_2pc_prepareTRAN_UNACTIVE_2PC_PREPARE
LOG_2PC_STARTlog_rv_analysis_2pc_startTRAN_UNACTIVE_2PC_COLLECTING_PARTICIPANT_VOTES
LOG_2PC_COMMIT_DECISIONlog_rv_analysis_2pc_commit_decisionTRAN_UNACTIVE_2PC_COMMIT_DECISION
LOG_2PC_ABORT_DECISIONlog_rv_analysis_2pc_abort_decisionTRAN_UNACTIVE_2PC_ABORT_DECISION
LOG_2PC_COMMIT_INFORM_PARTICPSlog_rv_analysis_2pc_commit_inform_particpsTRAN_UNACTIVE_COMMITTED_INFORMING_PARTICIPANTS
LOG_2PC_ABORT_INFORM_PARTICPSlog_rv_analysis_2pc_abort_inform_particpsTRAN_UNACTIVE_ABORTED_INFORMING_PARTICIPANTS
LOG_2PC_RECV_ACKlog_rv_analysis_2pc_recv_ack변경 없음 — tail_lsa만 전진

LOG_2PC_PREPARE는 참여자(participant) 측 레코드이고, 나머지는 코디네이터(coordinator) 레코드다. prepare와 start는 tdes->gtrid = LOG_2PC_NULL_GTRID도 심는다: 이는 본문(gtrid, 참여자, lock)이 아직 읽히지 않았음을 나타내는 sentinel이다 — “redo 패스나 finish_commit_protocol 패스 중에 읽어야 한다”(소스 주석). 4.9에서 소비된다.

4.6 체크포인트 페이로드 구조체

섹션 제목: “4.6 체크포인트 페이로드 구조체”

완성된 체크포인트는 두 레코드로 구성된다: 빈 LOG_START_CHKPT 앵커와, 본문이 LOG_REC_CHKPT 헤더, ntrans개의 LOG_INFO_CHKPT_TRANS 항목, 그리고 ntops개의 LOG_INFO_CHKPT_SYSOP 항목으로 이루어진 LOG_END_CHKPT이다(log_record.hpp).

LOG_REC_CHKPT (log_rec_chkpt)는 세 개의 필드를 가진다: redo_lsa — 더티(dirty) 데이터 페이지 중 가장 오래된 recovery LSA로, redo는 반드시 가장 오래된 미반영 변경점에서 시작해야 하기 때문이다(4.8 8단계); ntrans와 ntops — 뒤따르는 두 배열의 항목 수로, 배열은 자기 기술(self-delimiting)이 아니다(ntops는 흔히 0이다).

LOG_INFO_CHKPT_TRANS (log_info_chkpt_trans) — 살아 있는 각 트랜잭션의 직렬화된 TDES 하나:

필드역할존재 이유
isloose_endtdes->isloose_end로 복사클라이언트 loose end
trid트랜잭션 ID할당자의 병합 키
state스냅샷 시점 상태; TRAN_ACTIVE와 TRAN_UNACTIVE_ABORTED는 TRAN_UNACTIVE_UNILATERALLY_ABORTED로 재매핑, 나머지는 그대로추정 abort; 2PC/postpone 상태는 보존
head_lsatdes->head_lsa로 복사분석 윈도우 이전일 수 있음
tail_lsatdes->tail_lsa로 복사체인 재개 지점; 2PC 역방향 탐색 커서 (4.9)
undo_nxlsatdes->undo_nxlsa로 복사체크포인트 이전 CLR 재설정 (4.3)
posp_nxlsatdes->posp_nxlsa로 복사postpone 체인 시작점 (Chapter 5)
savept_lsatdes->savept_lsa로 복사체크포인트 이전 savepoint
tail_topresult_lsatdes->tail_topresult_lsa로 복사롤백 시 완료된 sysop 건너뛰기
start_postpone_lsatdes->rcv.tran_start_postpone_lsa로 복사postpone 완료 (Chapter 8)
user_nameset_system_internal_with_user로 tdes->client에 복사loose-end 소유자

LOG_INFO_CHKPT_SYSOP (log_info_chkpt_sysop) — postpone을 동반한 커밋(committing with postpone) 중인 sysop만 체크포인팅된다. 진행 중인 일반 sysop은 해당 트랜잭션과 함께 그냥 소멸한다:

필드역할존재 이유
trid소유 트랜잭션sysop 배열은 평탄(flat); 항목은 id로 결합
sysop_start_postpone_lsatdes->rcv.sysop_start_postpone_lsa로 복사非null이면 해당 레코드 재읽기 트리거 (4.8 7단계)
atomic_sysop_start_lsatdes->rcv.atomic_sysop_start_lsa로 복사atomic sysop abort 구동 (Chapter 8)

4.7 log_rv_analysis_start_checkpoint와 may_use_checkpoint 가드

섹션 제목: “4.7 log_rv_analysis_start_checkpoint와 may_use_checkpoint 가드”

LOG_START_CHKPT 분기는 조건 하나뿐이다 — if (LSA_EQ (log_lsa, start_lsa)) { *may_use_checkpoint = true; } — 그리고 이 조건이 설계의 핵심이다. start_lsa는 분석이 시작된 위치다: 체크포인트가 완료될 때만 갱신되는 log_Gl.hdr.chkpt_lsa(Chapter 3). 플래그는 앵커 start 레코드에 대해서만 활성화되며, 스캔 도중 중간에 만나는 LOG_START_CHKPT에는 활성화되지 않는다 — 그런 스냅샷은 “더 이상 존재하지 않는 내용을 포함할 수 있다”(소스 주석).

stateDiagram-v2
    [*] --> 비활성 : 분석 시작, 플래그 false
    비활성 --> 활성화됨 : start_lsa에서 LOG_START_CHKPT
    비활성 --> 비활성 : 다른 위치의 LOG_START_CHKPT, LSA_EQ 실패
    활성화됨 --> 소비됨 : LOG_END_CHKPT, 스냅샷 병합 후 플래그 해제
    비활성 --> 비활성 : LOG_END_CHKPT, 가드에서 조기 반환
    소비됨 --> 소비됨 : 이후 체크포인트 레코드 무시

Figure 4-1: may_use_checkpoint의 생명주기. 앵커 START와 쌍을 이루는 END만 스냅샷을 병합할 수 있다.

이는 크래시 윈도우 문제에 대한 답이다. START와 END 사이에 크래시 발생: 헤더는 여전히 이전 완성된 체크포인트를 가리키며, 미완성 윈도우의 START는 LSA_EQ를 통과하지 못하고, END는 기록되지 않았다. 범위 내의 두 번째 완성 윈도우(미디어 복구의 경우): 해당 START는 LSA_EQ를 통과하지 못하고, 해당 END는 4.8 가드에서 죽는다.

4.8 log_rv_analysis_end_checkpoint — 스냅샷 병합, 분기별 설명

섹션 제목: “4.8 log_rv_analysis_end_checkpoint — 스냅샷 병합, 분기별 설명”

가장 긴 분기; 모든 분기를 설명한다:

  1. 가드. if (*may_use_checkpoint == false) return NO_ERROR; — 쌍이 없는 END는 여기서 죽는다. 그 외에는 플래그가 즉시 해제된다: 단발(single-shot).
  2. 앵커 캡처. LSA_COPY (check_point, log_lsa)로 END의 LSA를 드라이버의 checkpoint_lsa에 저장 — run-postpone 분기(Chapter 5)와 4.9에서 사용된다.
  3. 헤더 읽기. LOG_REC_CHKPT를 값으로 복사한다(chkpt = *tmp_chkpt) — 이후 페이지 전진으로 해당 페이지가 evict될 수 있기 때문이다.
  4. Trans 배열 — 두 가지 분기. 페이지 내(log_lsa->offset + size < LOGAREA_SIZE)인 경우: 제자리에서 사용; 그 외: malloc + logpb_copy_from_log; malloc 실패는 fatal.
  5. chkpt.ntrans 항목에 대한 병합 루프 — 먼저 할당자(NULL: 빈 공간, logpb_fatal_error, ER_FAILED), 그리고:
// log_rv_analysis_end_checkpoint -- src/transaction/log_recovery.c
logtb_clear_tdes (thread_p, tdes); /* <- wipe what the loop built so far */
if (chkpt_one->state == TRAN_ACTIVE || chkpt_one->state == TRAN_UNACTIVE_ABORTED)
{
tdes->state = TRAN_UNACTIVE_UNILATERALLY_ABORTED; /* <- presumed-abort remap */
}
else
{
tdes->state = chkpt_one->state; /* <- 2PC / postpone states survive */
}
// ... condensed: isloose_end, six LSA_COPYs, rcv.tran_start_postpone_lsa, user name ...
if (LOG_ISTRAN_2PC (tdes))
{
*may_need_synch_checkpoint_2pc = true; /* <- defer 2PC body reads (4.9) */
}

불변 조건 — END 레코드와의 스냅샷 원자성. logtb_clear_tdes는 START와 END 사이의 레코드들로 이미 구축된 상태를 덮어쓴다. 이것이 안전한 이유는 logpb_checkpoint가 테이블을 스냅샷하고 LOG_END_CHKPT를 단일 log_Gl.prior_info.prior_lsa_mutex 보유 하에(prior_lsa_next_record_with_lock) append하기 때문이다: 그 사이에 아무것도 append되지 않으므로, 스냅샷이 START 이후의 모든 것을 대체한다. mutex를 일찍 해제하면 이 병합이 tail_lsa/undo_nxlsa를 조용히 퇴행시켜 undo가 살아있는 변경을 건너뛸 것이다. 6. Trans 영역 해제. free_and_init (area) — sysop 배열을 위해 area를 null로 만든다. 7. chkpt.ntops > 0으로 게이트된 sysop 병합. 4단계와 동일한 페이지-내-vs-malloc 분기. 항목별로: trid로 TDES 할당; tdes->topops.max == 0 || (tdes->topops.last + 1) >= tdes->topops.max인 경우 topops 스택 확장(logtb_realloc_topops_stack)(실패: free, fatal); 두 LSA를 tdes->rcv에 복사. sysop_start_postpone_lsa가 non-null인 경우: topops.last를 -1에서 0으로 증가 — 그렇지 않으면 assert (tdes->topops.last == 0), recovery 중 최대 한 단계 — 그리고 log_read_sysop_start_postpone가 프라이빗 페이지 버퍼에서 해당 레코드를 재읽어 체크포인트 항목이 생략한 topops.stack[last].lastparent_lsa와 .posp_lsa를 채운다. 분석이 오래된 레코드를 재읽는 유일한 지점; 오류 경로는 assert (false); return error_code; — logpb_fatal_error 없음(4.1). 8. Redo 시작점 당기기. if (LSA_LT (&chkpt.redo_lsa, start_redo_lsa)) LSA_COPY (start_redo_lsa, &chkpt.redo_lsa); — redo(Chapter 6)는 가장 오래된 더티 페이지의 recovery LSA에서 시작한다. 9. 최종 free_and_init (area) — sysop 복사본 해제(페이지 내인 경우 no-op), NO_ERROR 반환.

4.9 may_need_synch_checkpoint_2pc — 지연된 2PC 재구성

섹션 제목: “4.9 may_need_synch_checkpoint_2pc — 지연된 2PC 재구성”

메인 루프 이후, log_recovery_analysis는 저장된 checkpoint_lsa의 LOG_END_CHKPT를 다시 fetch하고, TDES가 여전히 LOG_ISTRAN_2PC를 만족하는 모든 trans 항목에 log_2pc_recovery_analysis_info (thread_p, tdes, &chkpt_trans[i].tail_lsa) (log_2pc.c)를 호출한다: 스냅샷 시점의 tail_lsa에서 시작하는 prev_tranlsa 역방향 체인 탐색으로, tdes->gtrid == LOG_2PC_NULL_GTRID인 동안 LOG_2PC_PREPARE 본문을, tdes->coord == NULL인 동안 LOG_2PC_START 본문을 읽고, ack를 수집한다. 스냅샷은 “큰 공간 오버헤드 때문에”(소스 주석, 예: lock) 2PC 본문을 생략하며, 이 본문들은 윈도우 이전에 있을 수 있다 — 역방향 탐색만이 이를 복원할 수 있다. 재확인은 스냅샷 이후 완료된 트랜잭션을 건너뛴다.

4.10 LOG_END_OF_LOG, next_trid, MVCCID 복원

섹션 제목: “4.10 LOG_END_OF_LOG, next_trid, MVCCID 복원”

트랜잭션별 재구성과 함께 두 가지 전역 상태가 따라간다. 첫째, EOF 분기 — log_rv_analysis_log_end는 하나의 조건이다, if (!logpb_is_page_in_archive (log_lsa->pageid)): 활성 로그의 EOF만 유효하다. 그 안에서 LOG_RESET_APPEND_LSA (log_lsa)는 재시작 후 쓰기가 EOF를 덮어쓰도록 append point를 재설정하며, log_Gl.hdr.next_trid = tran_id는 EOF 레코드 자체의 trid에서 TRANID 카운터를 재시작한다 — 재시작은 로그에서 이미 본 ID를 재발급하지 않는다. 아카이브 내의 EOF는 불완전한 로그를 아카이빙한 아티팩트이므로 건너뛴다; EOF가 발견되지 않는 경우의 복구 경로는 드라이버(Chapter 3) 소관이다.

둘째, MVCCID. 의도적으로 어떠한 분석 분기도 tdes->mvccinfo를 복원하지 않는다 — 재구성된 패자(loser)는 분석 후 MVCCID를 갖지 않는다. 대신 log_recovery_analysis의 마지막 문(그리고 불완전 복구 조기 반환의 마지막 문)은 log_Gl.mvcc_table.reset_start_mvccid () (mvcc_table.cpp)로, log_Gl.hdr.mvcc_next_id에서 활성 MVCCID 비트맵 시작점과 m_current_status_lowest_active_mvccid를 다시 시딩한다: 그보다 낮은 모든 MVCCID는 더 이상 활성이 아닌 것으로 처리된다. Redo는 헤더 값을 정제하며 — 재현된 각 MVCC 레코드는 log_Gl.hdr.mvcc_next_id를 자신의 ID 너머로 밀어낸다 — reset_start_mvccid는 redo 이후(Chapter 6)에 한 번 더 실행된다. 패자의 원래 MVCCID는 undo 중에만 재등장한다: logtb_rv_assign_mvccid_for_undo_recovery가 undo되는 레코드의 rcv->mvcc_id에서 tdes->mvccinfo.id를 설정한다(Chapter 9).

  1. log_rv_analysis_record는 로직이 없는 순수 역다중화기다. 알 수 없는 LOG_RECTYPE은 페이지 손상이고, 7개의 dummy/replication 타입은 no-op이다. 핸들러 실패는 logpb_fatal_error로 끝난다 — end-checkpoint의 sysop 재읽기는 예외로, release 빌드에서 버려진다.
  2. logtb_rv_find_allocate_tran_index는 추정 abort를 강제한다: 트랜잭션은 최초 발견 시 TRAN_UNACTIVE_UNILATERALLY_ABORTED로 태어나고, 시스템 워커는 별도의 log_system_tdes 맵에 산다.
  3. undo_nxlsa가 tail_lsa와 달라지는 것은 오직 log_rv_analysis_compensate뿐으로, CLR에 저장된 포인터로 이미 undo된 작업을 건너뛴다.
  4. log_rv_analysis_complete는 찾을 뿐 할당하지 않으며, 테이블 상태를 제거하는 유일한 분기다. stop_at 분기는 로그를 자르고 index를 유지한다 — 시점 복원.
  5. 7개의 2PC 분기는 스탬핑되는 TRAN_STATE만 다르다. prepare/start는 사후 루프에서 log_2pc_recovery_analysis_info 역방향 탐색이 소비하는 gtrid = LOG_2PC_NULL_GTRID sentinel을 심는다.
  6. LOG_END_CHKPT는 오직 정확히 start_lsa의 LOG_START_CHKPT에 의해 활성화된 경우에만 병합된다 — 반쯤 만들어진 또는 추가적인 체크포인트 윈도우는 구조적으로 무시된다. logtb_clear_tdes-후-덮어쓰기 병합은 logpb_checkpoint가 테이블을 스냅샷하고 END를 단일 prior_lsa_mutex 보유 하에 append하기 때문에 안전하다.
  7. 전역 카운터가 함께 간다: LOG_END_OF_LOG는 append point와 next_trid를 재설정한다. MVCCID는 트랜잭션별로 재구성되지 않는다 — reset_start_mvccid가 log_Gl.hdr.mvcc_next_id에서 MVCC 테이블을 다시 시딩하고, undo가 패자의 MVCCID를 지연적으로 재부착한다.

Chapter 5: 분석 단계의 Sysop·Postpone 어노테이션 기록

섹션 제목: “Chapter 5: 분석 단계의 Sysop·Postpone 어노테이션 기록”

시스템 연산(sysop) 중간, atomic sysop 범위 내, 또는 커밋 시점의 postpone 처리 도중에 크래시가 발생하면 — 이 “지저분한 중간 상태”들은 LOG_RCV_TDES에 다섯 개의 LSA 어노테이션으로 기록된다. 이 어노테이션들은 아래 설명하는 log_rv_analysis_* 핸들러들이 작성하며(드라이버: 3장, 디스패치: 4장), 이후 6장·8장의 복구 단계에서 소비된다. 이론적 배경은 상위 동반 문서 cubrid-recovery-manager.md를 참고한다.

5.1 LOG_RCV_TDES — 복구 어노테이션 블록

섹션 제목: “5.1 LOG_RCV_TDES — 복구 어노테이션 블록”

LOG_RCV_TDES(구조체 log_rcv_tdes, log_impl.h)는 다섯 개의 LOG_LSA 필드로 구성되며, 모든 LOG_TDES에 rcv 필드로 내장된다. 복구 단계 바깥에서는 다섯 필드 모두 null 상태를 유지한다.

필드역할존재 이유
sysop_start_postpone_lsa가장 최근의 열린 LOG_SYSOP_START_POSTPONE LSA; log_rv_analysis_sysop_start_postpone이 기록하고 체크포인트에서 복원되며(4장) log_rv_analysis_sysop_end가 초기화log_recovery_finish_sysop_postpone(8장)이 이를 읽어 sysop의 postpone 목록을 재개 — end 레코드는 이 LSA를 가리키지 않음
tran_start_postpone_lsa트랜잭션의 LOG_COMMIT_WITH_POSTPONE LSA; log_rv_analysis_commit_with_postpone 및 구버전 변형이 기록하고 체크포인트에서 복원(4장)null 여부가 sysop postpone 종료 시 복원할 상태를 결정(5.7); log_recovery_finish_postpone으로 전달됨
atomic_sysop_start_lsa매칭되지 않은 가장 최근의 LOG_SYSOP_ATOMIC_START LSA; log_rv_analysis_atomic_sysop_start이 기록하고 체크포인트에서 복원되며(4장), atomic 연산이 완료된 것이 확인되면 두 sysop 핸들러가 초기화redo 이후에도 set 상태이면 → log_recovery_abort_all_atomic_sysops(8장)가 postpone 재개 전에 이 LSA까지 롤백
analysis_last_aborted_sysop_lsa가장 최근의 ABORT 타입 LOG_SYSOP_END LSA; 해당 arm에서만 기록논리 redo 건너뜀 범위의 상한(log_recovery_needs_skip_logical_redo, 6장)
analysis_last_aborted_sysop_start_lsa동일한 aborted sysop end의 lastparent_lsa동일 건너뜀 범위의 하한
flowchart LR
    cwp["commit_with_postpone"] --> f1["tran_start_postpone_lsa"]
    ssp["sysop_start_postpone"] --> f2["sysop_start_postpone_lsa"]
    ats["atomic_sysop_start"] --> f3["atomic_sysop_start_lsa"]
    se["sysop_end"] --> f4["analysis_last_aborted_sysop_lsa<br/>+ _start_lsa"]
    se -. resets .-> f2
    se -. resets .-> f3
    f1 --> fp["finish_postpone (8장)"]
    f1 --> fsp["finish_sysop_postpone (8장)"]
    f2 --> fsp
    f3 --> aas["abort_all_atomic_sysops (8장)"]
    f4 --> skip["needs_skip_logical_redo (6장)"]

Figure 5-1: 어노테이션 기록자(왼쪽)와 redo 이후 소비자(오른쪽), 접두어 생략.

불변 조건 — 어노테이션은 해당 페이즈가 열려 있는 동안에만 유효하다. 분석 결과 크래시 전에 해당 페이즈가 정상 종료된 것이 확인되면 각 필드는 null로 초기화된다(초기화 가드, 5.7). atomic_sysop_start_lsa가 잔존하면 → 8장이 이미 커밋된 연산을 롤백하고; sysop_start_postpone_lsa가 잔존하면 → 이미 실행된 postpone 목록이 재실행된다.

5.2 LOG_REC_SYSOP_END와 LOG_SYSOP_END_TYPE

섹션 제목: “5.2 LOG_REC_SYSOP_END와 LOG_SYSOP_END_TYPE”

모든 시스템 연산은 LOG_SYSOP_END 레코드로 종료되며, 본문 구조체는 LOG_REC_SYSOP_END(log_record.hpp)이다 — 세 개의 고정 필드, vfid 포인터, 그리고 type으로 분기되는 union으로 구성된다.

필드역할존재 이유
lastparent_lsasysop 시작 직전 트랜잭션의 마지막 LSAsysop 범위를 건너뛰는 undo 점프 목표; 어노테이션과 비교해 중첩 순서 판별
prv_topresult_lsa이전에 완료된 top action의 LSAsysop 결과를 체인으로 연결해 부분 abort 시 건너뜀 가능(tail_topresult_lsa)
type여섯 가지 LOG_SYSOP_END_TYPE 값 중 하나union 해석과 복구 동작 선택
vfid소유 파일; MVCC undo의 경우 vacuum-info 파일과 일치TDE(암호화) 컨텍스트 조회
union undo논리 undo 페이로드(LOGICAL_UNDO)다중 페이지 연산의 undo를 하나의 논리 복구 함수로 처리
union mvcc_undoundo + MVCCID/vacuum 정보(LOGICAL_MVCC_UNDO)vacuum이 해당 연산의 MVCCID를 확인해야 함
union compensate_lsa다음 undo LSA(LOGICAL_COMPENSATE)sysop이 compensation 레코드를 대체; undo가 여기서 재개
union run_postponepostpone_lsa + is_sysop_postpone 플래그(LOGICAL_RUN_POSTPONE)LOG_RUN_POSTPONE를 대체; 플래그는 어느 postpone 목록의 커서를 전진시킬지 결정(5.7)

LOG_SYSOP_END_TYPE(enum log_sysop_end_type, log_record.hpp)은 여섯 가지 값을 가진다: LOG_SYSOP_END_COMMIT(“영구 변경”), LOG_SYSOP_END_ABORT(“취소된 시스템 연산”), 그리고 네 가지 LOG_SYSOP_END_LOGICAL_* 변형인 UNDO, MVCC_UNDO, COMPENSATE, RUN_POSTPONE. union은 오직 type에 의해서만 분기되며(LOG_SYSOP_END_TYPE_CHECK로 검증), 각 값의 분석 시점 동작은 5.7에서 상세히 다룬다.

5.3 Postpone 관련 핸들러: LOG_POSTPONE, LOG_DUMMY_HEAD_POSTPONE, LOG_RUN_POSTPONE

섹션 제목: “5.3 Postpone 관련 핸들러: LOG_POSTPONE, LOG_DUMMY_HEAD_POSTPONE, LOG_RUN_POSTPONE”

log_rv_analysis_postpone(LOG_POSTPONE)와 log_rv_analysis_dummy_head_postpone(no-op 마커 LOG_DUMMY_HEAD_POSTPONE)은 각각 두 가지 분기를 가진다: logtb_rv_find_allocate_tran_index == NULL일 때의 치명 에러 조기 반환(logpb_fatal_error, ER_FAILED)과, 첫 번째 postpone의 커서 캡처이다. LSA_ISNULL (posp_nxlsa) 조건이면 postpone 핸들러는 tail_lsa를 전진시키기 전에 이전 값을 posp_nxlsa에 복사(“set address early”)하고, dummy-head 핸들러는 먼저 전진한 뒤 캡처해 dummy head 자체를 가리킨다(“set address late”). posp_nxlsa는 log_recovery_find_first_postpone(8장)의 스캔 시작점이다.

log_rv_analysis_run_postpone는 LOG_RUN_POSTPONE(이미 실행되어 redo-log에 기록된 postpone)을 처리한다. 분기:

  1. tdes == NULL → 치명 에러, ER_FAILED.
  2. 상태가 {WILL_COMMIT, COMMITTED_WITH_POSTPONE, TOPOPE_COMMITTED_WITH_POSTPONE}에 속하지 않는 경우(TRAN_UNACTIVE_ 접두어 생략): 체크포인트된 tdes에 대해서는 불가능(SYSTEM ERROR 디버그 로그), 그 외에는 정상; 복구 측은 topops.last == -1이면 committed-with-postpone, 아니면 topope-committed로 추정.
  3. 상태가 이제 TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE면 → LSA_SET_NULL (undo_nxlsa): 남은 undo 없음.
  4. 본문 읽기(2장 매크로); run_posp->ref_lsa — 이 레코드가 실행한 LOG_POSTPONE의 LSA — 가 커서를 재설정: topope 상태이면 topops.stack[last].posp_lsa, 그 외 두 상태에서는 tdes->posp_nxlsa(assert로 확인).

불변 조건 — posp_nxlsa는 항상 아직 실행된 것으로 알려지지 않은 다음 postpone을 가리킨다. LOG_POSTPONE가 한 번 설정하고; 이후 모든 LOG_RUN_POSTPONE가 ref_lsa로 전진시킨다. 뒤처지면 → 8장이 동일 postpone을 두 번 실행하고; 너무 앞서면 → 미처리 작업이 조용히 유실된다.

5.4 Postpone을 동반한 트랜잭션 커밋

섹션 제목: “5.4 Postpone을 동반한 트랜잭션 커밋”

log_rv_analysis_commit_with_postpone는 LOG_COMMIT_WITH_POSTPONE를 처리한다: 결과는 확정됐으나 deferred 작업이 미완료일 수 있는 상태. 치명 에러 분기 이후 LOG_REC_START_POSTPONE(posp_lsa + at_time)를 읽고 is_media_crash 여부로 분기한다.

// log_rv_analysis_commit_with_postpone -- src/transaction/log_recovery.c
if (is_media_crash)
{
// ... condensed: stop_at test -> resetlog + *did_incom_recovery = true ...
}
else
{
tdes->state = TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE;
LSA_SET_NULL (&tdes->undo_nxlsa); /* Nothing to undo */
LSA_COPY (&tdes->tail_lsa, log_lsa);
tdes->rcv.tran_start_postpone_lsa = tdes->tail_lsa; /* <- annotation write */
LSA_COPY (&tdes->posp_nxlsa, &start_posp->posp_lsa);
}

media-crash arm은 시점 복구(point-in-time recovery)를 수행한다: stop_at != NULL && *stop_at != (time_t) (-1) && difftime (*stop_at, last_at_time) < 0 — 커밋 시각이 복원 목표 시각을 초과 — 이면 페이지를 해제하고 로그를 잘라낸 뒤(log_recovery_resetlog, 11장) *did_incom_recovery를 설정하며, 해당 트랜잭션은 커밋되지 않은 것으로 처리된다. stop_at 테스트를 통과하지 못하면(또는 stop_at이 NULL/-1이면) media-crash arm은 아무것도 하지 않는다 — 어노테이션 및 상태 갱신은 non-media-crash arm에서만 이루어진다.

log_rv_analysis_commit_with_postpone_obsolete(LOG_COMMIT_WITH_POSTPONE_OBSOLETE, 구 레이아웃 LOG_REC_START_POSTPONE_OBSOLETE — at_time 없음)는 non-media-crash arm만 수행한다 — 타임스탬프도 없고 시점 중단도 없다. 구버전 로그 읽기용으로만 유지되며 “maybe 12.0”에서 제거 예정이다.

LOG_SYSOP_START_POSTPONE는 sysop이 주요 작업을 마치고 자체 postpone 목록을 시작했음을 표시한다. 본문 LOG_REC_SYSOP_START_POSTPONE는 내장된 LOG_REC_SYSOP_END sysop_end(end 레코드에 기록될 내용)와 posp_lsa(sysop의 첫 번째 postpone)로 구성된다. 처리 순서:

  1. 치명 에러 분기 → ER_FAILED.
  2. tail_lsa/undo_nxlsa 전진; 어노테이션 기록: tdes->rcv.sysop_start_postpone_lsa = tdes->tail_lsa.
  3. 내장된 end 타입에 따른 세 갈래 분기: 상태가 이미 topope-committed이면 → assert_release (false)(두 개의 sysop postpone이 동시에 존재할 수 없음); sysop_end.type == LOG_SYSOP_END_LOGICAL_RUN_POSTPONE이면 → 중첩된 is_sysop_postpone == true가 불가능함을 assert하고, 트랜잭션-postpone 변형은 추정 상태와 무관하게 undo_nxlsa를 null로 설정(트랜잭션은 어떠한 상태에서도 커밋 진행 중); 그 외에는 assert (type != LOG_SYSOP_END_ABORT) — abort 중인 sysop은 postpone 페이즈를 시작할 수 없음.
  4. 상태 := TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE.
  5. 필요하면 logtb_realloc_topops_stack으로 topops 스택 확장(실패 시 ER_OUT_OF_VIRTUAL_MEMORY); topops.last는 반드시 -1이어야 하며 0으로 증가(assert (false) 위반 시); lastparent_lsa와 posp_lsa를 topops.stack[0]에 복사.
  6. LSA_LT (sysop_end.lastparent_lsa, rcv.atomic_sysop_start_lsa)이면 atomic 마커가 이 sysop 내부에서 기록된 것이며, start-postpone에 도달했다는 것은 atomic 부분이 완료됐음을 증명하므로 마커를 null로 초기화한다.

불변 조건 — 복구 중에 열린 sysop postpone은 최대 하나이므로 topops.last <= 0을 유지한다. 3단계와 5단계의 assert가 이를 강제하며, log_rv_analysis_sysop_end에서 재확인한다(assert (tdes->topops.last == 0)). 위반 시 run-postpone 핸들러가 잘못된 스택 엔트리의 posp_lsa를 전진시키게 된다.

가장 단순한 핸들러로 LOG_SYSOP_ATOMIC_START를 처리한다 — 두 분기: 치명 에러, 그리고 성공. 성공 분기는 tail_lsa/undo_nxlsa를 전진시킨 후 tdes->rcv.atomic_sysop_start_lsa = *log_lsa를 기록한다(이 레코드는 본문이 없으며 LSA 자체가 페이로드). 이 값이 지워지지 않으면(5.5, 5.7) log_recovery_abort_all_atomic_sysops → log_recovery_abort_atomic_sysop(8장)이 postpone 재개 전에 이 LSA까지 트랜잭션을 롤백한다.

5.7 log_rv_analysis_sysop_end — 복잡한 핵심 핸들러

섹션 제목: “5.7 log_rv_analysis_sysop_end — 복잡한 핵심 핸들러”

아직 추정 상태에 있는 트랜잭션의 알 수 없는 종류의 sysop를 닫는다. 프롤로그: 치명 에러 분기; tail_lsa, undo_nxlsa, tail_topresult_lsa 전진; LOG_REC_SYSOP_END 읽기; LOG_SYSOP_END_TYPE_CHECK. 이후 switch문에서 로컬 변수 commit_start_postpone이 이 end 레코드가 열린 sysop-postpone 페이즈도 함께 닫는지 결정한다.

// log_rv_analysis_sysop_end -- src/transaction/log_recovery.c
case LOG_SYSOP_END_ABORT:
// ... condensed: comment -- abort neither changes state nor finishes a topope postpone ...
if (tdes->state == TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE)
{
LSA_SET_NULL (&tdes->undo_nxlsa); /* no undo */
}
tdes->rcv.analysis_last_aborted_sysop_lsa = *log_lsa; /* <- skip-range upper bound */
tdes->rcv.analysis_last_aborted_sysop_start_lsa = sysop_end->lastparent_lsa; /* <- lower bound */
break;
case LOG_SYSOP_END_COMMIT:
assert (tdes->state != TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE); /* <- falls through to next cases */
case LOG_SYSOP_END_LOGICAL_UNDO:
case LOG_SYSOP_END_LOGICAL_MVCC_UNDO:
// ... condensed: todo comment ...
commit_start_postpone = true;
break;
case LOG_SYSOP_END_LOGICAL_COMPENSATE:
tdes->undo_nxlsa = sysop_end->compensate_lsa; /* <- jump undo over compensated range */
commit_start_postpone = true;
break;

ABORT arm은 aborted-sysop 추적기 역할을 한다: abort된 범위 안의 LOG_DBEXTERN_REDO_DATA 논리 redo는 크래시 전 롤백이 제거한 상태를 다시 만들어낼 수 있으므로, log_recovery_needs_skip_logical_redo(6장)는 analysis_last_aborted_sysop_start_lsa < lsa < analysis_last_aborted_sysop_lsa 구간의 레코드를 건너뛴다. ABORT end가 올 때마다 필드를 덮어쓴다 — 가장 최근의 aborted sysop 하나만 추적된다.

LOG_SYSOP_END_LOGICAL_RUN_POSTPONE arm: topope-committed 상태에서 run-postpone sysop은 두 postpone 범위 중 어느 쪽에 속할 수 있으며, run_postpone.is_sysop_postpone이 결정한다:

  • true(sysop의 postpone): topops.last < 0이거나 상태가 topope-committed가 아니면 스택을 새로 구성 — max == 0이면 realloc(치명 ER_OUT_OF_VIRTUAL_MEMORY), topops.last = 0, 상태를 topope-committed로 강제 설정; 그 후 topops.stack[last].posp_lsa = run_postpone.postpone_lsa. commit_start_postpone은 false로 유지 — 페이즈가 계속된다.
  • false(트랜잭션의 postpone): posp_nxlsa = run_postpone.postpone_lsa; topops.last != -1이면 assert로 topope 상태 확인, 아니면 상태 := TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE; undo_nxlsa null 처리; commit_start_postpone = true.

에필로그는 모든 arm에서 실행된다. topope-committed 상태(assert (topops.last == 0))에서 commit_start_postpone이 설정된 경우 sysop postpone 페이즈가 종료된 것이며, 이전 상태는 tran_start_postpone_lsa로 재구성된다: non-null이면 TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE로 복원(assert로 LSA_LE 확인 — sysop이 트랜잭션 postpone 페이즈 내부에서 실행됨); null이면 기본 복구 상태 TRAN_UNACTIVE_UNILATERALLY_ABORTED로 복원. 어느 경우든 topops.last = -1로 설정. commit_start_postpone이 false이면 페이즈가 계속된다(topops.last는 0 유지); topope 상태가 아닌 경우에는 -1로 (재)설정된다.

이어서 두 개의 대칭적 초기화 가드가 실행된다 — postpone 페이즈와 atomic sysop은 어느 쪽이든 먼저 시작될 수 있으며, end 레코드는 나중에 시작된 쪽에 속한다. atomic 가드는 다음 세 조건이 모두 성립할 때만 rcv.atomic_sysop_start_lsa를 null로 초기화한다: (1) non-null 상태; (2) sysop_start_postpone_lsa보다 LSA_GT — atomic 연산이 더 최근에 열린 페이즈; (3) LSA_GT (atomic_sysop_start_lsa, sysop_end->lastparent_lsa). 조건 3은 부활 방지 가드(resurrection guard)다: lastparent_lsa >= atomic_sysop_start_lsa이면, 이 end 레코드는 atomic 마커 이후에 시작된 sysop — 여전히 열린 atomic 연산 내부에 중첩된 sysop — 을 닫는 것이며, 그 end에서 어노테이션을 지우면 아직 미완료인 atomic 연산을 복구가 건너뛰게 된다. 마커보다 앞선 lastparent_lsa를 가진 end 레코드만 — 즉 마커를 포함하는 sysop의 end 레코드만 — atomic 연산이 완료됐음을 증명할 수 있으며 이를 초기화할 수 있다. 거울 이미지 가드는 sysop_start_postpone_lsa에도 같은 방식으로 동작한다.

  1. LOG_RCV_TDES는 모든 LOG_TDES에 내장된 다섯 LSA 어노테이션 블록으로, 분석 핸들러(및 4장 체크포인트 복원)가 기록하고 6장·8장이 소비하며, 해당 페이즈가 종료된 것이 확인되면 null로 초기화된다.
  2. log_rv_analysis_commit_with_postpone는 tran_start_postpone_lsa를 기록하고 미디어 크래시 시 시점 복구 중단 역할도 겸한다; 구버전 변형은 타임스탬프가 없다는 점만 다르다.
  3. log_rv_analysis_sysop_start_postpone는 sysop_start_postpone_lsa를 기록하고, topops.last를 -1에서 0으로 전환하며, postponing 중인 sysop 내부에 중첩된 것으로 확인된 atomic_sysop_start_lsa를 초기화한다.
  4. log_rv_analysis_sysop_end는 여섯 갈래 switch다: ABORT는 상태를 변경하지 않고 건너뜀 범위를 기록하며; COMMIT과 두 LOGICAL_UNDO 변형은 열린 sysop postpone 페이즈를 닫고; LOGICAL_COMPENSATE는 추가로 undo_nxlsa를 전환하며; LOGICAL_RUN_POSTPONE는 is_sysop_postpone으로 구분한다.
  5. sysop postpone 페이즈가 닫힐 때 이전 상태는 tran_start_postpone_lsa로 재구성된다: non-null → TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE, null → TRAN_UNACTIVE_UNILATERALLY_ABORTED; 이후 초기화 가드가 두 어노테이션을 각각 lastparent_lsa와 비교해 자신의 페이즈 어노테이션만 초기화한다.
  6. analysis_last_aborted_sysop_start_lsa < lsa < analysis_last_aborted_sysop_lsa는 log_recovery_needs_skip_logical_redo가 크래시 전 abort된 sysop 내부의 LOG_DBEXTERN_REDO_DATA 재실행을 억제하는 방법이다.

Chapter 6: Redo 패스 드라이버와 동기 적용

섹션 제목: “Chapter 6: Redo 패스 드라이버와 동기 적용”

log_recovery_redo는 분석 패스(Chapter 3)가 확정한 범위에 걸쳐, 재구성된 트랜잭션 테이블(Chapter 4)을 기반으로 로그를 순방향으로 재생한다 — 드라이버 루프, 그리고 LZ4/XOR 페이로드 조립까지 이어지는 동기 적용 경로를 다룬다. 이론적 배경: “Redo 패스” 동반 문서; 병렬 처리: Chapter 7; 미결 사항: Chapter 8; RV_fun 테이블: Chapter 10.

6.1 Redo 컨텍스트와 레코드별 구조체

섹션 제목: “6.1 Redo 컨텍스트와 레코드별 구조체”

log_rv_redo_context는 전체 적용 상태를 담는 구조체다. 생성자에서 두 개의 LOG_ZIP 버퍼를 모두 LOGAREA_SIZE 크기로 사전 할당한다.

필드역할
m_reader전용 로그 커서 (Chapter 2) — 병렬 워커는 각자 하나씩 가져야 함
m_redo_zipredo 페이로드 스크래치+출력 버퍼; rcv.data는 이 버퍼 내부를 가리키며, 레코드마다 malloc하지 않음
m_undo_zipdiff undoredo 레코드용 undo 스크래치 — diff redo는 undo 이미지와 XOR함
m_end_redo_lsaconst 하드 스톱; 이 값을 넘어선 레코드는 잘린 꼬리이며, §6.4 페이지 LSA assert의 경계값
m_reader_fetch_page_mode크래시 복구 시 NORMAL(스냅샷 신뢰), 복제 재페치 시 FORCE

복사 생성자는 메인 생성자에 위임한다 — 복사본들은 아무것도 공유하지 않으며, 이 덕분에 Chapter 7의 워커별 복사가 안전하다. 각 레코드는 log_rv_redo_rec_info<T> 값 스냅샷으로 전달되며, 정확히 세 개의 필드를 가진다: m_start_lsa — 레코드 헤더의 LSA로, 적용 후 페이지에 스탬프되는 멱등성 비교 기준; m_type — 구체적인 LOG_RECTYPE(T 하나가 일반 및 DIFF 레크타입 모두를 처리하며, diff 여부 판단에 필요함); m_logrec — reinterpret_copy_and_add_align으로 만든 타입 바디의 값 복사본으로, 큐에 쌓인 작업은 로그 페이지 포인터를 보유하지 않는다.

디버그 전용인 vpid_lsa_consistency_check(check / cleanup)는 정확히 두 개의 필드를 가진다: mtx — 병렬 redo 워커가 check를 동시에 호출하므로 동기화가 필요 — 와 consistency_check_map, (volid, pageid) 쌍별 최초 확인 LSA를 기록하는 맵(emplace는 기존 키를 덮어쓰지 않음); cleanup은 패스 종료 시 맵을 비운다.

불변식 — 페이지당 LSA 순서. 순서가 어긋난 적용은 갱신을 유실한다. 이는 디버그 빌드(rcv_phase != LOG_RESTARTED)에서 assert ((*map_it).second < a_log_lsa) 로 강제된다 — 각 새 LSA는 해당 페이지의 첫 번째 기록 LSA와 비교된다(쌍별 단조성보다 약한 조건; emplace는 원본 항목을 유지한다).

불변식 — m_redo_zip 버퍼 안정성. rcv.data는 redofun이 반환될 때까지 m_redo_zip.log_data를 참조한다. 스레드당 하나의 컨텍스트, 순차적 조립이라는 구조적 보장으로 강제된다. 조기 재활용 시 redofun은 쓰레기 값을 읽게 된다.

6.2 log_recovery_redo — 초기화와 외부 루프

섹션 제목: “6.2 log_recovery_redo — 초기화와 외부 루프”

드라이버는 로그 임계 구역을 해제한다(LOG_CS_EXIT; 꼬리 부분에서 재진입). log_recovery_get_redo_parallel_count — MAX (16, system_core_count) — 는 SERVER_MODE에서(Chapter 7) reusable_jobs와 cublog::redo_parallel의 크기를 결정한다. SA 모드에서는 parallel_recovery_redo가 nullptr로 유지되어 모든 적용이 동기적으로 이루어진다. 루프 진입 전 방어 코드: start_redolsa 오프셋이 페이지 끝에 너무 가까우면 assert (false)를 발생시키고 다음 페이지에서 재개한다. PRM_ID_RECOVERY_PROGRESS_LOGGING_INTERVAL(5초 하한)은 처리된/전체 페이지 수와 예상 잔여 시간을 담은 ER_LOG_RECOVERY_PROGRESS를 주기적으로 출력한다.

외부 루프는 lsa를 담은 페이지를 페치한다. 페치 실패 시 lsa > m_end_redo_lsa이면 정상적인 범위 초과로 goto exit한다. 약속된 범위 내부에서의 실패는 logpb_fatal_error다. 내부 루프는 lsa.pageid == m_reader.get_pageid ()인 동안 레코드를 순회한다. 각 반복에서 헤더를 읽기 전, set_lsa_and_fetch_page로 리더를 (필요 시 복구된) 레코드 lsa에 재위치시킨다.

flowchart TD
    A["lsa의 레코드"] --> B{"end_redo_lsa 초과?"}
    B -- yes --> Z["null lsa, break"]
    B -- no --> C["오프셋이 NULL이면 복구"]
    C --> H["lsa에서 재페치, 헤더 읽기, lsa = forw_lsa"]
    H --> K{"lsa가 엄격히 전진?"}
    K -- no --> L["fatal: 로그 루프 감지"]
    K -- yes --> M["log_rtype으로 switch"]
    M --> P["pageid 보정"] --> A

Figure 6-1: log_recovery_redo 내부 루프 한 반복의 골격; 아래 설명은 각 분기를 다룬다.

아카이브 페이지 경계 복구 — 불완전하게 아카이브된 레코드는 페이지 헤더 오프셋이나 forw_lsa를 NULL로 남긴다. lsa.offset이 NULL이면 페이지 헤더 오프셋을 사용한다. 그마저도 NULL이면, 아카이브 페이지의 경우 pageid + 1로, 활성 페이지의 경우 진짜 로그 끝(pageid = NULL_PAGEID)으로 이동 후 continue한다. 아카이브 페이지에서 forw_lsa가 NULL이어도 마찬가지로 pageid + 1로 전진한다. 로그 루프 방어 — 다음 lsa가 엄격히 전진하지 않으면 logpb_fatal_error를 발생시켜 무한 루프를 방지한다. switch 후 보정 — 멀티 페이지 바디 이후 lsa.pageid를 리더의 현재 페이지로 갱신하여 이미 소비한 페이지를 재페치하지 않도록 한다.

불변식 — 스캔은 엄격히 전진한다. 모든 경로는 lsa를 앞으로 이동시키거나 null로 만들어 종료한다. 그렇지 않으면 복구가 같은 범위를 영원히 재생하게 된다.

6.3 디스패치 switch — 레코드 타입별 분기

섹션 제목: “6.3 디스패치 switch — 레코드 타입별 분기”

헤더 파싱 후, 두 개의 로컬 매크로가 각 타입 분기를 처리한다. BUILD_RECORD_INFO (TEMPLATE_TYPE)은 rcv_lsa, log_rtype, 그리고 reinterpret_copy_and_add_align<TEMPLATE_TYPE> ()로 얻은 바디 복사본을 log_rv_redo_rec_info로 묶는다. INVOKE_REDO_RECORD는 이를 log_rv_redo_record_sync_or_dispatch_async로 전달하며, log_rv_need_sync_redo는 null-VPID 레코드와 RVDK_* rcvindex(Chapter 7에서 열거)를 동기 경로로 강제한다. 모든 분기, 완전 목록:

분기동작
LOG_UNDOREDO_DATA, LOG_DIFF_UNDOREDO_DATA, LOG_RUN_POSTPONE, LOG_COMPENSATE일반 build+invoke (§6.4)
LOG_UNDO_DATA, LOG_POSTPONE, LOG_SAVEPOINT, 포스트폰 마커(LOG_DUMMY_HEAD_POSTPONE, LOG_COMMIT_WITH_POSTPONE/_OBSOLETE, LOG_SYSOP_START_POSTPONE), 체크포인트, 2PC decision/inform, HA/복제/더미 타입, LOG_SUPPLEMENTAL_INFO, LOG_SYSOP_ATOMIC_START, LOG_END_OF_LOG명시적 no-op break
LOG_MVCC_UNDOREDO_DATA, LOG_MVCC_DIFF_UNDOREDO_DATAmvcc_next_id를 mvccid 이상으로 bump, mvcc_op_log_lsa = rcv_lsa 설정(vacuum용); invoke
LOG_MVCC_REDO_DATAmvcc_next_id만 bump — vacuum은 undo 데이터를 읽음; invoke
LOG_REDO_DATARVVAC_COMPLETE -> logpb_vacuum_reset_log_header_cache; invoke
LOG_DBEXTERN_REDO_DATA페이지 없음(pgptr = NULL, offset = -1); 아래 skip 검사로 게이트; log_rv_redo_record로 적용
LOG_2PC_PREPAREtran/tdes 없으면 break; 있으면 log_2pc_read_prepare로 gtrid 재읽기, TRAN_UNACTIVE_2PC_PREPARE 상태에서만 LOG_2PC_OBTAIN_LOCKS
LOG_2PC_STARTtran이 살아있고 LOG_ISTRAN_2PC이면 coordinator 정보 재구성; 할당 실패 -> fatal + break
LOG_COMMIT, LOG_ABORTassert 전용: 완료된 비시스템 트랜잭션은 이미 사라져 있어야 함
LOG_MVCC_UNDO_DATA부기만 — mvcc_next_id, mvcc_op_log_lsa; 적용하지 않음
LOG_SYSOP_ENDLOG_SYSOP_END_LOGICAL_MVCC_UNDO -> mvcc_op_log_lsa = rcv_lsa
default (+LOG_SMALLER/LARGER_LOGREC_TYPE)er_set (ER_LOG_PAGE_CORRUPTED); forw_lsa가 같은 레코드를 가리키면 lsa를 null로 설정

반복 크래시 방어 함수 log_recovery_needs_skip_logical_redo는 세 개의 조기 false 반환 — 레크타입 불일치, NULL_TRAN_INDEX, NULL tdes — 과 하나의 true 경로를 가진다:

// log_recovery_needs_skip_logical_redo -- src/transaction/log_recovery.c
if (LSA_LT (&tdes->rcv.analysis_last_aborted_sysop_start_lsa, lsa)
&& LSA_LT (lsa, &tdes->rcv.analysis_last_aborted_sysop_lsa))
{
/* ... condensed: er_log_debug ... */
return true; /* <- 이전 복구가 이미 중단한 sysop 내부에 엄격히 속함 */
}

이 창 밖의 LSA는 마지막 return false로 떨어진다. 분석 패스(Chapter 5)가 양 끝점을 기록했으며, 해당 레코드와 보상 레코드는 이미 이전 복구 사이클에서 로그에 기록된 상태다.

꼬리 시퀀스. (SERVER_MODE) parallel_recovery_redo->wait_for_termination_and_stop_execution ()이 모든 비동기 작업을 소진한다; LOG_CS_ENTER; log_Gl.mvcc_table.reset_start_mvccid ()가 MVCC 기준선을 재계산한다; Chapter 8 인계(log_recovery_abort_all_atomic_sysops, log_recovery_finish_all_postpone); 이후 logpb_flush_pages_direct, logpb_flush_header, pgbuf_flush_all. exit: 레이블 — 범위 초과 시의 도착지이기도 함 — 은 curr_rcv_rec_lsa를 null로 만들고, consistency-check cleanup ()을 실행하며, 성능 통계를 보고한다.

6.4 log_rv_redo_record_sync — 페이지 고정, 추출, 적용

섹션 제목: “6.4 log_rv_redo_record_sync — 페이지 고정, 추출, 적용”
// log_rv_redo_record_sync -- src/transaction/log_recovery_redo.hpp
// ... condensed: debug-only vpid_lsa_consistency_check.check (rcv_vpid, m_start_lsa) ...
const LOG_DATA &log_data = log_rv_get_log_rec_data<T> (record_info.m_logrec);
LOG_RCV rcv;
if (!log_rv_fix_page_and_check_redo_is_needed (thread_p, rcv_vpid, rcv, log_data.rcvindex,
record_info.m_start_lsa, redo_context.m_end_redo_lsa))
{
// ... condensed: assert (rcv.pgptr == nullptr) ...
return; /* <- 페이지가 사라졌거나 변경이 이미 디스크에 있음 */
}
scope_exit unfix_rcv_pgptr { [&thread_p, &rcv] ()
{ pgbuf_unfix_and_init_after_check (thread_p, rcv.pgptr); } }; /* <- 모든 exit 경로에서 unfix */
// ... condensed: rcv field extractors; payload assembly ...
rvfun::fun_t redofunc = log_rv_get_fun<T> (record_info.m_logrec, log_data.rcvindex);

요약된 꼬리 부분: 페이로드 조립 오류 -> logpb_fatal_error + return(scope_exit이 여전히 unfix 수행); null이 아닌 redofunc는 perfmon_counter_timer_raii_tracker(PSTAT_LOG_REDO_FUNC_EXEC) 하에 실행되며, 실패 시 -> logpb_fatal_error; null redofunc -> er_log_debug 경고만; null이 아닌 rcv.pgptr에는 pgbuf_set_lsa로 m_start_lsa를 스탬프한다.

게이트키퍼 log_rv_fix_page_and_check_redo_is_needed는 세 가지 결과를 낸다: (1) VPID가 null이 아닌데 log_rv_redo_fix_page가 null을 반환 — assert (log_is_in_crash_recovery ()), return false, 해제된 페이지 건너뜀; (2) 페이지는 고정됐지만 rcv_lsa <= *pgbuf_get_lsa (rcv.pgptr) — pgbuf_unfix_and_init, return false, 변경이 이미 디스크에 있음(또한 페이지 LSA가 end_redo_lsa를 넘으면 assert로 거부); (3) 그 외 return true, null-VPID 폴스루 포함(이 경우 rcv.pgptr == nullptr로 페이지 없는 레코드 처리). log_rv_redo_fix_page는 섹터 예약 검사 없이 RECOVERY_PAGE 모드로 페이지를 고정한다 — 섹터 테이블이 병렬로 재생되므로 페이지가 일시적으로 해제된 것처럼 보일 수 있다. 검사 비용이 건너뜀으로 얻는 이득보다 크며, NULL 반환은 assert_release 대상이다(“복구를 불가능하게 만들기 때문에 심각한 문제”).

불변식 — 페이지 LSA를 통한 redo 멱등성. rcv_lsa <= page LSA이면 건너뛰고, 적용 후 m_start_lsa를 스탬프한다. 스탬프를 깨면 이후 모든 크래시에서 비멱등적 redo가 이중 적용된다.

여섯 개의 extractor 템플릿 패밀리 — 기본형은 static_assert (sizeof (T) == 0)으로 직접 인스턴스화 불가 — 는 여섯 가지 레코드 형태를 하나의 범용 루틴으로 평탄화한다. 예외 케이스: log_rv_get_fun<LOG_REC_COMPENSATE>는 RV_fun[rcvindex].undofun을 반환한다(소스 코드 주석 “yes, undo”) — CLR의 redo 페이로드는 undo 이미지이므로, 재생 시 undo 함수를 실행한다. 이는 ARIES의 역사 반복(history repeating) 원칙이다(동반 문서: “Compensation log records”).

T_data / _vpid / _offset 출처_mvccid_redo_lengthlog_rv_get_fun
LOG_REC_MVCC_UNDOREDOundoredo.datamvccidundoredo.rlengthredofun
LOG_REC_UNDOREDOdataMVCCID_NULLrlengthredofun
LOG_REC_MVCC_REDOredo.datamvccidredo.lengthredofun
LOG_REC_REDOdataMVCCID_NULLlengthredofun
LOG_REC_RUN_POSTPONEdataMVCCID_NULLlengthredofun
LOG_REC_COMPENSATEdataMVCCID_NULLlengthundofun

6.5 페이로드 조립 — 압축 해제, diff, 전달

섹션 제목: “6.5 페이로드 조립 — 압축 해제, diff, 전달”

log_rv_get_log_rec_redo_data<T>가 페이로드를 디코딩한다. 네 개의 단일 이미지 특수화(LOG_REC_MVCC_REDO, LOG_REC_REDO, LOG_REC_RUN_POSTPONE, LOG_REC_COMPENSATE)는 undo 데이터 없이 log_rv_get_unzip_and_diff_redo_log_data를 호출한다. LOG_REC_MVCC_UNDOREDO는 내장된 undoredo 멤버를 log_rv_redo_rec_info<LOG_REC_UNDOREDO>로 재포장하고 위임한다. LOG_REC_UNDOREDO만 분기한다 — T가 아닌 m_type 기준: DIFF 레크타입 두 종(need_diff_with_undo)에 대해서는 먼저 log_rv_get_unzip_log_data로 undo 이미지를 m_undo_zip에 압축 해제하고(오류 시 fatal + return), 정렬 후 m_undo_zip.data_length / .log_data를 전달한다. 그 외에는 불필요한 undo 이미지를 건너뛰고(m_reader.skip (GET_ZIP_LEN (ulength)), 오류 시 fatal + ER_FAILED), 정렬 후 (0, nullptr)을 전달한다.

log_rv_get_unzip_log_data는 하나의 이미지를 완전하게 디코딩한다. 길이 필드의 부호 비트가 압축 플래그다 — MAKE_ZIP_LEN이 로깅 시점에 0x80000000을 설정하고, ZIP_CHECK로 검사하며, GET_ZIP_LEN으로 비트를 제거한다. 위의 skip 경로도 GET_ZIP_LEN을 거친다. is_zip = ZIP_CHECK (length); does_fit_in_current_page인 이미지는 페이지 버퍼를 직접 참조(alias)하고, 걸쳐 있는 이미지는 copy_from_log로 힙 복사한다. 압축된 경우 -> log_unzip(실패: fatal + ER_FAILED); 비압축 -> log_zip_realloc_if_needed(실패 시 fatal) + memcpy. 마지막으로, fits 케이스에서는 add_align, copy 케이스에서는 copy_from_log가 이미 리더를 전진시켰으므로 align ()만 호출한다.

log_rv_get_unzip_and_diff_redo_log_data는 그 위에 diff를 적용한다: 호출자의 redo_unzip에 log_rv_get_unzip_log_data를 수행한 뒤(실패: fatal + ER_FAILED), if (is_zip)이고 undo_length > 0 && undo_data != nullptr일 때만 un-diff — log_diff (undo_length, undo_data, redo_unzip.data_length, redo_unzip.log_data) — 를 수행하고, m_redo_zip 저장소를 빌려 rcv->length / rcv->data를 전달한다. is_zip 게이트가 동작하는 이유는 diff redo가 항상 압축 상태로만 존재하기 때문이다: 추가 시점에 XOR은 스크래치 복사본 위에서 실행되며, DIFF 레크타입은 is_redo_zip일 때만 설정된다. 압축에 실패하면 원본 un-diff 데이터를 비트 클리어 상태로 기록한다. log_unzip은 저장된 원본 길이 프리픽스 log_zip을 읽고, buf_size <= 0을 거부하며, log_zip_realloc_if_needed가 대상 버퍼를 확장하지 못하면 실패하고, LZ4 압축 해제 후 unzip_len == buf_size일 때만 성공한다 — 짧거나 음수면 잘림이 아닌 손상이다. log_diff는 MIN (undo_length, redo_length) 바이트에 걸쳐 *(p++) ^= *(q++)를 수행한다 — XOR은 자기 역원이므로 하나의 루틴이 양방향 모두를 처리한다.

페이지 없는 쌍둥이 log_rv_redo_record(LOG_DBEXTERN_REDO_DATA 분기)는 fix/skip 게이트 없이 같은 조립-후-호출 시퀀스를 실행한다: 페이로드 실패 -> fatal + return; redofun 실패 -> fatal; redofun == NULL -> 디버그 경고; rcv->pgptr != NULL -> pgbuf_set_lsa(여기서는 pgptr가 NULL이므로 무의미).

  1. log_rv_redo_context는 전체 redo 상태를 담는다 — 하나의 로그 커서와 rcv.data가 빌려 쓰는 두 개의 사전 할당된 LOG_ZIP 버퍼; share-nothing 복사본이 Chapter 7 워커를 가능하게 한다.
  2. switch는 mvcc_next_id, mvcc_op_log_lsa(undo 데이터를 가진 레코드에 한함), RVVAC_COMPLETE 리셋, 논리 redo skip 창도 함께 관리한다.
  3. 멱등성: rcv_lsa <= page LSA이면 건너뛰고, 적용 후 m_start_lsa를 스탬프한다. log_rv_redo_fix_page는 의도적으로 해제된 페이지를 허용한다.
  4. 여섯 개의 extractor 패밀리가 여섯 가지 레코드 형태를 하나의 적용 루틴으로 평탄화한다. log_rv_get_fun<LOG_REC_COMPENSATE>는 undofun을 반환한다 — CLR은 undo로 재생된다.
  5. 부호 비트 하나가 압축을 인코딩한다(MAKE_ZIP_LEN/ZIP_CHECK/GET_ZIP_LEN); diff redo는 항상 압축 상태로만 존재하므로, log_diff(XOR)는 is_zip일 때만 실행된다.
  6. 루프 이후의 모든 처리는 wait_for_termination_and_stop_execution이 병렬 redo를 소진할 때까지 실행되지 않는다. 그 이후에야 reset_start_mvccid, Chapter 8 마무리, 플러시가 수행된다.

6장의 드라이버는 redo 가능한 각 레코드를 log_rv_redo_record_sync_or_dispatch_async로 전달한다. 구체적인 페이지를 대상으로 하고 볼륨 레코드가 아닌 경우에만 비동기 처리되며, 모든 VPID는 고정된 태스크에 해시되기 때문에 푸시 순서로부터 페이지 단위 LSA 순서가 자동으로 보장된다.

7.1 디스패치 — log_rv_redo_record_sync_or_dispatch_async

섹션 제목: “7.1 디스패치 — log_rv_redo_record_sync_or_dispatch_async”

INVOKE_REDO_RECORD에 의해 레코드 타입별로 인스턴스화된다:

// log_rv_redo_record_sync_or_dispatch_async -- src/transaction/log_recovery_redo_parallel.hpp
const VPID rcv_vpid = log_rv_get_log_rec_vpid<T> (record_info.m_logrec);
#if defined (SERVER_MODE)
// ... condensed: log_data ref ...
const bool need_sync_redo = log_rv_need_sync_redo (rcv_vpid, log_data.rcvindex);
// ... condensed: PREP perf tick ...
if (parallel_recovery_redo == nullptr || need_sync_redo)
{
log_rv_redo_record_sync<T> (thread_p, redo_context, record_info, rcv_vpid);
// ... condensed: DO_SYNC perf tick ...
}
else
{
cublog::redo_job_impl *const job = a_reusable_jobs.blocking_pop (a_rcv_redo_perf_stat);
assert (job != nullptr);
job->set_record_info (rcv_vpid, record_info.m_start_lsa, record_info.m_type);
parallel_recovery_redo->add (job);
// ... condensed: DO_ASYNC perf tick ...
}
#else // !SERVER_MODE = SA_MODE
log_rv_redo_record_sync<T> (thread_p, redo_context, record_info, rcv_vpid);
#endif

SA_MODE에서는 cublog 클래스들이 빈 더미로 컴파일된다. Figure 7-1은 모든 분기 경로를 정리한다. 판별 술어는 다음과 같다:

// log_rv_need_sync_redo -- src/transaction/log_recovery.c
if (VPID_ISNULL (&a_rcv_vpid))
{
return true; /* <- no target page to hash */
}
switch (a_rcvindex)
{
case RVDK_NEWVOL: // ... condensed: RVDK_FORMAT, RVDK_INITMAP, RVDK_EXPAND_VOLUME, RVDK_VOLHEAD_EXPAND ...
return true; /* <- see Inv 7-A */
case RVDK_RESERVE_SECTORS: // ... condensed: RVDK_UNRESERVE_SECTORS ...
return true; /* <- "may be changed to async" */
default:
return false;
}

Invariant 7-A (동기 레코드는 happens-before 배리어). 메인 스레드는 sync 레코드를 적용한 뒤 이후 job을 푸시한다. 새로운 볼륨의 페이지는 그 이후 레코드에만 등장하므로, 어떤 워커도 아직 생성되지 않은 볼륨의 페이지를 fix하는 상황은 발생하지 않는다.

flowchart TD
  A["레코드"] --> B{"SERVER_MODE?"}
  B -- "아니오 (SA)" --> S1["동기 적용"]
  B -- 예 --> C{"인프라 null?"}
  C -- 예 --> S1
  C -- 아니오 --> D{"log_rv_need_sync_redo"}
  D -- "null VPID 또는 RVDK 볼륨/섹터" --> S1
  D -- false --> E["blocking_pop + set_record_info"]
  E --> G["add: VPID 해시로 고정 태스크에 배정"]

Figure 7-1: 디스패치 분기 경로.

7.2 크기 조정과 생성 — redo_parallel

섹션 제목: “7.2 크기 조정과 생성 — redo_parallel”

log_recovery_redo는 REGISTER_WORKERPOOL로 풀 수요를 등록하고, 순방향 스캔 전에 한 번 생성한다: reusable_jobs.initialize (count)와 new cublog::redo_parallel (count, false, MAX_LSA, redo_context)를 순서대로 호출한다. false와 MAX_LSA는 모니터링을 비활성화한다(7.8). 워커 수 계산:

// log_recovery_get_redo_parallel_count -- src/transaction/log_recovery.c
const int num_cpus = cubthread::system_core_count ();
const int minimum_threads_to_redo = 16; /* <- "determined experimentally" */
return MAX (minimum_threads_to_redo, num_cpus);

최소값 16은 소형 머신에서 과구독(oversubscription)을 유발하지만, 워커는 I/O 바운드이므로 실제로는 합리적이다. 생성자는 do_init_worker_pool(워커 수 = 슬롯 수 = a_task_count), 이어서 do_init_tasks와 모니터를 초기화한다.

필드역할존재 이유
m_task_countVPID 빈닝(binning) 모듈러스생성 시 고정됨 (Inv 7-B)
m_pool_entry_managerTT_RECOVERY 엔트리 팩토리워커에 실제 엔트리 필요
m_task_state_bookkeeping활성 태스크 비트셋무한 대기 (7.7)
m_worker_pool워커 풀 포인터OS 스레드 소유
m_redo_tasksvector<unique_ptr<redo_task>>소유자 관리; perf 통계 유지
m_vpid_hashstd::hash<VPID>add의 빈닝 함수
m_min_unapplied_log_lsa_calculation진행 상황 모니터 (7.8)복제 전용

7.3 VPID 해시 — 락 없는 순서 보장

섹션 제목: “7.3 VPID 해시 — 락 없는 순서 보장”
// redo_parallel::add -- src/transaction/log_recovery_redo_parallel.cpp
const std::size_t task_index = m_vpid_hash (a_job->get_vpid ()) % m_task_count;
redo_task *const task = m_redo_tasks[task_index].get ();
task->push_job (a_job);

Invariant 7-B (푸시 순서로부터 페이지 단위 순서 보장). 메인 스레드는 LSA 오름차순으로 푸시하고, 동일한 VPID는 항상 같은 태스크로 해시되며(m_task_count 불변), 각 태스크는 FIFO로 드레인한다 — 따라서 페이지 단위 적용 순서는 로그 순서와 일치하며, 락이 필요 없다. 이 세 조건 중 하나라도 깨지면 두 워커가 한 페이지 위에서 경쟁하게 되고, 이는 rcv_lsa <= page_lsa 건너뜀 로직에 의해 무증상으로 숨겨진다. 페이지 간 순서는 보존하지 않는다.

큐에 넣을 수 있는 단위인 redo_job_base:

필드역할존재 이유
m_vpid대상 페이지; 기본값일 때 null빈닝 키; get_vpid는 non-null 단언
m_log_lsa레코드의 LSA재읽기 위치 (7.5); 진행 표시자 (7.8)

redo_task::push_job은 모니터링이 활성화된 상태에서 큐가 비어 있을 때만 미적용 마커를 갱신하며, PRM_ID_RECOVERY_REDO_MINIMUM_JOB_COUNT(숨겨진 파라미터, 기본값 100)를 초과했을 때만 알림을 보낸다.

.cpp-private cubthread::task인 redo_task:

필드역할존재 이유
m_task_idx0..N-1 식별자비트셋 및 푸시 벡터의 인덱스
m_do_monitor_unapplied_log_lsa마커 유지 여부복구 시 false 전달
m_task_state_bookkeeping소유자 비트셋 참조생성자에서 set, 드레인 후 clear
m_perf_stats_definition / m_perf_stats태스크별 카운터7.9의 타이밍 측정
m_redo_context개인 컨텍스트 복사본독립적인 리더 및 zip 버퍼 (7.5)
m_produce_vec (+_mtx, _cv)잡 큐; ONE_M 예약배치당 락 한 번: 스왑 방식
m_adding_finished스트림 종료 플래그, mutex 하에 set큐가 빌 때만 확인
m_unapplied_log_lsaatomic<log_lsa>, 유휴 시 MAX_LSA전역 최솟값에 기여 (7.8)
// redo_task::execute -- src/transaction/log_recovery_redo_parallel.cpp
for ( ; ; )
{
bool adding_finished { false };
pop_jobs (jobs_vec, adding_finished);
if (jobs_vec.empty () && adding_finished)
{
break; /* <- only exit */
}
else
{
assert (!jobs_vec.empty ());
THREAD_ENTRY *const thread_entry = &context;
for (auto &job : jobs_vec)
{
// ... condensed: marker update ...
job->execute (thread_entry, m_redo_context);
job->retire (m_task_idx);
}
jobs_vec.clear (); /* <- jobs already recycled */
}
}
m_task_state_bookkeeping.set_inactive (m_task_idx);

pop_jobs는 사후 조건으로 empty와 finished가 정확히 XOR 관계임을 단언한다. 1초 주기의 wait_for(PRM_ID_RECOVERY_REDO_JOB_PERIOD_IN_SECS, 숨겨진 파라미터)는 알림 없이 스며드는 잡을 처리하며, notify_adding_finished는 동일한 mutex 하에 플래그를 뒤집으므로 웨이크업 누락이 발생하지 않는다.

flowchart TD
  W["wait_for, 1초 주기"] --> P{"큐 비어 있음?"}
  P -- 아니오 --> SW["로컬 jobs_vec으로 swap"]
  P -- 예 --> MK["마커 park, 모니터링 시만"]
  MK --> F{"m_adding_finished?"}
  F -- 아니오 --> W
  F -- 예 --> Z["empty + finished 반환"]
  SW --> EX["잡별: execute, retire"]
  EX --> W
  Z --> IN["set_inactive, cv notify"]

Figure 7-2: pop_jobs와 드레인 루프 분기 경로.

7.5 redo_job_impl::execute — 재인출(re-fetch)

섹션 제목: “7.5 redo_job_impl::execute — 재인출(re-fetch)”
redo_job_impl 필드역할존재 이유
m_reusable_job_stack풀 역참조 포인터, “이 인스턴스보다 오래 살아있음이 보장됨”retire = push (a_task_idx, this)
m_log_rtypeset_record_info에서 기록된 LOG_RECTYPE재읽기 시 log_rec_* 레이아웃 선택
// redo_job_impl::execute -- src/transaction/log_recovery_redo_parallel.cpp
const int err_fetch =
redo_context.m_reader.set_lsa_and_fetch_page (get_log_lsa (), redo_context.m_reader_fetch_page_mode);
if (err_fetch != NO_ERROR)
{
return err_fetch; /* <- sole error exit */
}
redo_context.m_reader.add_align (sizeof (LOG_RECORD_HEADER));
switch (m_log_rtype)
{
case LOG_REDO_DATA:
read_record_and_redo<log_rec_redo> (thread_p, redo_context);
break;
// ... condensed: 7 more labels (MVCC/diff undoredo, RUN_POSTPONE, COMPENSATE) ...
default:
assert (false); /* <- unreachable */
}

여덟 개의 레이블은 6장에서 다룬 페이지 바운드 redo 가능 타입들이다. read_record_and_redo<T>는 타입별 헤더를 재파싱한 뒤 동기 경로의 종착점인 log_rv_redo_record_sync<T>로 합류한다. 오류 반환은 redo_task::execute에서 무시되므로, fetch 실패 시 해당 레코드의 redo는 조용히 건너뛰어진다. log_rv_redo_context는 복사 생성은 가능하지만 대입은 불가능하므로, 각 태스크는 독립적인 리더와 zip 버퍼를 소유한다.

필드역할존재 이유
m_flush_push_at_countPARALLEL_REDO_REUSABLE_JOBS_FLUSH_BACK_COUNT (ONE_K)~1024번 retire마다 mutex 한 번
m_job_poolPARALLEL_REDO_REUSABLE_JOBS_COUNT (ONE_M) 크기의 vector<redo_job_impl>유일한 메모리 할당
m_pop_jobs소비자 스택, 비동기화 pop단일 소비자 (Inv 7-C)
m_push_jobs (+m_push_mtx, m_push_jobs_available_cv)공유 반납 버킷유일한 동기화 핸드오프 지점
m_per_task_push_jobs_vec태스크별 전용 벡터락 없는 retire 빠른 경로
// reusable_jobs_stack::blocking_pop -- src/transaction/log_recovery_redo_parallel.cpp
if (!m_pop_jobs.empty ())
{
redo_job_impl *const pop_job = m_pop_jobs.back ();
m_pop_jobs.pop_back (); /* <- no lock */
return pop_job;
}
else
{
{
std::unique_lock<std::mutex> locku { m_push_mtx };
// ... condensed: cv wait until !m_push_jobs.empty () ...
m_pop_jobs.swap (m_push_jobs); /* <- O(1) refill */
}
// ... condensed: pop_back ...
}

push (a_task_idx, a_job)는 대칭적으로 동작한다: 호출자의 전용 벡터에 추가하고, m_flush_push_at_count를 초과했을 때만 락을 걸어 일괄 삽입 후 clear하고 notify_one을 보낸다. 느린 경로는 백프레셔(backpressure) 밸브 역할을 한다: ONE_M개의 job이 모두 인플라이트 상태가 되면 메인 스레드는 한 배치가 반납될 때까지 블로킹된다.

Invariant 7-C (단일 소비자, job 보존). m_pop_jobs는 복구 메인 스레드만 blocking_pop을 호출하므로 비동기화 pop이 허용된다. 소멸자는 pop + push + sum(per_task) == m_job_pool.size ()를 단언한다 — 실행 후 retire하지 않은 job이 있으면 이 단언에서 잡힌다.

7.7 task_active_state_bookkeeping과 종료

섹션 제목: “7.7 task_active_state_bookkeeping과 종료”
필드역할존재 이유
m_size태스크 수, BITSET_MAX_SIZE (256) 미만 단언인덱스 경계 검사
m_valuesstd::bitset<256>, 태스크별 비트set_active/set_inactive는 이전 상태 단언
m_values_mtx / m_values_cv가드 + 웨이크업wait_for_termination은 m_values.none ()까지 대기

워커 풀 자체의 대기는 “하드코딩된 최대 대기 시간(60초)” 후 단언하지만, 이 전용 bookkeeping은 무한정 대기한다. 태스크는 생성자에서 비트를 설정하므로, 아직 시작되지 않은 태스크를 조기 대기가 놓치는 일이 없다. 종료 절차:

// redo_parallel::wait_for_termination_and_stop_execution -- src/transaction/log_recovery_redo_parallel.cpp
for (auto &redo_task: m_redo_tasks)
{
redo_task->notify_adding_finished ();
}
m_task_state_bookkeeping.wait_for_termination ();
// ... condensed: assert every task is_idle ...
m_worker_pool->stop_execution ();
// ... condensed: get_manager ()->destroy_worker_pool ...

redo_task::retire는 no-op으로(“자기 파괴 방지”), 태스크별 perf 통계가 이후에도 읽힐 수 있다. WAIT_FOR_PARALLEL은 최후 태스크 대기 시간을 측정한다. 양 끝에서 순서를 단언한다: push_job은 !m_adding_finished를, ~redo_parallel은 활성 태스크 없음과 null 풀을 단언한다 — 소멸 전에 이 블로킹 호출은 반드시 완료되어야 한다.

크래시 복구 시에는 비활성(false, MAX_LSA); 동일한 인프라가 페이지 서버에서 복제를 수행할 때 활성화된다. 생성자는 두 인자의 쌍을 단언한다: 모니터링에는 유효한 시작 LSA가 필요하고, 모니터링 없을 때는 MAX_LSA여야 한다.

필드역할존재 이유
m_do_monitor마스터 스위치모든 메서드에서 단언
m_main_thread_unapplied_log_lsa디스패처가 전진시키는 atomic<log_lsa>sync 레코드는 태스크 큐를 우회
m_redo_tasks태스크 벡터 const 참조calculate가 각 태스크 마커 읽음
m_calculated_log_lsam_calculate_mtx 하에 보관된 마지막 전역 최솟값대기자가 비교하는 기준값
m_calculate_mtx / m_calculate_cv / m_terminate_calculation / m_calculate_thread계산 스레드 플러밍가드, cv, 종료 플래그, 스레드

calculate는 메인 스레드 LSA와 태스크 마커들을 비교하여 최솟값을 구하며, 유휴 상태인 MAX_LSA 마커는 건너뛴다. wait_past_target_log_lsa는 두 가지 종료 경로를 가진다: a_target_lsa < m_calculated_log_lsa일 때의 비잠금(unlocked) 빠른 경로, 그렇지 않으면 notify_all(10 ms 낮잠을 깨움)로 킥하고 최솟값이 목표를 넘을 때까지 블로킹한다. redo_parallel::wait_past_target_lsa와 set_main_thread_unapplied_log_lsa는 포워더(forwarder)다.

perf_stats(log_recovery_redo_perf.hpp)는 cubperf 위에 올려진 nullable 래퍼다:

필드역할존재 이유
m_definitioncubperf::statset_definition에 대한 const 참조슬롯 이름/타입 (모두 COUNTER_AND_TIMER)
m_stats_setcubperf::statset *, 비활성 시 nullptr단일 분기점: 모든 메서드가 이를 확인

활성화는 측면별로 이루어진다(perf_stats_is_active_for_main / ..._for_async); do_not_record_t는 비활성 인스턴스를 생성한다. time_and_increment (id)는 이전 호출 이후 경과 시간을 누적한다:

  • 메인 스레드: FETCH_PAGE, READ_LOG, REDO_OR_PUSH_{PREP, DO_SYNC, POP_REUSABLE_DIRECT/_WAIT, DO_ASYNC}, COMMIT_ABORT, WAIT_FOR_PARALLEL, FINALIZE.
  • 워커: PARALLEL_POP, PARALLEL_SLEEP(실제로는 증가되지 않음), PARALLEL_EXECUTE, PARALLEL_RETIRE.

redo_parallel::log_perf_stats는 각 워커의 통계셋과 요소별 평균을 로깅한다 — EXECUTE 대 POP 비율로 포화도를 파악하고, DIRECT 대 WAIT 비율로 풀 스로틀링 여부를 확인할 수 있다.

  1. 디스패처 분기는 세 가지다: SA_MODE 항상 동기; null 인프라 또는 log_rv_need_sync_redo(null VPID, 볼륨 연산, 섹터 reserve/unreserve)에 의한 강제 동기; 재활용된 job의 비동기 디스패치.
  2. Invariant 7-A는 강제 동기 레코드를 happens-before 배리어로 만든다. Invariant 7-B(hash(VPID) % m_task_count 고정, 순서대로 푸시, FIFO 드레인)는 페이지 락 없이 페이지 단위 LSA 순서를 보장한다.
  3. 워커 수는 MAX (16, cores)로, I/O 바운드 워커를 위한 실험적 하한값이다. 종료 시 전용 비트셋을 사용하여 풀의 60초 단언을 우회한다.
  4. Job은 (vpid, lsa, rectype)을 담으며, redo_job_impl::execute는 태스크 전용 log_rv_redo_context로 로그 페이지를 재인출해 동기 적용 경로로 합류한다. 워커 루프는 오류 반환값을 버린다.
  5. reusable_jobs_stack은 ONE_M개의 job을 재활용한다 — 락 없는 pop, ONE_K 단위 플러시백, 보존 단언(Inv 7-C). 느린 경로는 백프레셔 역할을 한다. min_unapplied_log_lsa_monitoring과 perf_stats는 각각 복제와 진단을 위해 제공된다.

Chapter 8: 원자적 시스템 오퍼레이션 중단 및 Postpone 완료

섹션 제목: “Chapter 8: 원자적 시스템 오퍼레이션 중단 및 Postpone 완료”

Redo(6~7장)는 크래시 당시 상태를 정확히 재현하지만, undo로 넘어가기 전에 반드시 정리해야 할 두 가지 미결 사항이 남는다: 열린 원자적 시스템 오퍼레이션(atomic sysop), 그리고 postpone이 끝나지 않은 채 커밋된 트랜잭션/sysop이다. log_recovery_redo의 꼬리 단계가 이 두 가지를 처리한다. postpone/sysop 개념 자체는 상위 문서(cubrid-recovery-manager.md)를 참고한다.

두 단계 모두, 병렬 redo 풀이 모두 소진된 뒤 복구 메인 스레드에서 수행된다(7장). 런타임 로깅 경로(log_sysop_start / log_sysop_abort / log_run_postpone_op)로 새 로그 레코드를 추가하므로, 큐에 쌓인 모든 redo 작업이 적용된 이후에만 안전하게 실행할 수 있다.

// log_recovery_redo (tail) -- src/transaction/log_recovery.c
LOG_CS_ENTER (thread_p);
log_Gl.mvcc_table.reset_start_mvccid ();
/* ... er_set: "REDO" finishing-up notification ... */
log_recovery_abort_all_atomic_sysops (thread_p); /* <- must run FIRST */
log_recovery_finish_all_postpone (thread_p);
/* ... flush data pages, log pages, log header ... */

불변 조건 8-A — atomic-before-postpone. log_rcv_tdes 주석에 명시된 대로, 중단된 file_perm_alloc/file_perm_dealloc은 “반드시 원자적으로 실행되어야 하며 … 모든 postpone 완료 전에 실행되어야 한다”. 그렇지 않으면 postpone 액션(주로 영구 파일 삭제)이 반쯤 수정된 파일 헤더와 섹터 테이블을 건드려 크래시나 파일 트래커 손상으로 이어진다.

8.2 LOG_RCV_TDES — 복구 스크래치패드

섹션 제목: “8.2 LOG_RCV_TDES — 복구 스크래치패드”

4~5장의 분석에서 이 장이 소비하는 모든 정보를 tdes->rcv(struct log_rcv_tdes, log_impl.h)에 기록해 두었다. 다섯 개의 LOG_LSA 필드이며, NULL_LSA는 해당 미결 사항이 없음을 뜻한다.

필드역할존재 이유
sysop_start_postpone_lsaLOG_SYSOP_END가 기록되지 않은 채 커밋-with-postpone 상태인 sysop의 LOG_SYSOP_START_POSTPONE해당 레코드에 재실행할 LOG_REC_SYSOP_END가 내장되어 있음(8.6).
tran_start_postpone_lsa트랜잭션의 LOG_COMMIT_WITH_POSTPONE8.6의 (c)/(d) 분기 구분, 8.7의 중단 경계
atomic_sysop_start_lsa매칭되지 않은 마지막 LOG_SYSOP_ATOMIC_START; NULL이 아니면 원자 연산 도중에 크래시 발생8.4의 롤백 대상: 하나의 단위로 undo할 로그 접미사
analysis_last_aborted_sysop_lsa분석 단계에서 중단된 마지막 sysop의 종료 LSA6장 스킵 윈도우의 상한 경계
analysis_last_aborted_sysop_start_lsa해당 sysop의 lastparent_lsa스킵 윈도우의 하한 경계; 이 장에서는 미사용

마지막 입력은 로그 자체에 남아 있는 LOG_RUN_POSTPONE 경로이며, 8.8에서 소비된다.

8.3 LOG_REC_SYSOP_START_POSTPONE — 지연된 sysop 종료 레코드

섹션 제목: “8.3 LOG_REC_SYSOP_START_POSTPONE — 지연된 sysop 종료 레코드”

postpone과 함께 커밋하는 sysop은 미래의 종료 레코드를 미리 기록해 둔다. 실제 LOG_SYSOP_END가 디스크에 기록되지 않더라도 복구 시 커밋을 완료할 수 있도록 하기 위해서다.

// log_rec_sysop_start_postpone -- src/transaction/log_record.hpp
struct log_rec_sysop_start_postpone
{
LOG_REC_SYSOP_END sysop_end; /* log record used for end of system operation */
LOG_LSA posp_lsa; /* address where the first postpone operation start */
};
필드역할존재 이유
sysop_end미리 작성된 종료 레코드; log_read_sysop_start_postpone으로 읽어 log_sysop_end_recovery_postpone으로 추가postpone 실행 전에 커밋 결정을 영구화; type 필드가 완료 후 TDES 상태를 결정(8.6).
posp_lsa이 sysop의 첫 번째 LOG_POSTPONE전방 스캔의 시작점; 분석 단계에서 tdes->topops.stack[last].posp_lsa에 복사(5장).

8.6은 내장된 LOG_REC_SYSOP_END의 네 개 필드를 읽는다(전체 표는 5장 참고): type(분기 조건), lastparent_lsa(sysop 직전의 트랜잭션 LSA — 롤백 경계), run_postpone.postpone_lsa(이 sysop이 실행한 LOG_POSTPONE — 부모의 재개 지점), run_postpone.is_sysop_postpone(sysop 부모 — 불가능한 케이스로 단언 — 대 트랜잭션).

두 드라이버 모두 동일한 골격을 공유한다. 일반 TDES 슬롯 1..num_total_indices를 순회하되 tdes == NULL || trid == NULL_TRANID인 경우를 건너뛰고, 이후 분석 단계에서 log_system_tdes::map_all_tdes로 재구성된 시스템 TDES를 처리한다(systb_Mutex 잠금). 각 호출은 log_rv_simulate_runtime_worker / log_rv_end_simulation으로 감싸져 런타임 로깅 프리미티브가 가장(impersonation)된 TDES에 작용하도록 한다(시스템 TDES의 경우 log_system_tdes::rv_simulate_system_tdes). log_recovery_abort_atomic_sysop이 TDES 하나를 처리한다.

flowchart TD
    G1{"tdes NULL 또는<br/>trid NULL?"} -- yes --> R1["return"]
    G1 -- no --> G2{"atomic_sysop_start_lsa<br/>NULL?"}
    G2 -- yes --> R1
    G2 -- no --> G3{"start &gt;= undo_nxlsa?"}
    G3 -- yes --> R3["LSA 초기화 후 return"]
    G3 -- no --> G4{"TOPOPE 및 start postpone<br/>&gt; atomic start?"}
    G4 -- yes --> N1["원자 연산 내 중첩 postpone:\n먼저 완료"]
    G4 -- no --> G5{"TOPOPE?"}
    G5 -- yes --> N2["sysop postpone 중 원자 연산:\n지금 중단"]
    G5 -- no --> N3["단독 실행"]
    N1 --> RB["시작 페이지 fetch,\nprev = prev_tranlsa"]
    N2 --> RB
    N3 --> RB
    RB --> ERR{"fetch 실패?"}
    ERR -- yes --> F["logpb_fatal_error"]
    ERR -- no --> SIM["log_sysop_start,\nlastparent_lsa = prev,\nlog_sysop_abort"]
    SIM --> DONE["atomic_sysop_start_lsa 초기화"]

Figure 8-1: log_recovery_abort_atomic_sysop의 모든 분기.

중첩 케이스는 8.5와 순서를 맞춘다. sysop_start_postpone_lsa > atomic_sysop_start_lsa이면 원자 연산 내부에서 커밋-with-postpone sysop이 발생한 것이다 — postpone을 먼저 완료한 뒤 중단한다. 반대로 TOPOPE 케이스는 sysop의 postpone 도중에 원자 연산이 시작된 것이다 — 지금 중단하고 8.5에서 postpone을 완료한다. 소스 주석에는 이 두 가지 크래시 시나리오가 번호를 붙여 명시되어 있다.

롤백은 undo를 직접 호출하지 않고 런타임 sysop을 시뮬레이션한다. 소스 주석에서 lastparent_lsa 덮어쓰기를 “hack last parent”라 부르는 이유가 여기에 있다. 새 sysop의 롤백 경계를 LOG_SYSOP_ATOMIC_START의 prev_tranlsa로 설정함으로써 log_sysop_abort가 그 이후의 모든 것을 보상하고 중단 LOG_SYSOP_END를 기록한다.

불변 조건 8-B — 원자적 잔재 없음. 반환 시 모든 TDES의 atomic_sysop_start_lsa는 NULL_LSA 상태여야 한다. 각 종료 경로는 그것이 NULL임을 확인하거나, 초기화하거나, logpb_fatal_error로 종료한다. 이후 단계는 절반만 열린 원자적 파일 연산이 존재하지 않음을 전제할 수 있다.

TDES별로 log_recovery_finish_postpone이 실행된다: (1) tdes == NULL || trid == NULL_TRANID 가드에서 반환; (2) 항상 log_recovery_finish_sysop_postpone(8.6) 호출 — TOPOPE_COMMITTED_WITH_POSTPONE 상태를 해소하며 경우에 따라 COMMITTED_WITH_POSTPONE으로 승격; (3) 상태에 따라 분기:

// log_recovery_finish_postpone -- src/transaction/log_recovery.c
if (tdes->state == TRAN_UNACTIVE_WILL_COMMIT || tdes->state == TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE)
{
if (tdes->state == TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE)
{ /* make sure to abort interrupted logical postpone. */
log_recovery_abort_interrupted_sysop (thread_p, tdes, &tdes->rcv.tran_start_postpone_lsa);
LSA_SET_NULL (&tdes->undo_nxlsa); } /* <- committed: nothing left to undo */
/* ... find_first_postpone -> log_do_postpone -> log_complete ... */
}
else if (tdes->state == TRAN_UNACTIVE_COMMITTED)
{ /* log_complete + free index only; postpones already done */ }

TRAN_UNACTIVE_WILL_COMMIT은 커밋은 기록되었으나 postpone 시작은 미기록된 상태다. COMMITTED_WITH_POSTPONE는 중단된 논리적 run postpone을 먼저 중단시킨다(8.7). 생략된 본문: log_recovery_find_first_postpone(8.8), 결과가 NULL이 아니면 log_do_postpone(8.9), 이후 로컬 트랜잭션(tdes->coord == NULL)에 한해 log_complete가 LOG_COMMIT EOT를 추가하고 TRAN_UNACTIVE_COMMITTED로 전환한 뒤 logtb_free_tran_index로 슬롯을 해제한다(2PC: 11장). 시스템 TDES는 단계 (2)만 통과하며, 완료 불가능한 interrupted sysop은 TDES를 TRAN_UNACTIVE_UNILATERALLY_ABORTED(분기 (d))로 남겨 undo(9장) 대상이 된다.

TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE 상태에서만 실행된다. 분석 단계에서 정확히 하나의 topops 항목이 push되었다(assert (tdes->topops.last == 0)). 순서: rcv.sysop_start_postpone_lsa 기준으로 중단된 논리적 run postpone 중단(8.7); topops.stack[last].posp_lsa를 시작점으로 첫 번째 미실행 postpone 탐색(8.8); log_do_postpone(8.9); log_read_sysop_start_postpone으로 start-postpone 레코드 재독취(실패 시 assert_release로 포기); 미리 작성된 end를 log_sysop_end_recovery_postpone으로 추가. 네 가지 결과:

// log_recovery_finish_sysop_postpone (outcomes) -- src/transaction/log_recovery.c
if (sysop_start_postpone.sysop_end.type == LOG_SYSOP_END_LOGICAL_RUN_POSTPONE)
{
if (sysop_start_postpone.sysop_end.run_postpone.is_sysop_postpone)
{ /* (a) sysop postpone during sysop postpone? should not happen! */
assert (false);
tdes->state = TRAN_UNACTIVE_UNILATERALLY_ABORTED; tdes->undo_nxlsa = tdes->tail_lsa; }
else
{ /* (b) logical run postpone during transaction postpone */
tdes->state = TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE;
LSA_SET_NULL (&tdes->undo_nxlsa);
tdes->posp_nxlsa = sysop_start_postpone.sysop_end.run_postpone.postpone_lsa; }
}
else if (!LSA_ISNULL (&tdes->rcv.tran_start_postpone_lsa))
{ /* (c) sysop nested in transaction postpone phase */
tdes->state = TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE; }
else
{ /* (d) standalone: hand the rest to undo (Ch 9) */
tdes->state = TRAN_UNACTIVE_UNILATERALLY_ABORTED; tdes->undo_nxlsa = tdes->tail_lsa; }

(b)는 이 sysop이 실행한 postpone 이후 부모의 postpone을 재개한다(8.3); (b)/(c)는 동일한 호출 내에서 8.5의 분기로 fall-through한다; (d)는 TDES를 undo 대기 상태로 전환한다. 방어적 클램프가 assert_release 하에서 topops.last를 -1로 초기화한다.

8.7 log_recovery_abort_interrupted_sysop — 역방향 스캔

섹션 제목: “8.7 log_recovery_abort_interrupted_sysop — 역방향 스캔”

Postpone 실행 자체가 논리적 run postpone sysop(파일 삭제/해제)을 사용할 수 있으며, sysop 도중 크래시가 발생하면 재개 전에 중단해야 할 단편이 남는다. tdes->undo_nxlsa부터 postpone_start_lsa까지 undo 체인을 역방향으로 순회한다.

  • 조기 반환: undo_nxlsa가 NULL이거나 <= postpone_start_lsa이면 중단할 것이 없다.
  • 레코드별 처리(페이지 fetch 실패 시 logpb_fatal_error, 반환):
    • LOG_RUN_POSTPONE — 물리적 run postpone 완료: 중지, last_parent_lsa = iter_lsa.
    • LOG_SYSOP_END — type == LOG_SYSOP_END_LOGICAL_RUN_POSTPONE이면 마찬가지로 중지; 그렇지 않으면 sysop_end->lastparent_lsa로 hop하여 중첩 sysop 전체를 건너뜀.
    • 그 외 — prev_lsa = logrec_head.prev_tranlsa; postpone-start 타입은 assert로 금지.
  • 루프 소진 — assert (LSA_EQ (&iter_lsa, postpone_start_lsa)); 중단된 sysop이 첫 번째 postpone 액션이었다: last_parent_lsa = *postpone_start_lsa.

이후 8.4의 시뮬레이션-sysop 트릭을 stack[last].lastparent_lsa = last_parent_lsa로 적용한다. 마지막으로 완료된 run postpone 이후의 모든 것이 보상되고, 이미 완료된 것들은 그대로 유지된다.

8.8 log_recovery_find_first_postpone — run-postpone 경로

섹션 제목: “8.8 log_recovery_find_first_postpone — run-postpone 경로”

분석 후 tdes->posp_nxlsa는 중의적이다. 분석 단계는 스캔한 모든 LOG_RUN_POSTPONE의 run_posp->ref_lsa로 이를 전진시키는데, 이는 마지막 확인된 postpone이다. 아무것도 실행되지 않았다면 LOG_COMMIT_WITH_POSTPONE의 첫 번째 postpone을 가리킨다(4장). 전방 스캔 한 번으로 이 중의성을 해소한다. 가드: 크래시 복구 외부이거나 세 가지 postpone 상태 중 어느 것도 아닌 경우 — assert (0), ER_FAILED; start_postpone_lsa가 NULL인 경우 — NO_ERROR, NULL 결과. 스캔은 log_do_postpone의 중첩-top 범위 순회 및 페이지 fetch 오류 경로(8.9)를 재사용하며, 이 trid만 검사한다.

  • LOG_RUN_POSTPONE의 ref_lsa == start_postpone_lsa — 후보가 실행됨: start_postpone_lsa_wasapplied 설정 후 종료.
  • LOG_SYSOP_END_LOGICAL_RUN_POSTPONE 타입의 LOG_SYSOP_END — run_postpone.postpone_lsa에도 같은 검사를 수행(논리적 run postpone은 LOG_RUN_POSTPONE을 기록하지 않음).
  • LOG_POSTPONE — 후보가 아닌 첫 번째 항목을 next_postpone_lsa에 기록.
  • LOG_END_OF_LOG / NULL-offset — 8.9와 동일한 아카이브 경계 페이지 전진.

꼬리 처리: 후보가 실행되지 않았으면 ret_lsa = start_postpone_lsa; 실행되었으면 ret_lsa = next_postpone_lsa(다음 LOG_POSTPONE; 남은 것이 없으면 NULL).

8.9 log_do_postpone — 공유 전방 실행기

섹션 제목: “8.9 log_do_postpone — 공유 전방 실행기”

런타임 커밋 시 postpone을 실행하는 루틴이 여기서 재실행된다. log_get_next_nested_top이 중첩 sysop 범위 스택을 구성하고, 외부 루프는 범위의 시작까지 탐색, 종료 이후 재시작, 또는 start_seek_lsa == nxtop_range->end_lsa일 때 tdes->tail_lsa까지 실행하고 중지하는 세 가지 방식으로 각 범위를 탐색한다. 이는 커밋 또는 중단 여부와 무관하게 모든 완료된 중첩 sysop의 내부를 건너뛴다. 해당 LOG_POSTPONE 레코드는 포함 postpone 단계가 아닌 sysop에 속하기 때문이다. LOG_SYSOP_END_LOGICAL_RUN_POSTPONE 종료 레코드만 스캔 범위 내에 남으며(log_get_next_nested_top은 해당 범위를 한 레코드 앞에서 종료하여 end 레코드 자체가 처리될 수 있도록 한다), 두 전방 스캐너는 페이지 fetch 오류 경로를 공유한다. logpb_fetch_page 실패 시 logpb_fatal_error를 발생시키고 end 레이블로 점프하여 힙에 할당된 nxtop_stack을 해제한다.

범위 내 디스패치: 일반 데이터/더미/복제 타입은 무시; LOG_POSTPONE는 log_run_postpone_op로 즉시 실행 — 실패 시 goto end; LOG_COMMIT_WITH_POSTPONE(및 _OBSOLETE, LOG_SYSOP_START_POSTPONE, 2PC 시작 레코드들)는 forward_lsa를 NULL로 설정 — postpone 구간 종료; LOG_SYSOP_END는 start_seek_lsa에서만 허용되며 그 외에는 잘못된 범위로 디버그 로그에 기록된다. log_run_postpone_op는 LOG_REC_REDO 페이로드를 읽어(페이지 경계를 넘어 복사; OOM 시 logpb_fatal_error) log_execute_run_postpone을 호출한다. redo 함수를 적용하고 새 LOG_RUN_POSTPONE을 기록한다 — 두 번째 크래시가 발생해도 8.8이 소비할 경로만 연장될 뿐이다.

불변 조건 8-C — postpone은 정확히 한 번 실행. posp_nxlsa 경로, 8.8의 applied 검사, 그리고 각 실행 시 새로 기록되는 LOG_RUN_POSTPONE이 조합되어 크래시 횟수에 무관하게 각 LOG_POSTPONE이 정확히 한 번만 실행됨을 보장한다. 이미 실행된 LSA로 log_do_postpone을 시드하면 페이지 해제와 같은 비멱등적 redo가 이중 적용된다.

  1. 병렬 풀이 소진된 뒤 redo 꼬리 단계는 두 가지 정리를 수행한다: 열린 원자적 sysop 중단, 이후 미완 postpone 완료(불변 조건 8-A). 입력은 tdes->rcv(LOG_RCV_TDES)와 LOG_RUN_POSTPONE 경로다.
  2. 두 드라이버 모두 일반 TDES 슬롯 순회 후 log_system_tdes::map_all_tdes로 시스템 TDES를 처리하며, log_rv_simulate_runtime_worker로 각 트랜잭션을 가장한다.
  3. 롤백은 sysop을 시뮬레이션한다 — log_sysop_start, lastparent_lsa 덮어쓰기, log_sysop_abort — 순서는 8.4의 중첩 케이스 분기에 따른다.
  4. log_recovery_finish_sysop_postpone은 내장된 LOG_REC_SYSOP_END를 재실행하여 트랜잭션 postpone 경로로 진입하거나 undo를 위한 TRAN_UNACTIVE_UNILATERALLY_ABORTED로 전환한다.
  5. 완료된 각 워커 TDES는 log_complete (LOG_COMMIT)으로 TRAN_UNACTIVE_COMMITTED 상태로 종료된다 — 2PC 참여자는 예외(11장).

Chapter 9: Undo 패스와 보상 레코드

섹션 제목: “Chapter 9: Undo 패스와 보상 레코드”

Redo 패스(6장)는 패자(loser) 트랜잭션의 변경 사항도 그대로 남겨 두었다. Undo 패스는 그것들을 되돌린다. CLR 이론은 companion 문서(cubrid-recovery-manager.md)에서 다루므로, 이 장에서는 log_recovery_undo와 log_rv_undo_record의 각 분기, 그리고 롤백을 크래시 재시작 가능하게 만드는 sysop 브래킷을 집중적으로 살펴본다.

네 가지 구조체(log_rec_undo, log_rec_mvcc_undo, log_rec_compensate, log_rec_sysop_end)는 모두 log_record.hpp에 정의되어 있으며, 로그 페이지에서 직접 읽힌다.

LOG_REC_UNDO — LOG_UNDO_DATA의 본문:

필드역할
data (LOG_DATA)rcvindex + volid/pageid/offset: RV_fun 디스패치와 페이지 fix를 위한 단일 로케이터; NULL vpid이면 RCV_IS_LOGICAL_LOG를 유발
lengthundo 이미지의 바이트 수; ZIP_CHECK 플래그를 포함

LOG_REC_MVCC_UNDO — LOG_MVCC_UNDO_DATA의 본문:

필드역할
undo (LOG_REC_UNDO)내포된 일반 undo — 완전한 상위집합; 처리 루틴은 &mvcc_undo->undo를 추출
mvccid기록자의 MVCCID; undo 중 재활성화되어 해당 버전이 보이지 않게 유지됨
vacuum_info (LOG_VACUUM_INFO)prev_mvcc_op_log_lsa 체인 + vfid — vacuum이 MVCC op 레코드를 연결하는 리스트; undo 패스는 이를 건너뜀

LOG_REC_COMPENSATE — LOG_COMPENSATE(CLR)의 본문:

필드역할
data (LOG_DATA)보상의 redo를 위한 로케이터 + rcvindex — CLR은 redo 전용이며 redofun으로 재실행됨
undo_nxlsa보상된 레코드 이전에 캡처된 다음 undo 대상 — ARIES의 UndoNxtLSA; 재시작 시 이미 완료된 undo를 건너뜀 (9.3 분기 4)
lengthafter-image 길이

LOG_REC_SYSOP_END — LOG_SYSOP_END의 본문; type으로 키를 갖는 union:

필드역할
lastparent_lsasysop 이전의 마지막 LSA — undo가 이 지점으로 점프; 커밋된 sysop은 재차 undo되지 않음
prv_topresult_lsa이전에 완료된 top action — 중첩 sysop 체이닝 (5장)
type (LOG_SYSOP_END_TYPE)union 판별자 — 하나의 레코드에 여섯 가지 종료 형식
vfid영향받는 페이지들의 파일 — 후행 undo 데이터의 TDE 결정에 사용
undo (union)LOGICAL_UNDO용 LOG_REC_UNDO — 소유 트랜잭션이 abort될 때 sysop 자체의 undo 레시피
mvcc_undo (union)LOGICAL_MVCC_UNDO용 LOG_REC_MVCC_UNDO — 위와 동일하나 MVCCID 포함
compensate_lsa (union)LOGICAL_COMPENSATE의 재개 지점 — 브래킷 자체가 보상이었던 경우
run_postpone (union)LOGICAL_RUN_POSTPONE용 postpone_lsa + is_sysop_postpone — 분석 단계의 쌍둥이 (5장); undo는 이것을 절대 만나지 않는다고 assert

9.2 log_recovery_undo — 사전 패스와 패자 선별

섹션 제목: “9.2 log_recovery_undo — 사전 패스와 패자 선별”

LOG_RECOVERY_UNDO_PHASE 하에서 log_recovery에 의해 호출된다. 사전 패스(pre-pass)는 더 이상 undo할 것이 없는 패자를 제거한다: TRAN_UNACTIVE_UNILATERALLY_ABORTED 또는 TRAN_UNACTIVE_ABORTED 상태이면서 undo_nxlsa가 NULL인 TDES는 크래시 전에 롤백을 완료했지만 LOG_ABORT가 디스크에 기록되지 않은 것이다 — log_complete (… LOG_ABORT, LOG_DONT_NEED_NEWTRID, LOG_NEED_TO_WRITE_EOT_LOG)가 지금 이를 기록하고, logtb_free_tran_index가 슬롯을 해제한다. 시스템 TDES는 EOT가 필요 없으므로 log_system_tdes::rv_delete_all_tdes_if가 undo_nxlsa가 NULL인 모든 시스템 항목을 삭제한다.

패자 선별은 logtb_rv_read_only_map_undo_tdes (log_tran_table.c)가 수행한다: read 모드 TR_TABLE_CS 하에서 해당 두 상태의 비시스템 슬롯 전체에 펑터를 맵핑하고, 이어서 log_system_tdes::map_all_tdes로 시스템 워커에도 적용한다 — max-scan 람다가 max_undo_lsa를 산출한다(log_find_unilaterally_largest_undo_lsa도 max-scan을 중복 수행하지만 현재 호출처는 없다). 드라이버는 undo_unzip_ptr = log_zip_alloc (LOGAREA_SIZE)를 할당하고 선택적 진행 타이머를 설정한 후 LOG_CS를 빠져나와 그림 9-1과 같은 루프를 수행한다(fetch는 LOG_CS_FORCE_USE; 할당·fetch 실패는 fatal).

불변조건 (전역 내림차순 undo 순서). 매 반복에서 모든 패자에 걸쳐 가장 큰 undo_nxlsa인 max_undo_lsa를 undo한다 — 레코드마다 재계산된다. 모든 분기는 커서를 엄격히 뒤로 이동시킨다(prev_tranlsa, CLR의 undo_nxlsa, 또는 sysop의 lastparent_lsa). 내부의 while (max_undo_lsa.pageid == log_lsa.pageid)는 이전 페이지를 fetch하기 전에 현재 페이지를 소진한다; 커서가 앞으로 이동하면 라이브락이 발생한다.

flowchart TD
    A["완료된 패자 정리"] --> B["max_undo_lsa = 최대 undo_nxlsa"]
    B --> C{NULL?}
    C -- yes --> Z["unzip 버퍼 해제, LOG_CS_ENTER,<br/>로그 + 헤더 + 데이터 페이지 flush"]
    C -- no --> D["페이지 fetch; 같은 pageid 동안:<br/>tdes 확인, log_rtype에 따라 분기"]
    D --> G{prev_tranlsa NULL?}
    G -- yes --> H["체인 완료: log_complete +<br/>logtb_free_tran_index 또는 rv_delete_tdes"]
    G -- no --> I["undo_nxlsa = prev_tranlsa"]
    H --> B
    I --> B

그림 9-1: 드라이버 루프.

TDES 확인은 logtb_is_system_worker_tranid에 따라 분기한다: 워커는 log_system_tdes::rv_get_tdes (NULL이면 assert); 일반 트랜잭션은 logtb_find_tran_index + LOG_FIND_TDES — 조회 실패 시(분석 단계에서 등록되지 않은 trid) logtb_free_tran_index_with_undo_lsa가 해당 undo_nxlsa를 보유한 슬롯을 정리하고 레코드를 건너뛴다. if (tran_index != NULL_TRAN_INDEX && tdes != NULL)이 switch의 게이트가 된다; 워커 경로에서 tran_index는 낡은 값이므로 tdes만 의미가 있다.

9.3 레코드 타입 switch — 모든 분기

섹션 제목: “9.3 레코드 타입 switch — 모든 분기”

모든 분기에 앞서 무조건 LSA_COPY (&tdes->undo_nxlsa, &prev_tranlsa)가 실행된다 — 이 순서 자체가 핵심이다:

불변조건 (커서가 undo 실행 전에 먼저 이동한다). log_append_compensate는 자신이 기록하는 CLR에 tdes->undo_nxlsa를 복사한다; 드라이버가 이미 prev_tranlsa로 이동시켰으므로 CLR은 다음으로 undo할 레코드를 가리킨다. 순서를 바꾸면 롤백 도중 크래시 발생 시 동일한 undo가 두 번 재실행된다.

  1. UNDOREDO 계열 (LOG_UNDOREDO_DATA, LOG_DIFF_UNDOREDO_DATA, LOG_MVCC_* 쌍둥이) — MVCC 형식은 LOG_REC_MVCC_UNDOREDO를 읽고 rcv.mvcc_id를 설정하며, 일반 형식은 LOG_REC_UNDOREDO에 MVCCID_NULL을 사용한다; 내포된 LOG_DATA + ulength로 rcv를 채우고 log_rv_undo_record를 호출한다. DIFF는 redo에서만 의미가 있다.
  2. LOG_MVCC_UNDO_DATA / LOG_UNDO_DATA — LOG_REC_MVCC_UNDO / LOG_REC_UNDO와 undo->length를 사용하는 동일한 구조.
  3. Redo 전용 / 부기(bookkeeping) 타입 — LOG_REDO_DATA, LOG_MVCC_REDO_DATA, LOG_DBEXTERN_REDO_DATA, LOG_DUMMY_HEAD_POSTPONE, LOG_POSTPONE, LOG_SAVEPOINT, LOG_REPLICATION_DATA, LOG_REPLICATION_STATEMENT, LOG_DUMMY_HA_SERVER_STATE, LOG_DUMMY_OVF_RECORD, LOG_DUMMY_GENERIC, LOG_SUPPLEMENTAL_INFO, LOG_SYSOP_ATOMIC_START: /* Not for UNDO ... */, 이전 레코드로 fall through.
  4. LOG_COMPENSATE — LSA_COPY (&prev_tranlsa, &compensate->undo_nxlsa). 실제 작업 없음 — 커서가 크래시 전에 이미 undo된 모든 레코드를 뛰어넘는다.
  5. LOG_SYSOP_END — sysop_end->type에 따라:
    • LOGICAL_UNDO / LOGICAL_MVCC_UNDO: 커밋된 브래킷이 자체 undo 레시피를 포함한다. sysop_end->undo(또는 mvcc_undo.undo + rcv.mvcc_id)로 rcv를 채운다; log_rv_undo_record 실행 전에 prev_tranlsa와 tdes->undo_nxlsa 모두 lastparent_lsa로 이동하므로, 보상 기록이 sysop 전체를 건너뛴다(rcv_lsa는 갱신되지 않으므로 진단 메시지에 낡은 LSA가 출력될 수 있다).
    • LOGICAL_COMPENSATE: prev_tranlsa = sysop_end->compensate_lsa — 브래킷이 보상했던 레코드 이전으로 재개.
    • default (COMMIT, ABORT): prev_tranlsa = sysop_end->lastparent_lsa; assert는 LOGICAL_RUN_POSTPONE이 undo 단계에 도달하지 않음을 문서화한다(8장).
  6. 종단/불법 타입 (LOG_RUN_POSTPONE, LOG_COMMIT* 트리오, LOG_SYSOP_START_POSTPONE, LOG_ABORT, 체크포인트 및 2PC 레코드, LOG_DUMMY_CRASH_RECOVERY, LOG_END_OF_LOG)과 default 분기(손상된 타입 → ER_LOG_PAGE_CORRUPTED) — 분석 단계에 오류가 있었음을 의미한다: assert (false) 이후, 릴리스 빌드는 해당 TDES를 절단한다 — tdes->mvccinfo.id를 초기화하고, log_system_tdes::rv_delete_tdes(워커) 또는 log_complete (… LOG_ABORT …) + logtb_free_tran_index를 수행하며, tdes = NULL로 설정하여 에필로그가 이를 건너뛰게 한다.

에필로그(if (tdes != NULL)): NULL prev_tranlsa는 체인의 끝을 의미한다 — tdes->mvccinfo.id를 초기화하고, 사전 패스에서와 같이 rv_delete_tdes(워커) 또는 log_complete + logtb_free_tran_index를 수행한다(#ifdef CCI_XA 빌드는 TRAN_UNACTIVE_2PC_PREPARE 상태에서 completion을 건너뜀). 그렇지 않으면 prev_tranlsa를 tdes->undo_nxlsa에 다시 기록하여, 분기 4-5가 리다이렉트했을 수 있는 커서를 재확인한다. 루프 종료 후: unzip 버퍼를 해제하고, LOG_CS에 재진입하며, 로그·헤더·데이터 페이지를 force-flush한다.

log_complete 내부에서 업데이터는 log_append_abort_log + log_change_tran_as_completed와 unlock_global_oldest_visible_mvccid를 받고, 업데이트가 없는 패자(LSA_ISNULL (&tdes->tail_lsa))는 상태만 전환된다.

9.4 log_rv_undo_record — 단일 undo 단계의 모든 분기

섹션 제목: “9.4 log_rv_undo_record — 단일 undo 단계의 모든 분기”

런타임 log_rollback_rec의 복구 단계 쌍둥이; log_rv_simulate_runtime_worker / log_rv_end_simulation으로 동작을 시뮬레이션하며 페이지 잠금은 없다. 디스패치 전 처리: (1) 유효한 rcv->mvcc_id는 logtb_rv_assign_mvccid_for_undo_recovery로 재활성화된다; (2) RCV_IS_LOGICAL_LOG (rcv_vpid, rcvindex) — NULL vpid이거나 논리적 rcvindex이면 rcv->pgptr = NULL로 남기고, 그렇지 않으면 pgbuf_fix가 무조건 write latch를 획득한다(실패 시 assert 후 허용); (3) ZIP_CHECK (rcv->length)가 압축 플래그를 제거한다; 이미지는 페이지에 맞으면 로그 페이지에서 별칭(alias)으로 참조되고, 그렇지 않으면 malloc + logpb_copy_from_log로 복사되며, 압축된 이미지는 log_unzip으로 undo_unzip_ptr에 풀린다(할당/unzip 실패는 fatal — 6장의 log_rv_get_unzip_log_data / log_rv_get_unzip_and_diff_redo_log_data 쌍둥이와 동일). 이어서 if (rcv->pgptr != NULL || RCV_IS_LOGICAL_LOG (…)) 하에서:

// log_rv_undo_record -- src/transaction/log_recovery.c
if (rcvindex == RVBT_MVCC_INCREMENTS_UPD)
{ /* nothing to do during recovery */ }
else if (rcvindex == RVBT_MVCC_NOTIFY_VACUUM || rcvindex == RVES_NOTIFY_VACUUM)
{ /* nothing to do */ }
else if (rcvindex == RVBT_LOG_GLOBAL_UNIQUE_STATS_COMMIT)
{ /* <- in-memory only: undo on every restart, cannot compensate */
error_code = (*RV_fun[rcvindex].undofun) (thread_p, rcv);
assert (error_code == NO_ERROR);
}
else if (RCV_IS_LOGICAL_COMPENSATE_MANUAL (rcvindex))
{ /* <- undofun logs its own compensation */
LSA_COPY (&rcv->reference_lsa, &tdes->undo_nxlsa);
error_code = (*RV_fun[rcvindex].undofun) (thread_p, rcv);
// ... condensed ... logpb_fatal_error on failure; optional b-tree trace
}
else if (!RCV_IS_LOGICAL_LOG (rcv_vpid, rcvindex))
{ /* <- PHYSICAL undo: CLR first, then apply before-image */
log_append_compensate (thread_p, rcvindex, rcv_vpid, rcv->offset, rcv->pgptr,
rcv->length, rcv->data, tdes);
error_code = (*RV_fun[rcvindex].undofun) (thread_p, rcv);
// ... condensed ... logpb_fatal_error on failure
}
else
{ /* <- LOGICAL undo: bracket in a system operation */
save_state = tdes->state;
LSA_COPY (&rcv->reference_lsa, &tdes->undo_nxlsa);
log_sysop_start (thread_p);
(void) (*RV_fun[rcvindex].undofun) (thread_p, rcv);
log_sysop_end_logical_compensate (thread_p, &rcv->reference_lsa);
tdes->state = save_state;
}

페이지 fix에 실패한 물리적 레코드(guard의 else 경로)도 log_append_compensate에서 pgptr = NULL로 CLR을 기록하여 체인의 재시작 가능성을 유지한다 — 그리고 해당 볼륨을 명시하는 ER_LOG_MAYNEED_MEDIA_RECOVERY를 발생시킨다; undofun은 건너뛰고 복구는 계속된다(log-and-skip). end: 레이블에서 영역을 해제하고, 페이지 fix를 풀고, log_rv_end_simulation을 호출한다.

불변조건 (모든 undo 단계는 실행 전 또는 실행 중에 기록된다). 물리적 undo는 undofun 전에 CLR을 기록한다; 논리적 undo는 먼저 log_sysop_start를 열어 모든 페이지 변경이 브래킷 내에 포함되도록 하고, log_sysop_end_logical_compensate로 compensate_lsa = rcv->reference_lsa를 설정하여 봉인한다. 브래킷 내부에서 크래시: 분석 단계가 sysop을 abort하고(8장), undo는 원래 레코드에서 재개된다. 봉인 후 크래시: LOGICAL_COMPENSATE 분기가 compensate_lsa로 점프한다. 어느 경우든 논리적 undo는 정확히 한 번 실행된다.

recovery.h는 수동(manual) 집합을 정의한다: 더 넓은 RCV_IS_LOGICAL_COMPENSATE_MANUAL (추가로 RVFL_ALLOC, RVFL_USER_PAGE_MARK_DELETE, RVPGBUF_DEALLOC, RVFL_TRACKER_HEAP_REUSE, RVHF_LOB_REMOVE_DIR, RVFL_TRACKER_UNREGISTER) 안에 포함된 RCV_IS_BTREE_LOGICAL_LOG(열 개의 RVBT_* 객체 수준 연산). 이들의 undofun은 저장된 rcv->reference_lsa로 log_append_compensate_with_undo_nxlsa를 호출해 페이지 수준 보상을 직접 추가한다 — b-tree undo는 보상 전에 페이지 분할이나 병합을 수행할 수 있으므로 별도의 브래킷은 불필요하다.

9.5 log_append_compensate — CLR 기록자

섹션 제목: “9.5 log_append_compensate — CLR 기록자”

log_append_compensate와 log_append_compensate_with_undo_nxlsa는 log_append_compensate_internal (log_manager.c)을 래핑한다; 후자는 b-tree 케이스를 위해 명시적 undo_nxlsa를 전달하고, 전자는 NULL을 전달한다:

// log_append_compensate_internal -- src/transaction/log_manager.c
// ... condensed ... node = prior_lsa_alloc_and_copy_data (.., LOG_COMPENSATE, ..); NULL -> silent return
LSA_COPY (&prev_lsa, &tdes->undo_nxlsa); /* <- next record to undo, saved */
compensate = (LOG_REC_COMPENSATE *) node->data_header;
// ... condensed ... fill compensate->data; store the undo_nxlsa parameter
// into compensate->undo_nxlsa if non-NULL (b-tree override), else prev_lsa
start_lsa = prior_lsa_next_record (thread_p, node, tdes);
// ... condensed ... pgbuf_set_lsa (pgptr, start_lsa) when pgptr != NULL
/* Go back to our undo link */
LSA_COPY (&tdes->undo_nxlsa, &prev_lsa); /* <- CLR must not become next undo target */

분기 처리: prior_lsa_alloc_and_copy_data 실패 시 조용히 반환한다 — undo는 기록 없이 진행되며, undo_nxlsa가 해당 레코드를 지나쳐 이동하지 않았으므로 재크래시 시 단순히 다시 undo된다(before-image를 재적용하는 것은 무해하다). NULL pgptr(미디어 경로, 9.4)은 TDE 마킹과 pgbuf_set_lsa를 건너뛴다; pgbuf_set_lsa 실패 시 assert 후 반환한다. 마지막 줄이 핵심이다: prior_lsa_next_record가 tail_lsa와 함께 undo_nxlsa를 앞으로 끌어당기므로, prev_lsa를 복원하여 롤백 커서를 CLR 뒤에 유지한다 — 헤더 주석에서 명시하듯 CLR은 “절대 undo되지 않는다.”

  1. 사전 패스는 undo_nxlsa가 NULL인 패자를 제거한다 — log_complete가 누락된 LOG_ABORT를 기록하고, logtb_free_tran_index가 슬롯을 해제하며, rv_delete_all_tdes_if가 완료된 시스템 TDES를 정리한다.
  2. 드라이버는 항상 전역적으로 가장 큰 undo_nxlsa를 undo한다(logtb_rv_read_only_map_undo_tdes로 레코드마다 재계산): 엄격히 역방향으로 진행되는 페이지 단위 스윕.
  3. tdes->undo_nxlsa는 undo 실행 전에 이동하므로 모든 CLR이 올바른 재개 지점을 가진다; log_append_compensate_internal은 추가 후 이를 복원하여 CLR이 절대 undo되지 않도록 한다 — undo가 undo를 undo하지 않으므로 패스는 반복 크래시에 걸쳐 멱등성(idempotent)을 갖는다.
  4. LOG_COMPENSATE와 LOG_SYSOP_END는 undo 중에 순수한 커서 리다이렉션이다(compensate->undo_nxlsa, lastparent_lsa / compensate_lsa) — 크래시된 롤백은 중복 없이 재개된다.
  5. log_rv_undo_record는 RCV_IS_LOGICAL_LOG에 따라 분기한다: 물리적 undo = CLR 후 undofun; 논리적 undo = sysop 브래킷(log_sysop_start부터 log_sysop_end_logical_compensate)으로 분석 단계가 미완료 시 abort하고 undo는 봉인 시 건너뛴다; RCV_IS_LOGICAL_COMPENSATE_MANUAL undofun은 수동으로 보상을 기록한다.
  6. 유일하게 허용되는 실패는 fix 불가능한 데이터 페이지뿐이다 — CLR은 (pgptr = NULL로) 여전히 기록되고 ER_LOG_MAYNEED_MEDIA_RECOVERY가 발생한다; 그 외 모든 것은 logpb_fatal_error다. CLR이 디스크에 있는 상태에서 절반만 적용된 undo는 다음 재시작 시 거짓말을 하기 때문이다.

redo, undo, 보상 재실행, logdump 출력 — 이 모든 경로는 결국 하나의 전역 배열로 귀결된다: recovery.c의 RV_fun[]이다. 6장, 7장, 9장의 드라이버들은 힙이나 B-트리의 의미론을 알지 못한다. 그들이 아는 것은 오직 올바른 함수 포인터를 찾는 방법뿐이다. 이 챕터는 엔트리 레이아웃, 인덱스-위치 동치 불변식, NULL 암(arm), 공유 packed-change 기계장치를 다룬다. 이론적 배경은 상위 레벨 동반 문서(“CUBRID의 ARIES”, “복구 함수 디스패치”)에서 다룬다.

각 슬롯은 struct rvfun(recovery.h)이다:

// rvfun -- src/transaction/recovery.h
struct rvfun
{
using fun_t = int (*)(THREAD_ENTRY * thread_p, LOG_RCV * logrcv);
using dump_fun_t = void (*)(FILE * fp, int length, void *data);
LOG_RCVINDEX recv_index; /* For verification */
const char *recv_string;
fun_t undofun;
fun_t redofun;
dump_fun_t dump_undofun;
dump_fun_t dump_redofun;
};
필드역할존재 이유
recv_index이 슬롯이 주장하는 LOG_RCVINDEXrv_check_rvfuns가 슬롯 위치와 비교
recv_string출력용 이름 ("RVHF_INSERT")logdump 및 치명적 오류 시 rv_rcvindex_string으로 조회
undofun롤백, undo 패스 (9장), LOG_COMPENSATE의 redoCLR 페이로드는 undo 방향; NULL = undo 데이터를 로그에 기록하지 않음
redofunredo 패스 (6/7장), run-postpone (8장)NULL = redo 데이터를 로그에 기록하지 않음 (undo 전용 논리 레코드)
dump_undofun디버그 출력기, undo 페이로드logdump 전용, log_dump_data로 호출
dump_redofun디버그 출력기, redo 페이로드NULL = 페이로드 포맷 없음

RV_fun[]은 집합(aggregate) 초기화자로 구성되며, LOG_RCVINDEX마다 리터럴 하나씩, RVDK_NEWVOL(undo 암 NULL — 볼륨 생성은 redo 전용)부터 RVHF_LOB_REMOVE_DIR까지 이어진다. 암은 흔히 거울 쌍으로 구성된다(RVDK_UNRESERVE_SECTORS: undo는 disk_rv_reserve_sectors, redo는 disk_rv_unreserve_sectors).

flowchart LR
    REDO["redo + run-postpone Ch 6-8"] --> R["redofun"]
    CLR["LOG_COMPENSATE replay"] --> U["undofun"]
    UNDO["undo + rollback Ch 9"] --> U
    DUMP["logdump"] --> D["dump arms"]

Figure 10-1: 각 rvfun 암의 소비자. 보상(compensate) 재실행은 undofun으로 교차된다.

교차 배선은 log_rv_get_fun<LOG_REC_COMPENSATE>(log_recovery_redo.hpp)에 명시되어 있다: 함수 본문이 return RV_fun[rcvindex].undofun; — 주석은 // yes, undo. 따라서 RVBT_RECORD_MODIFY_COMPENSATE는 btree_rv_redo_record_modify를 undofun으로 등록하고 redo는 NULL로 둔다. CLR 페이로드가 redo 형식이며 undofun으로만 재실행되기 때문이다.

LOG_RCVINDEX(recovery.h)는 명시적으로 번호가 매겨진 열거형으로, RVDK_NEWVOL = 0부터 RVHF_LOB_REMOVE_DIR = 129까지, 그리고 두 개의 특수 값으로 닫힌다: RV_LAST_LOGID = RVHF_LOB_REMOVE_DIR(별칭, 슬롯 아님)과 RV_NOT_DEFINED = 999(센티넬; 절대 RV_fun을 인덱싱해서는 안 됨). 헤드 주석은 새 엔트리를 맨 아래에 추가하도록 명시한다. “구 데이터베이스가 구 파일로 복구되는 사태를 AVOID하기 위해서”다.

불변식(테이블 순서): [0, DIM(RV_fun))의 모든 i에서 RV_fun[i].recv_index == i. 온디스크 로그 레코드의 rcvindex가 곧 배열 첨자다 — 디스패치는 검사 없는 배열 로드다. 검사는 시작 시 한 번, 디버그 빌드에서만 실행된다:

// rv_check_rvfuns -- src/transaction/recovery.c
for (i = 0; i < num_indices; i++) /* num_indices = DIM (RV_fun) */
if (RV_fun[i].recv_index != i)
{
// ... condensed: er_log_debug "out of sequence" ...
er_set (ER_FATAL_ERROR_SEVERITY, ARG_FILE_LINE, ER_GENERIC_ERROR, 0);
assert (false);
break; /* <- first mismatch only; one insertion shifts all later slots */
}

분기 회계: 루프 하나, 조건 하나 — 일치하면 통과, 불일치하면 로그를 남기고 fatal-severity 오류를 발생시키며 assert 후 break한다. 함수와 호출 지점은 NDEBUG 아래서 사라진다(호출은 log_initialize_internal, log_manager.c에서 열린다). 릴리즈 빌드에서 순서가 잘못된 테이블은 아무것도 잡아내지 못한다 — 복구는 각 페이로드에 전혀 다른 인덱스의 함수를 적용하게 된다.

rv_rcvindex_string은 불변식을 신뢰한다: 전체 본문이 return RV_fun[rcvindex].recv_string; — 경계 검사 없음. 따라서 RV_NOT_DEFINED가 이 함수에 도달해서는 안 된다. (낡은 recovery.c 헤더 주석은 여전히 새 이름 추가를 위해 rv_rcvindex_string()을 참조하도록 안내하고 있다.)

NULL 암은 복구 시점의 폴백이 아니라 로깅에 관한 계약이다: 이 rcvindex를 가진 레코드는 해당 방향의 데이터를 절대 포함하지 않는다. 강제는 append 시점, CUBRID_DEBUG 블록 안(log_manager.c)에서 이루어진다: log_append_undoredo_crumbs는 두 암 모두 비-NULL임을 assert하고, log_append_undo_crumbs는 undofun만, log_append_redo_crumbs는 redofun만 확인한다. rollback은 assert (RV_fun[rcvindex].undofun != NULL)을 추가한다. 복구 시점에는 log_rv_redo_record만 방어적이다 — NULL redofun이면 경고 로그를 남기는 데 그친다 — 반면 log_rv_undo_record는 NULL 검사 없이 암을 호출한다: append 시점의 계약이 유일한 안전망이다.

dump 암은 (FILE *, int length, void *data)를 받는다 — 페이지 없이 페이로드만 — logdump가 오프라인으로 실행되기 때문이다. 출력기는 범용(log_rv_dump_char, log_rv_dump_hexa)이거나 서브시스템별 디코더(disk_rv_dump_hdr)다.

10.4 인덱스 패밀리와 RCV_IS_* 매크로 오버레이

섹션 제목: “10.4 인덱스 패밀리와 RCV_IS_* 매크로 오버레이”

접두사가 소유 서브시스템을 나타낸다. append-only 규칙에 따라 후기 추가분은 패밀리와 무관하게 124–129에 흩어진다: RVDK_* 0–9 (disk) · RVFL_* 10–32, 128 (file mgr; 128 = TDE) · RVHF_* 33–53, 126, 129 (heap) · RVOVF_* 54–57 (overflow) · RVEH_* 58–65 (ext hash) · RVBT_* 66–91, 124–125 (b-tree; 124–125 = online index) · RVCT_* 92–96 (catalog) · RVLOG_* 97 (log no-op) · RVREPL_* 98–103 (replication; HA 전송용, 페이지 복구 아님) · RVVAC_* 104–117 (vacuum) · RVES_* 118 (external storage) · RVLOC_* 119 (locator dummy) · RVPGBUF_* 120–123, 127 (page buffer; 127 = TDE).

RCV_IS_* 매크로들(1장)은 두 번째 축이다: 인덱스 값이 함수를 선택하고, 인덱스의 매크로 소속이 log_rv_undo_record의 여섯 단계 사다리(9장)에서 호출 주변의 프로토콜을 선택한다. RCV_IS_LOGICAL_COMPENSATE_MANUAL에 속하는 인덱스(RCV_IS_BTREE_LOGICAL_LOG가 공급)는 tdes->undo_nxlsa로부터 rcv->reference_lsa를 미리 적재하고 undofun이 자체 CLR을 로깅한다. RCV_IS_LOGICAL_LOG를 통과하지 못하는 인덱스는 드라이버 측에서 log_append_compensate가 먼저 실행된다. RCV_IS_LOGICAL_RUN_POSTPONE_MANUAL은 postpone 유사체다(8장).

불변식(매크로/테이블 정합성): RCV_IS_* 매크로에 명시된 모든 인덱스는 해당 매크로가 라우팅하는 프로토콜에 맞는 내부 로깅을 수행하는 암을 유지해야 한다. 이를 기계적으로 검사하는 장치는 없다. RCV_IS_LOGICAL_COMPENSATE_MANUAL에 속한 인덱스의 undofun이 CLR을 로깅하지 않으면 undo_nxlsa가 같은 레코드를 가리키는 상태가 유지된다: 무한 롤백 루프, 또는 다음 재시작 시 CLR 누락으로 인한 크래시다.

슬롯 페이지 엔트리 다수는 전체 레코드 대신 스플라이스(splice) 시퀀스를 로깅한다. 스플라이스 단위 하나는 short offset_to_data | byte A | byte B | payload, INT_ALIGNMENT로 패딩되며, log_recovery.c의 패커가 생성한다:

// log_rv_pack_redo_record_changes -- src/transaction/log_recovery.c
assert (offset_to_data >= 0 && offset_to_data <= 0x8FFF);
/* <- intends flag bits clear; mask 0xC000 needs <= 0x3FFF, so 0x8FFF looks like a source typo */
// ... condensed: asserts both sizes <= 255 (single wire bytes); PTR_ALIGN to INT_ALIGNMENT ...
OR_PUT_SHORT (ptr, (short) offset_to_data); ptr += OR_SHORT_SIZE;
OR_PUT_BYTE (ptr, (INT16) old_data_size); ptr += OR_BYTE_SIZE;
OR_PUT_BYTE (ptr, (INT16) new_data_size); ptr += OR_BYTE_SIZE;
if (new_data_size > 0)
{ memcpy (ptr, new_data, new_data_size); ptr += new_data_size; }
// ... condensed: trailing PTR_ALIGN ...

log_rv_pack_undo_record_changes는 정확히 두 가지만 다르다: OR_PUT_BYTE 두 줄의 순서가 뒤바뀐다 — new_data_size 먼저 — 그리고 memcpy 페이로드가 old_data이며 old_data_size > 0으로 가드된다. 이 비대칭성이 핵심 트릭이다:

와이어 필드redo 데이터에서undo 데이터에서
offset_to_data스플라이스 위치스플라이스 위치 (동일)
byte A (“remove size”)런타임 old size런타임 new size — undo가 제거하는 바이트 수
byte B (“insert size”)런타임 new size런타임 old size — undo가 복원하는 바이트 수
payloadnew dataold data

패커가 사전에 스왑하므로, 방향 플래그 없이 하나의 인터프리터가 양방향을 모두 처리한다.

10.6 인터프리터: 순방향 재실행과 역방향 역적용

섹션 제목: “10.6 인터프리터: 순방향 재실행과 역방향 역적용”

log_rv_undoredo_record_partial_changes는 페이로드를 OR_BUF로 감싸고 재귀 코어를 호출하는 세 개의 assert 래퍼다. undo는 스플라이스를 로그의 역순으로 적용해야 하기 때문이다 — 각 offset_to_data는 해당 스플라이스가 처리하던 시점의 레코드 상태를 기준으로 계산되었다:

// log_rv_undoredo_partial_changes_recursive -- src/transaction/log_recovery.c
if (rcv_buf->ptr == rcv_buf->endptr)
return NO_ERROR; /* (1) clean termination */
if (rcv_buf->ptr + OR_SHORT_SIZE + 2 * OR_BYTE_SIZE > rcv_buf->endptr)
{ assert_release (false); return ER_TF_BUFFER_OVERFLOW; } /* (2) truncated unit */
offset_to_data = (int) or_get_short (rcv_buf, &error_code); /* (3,4,5) per-field errors */
// ... condensed: old_data_size, new_data_size; each returns error_code on failure ...
if (new_data_size > 0)
{ new_data = rcv_buf->ptr;
error_code = or_advance (rcv_buf, new_data_size); /* (6) payload overruns buffer */ }
else
new_data = NULL; /* (7) pure deletion splice */
or_align (rcv_buf, INT_ALIGNMENT); /* <- mirrors packer's PTR_ALIGN */
if (!is_undo)
RECORD_REPLACE_DATA (record, offset_to_data, old_data_size, new_data_size, new_data);
error_code = log_rv_undoredo_partial_changes_recursive (thread_p, rcv_buf, record, is_undo);
if (error_code != NO_ERROR)
{ assert_release (false); return error_code; } /* (8) deeper error skips this splice */
if (is_undo)
RECORD_REPLACE_DATA (record, offset_to_data, old_data_size, new_data_size, new_data);
return NO_ERROR;

(7)은 정상적인 경우다. RECORD_REPLACE_DATA(storage_common.h)는 insert size가 0이면 memcpy를 건너뛰기 때문이다.

flowchart TD
    A["스플라이스 i 파싱"] --> B{"is_undo?"}
    B -- "no" --> C["스플라이스 i 적용 후 i+1로 재귀"]
    B -- "yes" --> D["i+1로 재귀 후 언와인드에서 스플라이스 i 적용"]

Figure 10-2: redo는 재귀 전에 적용하고, undo는 언와인드 시 적용한다 — 이로써 순서 역전이 자동으로 이루어진다.

10.7 log_rv_record_modify_internal과 얇은 래퍼들

섹션 제목: “10.7 log_rv_record_modify_internal과 얇은 래퍼들”

범용 레코드 수정자는 rcv->offset에 숨겨진 두 개의 플래그 비트를 읽는다(LOG_RV_RECORD_SET_MODIFY_MODE, 마스크 LOG_RV_RECORD_MODIFY_MASK = 0xC000, log_append.hpp; §10.5의 0x8FFF assert는 이 비트를 보호하려는 의도지만, 플래그에 안전한 경계는 0x3FFF여야 한다):

flags의미Redo 동작Undo 동작
LOG_RV_RECORD_INSERT (0x8000)레코드 삽입됨spage_insert_atspage_delete
LOG_RV_RECORD_DELETE (0x4000)레코드 삭제됨spage_deletespage_insert_at
LOG_RV_RECORD_UPDATE_ALL (0xC000)전체 교체spage_updatespage_update (암별 페이로드)
LOG_RV_RECORD_UPDATE_PARTIAL (0x0000)스플라이스 체인순방향 스플라이스 후 spage_update역방향 스플라이스 후 spage_update
// log_rv_record_modify_internal -- src/transaction/log_recovery.c
INT16 flags = rcv->offset & LOG_RV_RECORD_MODIFY_MASK;
PGSLOTID slotid = rcv->offset & (~LOG_RV_RECORD_MODIFY_MASK);
if ((!is_undo && LOG_RV_RECORD_IS_INSERT (flags)) || (is_undo && LOG_RV_RECORD_IS_DELETE (flags)))
{ /* ... condensed: unpack type byte + body; spage_insert_at ... */ }
else if ((!is_undo && LOG_RV_RECORD_IS_DELETE (flags)) || (is_undo && LOG_RV_RECORD_IS_INSERT (flags)))
{ /* ... condensed: spage_delete ... */ }
else if (LOG_RV_RECORD_IS_UPDATE_ALL (flags))
{ /* ... condensed: unpack type + body; spage_update ... */ }
else
{
assert (LOG_RV_RECORD_IS_UPDATE_PARTIAL (flags));
// ... condensed: spage_get_record (..., COPY); /* <- splice on a private copy */
// log_rv_undoredo_record_partial_changes (..., is_undo); spage_update ...
}
pgbuf_set_dirty (thread_p, rcv->pgptr, DONT_FREE); /* <- every success path lands here */
return NO_ERROR;

네 개의 암은 상호 배타적이고 전체를 포괄한다. 실패 경로는 모두 dirty 마크 이전에 assert_release (false) + ER_FAILED를 거치므로, 실패한 암이 반만 적용된 페이지를 광고하는 일은 없다. (한 가지 흠: UPDATE_PARTIAL 암의 실패한 spage_update는 error_code를 반환하지만 — 그 시점엔 여전히 NO_ERROR — assert되기는 하나 전파되지 않는다.) log_rv_redo_record_modify / log_rv_undo_record_modify는 is_undo를 각각 false/true로 바인딩하는 한 줄짜리 래퍼로, 테이블에 두 개의 구별된 포인터를 제공한다.

B-트리 인덱스 RVBT_RECORD_MODIFY_UNDOREDO / _NO_UNDO / _COMPENSATE는 범용 래퍼 대신 btree_rv_redo_record_modify / btree_rv_undo_record_modify(btree.c)를 등록한다. 내부의 btree_rv_record_modify_internal은 이 사다리를 복제하되(더 넓은 BTREE_RV_FLAGS_MASK, 동일한 log_rv_undoredo_record_partial_changes 호출), 노드 헤더 관리가 추가된다.

10.8 등록 절차 — 새 LOG_RCVINDEX 추가하기

섹션 제목: “10.8 등록 절차 — 새 LOG_RCVINDEX 추가하기”
  1. 열거자를 LOG_RCVINDEX의 맨 아래에 추가하고, RV_LAST_LOGID를 재지정한다. 재번호 매기기는 절대 금지 — 값이 온디스크 로그에 영구 저장된다.
  2. 대응하는 rvfun 리터럴을 RV_fun[]의 마지막 슬롯에 추가한다. string은 열거자 이름 그대로.
  3. 로깅 방식에 따라 암을 선택한다: undoredo에는 둘 다, redo/postpone 전용에는 redo만, 논리 undo 또는 보상 재실행에는 undo만(§10.1의 교차 배선).
  4. 해당 인덱스가 수동 보상/postpone 프로토콜을 필요로 하면 올바른 RCV_IS_* 매크로에 추가하고 그 프로토콜을 구현한다(§10.4 불변식).
  5. assert를 켠 채로 빌드하고 부팅한다: rv_check_rvfuns가 유일한 기계적 검사다. 건너뛰거나 잘못 배치된 슬롯은 여기서 치명적 오류로 죽는다. 릴리즈 빌드는 오류 슬롯 이후의 모든 레코드에 잘못된 함수를 적용한다.
  1. RV_fun[]은 검사 없는 배열 로드로 온디스크 rcvindex를 함수 포인터에 매핑한다. recv_index == position이라는 불변식은 디버그 시작 시 rv_check_rvfuns만이 검사하며, 모든 패스에서 하중을 지탱한다. 열거형은 번호가 로그에 영구 보존되므로 append-only다.
  2. NULL 암은 로깅 계약을 인코딩하며 append 시점에 폴리싱된다. 복구는 대부분 이를 신뢰한다.
  3. redo 패스는 LOG_COMPENSATE를 undofun 슬롯으로 재실행한다. 따라서 compensate 전용 인덱스는 redo 방향 함수를 undofun으로 등록한다.
  4. RCV_IS_* 매크로는 log_rv_undo_record(9장)의 undofun 호출을 감싸는 보상/postpone 프로토콜을 선택한다.
  5. packed splice 포맷은 방향에 무관하다 — undo 패커가 size 바이트를 사전 스왑하고 old data를 저장한다 — 따라서 하나의 인터프리터가 redo는 순방향으로, undo는 언와인드 역방향으로 재실행한다. log_rv_record_modify_internal은 그 위에 네 단계 사다리를 쌓으며, insert/delete 암은 undo 아래서 스왑된다. B-트리는 범용 래퍼를 등록하는 대신 이 기계장치를 복제한다.

주 크래시-재시작 생애주기 외의 경로: 특정 시점 복원, 로그 절단, 복원 후 아카이브/볼륨 폐기, 추가 지점 수리, 실행 컨텍스트 shim, 그리고 2PC 인계.

두 log_recovery 매개변수가 모든 것을 제어한다: ismedia_crash와 stopat (상위 문서, “Restart orchestrator”):

// log_recovery -- src/transaction/log_recovery.c
if (ismedia_crash != false)
{
/* Media crash, we may have to start from an older checkpoint... check disk headers */
(void) fileio_map_mounted (thread_p, (bool (*)(THREAD_ENTRY *, VOLID, void *)) log_rv_find_checkpoint, &rcv_lsa);
}
else
{
// ... condensed ...
if (stopat != NULL)
{
*stopat = -1; /* <- normal restart: point-in-time target forcibly disabled */
}
}

불변 조건 — 불완전 복구는 미디어 크래시 경로에서만 발생한다. 일반 재시작은 stopat을 무효화하며, 미디어 복구 범위 밖에서 깨진 페이지가 발견되면 절단이 아닌 치명적 오류로 처리된다 — 그렇지 않으면 분석이 커밋된 트랜잭션을 조용히 소멸시킬 수 있기 때문이다. log_rv_find_checkpoint는 복원된 볼륨들 중 가장 오래된 체크포인트를 선택한다.

분석 시점에 로그를 절단하는 세 가지 트리거가 있으며, 모두 *did_incom_recovery = true를 설정한 채 log_recovery_resetlog로 수렴한다:

  1. 목표 시점보다 새로운 커밋/중단. 미디어 크래시가 없는 경우, log_rv_analysis_complete는 goto end로 점프해 트랜잭션 인덱스를 해제한다(챕터 4). 미디어 크래시 경로에서는 LOG_REC_DONETIME을 읽고, *stop_at이 설정된 상태에서 difftime (*stop_at, last_at_time) < 0이면 보유 중인 페이지를 해제하고(log_lsa->pageid = NULL_PAGEID), 레코드의 헤더 LSA를 기준으로 log_recovery_resetlog를 호출한다 — 새 로그는 너무 새로운 레코드 이전에서 끝난다 — 그리고 NO_ERROR를 반환한다.

  2. 목표 시점보다 새로운 commit-with-postpone. log_rv_analysis_commit_with_postpone는 if (is_media_crash) 분기 내에서 LOG_REC_START_POSTPONE.at_time에 동일한 difftime 검사를 수행한다; else 분기는 챕터 5의 일반적인 처리이다. 미디어 분기에서 시간 검사를 통과하면 아무것도 하지 않는다.

  3. 물리적으로 깨진 페이지. log_recovery_analysis 루프에서 logpb_fetch_page가 실패하면, 미디어 분기는 last_at_time을 *stop_at에 저장하고, 마지막 트랜잭션의 tail_lsa/undo_nxlsa를 log_rec->prev_tranlsa로 되감으며(반쪽만 기록된 레코드는 undo되지 않는다), 이전 페이지를 다시 가져오고(실패 시 치명적: “reset log is impossible”), prev_lsa/prev_prev_lsa로 log_recovery_resetlog를 호출하고, log_Gl.mvcc_table.reset_start_mvccid()를 재설정한 후 반환한다. 미디어 크래시가 아닌 분기는 치명적 오류다 — er_errid()가 ER_TDE_CIPHER_IS_NOT_LOADED일 때는 TDE 전용 메시지가 표시된다.

네 번째, 좀 더 완만한 수리: 레코드의 forward LSA가 NULL이면서 log_rtype != LOG_END_OF_LOG이고 *did_incom_recovery == false일 때, 분석 루프는 log_Gl.hdr.append_lsa를 인자로 log_startof_nxrec(§11.5)을 호출하고, log_rec->forw_lsa를 패치하며, 페이지를 재기록(logpb_write_page_to_disk)하고, 같은 블록에서 log_Gl.hdr.next_trid = tran_id를 설정한다; 실패 시 추가 지점은 end_redo_lsa로 폴백한다(“we may destroy a record”).

11.2 log_recovery_resetlog — 절단과 재초기화

섹션 제목: “11.2 log_recovery_resetlog — 절단과 재초기화”

log_recovery_resetlog는 역사를 다시 쓰는 유일한 함수이다. LOG_CS_OWN_WRITE_MODE와 NULL이 아닌 new_prev_lsa를 단언한 후 여섯 단계를 실행한다:

  1. 현재 내용을 플러시한다. log_Gl.append.vdes != NULL_VOLDES이고 추가 페이지가 보유 중이면: logpb_flush_pages_direct + logpb_invalid_all_append_pages.
  2. 새 추가 LSA를 결정한다. NULL new_append_lsa → 헤더를 0|0에서 재시작한다. 그렇지 않으면, 활성 로그가 없거나 페이지 중간 오프셋에서 과거 시점으로 재설정하는 경우, 추가 페이지를 저장해 살아남은 접두사가 재생성된 로그에 전달되도록 한다:
// log_recovery_resetlog -- src/transaction/log_recovery.c
if (log_Gl.append.vdes == NULL_VOLDES
|| (log_Gl.hdr.fpageid > new_append_lsa->pageid && new_append_lsa->offset > 0))
{
// ... condensed ... (rationale comment)
newappend_pgptr = (LOG_PAGE *) aligned_newappend_pgbuf;
if ((logpb_fetch_page (thread_p, new_append_lsa, LOG_CS_FORCE_USE, newappend_pgptr)) != NO_ERROR)
{
newappend_pgptr = NULL; /* <- tolerated: the page copy is best-effort */
}
}
LOG_RESET_APPEND_LSA (new_append_lsa);
  1. 헤더 상태를 초기화한다. chkpt_lsa = append_lsa (절단된 꼬리가 새 체크포인트가 된다), is_shutdown = false, logpb_invalidate_pool.
  2. 두 가지 처리 방식. log_Gl.append.vdes == NULL_VOLDES || log_Gl.hdr.fpageid > log_Gl.hdr.append_lsa.pageid인 경우 — 활성 로그가 없거나 추가 지점이 활성 범위 이전으로 이동한 경우 — 로그를 재구축한다: arv_num = logpb_get_archive_number(추가 페이지 - 1) + 1이 불필요한 첫 번째 아카이브를 지정한다(-1은 치명적); log_recovery_notpartof_archives(§11.3)가 거기서부터 위를 제거한다(이유는 strdup으로 복사, 실패 시 원본 폴백); 헤더는 로그가 여기서 시작된 것처럼 재작성된다: fpageid = nxarv_pageid = append_lsa.pageid, nxarv_num = arv_num, last_arv_num_for_syscrashes = last_deleted_arv_num = -1. 활성 로그 파일이 없으면 재생성 — disk_get_db_creation, fileio_format, logpb_create_header_page, logpb_flush_page, 실패 시 치명적 — 어느 경우든 새 첫 번째 추가 페이지가 생성되어 플러시된다. 그 외에는 새 추가 페이지를 지나쳤을 경우에만 nxarv_pageid를 낮춘다.
  3. 추가 페이지를 재초기화한다. logpb_fetch_start_append_page; 성공 시 2단계에서 저장한 이미지를 가져온 버퍼에 memcpy하고, 더티로 표시한 후 직접 플러시한다. logpb_fetch_start_append_page가 실패하면 복원 및 플러시를 조용히 건너뛴다 — 오류가 발생하지 않는다 — 그리고 마무리는 계속 진행된다.
  4. 마무리. LOG_RESET_PREV_LSA (new_prev_lsa); mvcc_op_log_lsa.set_null()과 vacuum_last_blockid = 0으로 vacuum을 절단된 범위에서 분리한다; was_active_log_reset = true; logpb_flush_header; logpb_decache_archive_info.

불변 조건 — resetlog 이후, 위치 관련 헤더 필드는 모두 새 추가 LSA 이하를 가리킨다. chkpt_lsa, fpageid, nxarv_pageid, prev_lsa, vacuum 앵커 모두 하나의 LOG_CS 임계 구역 안에서 재작성된다; 하나라도 빠지면 vacuum이나 아카이버가 절단된 페이지를 뒤쫓게 된다.

start_arv_num부터 그 이상의 아카이브는 절단된 페이지를 기술한다. 활성 로그(신뢰할 수 있는 헤더)가 마운트되어 있는지 여부에 따라 두 가지 스캔 방식이 선택된다:

// log_recovery_notpartof_archives -- src/transaction/log_recovery.c
if (log_Gl.append.vdes != NULL_VOLDES)
{
/* Trust the current log header */
// ... condensed ... (unformat archives start_arv_num .. nxarv_num - 1)
}
else
{
/* We don't know where to stop. Stop when an archive is not in the OS */
for (i = start_arv_num; i <= INT_MAX; i++)
{
fileio_make_log_archive_name (logarv_name, log_Archive_path, log_Prefix, i);
if (fileio_is_volume_exist (logarv_name) == false)
{
// ... condensed ... /* <- rebuild name of archive i-1, the LAST removed */
break;
}
fileio_unformat (thread_p, logarv_name);
}
}

info_reason이 NULL이 아니고 적어도 하나의 아카이브가 제거된 경우(start_arv_num != i), log_dump_log_info로 로그 정보 파일에 REMOVE ... REASON 행이 기록된다(단수/범위 형식 분기); ER_LOG_MOUNT_FAIL 이외의 오류는 헤더 업데이트 전에 조기 반환한다(파일은 이미 삭제된 상태다). 마지막으로 log_Gl.hdr.last_deleted_arv_num = (start_arv_num == i) ? i : i - 1(아무 작업도 하지 않은 호출에서도 설정된다 — 하나의 특이점); 헤더는 활성 로그가 마운트되어 있을 때만 플러시된다; logpb_decache_archive_info는 호출자에게 맡겨진다.

did_incom_recovery가 설정된 경우, 드라이버는 undo 패스 이후 log_recovery_notpartof_volumes를 호출한다. 경계는 start_volid = boot_find_next_permanent_volid(thread_p), 즉 복원된 카탈로그가 모르는 첫 번째 볼륨 ID이다. 두 차례 스윕을 수행한다:

스윕 1 — 이미 마운트된 볼륨. fileio_map_mounted가 마운트된 볼륨마다 log_unformat_ahead_volumes를 실행한다: volid != NULL_VOLID && volid >= *start_volid이면, 먼저 버퍼 풀 페이지를 드롭하고(pgbuf_invalidate_all, 오래된 더티 페이지가 나중에 플러시되지 않도록), 볼륨을 fileio_unformat하고 레이블을 해제한다. 무효화에 실패하면 콜백이 false를 반환해 맵 순회를 조기 중단하며; 나머지는 스윕 2로 넘어간다.

스윕 2 — 디스크에 남아 있는 볼륨. 확장 이름의 후보들을 start_volid부터 LOG_MAX_DBVOLID까지 탐색하되, 첫 번째 누락된 이름에서 중단한다. 각 후보를 마운트하고, disk_get_db_creation으로 생성 시각을 읽고, 마운트 해제한다; difftime(vol_dbcreation, log_Gl.hdr.db_creation) == 0인 경우에만 unformat한다. db_creation 타임스탬프가 동일성 검사 기준이다 — 관계없는 데이터베이스에 같은 이름의 볼륨이 있어도 의도적으로 건드리지 않는다. 존재하지만 마운트에 실패한 후보(vdes == NULL_VOLDES)는 조용히 건너뛰며 — 절대 unformat하지 않는다. 확장 디렉터리는 log_Db_fullname에서 유도된다(malloc 또는 fileio_get_directory_path 실패 시 빈 문자열로 폴백). 이후 logpb_recreate_volume_info가 볼륨 정보 파일을 재구축한다.

log_startof_nxrec은 다음 레코드가 어디서 시작하는지를 답한다. 분석 단계에서(§11.1, 수리 4) 마지막 레코드의 forw_lsa가 NULL이지만 레코드 자체는 완전할 때 이 함수를 사용한다. 분기:

  • NULL 입력 LSA → NULL 반환; logpb_fetch_page 실패 → goto error. lsa->offset == NULL_OFFSET (아카이브가 레코드 중간에서 절단된 페이지) → log_pgptr->hdr.offset, 즉 페이지가 알고 있는 첫 번째 레코드를 채택한다; 그래도 NULL이면 오류.
  • canuse_forwaddr == true → log_rec->forw_lsa를 취한다; NULL이지만 페이지가 아카이브에 있다면, 다음 레코드는 pageid + 1에만 있을 수 있다(불완전한 레코드가 아카이브되고 나중에 완성된 경우). 그래도 NULL이면 수동 스캔으로 넘어간다.
  • 수동 스캔: LOG_RECORD_HEADER를 지나 switch(type)으로 타입별 헤더와 모든 가변 페이로드를 건너뛴다 — undo/redo 이미지(GET_ZIP_LEN 디코딩), postpone/compensate 길이, 체크포인트 배열, savepoint 이름, 2PC 및 복제 페이로드, sysop 계열의 조건부 내장 undo 이미지(챕터 5); 고정 크기 마커는 그냥 break. 에필로그 LOG_READ_ADVANCE_WHEN_DOESNT_FIT은 다른 레코드 헤더가 맞지 않을 때 다음 페이지로 올림한다.
  • 두 가지 특이점: LOG_SUPPLEMENTAL_INFO에는 break가 없어 마커 그룹으로 폴스루한다 — 해당 케이스들이 아무것도 하지 않기 때문에 현재는 무해하다; LOG_END_OF_LOG는 assert(false) — end-of-log 이후 레코드를 요청하는 호출자는 없다.

Undo, postpone, sysop-abort 코드는 thread_p->tran_index 또는 연결된 시스템 tdes의 현재 트랜잭션을 기대한다. 복구 스레드는 LOG_SYSTEM_TRAN_INDEX를 소유하면서 다른 트랜잭션의 체인을 순회하므로, tdes별 작업마다 shim 쌍으로 감싼다:

// log_rv_simulate_runtime_worker -- src/transaction/log_recovery.c
if (tdes->is_active_worker_transaction ())
{
thread_p->tran_index = tdes->tran_index; /* <- runtime code now sees this tdes as "mine" */
// ... condensed ... (SA_MODE: mirror via LOG_SET_CURRENT_TRAN_INDEX)
}
else if (tdes->is_system_worker_transaction ())
{
log_system_tdes::rv_simulate_system_tdes (tdes->trid); /* <- attach system tdes to thread */
}
else
{
assert (false);
}
// log_rv_end_simulation -- src/transaction/log_recovery.c
thread_p->reset_system_tdes ();
thread_p->tran_index = LOG_SYSTEM_TRAN_INDEX; /* <- unconditional restore */
// ... condensed ... (SA_MODE: mirror restore)

두 shim 모두 SA-mode 전역 미러를 유지한다(#if defined (SA_MODE) 아래 LOG_SET_CURRENT_TRAN_INDEX). 시스템 워커 트랜잭션(챕터 4에서 재구축한 log_system_tdes 집합)의 경우 rv_simulate_system_tdes가 systb_System_tdes에서 trid를 찾아(미스 시 단언) set_system_tdes로 설치한다.

불변 조건 — 모든 simulate는 반드시 end와 쌍을 이루며, 트랜잭션 사이에 스레드는 LOG_SYSTEM_TRAN_INDEX로 복귀한다. log_rv_undo_record(챕터 9)는 end: 레이블 이후 쌍을 닫으므로 오류 경로도 스레드를 복원한다; log_recovery_finish_all_postpone와 log_recovery_abort_all_atomic_sysops(챕터 8)는 tdes별 람다로 감싸고 진입 시 tran_index == LOG_SYSTEM_TRAN_INDEX를 단언한다. end가 빠지면 오래된 시스템 tdes가 연결된 채로 남아 엉뚱한 트랜잭션을 위해 로깅하게 된다.

undo와 (필요 시) log_recovery_notpartof_volumes 이후, 드라이버는 분산 미결 항목의 수를 세다:

// log_recovery -- src/transaction/log_recovery.c
(void) logtb_set_num_loose_end_trans (thread_p);
/* Try to finish any 2PC blocked transactions */
if (log_Gl.trantable.num_coord_loose_end_indices > 0 || log_Gl.trantable.num_prepared_loose_end_indices > 0)
{
log_Gl.rcv_phase = LOG_RECOVERY_FINISH_2PC_PHASE;
// ... condensed ...
log_2pc_recovery (thread_p);
/* Check number of loose end transactions again.. */
// ... condensed ... (reset rcv_tdes, re-bind tran index)
(void) logtb_set_num_loose_end_trans (thread_p);
}

logtb_set_num_loose_end_trans는 TR_TABLE_CS_ENTER 하에서 두 카운터를 0으로 초기화하고, 유효한 trid를 가진 모든 비시스템 tdes를 logtb_set_loose_end_tdes로 순회한다: LOG_ISTRAN_2PC_PREPARE는 isloose_end를 설정하고 num_prepared_loose_end_indices를 증가시키며(in-doubt 참여자; 락을 유지한다); LOG_ISTRAN_2PC_IN_SECOND_PHASE 또는 TRAN_UNACTIVE_2PC_COLLECTING_PARTICIPANT_VOTES는 num_coord_loose_end_indices를 증가시킨다(코디네이터가 자신의 결정을 재구동한다). 드라이버는 반환된 합계가 아닌 두 전역 변수를 기준으로 판단한다.

log_2pc_recovery는 테이블을 순회하면서 tdes == NULL, NULL_TRANID, !LOG_ISTRAN_2PC(tdes)를 건너뛰고 tdes->state에 따라 분기한다: 투표 수집 중이면 미결정 코디네이터를 중단시키고, 중단/커밋 결정이면 그 결정을 재실행하며, TRAN_UNACTIVE_WILL_COMMIT / TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE는 [[fallthrough]]로 참여자에게 통보한다 — 로컬 postpone은 이미 완료되어 있다(챕터 8). 투표 메커니즘은 2PC 문서와 상위 문서의 “Transaction table with loose-end annotations”에서 다루며, 여기서는 인계 조건만 중요하다: 분석 이후 코디네이터 또는 prepared loose end가 남아 있을 때만 네 번째 페이즈가 실행된다. 결정을 받지 못한 prepared 참여자는 in-doubt 상태로 합당하게 남을 수 있다 — 따라서 assert-zero가 아닌 재집계를 수행한다.

  1. 불완전 복구는 미디어 크래시에서만 발생한다: 일반 재시작은 *stopat = -1을 강제하고, 미디어 복구 범위 밖에서 깨진 페이지는 절단이 아닌 치명적 오류로 처리한다.
  2. 로그를 절단하는 세 가지 트리거가 있다 — stopat보다 새로운 완료 또는 commit-with-postpone, 또는 읽을 수 없는 페이지 — 모두 log_recovery_resetlog로 수렴한다; end-of-log 누락은 log_startof_nxrec으로 패치한다.
  3. log_recovery_resetlog는 하나의 LOG_CS 구역 안에서 모든 위치 관련 헤더 필드를 재작성하고, 아카이브 제거는 log_recovery_notpartof_archives에 위임한다.
  4. 볼륨 폐기는 두 단계로 이루어지며 db_creation으로 동일성을 검사한다; 버퍼 페이지는 unformat 전에 무효화된다; 마운트 실패는 건너뛴다.
  5. log_startof_nxrec은 레코드 타입별로 길이를 하나씩 순회한다 — 새 페이로드 레이아웃이 추가되면 새 switch 분기가 필요하다; LOG_SUPPLEMENTAL_INFO의 break 누락은 우연히 무해할 뿐이다.
  6. simulate/end shim은 런타임 코드가 수정 없이 실행될 수 있도록 스레드를 worker 또는 시스템 tdes에 바인딩한다; 쌍 구조는 구조적이며, 두 shim 모두 SA-mode 미러를 유지한다.
  7. LOG_RECOVERY_FINISH_2PC_PHASE는 코디네이터 또는 prepared loose end가 살아남을 때만 실행된다; prepared 참여자는 in-doubt 상태로 남을 수 있다.

아래 줄 번호는 2026-06-23 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.

SymbolFileLine
vacuum_notify_server_crashedsrc/query/vacuum.c7570
btree_rv_record_modify_internalsrc/storage/btree.c29757
NULL_OFFSETsrc/storage/storage_common.h49
RECORD_REPLACE_DATAsrc/storage/storage_common.h231
log_2pc_recovery_analysis_infosrc/transaction/log_2pc.c2029
log_2pc_recoverysrc/transaction/log_2pc.c2303
LOG_RV_RECORD_MODIFY_MASKsrc/transaction/log_append.hpp139
LOG_PAGE_INIT_VALUEsrc/transaction/log_common_impl.h46
log_zipsrc/transaction/log_compress.c45
log_unzipsrc/transaction/log_compress.c112
log_diffsrc/transaction/log_compress.c176
log_zip_realloc_if_neededsrc/transaction/log_compress.c203
log_zip_allocsrc/transaction/log_compress.c238
log_zip_freesrc/transaction/log_compress.c279
GET_ZIP_LENsrc/transaction/log_compress.h36
ZIP_CHECKsrc/transaction/log_compress.h39
log_zipsrc/transaction/log_compress.h53
LOG_ISTRAN_2PCsrc/transaction/log_impl.h173
LOG_HAS_LOGGING_BEEN_IGNOREDsrc/transaction/log_impl.h190
log_rcv_tdessrc/transaction/log_impl.h458
log_recvphasesrc/transaction/log_impl.h625
log_cs_access_modesrc/transaction/log_impl.h923
log_initialize_internalsrc/transaction/log_manager.c1100
log_append_compensatesrc/transaction/log_manager.c2985
log_append_compensate_with_undo_nxlsasrc/transaction/log_manager.c3011
log_append_compensate_internalsrc/transaction/log_manager.c3047
log_sysop_end_recovery_postponesrc/transaction/log_manager.c4024
log_completesrc/transaction/log_manager.c5653
log_rollback_recordsrc/transaction/log_manager.c7349
log_get_next_nested_topsrc/transaction/log_manager.c8023
log_do_postponesrc/transaction/log_manager.c8237
log_run_postpone_opsrc/transaction/log_manager.c8481
log_execute_run_postponesrc/transaction/log_manager.c8543
log_read_sysop_start_postponesrc/transaction/log_manager.c9962
LOGPB_IS_ARCHIVE_PAGEsrc/transaction/log_page_buffer.c155
logpb_page_has_valid_checksumsrc/transaction/log_page_buffer.c523
logpb_fetch_pagesrc/transaction/log_page_buffer.c1739
logpb_copy_pagesrc/transaction/log_page_buffer.c1871
logpb_read_page_from_filesrc/transaction/log_page_buffer.c2003
logpb_fetch_start_append_pagesrc/transaction/log_page_buffer.c2504
logpb_page_get_first_null_block_lsasrc/transaction/log_page_buffer.c3190
logpb_is_page_in_archivesrc/transaction/log_page_buffer.c4994
logpb_copy_from_logsrc/transaction/log_page_buffer.c6532
logpb_checkpointsrc/transaction/log_page_buffer.c6877
logpb_page_check_corruptionsrc/transaction/log_page_buffer.c11508
log_readersrc/transaction/log_reader.hpp36
log_reader::set_lsa_and_fetch_pagesrc/transaction/log_reader.hpp162
LOG_READ_ALIGNsrc/transaction/log_reader.hpp315
log_rec_undosrc/transaction/log_record.hpp176
log_vacuum_infosrc/transaction/log_record.hpp192
log_rec_mvcc_undosrc/transaction/log_record.hpp211
log_rec_compensatesrc/transaction/log_record.hpp262
log_sysop_end_typesrc/transaction/log_record.hpp285
log_rec_sysop_endsrc/transaction/log_record.hpp305
log_rec_sysop_start_postponesrc/transaction/log_record.hpp328
log_rec_chkptsrc/transaction/log_record.hpp345
log_info_chkpt_transsrc/transaction/log_record.hpp354
log_info_chkpt_sysopsrc/transaction/log_record.hpp372
log_rv_undo_recordsrc/transaction/log_recovery.c163
log_rv_redo_recordsrc/transaction/log_recovery.c430
log_rv_fix_page_and_check_redo_is_neededsrc/transaction/log_recovery.c494
log_rv_need_sync_redosrc/transaction/log_recovery.c541
log_rv_find_checkpointsrc/transaction/log_recovery.c579
log_rv_get_unzip_log_datasrc/transaction/log_recovery.c609
log_rv_get_unzip_and_diff_redo_log_datasrc/transaction/log_recovery.c699
log_recoverysrc/transaction/log_recovery.c736
log_rv_analysis_undo_redosrc/transaction/log_recovery.c965
log_rv_analysis_dummy_head_postponesrc/transaction/log_recovery.c1000
log_rv_analysis_postponesrc/transaction/log_recovery.c1042
log_rv_analysis_run_postponesrc/transaction/log_recovery.c1086
log_rv_analysis_compensatesrc/transaction/log_recovery.c1181
log_rv_analysis_commit_with_postponesrc/transaction/log_recovery.c1230
log_rv_analysis_commit_with_postpone_obsoletesrc/transaction/log_recovery.c1315
log_rv_analysis_sysop_start_postponesrc/transaction/log_recovery.c1365
log_rv_analysis_atomic_sysop_startsrc/transaction/log_recovery.c1472
log_rv_analysis_completesrc/transaction/log_recovery.c1509
log_rv_analysis_sysop_endsrc/transaction/log_recovery.c1612
log_rv_analysis_start_checkpointsrc/transaction/log_recovery.c1797
log_rv_analysis_end_checkpointsrc/transaction/log_recovery.c1830
log_rv_analysis_save_pointsrc/transaction/log_recovery.c2077
log_rv_analysis_2pc_preparesrc/transaction/log_recovery.c2114
log_rv_analysis_2pc_startsrc/transaction/log_recovery.c2153
log_rv_analysis_2pc_commit_decisionsrc/transaction/log_recovery.c2190
log_rv_analysis_2pc_abort_decisionsrc/transaction/log_recovery.c2224
log_rv_analysis_2pc_commit_inform_particpssrc/transaction/log_recovery.c2258
log_rv_analysis_2pc_abort_inform_particpssrc/transaction/log_recovery.c2293
log_rv_analysis_2pc_recv_acksrc/transaction/log_recovery.c2328
log_rv_analysis_log_endsrc/transaction/log_recovery.c2355
log_rv_analysis_recordsrc/transaction/log_recovery.c2378
log_is_page_of_record_brokensrc/transaction/log_recovery.c2518
log_recovery_analysissrc/transaction/log_recovery.c2587
log_recovery_needs_skip_logical_redosrc/transaction/log_recovery.c3153
log_recovery_get_redo_parallel_countsrc/transaction/log_recovery.c3197
log_recovery_redosrc/transaction/log_recovery.c3251
BUILD_RECORD_INFOsrc/transaction/log_recovery.c3468
INVOKE_REDO_RECORDsrc/transaction/log_recovery.c3471
log_recovery_abort_interrupted_sysopsrc/transaction/log_recovery.c3960
log_recovery_finish_sysop_postponesrc/transaction/log_recovery.c4064
log_recovery_finish_postponesrc/transaction/log_recovery.c4174
log_recovery_finish_all_postponesrc/transaction/log_recovery.c4243
log_recovery_abort_all_atomic_sysopssrc/transaction/log_recovery.c4280
log_recovery_abort_atomic_sysopsrc/transaction/log_recovery.c4317
log_recovery_undosrc/transaction/log_recovery.c4418
log_recovery_notpartof_archivessrc/transaction/log_recovery.c4997
log_unformat_ahead_volumessrc/transaction/log_recovery.c5100
log_recovery_notpartof_volumessrc/transaction/log_recovery.c5132
log_recovery_resetlogsrc/transaction/log_recovery.c5221
log_startof_nxrecsrc/transaction/log_recovery.c5414
log_recovery_find_first_postponesrc/transaction/log_recovery.c5793
log_rv_undoredo_partial_changes_recursivesrc/transaction/log_recovery.c6048
log_rv_undoredo_record_partial_changessrc/transaction/log_recovery.c6144
log_rv_redo_record_modifysrc/transaction/log_recovery.c6173
log_rv_undo_record_modifysrc/transaction/log_recovery.c6191
log_rv_record_modify_internalsrc/transaction/log_recovery.c6210
log_rv_pack_redo_record_changessrc/transaction/log_recovery.c6310
log_rv_pack_undo_record_changessrc/transaction/log_recovery.c6352
log_rv_redo_fix_pagesrc/transaction/log_recovery.c6390
log_rv_simulate_runtime_workersrc/transaction/log_recovery.c6417
log_rv_end_simulationsrc/transaction/log_recovery.c6438
log_cnt_pages_containing_lsasrc/transaction/log_recovery.c6449
log_find_unilaterally_largest_undo_lsasrc/transaction/log_recovery.c6470
vpid_lsa_consistency_check::checksrc/transaction/log_recovery_redo.cpp28
log_rv_redo_context::log_rv_redo_contextsrc/transaction/log_recovery_redo.cpp52
log_rv_redo_contextsrc/transaction/log_recovery_redo.hpp33
log_rv_redo_rec_infosrc/transaction/log_recovery_redo.hpp53
log_rv_get_log_rec_datasrc/transaction/log_recovery_redo.hpp112
log_rv_get_log_rec_mvccidsrc/transaction/log_recovery_redo.hpp163
log_rv_get_log_rec_vpidsrc/transaction/log_recovery_redo.hpp206
log_rv_get_log_rec_redo_lengthsrc/transaction/log_recovery_redo.hpp273
log_rv_get_log_rec_offsetsrc/transaction/log_recovery_redo.hpp316
log_rv_get_funsrc/transaction/log_recovery_redo.hpp359
log_rv_get_fun<LOG_REC_COMPENSATE>src/transaction/log_recovery_redo.hpp396
log_rv_get_funsrc/transaction/log_recovery_redo.hpp396
log_rv_get_log_rec_redo_datasrc/transaction/log_recovery_redo.hpp457
vpid_lsa_consistency_checksrc/transaction/log_recovery_redo.hpp558
log_rv_redo_record_syncsrc/transaction/log_recovery_redo.hpp587
redo_tasksrc/transaction/log_recovery_redo_parallel.cpp99
redo_task::executesrc/transaction/log_recovery_redo_parallel.cpp221
redo_parallel::addsrc/transaction/log_recovery_redo_parallel.cpp626
redo_parallel::wait_for_termination_and_stop_executionsrc/transaction/log_recovery_redo_parallel.cpp635
redo_parallel::wait_past_target_lsasrc/transaction/log_recovery_redo_parallel.cpp728
redo_job_impl::executesrc/transaction/log_recovery_redo_parallel.cpp752
reusable_jobs_stack::blocking_popsrc/transaction/log_recovery_redo_parallel.cpp868
redo_parallelsrc/transaction/log_recovery_redo_parallel.hpp55
task_active_state_bookkeepingsrc/transaction/log_recovery_redo_parallel.hpp100
min_unapplied_log_lsa_monitoringsrc/transaction/log_recovery_redo_parallel.hpp131
redo_job_basesrc/transaction/log_recovery_redo_parallel.hpp215
redo_job_implsrc/transaction/log_recovery_redo_parallel.hpp269
reusable_jobs_stacksrc/transaction/log_recovery_redo_parallel.hpp306
log_rv_redo_record_sync_or_dispatch_asyncsrc/transaction/log_recovery_redo_parallel.hpp382
perf_statssrc/transaction/log_recovery_redo_perf.hpp105
log_system_tdes::rv_simulate_system_tdessrc/transaction/log_system_tran.cpp174
log_system_tdes::map_all_tdessrc/transaction/log_system_tran.cpp253
log_system_tdes::rv_delete_all_tdes_ifsrc/transaction/log_system_tran.cpp265
log_system_tdes::rv_delete_tdessrc/transaction/log_system_tran.cpp281
logtb_rv_find_allocate_tran_indexsrc/transaction/log_tran_table.c1056
logtb_rv_assign_mvccid_for_undo_recoverysrc/transaction/log_tran_table.c1115
logtb_free_tran_indexsrc/transaction/log_tran_table.c1202
logtb_free_tran_index_with_undo_lsasrc/transaction/log_tran_table.c1281
logtb_set_loose_end_tdessrc/transaction/log_tran_table.c4124
logtb_set_num_loose_end_transsrc/transaction/log_tran_table.c4170
logtb_rv_read_only_map_undo_tdessrc/transaction/log_tran_table.c4204
mvcctable::reset_start_mvccidsrc/transaction/mvcc_table.cpp600
RV_funsrc/transaction/recovery.c54
rv_rcvindex_stringsrc/transaction/recovery.c857
rv_check_rvfunssrc/transaction/recovery.c872
LOG_RCVINDEXsrc/transaction/recovery.h36
log_rcvsrc/transaction/recovery.h197
rvfunsrc/transaction/recovery.h221
RCV_IS_BTREE_LOGICAL_LOGsrc/transaction/recovery.h241
RCV_IS_LOGICAL_COMPENSATE_MANUALsrc/transaction/recovery.h253
RCV_IS_LOGICAL_RUN_POSTPONE_MANUALsrc/transaction/recovery.h261
RCV_IS_LOGICAL_LOGsrc/transaction/recovery.h267
  • cubrid-recovery-manager.md — 상위 수준 동반 문서. 함께 참고할 문서로 cubrid-log-manager-detail.md (재생되는 레코드가 어떻게 append되는지)와 cubrid-checkpoint.md (재시작 앵커 역할을 하는 checkpoint) 가 있다.
  • 원시 분석 자료: raw/code-analysis/cubrid/storage/recovery_manager/ 하위 파일들.
  • 코드: src/transaction/log_recovery.{c,h}, log_recovery_redo.{cpp,hpp}, log_recovery_redo_parallel.{cpp,hpp}, recovery.{c,h}.
  • 방법론: knowledge/methodology/code-analysis-detail-doc.md.