콘텐츠로 이동

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.datarcv.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 rvfunRV_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_datarcv.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_pageconst 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_dataLOG_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 */
필드역할존재 이유
mtxcheckcleanup을 직렬화맵(log_Gl_recovery_redo_consistency_check 전역)이 모든 적용자에서 접근됨
consistency_check_map페이지 단위 기준선 — vpid_log_lsa_map_tvpid_key_t = (volid, pageid)를 해당 페이지에 처음 적용된 LSA에 매핑; emplace는 기존 키를 덮어쓰지 않으므로 기준선이 전진하지 않음assert는 이후의 모든 적용이 기준선 LSA보다 높은 LSA를 가져야 함을 요구 — 완전한 쌍별 단조성(pairwise monotonicity)보다 약한 조건이지만(나중 두 적용의 순서 뒤바뀜은 통과), 첫 번째 적용보다 오래된 이미지는 잡아낼 수 있음

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

LOG_RCV_TDES (log_tdesrcv 필드, 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_SYSOPLOG_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_unziprcv.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. 이들의 undofunrcv.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_DIRRVBT_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는 유일한 호출 규약이다; datapgptr은 빌림(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_crashedrcv_lsavacuum_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_recordLOG_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_readerLOG_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를 발생시키며, 버퍼 히트 시에는 memcpylog_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_pageOLD_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_pagedo_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_pageskip 경유)뿐이며, 위임 멤버들은 패밀리 기본값인 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의 offsetNULL_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_freelog_zip_free_data를 실행한 뒤 구조체를 해제한다. redo 컨텍스트는 두 zip 버퍼를 미리 할당하고(섹션 2.6), undo 패스는 log_zip_alloc(LOGAREA_SIZE)undo_unzip_ptr를 할당하여 log_recovery_undo의 모든 종료 경로에서 해제한다. 공유 소비자 log_rv_get_unzip_log_dataZIP_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_fileLOGPB_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_lsalsa, 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_nxlsalog_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_corruptionlogpb_page_has_valid_checksum(CRC32 대 hdr.checksum)을 래핑한다; 헬퍼 오류는 치명적이다. 손상된 아카이브 페이지는 치명적(/* Should not happen. */ — 아카이브는 한 번만 기록됨); 손상된 활성 페이지는 부분 페이지 플러시를 의미한다. logpb_page_get_first_null_block_lsa가 티어 위치를 찾는다: null_blockmemcmp 결과가 같은 첫 번째 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_lsaprev_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.pageidLSA_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_atlog_recovery에서 온다 — 일반 재시작에서는 -1(제한 없음), 미디어 크래시에서는 restoredb -d 타임스탬프. 드라이버 자체는 커밋 시각을 직접 읽지 않는다 — log_rv_analysis_completeLOG_COMMIT / LOG_ABORT를 처리하며(log_rv_analysis_commit_with_postponeLOG_REC_START_POSTPONEat_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].tridlogtb_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_lsaprev_lsa를 깨진 레코드 위에 파킹하여 resetlog가 그 위치에서 자른다.
  5. redo 범위는 정직하다(불변 조건 3-B): end_redo_lsa 앞의 모든 것은 읽을 수 있고 완전하며; 경계는 완전히 탐색됐거나, redo가 해당 위치를 읽기 전에 재초기화된다.
  6. 시점 지정 복원은 log_rv_analysis_complete(LOG_REC_DONETIME)에 있고 드라이버에는 없다; 분석은 읽기 전용도 아니다 — 누락된 LOG_END_OF_LOGlog_startof_nxrec, forw_lsa 패치, 페이지 플러시로 물리적 복구된다.

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

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

Chapter 3의 드라이버는 읽어 들인 모든 LOG_RECORD_HEADERlog_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로 매핑하며, 처음 발견할 때 할당한다. 세 가지 분기가 있다: B1logtb_is_system_worker_tranid (trid)log_system_tdes::rv_get_or_alloc_tdes로 단락(short-circuit)하여 시스템 워커를 테이블 밖에 유지한다. B2logtb_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_compensateLOG_COMPENSATE — 어떤 업데이트가 이미 undo되었다는 증거인 CLR(Compensation Log Record) — 를 처리하며, undo_nxlsatail_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_COMMITLOG_ABORTlog_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단계); ntransntops — 뒤따르는 두 배열의 항목 수로, 배열은 자기 기술(self-delimiting)이 아니다(ntops는 흔히 0이다).

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

필드역할존재 이유
isloose_endtdes->isloose_end로 복사클라이언트 loose end
trid트랜잭션 ID할당자의 병합 키
state스냅샷 시점 상태; TRAN_ACTIVETRAN_UNACTIVE_ABORTEDTRAN_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_usertdes->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_checkpointmay_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_lsaLOG_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_nxlsatail_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_lsaLOG_START_CHKPT에 의해 활성화된 경우에만 병합된다 — 반쯤 만들어진 또는 추가적인 체크포인트 윈도우는 구조적으로 무시된다. logtb_clear_tdes-후-덮어쓰기 병합은 logpb_checkpoint가 테이블을 스냅샷하고 END를 단일 prior_lsa_mutex 보유 하에 append하기 때문에 안전하다.
  7. 전역 카운터가 함께 간다: LOG_END_OF_LOG는 append point와 next_trid를 재설정한다. MVCCID는 트랜잭션별로 재구성되지 않는다 — reset_start_mvccidlog_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_TDESrcv 필드로 내장된다. 복구 단계 바깥에서는 다섯 필드 모두 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_nxlsalog_recovery_find_first_postpone(8장)의 스캔 시작점이다.

log_rv_analysis_run_postponeLOG_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_POSTPONEref_lsa로 전진시킨다. 뒤처지면 → 8장이 동일 postpone을 두 번 실행하고; 너무 앞서면 → 미처리 작업이 조용히 유실된다.

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

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

log_rv_analysis_commit_with_postponeLOG_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_OBSOLETEat_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_lsaposp_lsatopops.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_sysopslog_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.last0 유지); 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_postponetran_start_postpone_lsa를 기록하고 미디어 크래시 시 시점 복구 중단 역할도 겸한다; 구버전 변형은 타임스탬프가 없다는 점만 다르다.
  3. log_rv_analysis_sysop_start_postponesysop_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_lsalog_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_logrecreinterpret_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_countMAX (16, system_core_count) — 는 SERVER_MODE에서(Chapter 7) reusable_jobscublog::redo_parallel의 크기를 결정한다. SA 모드에서는 parallel_recovery_redonullptr로 유지되어 모든 적용이 동기적으로 이루어진다. 루프 진입 전 방어 코드: 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이 아닌 redofuncperfmon_counter_timer_raii_tracker(PSTAT_LOG_REDO_FUNC_EXEC) 하에 실행되며, 실패 시 -> logpb_fatal_error; null redofunc -> er_log_debug 경고만; null이 아닌 rcv.pgptr에는 pgbuf_set_lsam_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_unziplog_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_diffMIN (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_redoREGISTER_WORKERPOOL로 풀 수요를 등록하고, 순방향 스캔 전에 한 번 생성한다: reusable_jobs.initialize (count)new cublog::redo_parallel (count, false, MAX_LSA, redo_context)를 순서대로 호출한다. falseMAX_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::taskredo_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_terminationm_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_lsaset_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는 각 워커의 통계셋과 요소별 평균을 로깅한다 — EXECUTEPOP 비율로 포화도를 파악하고, DIRECTWAIT 비율로 풀 스로틀링 여부를 확인할 수 있다.

  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_monitoringperf_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_STARTprev_tranlsa로 설정함으로써 log_sysop_abort가 그 이후의 모든 것을 보상하고 중단 LOG_SYSOP_END를 기록한다.

불변 조건 8-B — 원자적 잔재 없음. 반환 시 모든 TDES의 atomic_sysop_start_lsaNULL_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_completeLOG_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_ENDtype == 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_POSTPONErun_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_POSTPONEref_lsa == start_postpone_lsa — 후보가 실행됨: start_postpone_lsa_wasapplied 설정 후 종료.
  • LOG_SYSOP_END_LOGICAL_RUN_POSTPONE 타입의 LOG_SYSOP_ENDrun_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_POSTPONElog_run_postpone_op즉시 실행 — 실패 시 goto end; LOG_COMMIT_WITH_POSTPONE(및 _OBSOLETE, LOG_SYSOP_START_POSTPONE, 2PC 시작 레코드들)는 forward_lsa를 NULL로 설정 — postpone 구간 종료; LOG_SYSOP_ENDstart_seek_lsa에서만 허용되며 그 외에는 잘못된 범위로 디버그 로그에 기록된다. log_run_postpone_opLOG_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_undolog_rv_undo_record의 각 분기, 그리고 롤백을 크래시 재시작 가능하게 만드는 sysop 브래킷을 집중적으로 살펴본다.

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

LOG_REC_UNDOLOG_UNDO_DATA의 본문:

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

LOG_REC_MVCC_UNDOLOG_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_COMPENSATELOG_COMPENSATE(CLR)의 본문:

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

LOG_REC_SYSOP_ENDLOG_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_UNDOLOG_REC_UNDO — 소유 트랜잭션이 abort될 때 sysop 자체의 undo 레시피
mvcc_undo (union)LOGICAL_MVCC_UNDOLOG_REC_MVCC_UNDO — 위와 동일하나 MVCCID 포함
compensate_lsa (union)LOGICAL_COMPENSATE의 재개 지점 — 브래킷 자체가 보상이었던 경우
run_postpone (union)LOGICAL_RUN_POSTPONEpostpone_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_ifundo_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_nxlsamax_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_UNDOREDOMVCCID_NULL을 사용한다; 내포된 LOG_DATA + ulengthrcv를 채우고 log_rv_undo_record를 호출한다. DIFF는 redo에서만 의미가 있다.
  2. LOG_MVCC_UNDO_DATA / LOG_UNDO_DATALOG_REC_MVCC_UNDO / LOG_REC_UNDOundo->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_COMPENSATELSA_COPY (&prev_tranlsa, &compensate->undo_nxlsa). 실제 작업 없음 — 커서가 크래시 전에 이미 undo된 모든 레코드를 뛰어넘는다.
  5. LOG_SYSOP_ENDsysop_end->type에 따라:
    • LOGICAL_UNDO / LOGICAL_MVCC_UNDO: 커밋된 브래킷이 자체 undo 레시피를 포함한다. sysop_end->undo(또는 mvcc_undo.undo + rcv.mvcc_id)로 rcv를 채운다; log_rv_undo_record 실행 전에 prev_tranlsatdes->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; assertLOGICAL_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_tranlsatdes->undo_nxlsa에 다시 기록하여, 분기 4-5가 리다이렉트했을 수 있는 커서를 재확인한다. 루프 종료 후: unzip 버퍼를 해제하고, LOG_CS에 재진입하며, 로그·헤더·데이터 페이지를 force-flush한다.

log_complete 내부에서 업데이터는 log_append_abort_log + log_change_tran_as_completedunlock_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_idlogtb_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_compensatecompensate_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_lsalog_append_compensate_with_undo_nxlsa를 호출해 페이지 수준 보상을 직접 추가한다 — b-tree undo는 보상 전에 페이지 분할이나 병합을 수행할 수 있으므로 별도의 브래킷은 불필요하다.

9.5 log_append_compensate — CLR 기록자

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

log_append_compensatelog_append_compensate_with_undo_nxlsalog_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_recordtail_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_COMPENSATELOG_SYSOP_END는 undo 중에 순수한 커서 리다이렉션이다(compensate->undo_nxlsa, lastparent_lsa / compensate_lsa) — 크래시된 롤백은 중복 없이 재개된다.
  5. log_rv_undo_recordRCV_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.cRV_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_COMPENSATEbtree_rv_redo_record_modifyundofun으로 등록하고 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_crumbsundofun만, log_append_redo_crumbsredofun만 확인한다. 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_updateerror_code를 반환하지만 — 그 시점엔 여전히 NO_ERROR — assert되기는 하나 전파되지 않는다.) log_rv_redo_record_modify / log_rv_undo_record_modifyis_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_COMPENSATEundofun 슬롯으로 재실행한다. 따라서 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_crashstopat (상위 문서, “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_completegoto 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_postponeif (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_nxlsalog_rec->prev_tranlsa로 되감으며(반쪽만 기록된 레코드는 undo되지 않는다), 이전 페이지를 다시 가져오고(실패 시 치명적: “reset log is impossible”), prev_lsa/prev_prev_lsalog_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 == truelog_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_LOGassert(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_tdessystb_System_tdes에서 trid를 찾아(미스 시 단언) set_system_tdes로 설치한다.

불변 조건 — 모든 simulate는 반드시 end와 쌍을 이루며, 트랜잭션 사이에 스레드는 LOG_SYSTEM_TRAN_INDEX로 복귀한다. log_rv_undo_record(챕터 9)는 end: 레이블 이후 쌍을 닫으므로 오류 경로도 스레드를 복원한다; log_recovery_finish_all_postponelog_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_transTR_TABLE_CS_ENTER 하에서 두 카운터를 0으로 초기화하고, 유효한 trid를 가진 모든 비시스템 tdes를 logtb_set_loose_end_tdes로 순회한다: LOG_ISTRAN_2PC_PREPAREisloose_end를 설정하고 num_prepared_loose_end_indices를 증가시키며(in-doubt 참여자; 락을 유지한다); LOG_ISTRAN_2PC_IN_SECOND_PHASE 또는 TRAN_UNACTIVE_2PC_COLLECTING_PARTICIPANT_VOTESnum_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_INFObreak 누락은 우연히 무해할 뿐이다.
  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.