CUBRID Recovery Manager — 코드 수준 심층 분석
이 문서의 위치: 상위 수준 분석 문서인
cubrid-recovery-manager.md는 설계 의도와 이론적 배경을 다룬다. 이 문서는 모든 분기와 필드를 코드 수준에서 추적한다. 각 장은 독립적으로 읽을 수 있지만, 순서대로 읽으면 커널 내부에서 크래시된 데이터베이스가 재시작되는 전 과정을 따라갈 수 있다.
목차:
Chapter 1: 자료구조 지도
섹션 제목: “Chapter 1: 자료구조 지도”이론적 배경은 동반 문서 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에 채워 넣는다.
1.2 LOG_RCV — 범용 복구 인자
섹션 제목: “1.2 LOG_RCV — 범용 복구 인자”undo, redo, compensate, run-postpone 함수는 모두 int (*)(THREAD_ENTRY *, LOG_RCV *) 시그니처를 공유한다. LOG_RCV는 이 모든 경로를 하나로 묶는 좁은 허리(narrow waist)다.
// log_rcv -- src/transaction/recovery.hstruct 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_id | MVCC 클래스 레코드의 MVCCID, 그 외는 MVCCID_NULL | 레코드 헤더 필드; log_rv_get_log_rec_mvccid MVCC 특수화에서만 설정 |
pgptr | 복구 대상 페이지, 드라이버가 fix; 논리적 레코드의 경우 nullptr | fix/unfix는 드라이버에서 중앙 처리 |
offset | pgptr 내 오프셋 또는 슬롯 ID, LOG_DATA.offset에서 유래 | 물리적 복구는 page+offset 주소 방식 |
length | data의 바이트 길이 | 종단자 없는 raw 버퍼 |
data | redo 교체 이미지 또는 undo before-image | LOG_ZIP 버퍼나 로그 페이지를 가리키는 포인터 — 수명 규칙은 아래 참조 |
reference_lsa | compensate: undo 시점의 트랜잭션 undo_nxlsa — undo 체인이 재개할 다음 LSA로, log_sysop_end_logical_compensate에 전달됨; log_rollback_record(런타임 롤백)와 log_rv_undo_record(재시작 undo) 모두 채움. Run-postpone: 원본 postpone 레코드의 LSA, log_execute_run_postpone가 채움 | 보상 / run-postpone 레코드를 직접 추가하는 수동 논리 함수(1.7)의 앵커 |
불변 조건 (빌린 데이터의 수명). rcv.data와 rcv.pgptr은 빌림(loan)이다. data는 압축 해제 버퍼(m_redo_zip.log_data) 또는 로그 페이지를 가리키고, pgptr은 반환 후 호출자가 unfix한다. 강제 수단: 네 가지 복사/이동 연산이 모두 삭제됨; log_rv_redo_record_sync는 레코드마다 스택 로컬 LOG_RCV를 새로 생성하고, scope_exit으로 pgptr을 unfix한다. rcv->data를 보관하면 다음 레코드의 압축 해제 시 재생(replay) 내용이 조용히 깨진다.
1.3 rvfun과 RV_fun[] 디스패치 테이블
섹션 제목: “1.3 rvfun과 RV_fun[] 디스패치 테이블”rvfun(recovery.h)은 fun_t = int (*)(THREAD_ENTRY *, LOG_RCV *), dump_fun_t = void (*)(FILE *, int, void *), 그리고 여섯 개 필드를 묶는다. extern struct rvfun RV_fun[]은 recovery.c에서 초기화된다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
recv_index | 항목 자신의 인덱스 복사본 (/* For verification */) | rv_check_rvfuns가 디버그 시작 시 RV_fun[i].recv_index == i를 assert |
recv_string | 이름, 예: "RVDK_FORMAT" | rv_rcvindex_string을 통한 trace/dump 출력 |
undofun | undo/롤백에서 적용됨 — compensate 레코드의 redo에서도 호출: log_rv_get_fun<LOG_REC_COMPENSATE>가 undofun을 반환(// yes, undo) | CLR의 redo는 곧 원래 undo |
redofun | redo 패스, 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–9 | disk manager |
RVFL_* | 10–32, 128 | file manager |
RVHF_* | 33–53, 126, 129 | heap |
RVOVF_* | 54–57 | overflow records |
RVEH_* | 58–65 | extendible hash |
RVBT_* | 66–91, 124–125 | b-tree, 논리 키 집합 포함 (1.7) |
RVCT_* | 92–96 | catalog pages |
RVLOG_* | 97 | 논리 redo noop 마커 |
RVREPL_* | 98–103 | replication, HA 적용자 전용 |
RVVAC_* | 104–117 | vacuum |
RVES_* | 118 | external storage (LOB) |
RVLOC_* | 119 | locator classname dummy |
RVPGBUF_* | 120–123, 127 | page buffer |
1.5 현대적 redo 측 타입
섹션 제목: “1.5 현대적 redo 측 타입”redo 패스(Ch 6)와 각 병렬 redo 적용자(Ch 7)는 각자 log_rv_redo_context(log_recovery_redo.hpp) 하나를 소유한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_reader | LOG_CS_SAFE_READER로 생성된 log_reader 커서 | 적용자마다 독립적인 로그 위치 |
m_redo_zip | redo 페이로드의 압축 해제 대상; 그 log_data가 rcv.data가 됨 | 출력이 복구 함수 호출보다 오래 유지되어야 함 |
m_undo_zip | diff undoredo 레코드의 undo 절반 압축 해제 대상 | LOG_DIFF_UNDOREDO_DATA는 redo를 undo에 대한 XOR diff로 저장 |
m_end_redo_lsa | const 상한; 이 LSA 이상의 레코드는 redo하지 않음 | 패스 시작 전에 redo 지평(horizon)을 고정 |
m_reader_fetch_page_mode | set_lsa_and_fetch_page의 const fetch 모드; NORMAL은 pageid가 바뀔 때만 재fetch(do_fetch_page = FORCE || m_lsa.pageid != lsa.pageid) | 유일한 생성자 호출(redo 패스, log_recovery.c)은 NORMAL을 전달; FORCE는 향후 재사용을 위해 미사용 상태로 보존(log_reader.hpp 주석) |
기본 생성자는 삭제됨; 인자 두 개짜리 생성자는 두 버퍼를 LOGAREA_SIZE로 사전 확장; 이동과 양방향 대입은 삭제됨. 허용된 유일한 복사 생성자는 (o.m_end_redo_lsa, o.m_reader_fetch_page_mode)로 위임하므로, 두 const 파라미터만 살아남고 각 병렬 redo 워커는 신선한 버퍼와 reader를 갖게 된다.
각 적용 레코드는 log_rv_redo_rec_info<T>다. (log_lsa, LOG_RECTYPE, const T &) 생성자를 제외한 모든 특수 멤버가 삭제됨 — 한 번 생성 시 완전히 초기화되며, 재할당되지 않는다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_start_lsa | 레코드 헤더의 LSA | 적용 후 페이지에 찍힘(pgbuf_set_lsa); 아래 검사의 키 |
m_type | LOG_RECTYPE | log_rv_get_log_rec_redo_data의 LOG_DIFF_UNDOREDO_DATA XOR diff 분기를 결정 |
m_logrec | 타입별 바디 T의 값 복사본 — LOG_REC_{UNDOREDO, MVCC_UNDOREDO, REDO, MVCC_REDO, RUN_POSTPONE, COMPENSATE} 중 하나 | reader가 앞으로 이동할 수 있게 해방; log_rv_get_log_rec_* 특수화들이 vpid/mvccid/length/offset을 추출 |
불변 조건 (페이지 단위 LSA 순서, 디버그 빌드). 한 페이지에 대한 redo는 스레드를 넘어서도 로그 순서대로 적용되어야 한다; vpid_lsa_consistency_check(NDEBUG에서는 컴파일 제외)가 그 필요 조건을 확인한다.
// vpid_lsa_consistency_check::check -- src/transaction/log_recovery_redo.cpp std::lock_guard<std::mutex> lck (mtx); const vpid_key_t key {a_vpid.volid, a_vpid.pageid}; const auto map_it = consistency_check_map.find (key); if (map_it != consistency_check_map.cend ()) { assert ((*map_it).second < a_log_lsa); /* <- later applies must beat the stored LSA */ } consistency_check_map.emplace (key, a_log_lsa); /* <- emplace never overwrites an existing key */| 필드 | 역할 | 존재 이유 |
|---|---|---|
mtx | check와 cleanup을 직렬화 | 맵(log_Gl_recovery_redo_consistency_check 전역)이 모든 적용자에서 접근됨 |
consistency_check_map | 페이지 단위 기준선 — vpid_log_lsa_map_t는 vpid_key_t = (volid, pageid)를 해당 페이지에 처음 적용된 LSA에 매핑; emplace는 기존 키를 덮어쓰지 않으므로 기준선이 전진하지 않음 | assert는 이후의 모든 적용이 기준선 LSA보다 높은 LSA를 가져야 함을 요구 — 완전한 쌍별 단조성(pairwise monotonicity)보다 약한 조건이지만(나중 두 적용의 순서 뒤바뀜은 통과), 첫 번째 적용보다 오래된 이미지는 잡아낼 수 있음 |
cleanup()은 패스 종료 후 맵을 지운다; log_rv_redo_record_sync는 log_Gl.rcv_phase != LOG_RESTARTED인 동안에만 참조한다.
1.6 분석 패스 측 상태
섹션 제목: “1.6 분석 패스 측 상태”LOG_RCV_TDES (log_tdes의 rcv 필드, log_impl.h)는 분석 패스의 발견 사항을 이후 패스(Ch 4, 5, 8)로 전달한다. 다섯 개의 LSA로 구성된다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
sysop_start_postpone_lsa | 크래시 시점에 진행 중이던 LOG_SYSOP_START_POSTPONE의 LSA | sysop postpone 단계의 재개 앵커 (Ch 8) |
tran_start_postpone_lsa | 트랜잭션 수준 postpone이 시작된 위치 | ”커밋됨, postpone 대기 중”을 단순 active와 구분 |
atomic_sysop_start_lsa | 중단된 atomic 파일 연산(file_perm_alloc / file_perm_dealloc)의 시작 LSA | postpone 실행 전에 완전히 완료되거나 롤백되어야 함 (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_STATE | loose-end 분류 초기값 |
head_lsa | 트랜잭션의 첫 번째 로그 레코드 | 역방향 체인의 경계 |
tail_lsa | 체크포인트 시점의 마지막 레코드 | 분석 스캔이 여기서 재개 |
undo_nxlsa | undo 중 CLR이 기록된 경우 다음 undo 대상 레코드 | 이미 보상된 작업을 롤백이 건너뜀 |
posp_nxlsa | 첫 번째 postpone 레코드 | postpone 실행 시작 위치 |
savept_lsa | 마지막 세이브포인트 | 세이브포인트 체인 헤드, 부분 롤백 |
tail_topresult_lsa | 마지막 부분 abort/commit | 중첩 sysop 해소 |
start_postpone_lsa | postpone 도중이라면 start-postpone 주소 | 해당 트랜잭션은 undo가 아닌 postpone을 완료해야 함 |
user_name | 클라이언트 이름 (char[LOG_USERNAME_MAX]) | TDES에 복원 |
LOG_INFO_CHKPT_SYSOP는 LOG_RCV_TDES의 두 영속 sysop 앵커를 스냅샷한다. 나머지 세 LOG_RCV_TDES LSA는 여기에 포함되지 않는다: analysis_last_aborted_* 쌍은 현재 분석 실행의 산물이라 영속화되지 않고, tran_start_postpone_lsa는 트랜잭션 항목의 LOG_INFO_CHKPT_TRANS.start_postpone_lsa로 전달된다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
trid | 두 LSA를 복원할 대상 트랜잭션의 TDES | 트랜잭션 배열과 병렬이 아닌, 트랜잭션 기준 키 |
sysop_start_postpone_lsa | 저장된 rcv.sysop_start_postpone_lsa | sysop 상태가 체크포인트 이전부터 존재할 수 있음 |
atomic_sysop_start_lsa | 저장된 rcv.atomic_sysop_start_lsa | 동일, 중단된 atomic 파일 연산 |
1.7 LOG_ZIP과 논리 분류 매크로
섹션 제목: “1.7 LOG_ZIP과 논리 분류 매크로”LOG_ZIP (log_compress.h), 쓰기 경로와 (1.5) redo context의 압축 워크스페이스로, log_data를 소유한다(log_zip_free_data로 해제). 네 가지 복사/이동 연산은 모두 삭제됨 — 멤버별 복사는 이중 해제(double-free)를 유발한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
data_length | log_data 내 현재 유효 바이트 수 | log_unzip 후 rcv.length에 전달되는 길이 |
buf_size | 할당된 용량 | log_zip_realloc_if_needed가 확장; 레코드 간에 유지됨 |
log_data | 소유된 버퍼 (“used as data buffer”) | rcv.data가 빌려 쓰는 저장소 — 1.2의 수명 규칙 |
저장된 길이는 최상위 비트로 압축 여부를 표시한다: MAKE_ZIP_LEN(l)은 0x80000000을 설정하고, ZIP_CHECK(l)이 검사하며, GET_ZIP_LEN(l)이 제거한다.
분류 매크로. LOG_RCVINDEX에 대한 순수한 OR 조합 네 가지로 구성된다; 각 분기는 열거된 인덱스 그대로다.
// RCV_IS_BTREE_LOGICAL_LOG -- src/transaction/recovery.h#define RCV_IS_BTREE_LOGICAL_LOG(idx) \ ((idx) == RVBT_DELETE_OBJECT_PHYSICAL \ || (idx) == RVBT_MVCC_DELETE_OBJECT \ || (idx) == RVBT_MVCC_INSERT_OBJECT \ || (idx) == RVBT_NON_MVCC_INSERT_OBJECT \ || (idx) == RVBT_MARK_DELETED \ || (idx) == RVBT_DELETE_OBJECT_POSTPONE \ || (idx) == RVBT_MVCC_INSERT_OBJECT_UNQ \ || (idx) == RVBT_MVCC_NOTIFY_VACUUM \ || (idx) == RVBT_ONLINE_INDEX_UNDO_TRAN_DELETE \ || (idx) == RVBT_ONLINE_INDEX_UNDO_TRAN_INSERT)이 열 개 연산은 페이지 이미지가 아닌 키 값으로 기록되므로, undo는 트리를 재탐색하며 하나의 고정 페이지에 대해서는 실행되지 않는다.
RCV_IS_LOGICAL_COMPENSATE_MANUAL은 btree 집합에 정확히 여섯 개를 더한 것이다: RVFL_ALLOC, RVFL_USER_PAGE_MARK_DELETE, RVPGBUF_DEALLOC, RVFL_TRACKER_HEAP_REUSE, RVHF_LOB_REMOVE_DIR, RVFL_TRACKER_UNREGISTER. 이들의 undofun은 rcv.reference_lsa로 보상 레코드를 직접 추가하므로, 롤백 드라이버가 자동으로 LOG_COMPENSATE를 추가해서는 안 된다 — 재크래시 시 이중 undo가 발생한다. RCV_IS_LOGICAL_RUN_POSTPONE_MANUAL은 정확히 네 개와 일치한다: RVFL_DEALLOC, RVHF_MARK_DELETED, RVHF_LOB_REMOVE_DIR, RVBT_DELETE_OBJECT_POSTPONE. postpone 액션으로서 이들의 redofun은 표준 LOG_RUN_POSTPONE가 아닌 LOG_SYSOP_END_LOGICAL_RUN_POSTPONE로 마무리된다 (Ch 8). RVHF_LOB_REMOVE_DIR과 RVBT_DELETE_OBJECT_POSTPONE는 두 집합 모두에 속한다.
RCV_IS_LOGICAL_LOG (vpid, idx)는 마스터 테스트이자 주소를 검사하는 유일한 매크로다:
((vpid)->volid == NULL_VOLID) || ((vpid)->pageid == NULL_PAGEID) 조건이 참이면 인덱스에 무관하게 논리적으로 단락(short-circuit)된다; 이후 RCV_IS_BTREE_LOGICAL_LOG (idx); 그다음 열한 개 인덱스: RVBT_MVCC_INCREMENTS_UPD, RVPGBUF_FLUSH_PAGE, RVFL_DESTROY, RVFL_ALLOC, RVFL_DEALLOC, RVVAC_NOTIFY_DROPPED_FILE, RVPGBUF_DEALLOC, RVES_NOTIFY_VACUUM, RVHF_MARK_DELETED, RVFL_TRACKER_HEAP_REUSE, RVFL_TRACKER_UNREGISTER. 새 논리 인덱스가 여기에 빠지면 복구가 존재하지 않는 페이지를 fix하려 하다 — 버그가 배포된 한참 후에 롤백 도중 fix 오류로 나타난다.
flowchart TD
A["레코드 vpid + rcvindex"] --> B{"volid 또는 pageid가 NULL?"}
B -- 예 --> L["논리적: undofun은 pgptr = nullptr를 받음"]
B -- 아니오 --> C{"RCV_IS_BTREE_LOGICAL_LOG?"}
C -- 예 --> L
C -- 아니오 --> D{"열한 개 인덱스 중 하나?"}
D -- 예 --> L
D -- 아니오 --> P["물리적: 드라이버가 페이지를 fix하고 pgptr 전달"]
Figure 1-2 — undo/롤백 드라이버가 평가하는 RCV_IS_LOGICAL_LOG.
1.8 챕터 요약 — 핵심 정리
섹션 제목: “1.8 챕터 요약 — 핵심 정리”LOG_RCV는 유일한 호출 규약이다;data와pgptr은 빌림(loan)이므로 네 가지 복사/이동 연산이 모두 삭제된다.RV_fun[]은 추가 전용(append-only)LOG_RCVINDEX로 인덱싱된다; 디버그 시작 시rv_check_rvfuns는 순서 오류만 잡고 범위 초과 조회는 방어하지 않는다.- Compensate 레코드는
undofun으로 redo된다(log_rv_get_fun<LOG_REC_COMPENSATE>): CLR의 redo는 원래 undo를 다시 수행하는 것이다. log_rv_redo_context(reader + 두 개의 zip 버퍼 + 동결된m_end_redo_lsa; 복사 시 신선한 버퍼 재생성;NORMALfetch 모드만 사용)는 불변log_rv_redo_rec_info<T>스냅샷을 공급한다; 디버그 전용vpid_lsa_consistency_check는 페이지 단위 모든 후속 적용이 첫 번째 적용 LSA보다 높아야 함을 assert한다 — 로그 순서의 필요 조건이지, 완전한 쌍별 단조성은 아니다.- 분석 상태 =
LOG_RCV_TDES(다섯 개 LSA),LOG_REC_CHKPT+LOG_INFO_CHKPT_TRANS+LOG_INFO_CHKPT_SYSOP에서 초기화됨 (두 개의 sysop 앵커만 영속; 트랜잭션 수준 postpone은 트랜잭션 항목에 전달됨),LOG_RECVPHASE로 게이트됨. RCV_IS_*매크로는 물리적 vs 논리적, 자동 vs 수동을 구분한다;RCV_IS_LOGICAL_LOG에서 누락된 새 논리 인덱스는 기능이 배포된 훨씬 뒤에 롤백을 깨뜨린다.
Chapter 2: 재시작 진입점과 로그 페이지 접근
섹션 제목: “Chapter 2: 재시작 진입점과 로그 페이지 접근”서버 시작 시 리커버리를 누가 주도하는지, 체크포인트 앵커를 어떻게 찾고 미디어 크래시 시 어떻게 다운그레이드하는지, 그리고 각 패스(3장, 6장, 9장)가 로그 페이지를 물리적으로 어떻게 읽는지를 다룬다. 이론적 배경은 companion 문서 cubrid-recovery-manager.md를 참고하라.
2.1 log_recovery — 재시작 오케스트레이터, 분기별 분석
섹션 제목: “2.1 log_recovery — 재시작 오케스트레이터, 분기별 분석”log_recovery(log_recovery.c)의 호출자는 log_initialize_internal 하나이며,
init_emergency == false && (log_Gl.hdr.is_shutdown == false || ismedia_crash == true) 조건을 만족할 때만 진입한다(restoredb는 ismedia_crash를 전달하고, 긴급 시작은 리커버리를 건너뛴다). 호출자는 로그 CS를 쓰기 모드로 보유한 상태여야 한다(assert (LOG_CS_OWN_WRITE_MODE)).
// log_recovery -- src/transaction/log_recovery.c /* ... condensed: branch 1 -- NULL LOG_FIND_TDES is er_set + logpb_fatal_error, return ... */ rcv_tdes->state = TRAN_RECOVERY; /* <- the recovery "transaction" */ if (LOG_HAS_LOGGING_BEEN_IGNORED ()) { /* <- branch 2: fatal, then clear the flag */ /* ... condensed ... */ } /* ... condensed ... */ LSA_COPY (&rcv_lsa, &log_Gl.hdr.chkpt_lsa); if (ismedia_crash != false) { /* <- branch 3a: downgrade anchor */ (void) fileio_map_mounted (thread_p, (bool (*)(THREAD_ENTRY *, VOLID, void *)) log_rv_find_checkpoint, &rcv_lsa); } /* ... condensed: else, branch 3b -- if (stopat != NULL) *stopat = -1 ... */ vacuum_notify_server_crashed (&rcv_lsa);분기 1과 2는 logpb_fatal_error로 종료된다. 분기 2는
LOG_HAS_LOGGING_BEEN_IGNORED()(log_impl.h)가 log_Gl.hdr.has_logging_been_skipped를 감지할 때 발동하며, 로깅이 건너뛰어진 상태에서 크래시가 발생하면 복구가 불가능하다(ER_LOG_CORRUPTED_DB_DUE_CRASH_NOLOGGING). 분기 3a: 복원된 볼륨이 헤더 체크포인트보다 오래되었을 수 있으므로, log_rv_find_checkpoint를 모든 볼륨에 걸쳐 순회하면서
LSA_ISNULL (rcv_lsa) || LSA_LT (&chkpt_lsa, rcv_lsa) 조건이 맞으면 해당 볼륨의 disk_get_checkpoint LSA를 rcv_lsa에 복사한다. 반환값은 항상 true여서 전 볼륨을 방문하고, 가장 오래된 체크포인트가 최종 앵커가 된다.
vacuum_notify_server_crashed는 rcv_lsa를 vacuum_Data.recovery_lsa에 복사해, 분석 단계에서 MVCC op 레코드를 찾지 못했을 때 vacuum의 역방향 스캔 시작점으로 쓰인다.
불변 조건 — 분석 시작 LSA는 영구 볼륨에 기록된 체크포인트보다 새로울 수 없다. 볼륨 헤더는 마지막 flush 시점의 체크포인트 LSA를 저장하며, 재플레이는 반드시 그 시점 이전부터 시작해야 한다. 그렇지 않으면 복원된 볼륨이 받지 못한 업데이트를 redo가 건너뛰게 된다. Figure 2-1은 이후 흐름을 정리한다.
flowchart TD
A["분석 Ch 3"] --> B["logpb_fetch_start_append_page<br/>오류: fatal 종료"]
B --> C{did_incom_recovery}
C -->|false| D["EOF back_lsa 기반으로 LOG_RESET_PREV_LSA"] --> G
C -->|true| G["LOG_DUMMY_CRASH_RECOVERY 추가<br/>rcv_phase_lsa = tail_lsa"]
G --> H["REDO Ch 6, UNDO Ch 9<br/>log_system_tdes::rv_final"] --> K{did_incom_recovery}
K -->|true| L["log_recovery_notpartof_volumes"] --> N
K -->|false| N["TRAN_ACTIVE, logtb_set_num_loose_end_trans"]
N --> O{2PC 미완료 항목}
O -->|yes| P["FINISH_2PC: log_2pc_recovery Ch 11"] --> R
O -->|no| R["logpb_decache_archive_info<br/>CS 해제, logpb_checkpoint, CS 재진입"]
R --> S["전체 flush + 헤더, locator_initialize,<br/>heap_classrepr_restart_cache — 각 오류: fatal"]
Figure 2-1: 앵커 확정 이후 log_recovery의 흐름.
섹션 2.5는 어펜드 포인트 재설정을 다루며, log_append_empty_record가
LOG_DUMMY_CRASH_RECOVERY를 기록하면 그 LSA가 log_Gl.rcv_phase_lsa가 되어 undo(9장)의 크래시 경계 기준점이 된다. stopat 컷오프가 설정되면 did_incom_recovery가 세워지고(3장), 이후 log_recovery_notpartof_volumes가 복원 시점 이후에 생성된 볼륨을 제거한다. 마무리 단계에서는 logpb_checkpoint 호출을 전후로 로그 CS에서 잠깐 빠져나왔다가 재진입하고, 더티 페이지와 헤더를 flush하며, 카탈로그 트래커와 클래스 표현을 다시 캐싱한다 — 여기서도 두 개의 fatal 분기가 있다. 호출자는 이후 LOG_RESTARTED를 설정한다.
2.2 rcv_phase 전환
섹션 제목: “2.2 rcv_phase 전환”log_Gl.rcv_phase(enum log_recvphase, log_impl.h)는 전역 리커버리 모드를 나타내며, LOG_ISRESTARTED()는 LOG_RESTARTED 상태인지를 검사한다. 이 값은 위의 패스들이 끝난 뒤 호출자가 설정한다. logpb_copy_page는 !LOG_ISRESTARTED()일 때만 리커버리 캐시를 채우고, 물리 리더는 LOG_RESTARTED 상태에서만 디버그 체크섬 검사를 수행한다 — 리커버리 중에 발생하는 torn tail은 논리적으로 복구된다(섹션 2.7).
2.3 logpb_fetch_page — 단일 물리 읽기 진입점
섹션 제목: “2.3 logpb_fetch_page — 단일 물리 읽기 진입점”logpb_fetch_page(log_page_buffer.c)는 enum log_cs_access_mode(log_impl.h)를 인자로 받는다. 분석과 undo 스캔의 고전적 방식은 LOG_CS_FORCE_USE로 호출한다(이 패스들은 log_recovery가 보유한 로그 CS 안에서 실행된다). 반면 redo 머신러리의 log_reader는 LOG_CS_SAFE_READER를 전달하여 위치 기반 fetch가 CS를 잡지 않도록 한다(섹션 2.6).
// logpb_fetch_page -- src/transaction/log_page_buffer.c if (LSA_LE (&append_lsa, req_lsa) /* <- case 1: page beyond flushed area */ || LSA_LE (&append_prev_lsa, req_lsa)) /* <- case 2: page may hold a temp EOL */ { LOG_CS_ENTER (thread_p); /* ... condensed ... */ if (LSA_LE (&log_Gl.hdr.append_lsa, req_lsa)) /* retry with mutex */ { logpb_prior_lsa_append_all_list (thread_p); /* <- drain prior list to buffers */ } LOG_CS_EXIT (thread_p); } rv = logpb_copy_page (thread_p, req_lsa->pageid, access_mode, log_pgptr); /* ... condensed: rv != NO_ERROR is the only error exit ... */앞단 게이트는 인메모리 prior-LSA 리스트를 페이지 버퍼로 드레인하여, 어펜드 포인트 근처에서 읽는 리더가 낡은 tail을 보지 않도록 보장한다. 이후 logpb_copy_page는 네 가지 경로로 분기한다: LOGPB_HEADER_PAGE_ID 요청은 캐시된 header_buffer에서 처리하고(캐시 미스 시 파일 읽기), 버퍼 인덱스가 범위를 벗어나면 ER_LOG_PAGE_CORRUPTED를 발생시키며, 버퍼 히트 시에는 memcpy 후 log_bufptr->pageid를 재검증한다 — safe-reader 모드는 읽기 CS를 잡지 않으므로 이 재검증이 lock-free 유효성 검사 역할을 한다. 나머지 경우는 logpb_read_page_from_file로 떨어지며, !LOG_ISRESTARTED() 동안은 페이지를 전진 방향으로만 캐싱한다.
2.4 활성 로그 대 아카이브: logpb_read_page_from_file
섹션 제목: “2.4 활성 로그 대 아카이브: logpb_read_page_from_file”페이지 ID가 아카이브에 속하는지는 LOGPB_IS_ARCHIVE_PAGE(pageid)로 판단한다 — 헤더 페이지가 아니면서 LOGPB_NEXT_ARCHIVE_PAGE_ID(log_Gl.hdr.nxarv_pageid)보다 작을 때이다. logpb_is_page_in_archive가 이를 래핑한다. LOG_CS_SAFE_READER 모드는 읽기 CS를 직접 잡고(log_csect_entered 설정), 다른 모드는 assert (LOG_CS_OWN)로 이미 CS를 보유하고 있음을 확인한다. CS는 아카이브 집합을 보호한다 — 읽기 도중 아카이브가 새로 생성되면 logpb_to_physical_pageid가 stale해지는 버그가 실제로 있었으며, 코드 주석에도 기록되어 있다.
// logpb_read_page_from_file -- src/transaction/log_page_buffer.c bool fetch_from_archive = logpb_is_page_in_archive (pageid); if (fetch_from_archive) { bool is_archive_page_in_active_log = (pageid + LOGPB_ACTIVE_NPAGES) > log_Gl.hdr.append_lsa.pageid; bool dont_fetch_archive_from_active = !LOG_ISRESTARTED () || log_Gl.hdr.was_active_log_reset; if (is_archive_page_in_active_log && !dont_fetch_archive_from_active) { fetch_from_archive = false; /* <- slot not yet lapped in circular active file */ } }단축 경로의 원리: 활성 로그 파일은 LOGPB_ACTIVE_NPAGES(= log_Gl.hdr.npages) 슬롯의 원형 구조이므로, 아카이빙된 페이지라도 해당 슬롯이 다시 사용되기 전까지는 활성 파일에서 읽을 수 있다 — 단, 리커버리 중이거나 활성 로그가 리셋된 경우에는 비활성화된다. 이때의 활성 tail은 크래시가 만들어낸 상태 그대로이기 때문이다.
나머지 경로: 아카이브 fetch(logpb_fetch_from_archive)가 NULL을 반환하면 goto error로 처리한다. 활성 파일 fetch는 logpb_to_physical_pageid로 슬롯을 매핑한 뒤 fileio_read를 호출하며(ER_LOG_READ, goto error), 셀프 ID 검사에서 hdr.logical_pageid == pageid이면 정상이고 이후 암호화된 경우 tde_decrypt_log_page를 수행한다(아카이브는 logpb_fetch_from_archive 내부에서 복호화). == pageid + LOGPB_ACTIVE_NPAGES이면 검사 사이에 랩어라운드가 발생한 것이므로 아카이브에서 재시도하고, 그 외는 ER_LOG_PAGE_CORRUPTED로 처리한다. 두 종료 경로 모두 log_csect_entered가 세워져 있으면 CS를 해제하며, 디버그 체크섬은 LOG_RESTARTED 상태에서만 수행된다.
불변 조건 — 모든 로그 페이지는 자신을 식별한다. 활성 파일 읽기는 hdr.logical_pageid가 요청한 pageid와 일치할 때만 유효하다. 유일하게 허용되는 불일치는 정확히 한 바퀴 랩어라운드된 경우(pageid + LOGPB_ACTIVE_NPAGES)이며, 이 검사가 없으면 랩어라운드된 슬롯이 이전 페이지로 재플레이될 것이다.
2.5 logpb_fetch_start_append_page — 어펜드 포인트 재설정
섹션 제목: “2.5 logpb_fetch_start_append_page — 어펜드 포인트 재설정”분석과 redo 사이에 로그는 다시 쓰기 가능 상태가 되어야 한다. 네 가지 분기가 있다: 비어 있는 로그(append_lsa offset 0, pageid 0 — 디버그 빌드: PRM_ID_FIRST_LOG_PAGEID)는 logpb_locate_page에 OLD_PAGE 대신 NEW_PAGE를 요청하고, 남아 있는 log_Gl.append.log_pgptr는 버리며(logpb_invalid_all_append_pages), logpb_locate_page가 NULL을 반환하면 유일한 오류 종료(ER_FAILED, log_recovery에서 fatal 처리)이고, 성공 시에는 set_nxio_lsa(log_Gl.hdr.append_lsa)를 기록하고 해당 페이지를 flush_info->toflush에 추가하여 배열이 가득 차면 flush한다(logpb_flush_pages_direct).
2.6 log_reader — redo 머신러리를 위한 C++ fetch 래퍼
섹션 제목: “2.6 log_reader — redo 머신러리를 위한 C++ fetch 래퍼”현대적인 redo 경로(6장, 7장)는 log_reader(log_reader.hpp, final 클래스, 헤더 전용)를 사용한다. 형제 파일인 log_reader.cpp는 낡은 파일로 CMakeLists에 포함되지 않는다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_thread_entry | 지연 캐시된 THREAD_ENTRY * | 단일 스레드 계약, 매 사용 시 assert |
m_lsa | 읽기 위치; 초기값 NULL_LSA | fetch pageid, 페이지 내 offset, 메모이제이션을 결정 |
m_cs_access | logpb_fetch_page에 전달되는 모드; 기본값 LOG_CS_FORCE_USE | CS 보유 패스 vs CS 없는 리더 (LETS 포트 잔재) |
m_page | 생성자에서 m_area_buffer에 정렬된 log_page * | 전용 fetch 목적지 — 공유 버퍼 락 불필요 |
m_area_buffer | char [IO_MAX_PAGE_SIZE + DOUBLE_ALIGNMENT] | 인라인 힙 불필요 저장소; 복사된 워커는 각자 보유 |
set_lsa_and_fetch_page는
do_fetch_page { fetch_page_mode == fetch_mode::FORCE || m_lsa.pageid != lsa.pageid }를 계산하고, m_lsa = lsa를 할당한 뒤 해당 조건이 참일 때만 fetch(logpb_fetch_page(.., m_cs_access, m_page), 실패 시 fatal)를 수행한다: NORMAL 모드는 현재 페이지를 메모이제이션하고, FORCE 모드는 항상 재fetch한다. align, add_align, advance_when_does_not_fit, copy_from_log은 고전적인 LOG_READ_ALIGN 패밀리와 logpb_copy_from_log(동일 헤더 하단)에 위임하며, 페이지 경계에서 재fetch한다 — 단, m_cs_access를 전달하는 것은 fetch_page(set_lsa_and_fetch_page 및 skip 경유)뿐이며, 위임 멤버들은 패밀리 기본값인 LOG_CS_FORCE_USE를 사용하므로 safe reader라도 레코드 중간 페이지 경계에서 잠깐 읽기 CS를 잡는다.
소유 애그리게이트 log_rv_redo_context(log_recovery_redo.hpp):
| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_reader | log_reader { LOG_CS_SAFE_READER } | 컨텍스트별 전용 리더; CS 없는 위치 기반 fetch |
m_redo_zip, m_undo_zip | LOG_ZIP 스크래치 버퍼 | 레코드 간 재사용되는 압축 해제 대상 (섹션 2.8) |
m_end_redo_lsa | const LOG_LSA redo 종료 경계 | 워커가 전역 변수 접근 없이 레코드 LSA를 비교 |
m_reader_fetch_page_mode | const log_reader::fetch_mode | NORMAL은 페이지 메모이제이션; FORCE는 재사용 목적 유지 |
동기 redo 드라이버는 fetch_mode::NORMAL로 생성하고, 복사 생성자는 메인 생성자를 재실행하여 각 병렬 워커(7장)가 독립적인 버퍼를 갖도록 한다. 생성자는 두 zip 버퍼를 LOGAREA_SIZE로 미리 할당하며, 소멸자는 해제한다(log_zip_free_data).
2.7 불완전하게 아카이빙된 레코드를 위한 NULL_OFFSET 규약
섹션 제목: “2.7 불완전하게 아카이빙된 레코드를 위한 NULL_OFFSET 규약”NULL_OFFSET은 (-1)이다(storage_common.h). 아카이버가 마지막 레코드가 다음 페이지로 이어지는 활성 페이지를 복사할 때, 그 연속 부분을 가리키는 LSA의 offset이 NULL_OFFSET을 가질 수 있다: 레코드 완성이 아카이빙 이후에 이루어졌기 때문이다. 분석, redo, log_startof_nxrec 같은 워커를 포함한 모든 전진 스캔은 역참조 전에 이를 복구해야 한다:
// log_recovery_analysis (record loop) -- src/transaction/log_recovery.c if (lsa.offset == NULL_OFFSET) { lsa.offset = log_page_p->hdr.offset; /* <- page's first record offset */ if (lsa.offset == NULL_OFFSET) { /* Continue with next pageid */ if (logpb_is_page_in_archive (log_lsa.pageid)) { lsa.pageid = log_lsa.pageid + 1; /* <- archive: keep walking */ } else { lsa.pageid = NULL_PAGEID; /* <- active: stop scan */ } continue; } }hdr.offset 자체도 NULL_OFFSET인 페이지는 레코드 시작점이 없는 순수 연속 페이지다 — 아카이브에서는 다음 페이지로 진행하고, 활성 로그에서는 스캔을 종료한다. 분석용 스크래치 페이지는 hdr.offset = NULL_OFFSET으로 초기화된다.
2.8 각 패스가 인스턴스화하는 LOG_ZIP 할당 헬퍼
섹션 제목: “2.8 각 패스가 인스턴스화하는 LOG_ZIP 할당 헬퍼”LOG_ZIP(struct log_zip, log_compress.h)은 grow-only 버퍼다:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
data_length | 현재 저장된 바이트 수 | 소비자는 정확히 이 크기만큼 읽는다; 용량은 더 클 수 있다 |
buf_size | log_data의 용량 | grow-only 검사 기준; LZ4 최악 케이스에 맞춰 크기 결정 |
log_data | 버퍼(char *) | 레코드 간 재사용; 첫 크기 설정 전까지 nullptr |
log_zip_realloc_if_needed(log_zip, new_size)(log_compress.c)는 new_size > 0 && new_size > log_zip.buf_size일 때만 LOG_ZIP_BUF_SIZE(LZ4, new_size)로 확장한다(실패 시 ER_OUT_OF_VIRTUAL_MEMORY). 두 번째 검사인 new_size > 0 && log_zip.log_data == nullptr는 필드를 초기화하고 false를 반환하며(호출자가 fatal 처리), true는 성공 또는 확장 불필요를 의미한다. log_zip_alloc은 구조체를 malloc + 초기화하고 동일한 방식으로 크기를 설정한다(실패 시 nullptr 반환, husk 해제). log_zip_free는 log_zip_free_data를 실행한 뒤 구조체를 해제한다. redo 컨텍스트는 두 zip 버퍼를 미리 할당하고(섹션 2.6), undo 패스는 log_zip_alloc(LOGAREA_SIZE)로 undo_unzip_ptr를 할당하여 log_recovery_undo의 모든 종료 경로에서 해제한다. 공유 소비자 log_rv_get_unzip_log_data는 ZIP_CHECK(length)로 압축 여부를 구분하여 log_zip_realloc_if_needed 이후 log_unzip 또는 memcpy를 선택한다.
2.9 챕터 요약 — 핵심 정리
섹션 제목: “2.9 챕터 요약 — 핵심 정리”log_recovery는 이미 쓰기 모드로 획득된 로그 CS 아래에서TRAN_RECOVERY시스템 트랜잭션으로 실행된다. 호출자는 오직log_initialize_internal뿐이며, 긴급 시작은 리커버리를 건너뛴다.- 분석 앵커는
log_Gl.hdr.chkpt_lsa이며, 미디어 크래시 시log_rv_find_checkpoint가 찾은 볼륨별 최고참 체크포인트로 다운그레이드된다. - 분석과 redo 사이에 어펜드 포인트가 재설정되고
LOG_DUMMY_CRASH_RECOVERY가 추가된다. 그 LSA(log_Gl.rcv_phase_lsa)가 undo의 크래시 경계 기준점이 된다. - 고전적인 분석·undo 스캔은 보유 중인 로그 CS 아래에서
LOG_CS_FORCE_USE로 fetch하고,log_reader는 위치 기반 fetch에LOG_CS_SAFE_READER를 전달한다. 단, 페이지 경계를 넘는 헬퍼는 여전히LOG_CS_FORCE_USE를 기본값으로 사용하여 읽기 CS를 잠깐 잡는다. logpb_read_page_from_file은LOGPB_IS_ARCHIVE_PAGE를 기준으로 활성 로그와 아카이브를 분리한다. 셀프 ID 불일치 중 유일하게 허용되는 케이스는 한 바퀴 랩어라운드된 경우(pageid + LOGPB_ACTIVE_NPAGES)이며, 아카이빙되었지만 활성 파일에서도 읽을 수 있는 단축 경로는 리커버리 중에 비활성화된다.NULL_OFFSET(-1)은 불완전하게 아카이빙된 레코드의 LSA를 표시한다. 모든 전진 스캔은hdr.offset으로 복구하며, 아카이브에서는 한 페이지 전진하고 활성 로그에서는 스캔을 종료한다.
Chapter 3: 분석 패스 드라이버
섹션 제목: “Chapter 3: 분석 패스 드라이버”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_lsa | resetlog의 new_prev_lsa | prev_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_block | LOG_PAGE_INIT_VALUE(0xff, log_common_impl.h)로 채워진 4 KB | 티어(tear) 탐지용 memcmp 피연산자 |
checkpoint_lsa | LOG_END_CHKPT 디스패치(4장)에서 설정 | 2PC 꼬리 재독 (3.8) |
may_use_checkpoint / may_need_synch_checkpoint_2pc | 디스패치 플래그 (4장) | 두 번째 플래그가 2PC 꼬리를 활성화 |
last_at_time | 드라이버에서는 -1을 유지 | *stop_at으로의 에코는 현재 비활성 (3.7) |
초기화 단계에서는 start_lsa를 lsa, start_redo_lsa, end_redo_lsa, prev_lsa에 복사하며 — 검증 전까지의 퇴화된 redo 범위 — 나머지는 모두 null 또는 0으로 설정한다.
3.2 외부 루프: is_log_page_broken 분기
섹션 제목: “3.2 외부 루프: is_log_page_broken 분기”외부 루프는 매 반복마다 커서 위치의 페이지를 logpb_fetch_page로 가져온다. 플러시된 로그 끝을 넘어감, 아카이브 누락, TDE 복호화 오류 등 모든 실패가 is_log_page_broken을 세우며, 내부 루프의 꼬리 누락 break(3.4) 역시 마찬가지다. 하나의 분기가 “broken”의 의미를 결정한다.
미디어 크래시 경로: 잘림 후 수락 — 복원 지점 이후의 로그는 정상적으로 존재하지 않을 수 있다. last_at_time을 *stop_at으로 에코하고, 마지막 레코드 소유자 tdes의 tail_lsa/undo_nxlsa를 log_rec->prev_tranlsa로 되돌려 undo가 잘린 레코드를 따라가지 않도록 한다. 그다음 prev_lsa 페이지를 재페치(실패한 페치가 페이지를 덮어씌웠으므로; 재실패 시 치명)하고, log_recovery_resetlog (thread_p, &prev_lsa, &prev_prev_lsa)로 prev_lsa를 새 추가(append) 지점으로 만든다(11장). *did_incom_recovery를 세우고, MVCC 테이블을 초기화한 뒤 반환 — 2PC 꼬리(3.8)는 건너뛴다. 일반 크래시 경로: 치명 — 단순 크래시 후에는 eof_lsa까지의 모든 페이지가 읽힐 수 있어야 한다. er_errid ()가 ER_TDE_CIPHER_IS_NOT_LOADED이면 메시지는 TDE를 명시한다: 페이지 자체는 온전하지만 복호화 불가.
불변 조건 3-B (redo 범위 정직성). 반환 시, [start_redo_lsa, end_redo_lsa) 내 모든 레코드는 읽을 수 있고 구조적으로 완전하다; 경계 자체는 마지막으로 완전히 탐색된 레코드이거나, redo가 해당 위치를 읽기 전에 재초기화된 위치이다. 정상 종료: 마지막 디스패치된 레코드. 잘림(3.6 8단계): prev_lsa로 되돌림. 깨진 레코드 탐색(3.4): 의도적으로 깨진 레코드 위치로 전진 — prev_lsa와 동일 — 하여 resetlog가 해당 위치를 새 추가 지점으로 삼고, redo 실행 전에 LOG_DUMMY_CRASH_RECOVERY로 덮어쓴다. 위반 시: redo(6장)가 반쪽짜리 본문을 적용하게 된다.
flowchart TD
A["lsa 위치의 페이지 페치"] --> B{"broken?"}
B -- 아니오 --> C["내부 루프 3.3-3.6"] --> D{"lsa null?"}
D -- 아니오 --> A
D -- 예 --> E["2PC 꼬리; reset_start_mvccid"]
B -- "예, 미디어 크래시" --> F["prev_lsa에서 resetlog; did_incom_recovery; 반환"]
B -- "예, 일반 크래시" --> G["치명 (TDE 또는 일반)"]
Figure 3-1 — log_recovery_analysis의 외부 루프.
3.3 내부 루프 진입: NULL_OFFSET 복구와 손상 탐색
섹션 제목: “3.3 내부 루프 진입: NULL_OFFSET 복구와 손상 탐색”내부 루프는 커서가 페치된 페이지에 머무는 동안 실행된다: while (!LSA_ISNULL (&lsa) && lsa.pageid == log_lsa.pageid). 레코드 접근 전에 두 가지 준비 단계가 선행된다.
NULL_OFFSET 복구. 불완전한 상태로 아카이브된 레코드는 연속 부분의 오프셋을 알 수 없게 만든다: 커서가 (pageid, NULL_OFFSET)으로 도착하면 이 페이지에서 시작하는 첫 번째 헤더인 log_page_p->hdr.offset으로 재앵커한다. 그 값도 NULL_OFFSET이면(이 페이지에는 연속 바이트만 존재): 아카이브 페이지라면 lsa.pageid = log_lsa.pageid + 1로 설정해 레코드 중간을 계속 걷고, 활성 페이지라면 lsa.pageid = NULL_PAGEID로 스캔을 종료한다. 두 경우 모두 continue.
페이지 단위 손상 탐색. last_checked_page_id로 가드되어 페이지당 한 번만 실행한다. logpb_page_check_corruption은 logpb_page_has_valid_checksum(CRC32 대 hdr.checksum)을 래핑한다; 헬퍼 오류는 치명적이다. 손상된 아카이브 페이지는 치명적(/* Should not happen. */ — 아카이브는 한 번만 기록됨); 손상된 활성 페이지는 부분 페이지 플러시를 의미한다. logpb_page_get_first_null_block_lsa가 티어 위치를 찾는다: null_block과 memcmp 결과가 같은 첫 번째 4 KB 블록이 (hdr.logical_pageid, i * block_size)를 산출하며, 비영(nonzero)일 때는 sizeof (LOG_HDRPAGE)를 빼는데 — LSA 오프셋은 area[]를 인덱싱하고 raw 페이지는 그보다 앞서 시작하기 때문이다.
어떤 블록도 매치되지 않으면(손상됐지만 모든 블록에 데이터가 있음), first_corrupted_rec_lsa는 NULL로 유지된다: 3.5의 컷오프와 그 안전망(!is_log_page_corrupted로 게이팅됨)은 건너뛰고, 불변 조건 3-C의 페이지-전진 금지와 EOF 정지만 적용된다.
3.4 다중 페이지 레코드: log_is_page_of_record_broken
섹션 제목: “3.4 다중 페이지 레코드: log_is_page_of_record_broken”log_rec = LOG_GET_LOG_RECORD_HEADER (log_page_p, &log_lsa) 이후, 미디어 크래시 경로에서 추가 탐색을 한 번 더 수행한다 — 헤더가 마지막으로 복원된 페이지에 있고 본문이 복원되지 않은 페이지로 넘어가는 상황이 가능하기 때문이다:
// log_is_page_of_record_broken -- src/transaction/log_recovery.c LSA_COPY (&fwd_log_lsa, &log_rec_header->forw_lsa); /* TODO - Do we need to handle NULL fwd_log_lsa? */ if (!LSA_ISNULL (&fwd_log_lsa)) { if (LSA_GE (log_lsa, &fwd_log_lsa) || (!LSA_ISNULL (&log_Gl.hdr.eof_lsa) && LSA_GT (&fwd_log_lsa, &log_Gl.hdr.eof_lsa))) { is_log_page_broken = true; /* <- forw_lsa is nonsense */ } else { if (fwd_log_lsa.pageid != log_lsa->pageid && (fwd_log_lsa.offset != 0 || fwd_log_lsa.pageid > log_lsa->pageid + 1)) { // ... condensed: record spans pages -- probe-fetch fwd_log_lsa page; // failure -> broken ... } } }분기별 설명: (1) forw_lsa NULL — 판단 포기; 3.5 안전망이 대신 판단한다(TODO 주석이 이 공백을 인정). (2) forw_lsa가 현재 레코드 뒤에 있지 않거나, eof_lsa를 넘어섬 — 헤더 자체가 쓰레기: broken(eof_lsa는 NULL-guard됨: 활성 볼륨 없이 복원하면 분석 중에만 복구됨). (3) forw_lsa가 다음 페이지에서 비영 오프셋에 있거나, 두 페이지 이상 앞서 있음 — 본문이 해당 페이지까지 확실히 뻗음: 탐색 페치 수행; 실패 시 꼬리 누락, 성공 시 정상. 제외된 경우 — 다음 페이지 오프셋 0의 다음 레코드 — 는 아무것도 증명하지 못하므로 페치를 소비하지 않는다.
broken 판정이 나면 내부 루프는 end_redo_lsa = lsa를 복사하고, prev_lsa와 prev_prev_lsa를 그 위치로 설정한 뒤, 디버그 트레이스 후 break한다 — resetlog는 3.2에서 수행되고, 이때 prev_lsa는 깨진 레코드 자체다: resetlog는 그 위치에서 잘라 이전의 모든 것을 보존한다.
3.5 first_corrupted_rec_lsa 컷오프
섹션 제목: “3.5 first_corrupted_rec_lsa 컷오프”체크섬에 실패한 페이지는 레코드 단위로 해당 레코드가 티어 영역 이전에 있는지를 판단한다. 판정을 넓히는 두 가지 안전망이 먼저 적용된다(is_log_page_corrupted가 false인 동안에만): (1) end-of-log 누락 — 활성 로그에서 LOG_END_OF_LOG가 아닌 레코드의 forw_lsa가 NULL인 것은 불가능하다(모든 체인은 EOF 레코드로 끝남): 페이지를 손상으로 선언하고 null-block 스캔의 컷오프로 삼는다. (2) null 블록을 가로지르는 본문 — forw_lsa가 페이지 내에 머문다면, 레코드 시작과 forw_lsa - 1을 블록 인덱스로 매핑한다((offset + sizeof (LOG_HDRPAGE)) / block_size); 인덱스가 다르고 종료 블록이 null_block과 같으면, 본문이 완전히 플러시되지 않은 것: 컷오프를 레코드 자체로 설정한다.
컷오프가 non-NULL이면 세 가지 결과가 있다. 티어보다 엄격히 뒤에 있는 레코드는 이전의 정상 레코드에서 스캔을 종료한다:
// log_recovery_analysis -- src/transaction/log_recovery.c if (LSA_GT (&log_lsa, &first_corrupted_rec_lsa)) { LOG_RESET_APPEND_LSA (end_redo_lsa); /* <- starts past the tear */ LSA_SET_NULL (&lsa); break; }else 분기는 log_lsa == first_corrupted_rec_lsa일 때, forw_lsa가 티어를 넘어 가리킬 때, 또는 헤더의 DB_ALIGN 정렬된 끝이 LOGAREA_SIZE를 초과하거나 티어를 지날 때 레코드를 손상으로 표시한다; 그다음 LOG_RESET_APPEND_LSA (&log_lsa) — 첫 번째 피해 레코드의 위치 자체가 새 추가 지점 — 가 lsa를 null로 만들고 break한다. 티어보다 확실히 앞에 있는 레코드는 정상적으로 처리된다.
불변 조건 3-C (손상은 페이지 단위 종단 조건). is_log_page_corrupted가 true가 된 이후, 커서는 절대로 다른 페이지로 전진하지 않는다. 전진 후 null 처리(손상됨, LOG_END_OF_LOG 아님, lsa.pageid != log_lsa.pageid → LSA_SET_NULL)와 LOG_END_OF_LOG 디스패치 후 정지로 강제된다. 이전 로그 랩(wrap)에서 재사용된 페이지에는 유효해 보이는 오래된 레코드가 남아 있을 수 있다; 그것을 따라가면 이전 에포크(epoch)를 다시 재생하게 된다.
3.6 커서 전진: 나머지 모든 분기
섹션 제목: “3.6 커서 전진: 나머지 모든 분기”내부 루프 본문의 나머지 단계, 순서대로:
end_redo_lsa = lsa; lsa = log_rec->forw_lsa— 범위 끝이 디스패치 전에 먼저 이동한다.- 손상된 페이지의 페이지-전진 금지 (불변 조건 3-C).
- 아카이브 null-forward 수정: 아카이브 페이지에서
lsa가 NULL이면 →log_lsa.pageid + 1— 불완전한 아카이빙이며 로그 끝이 아님. - 루프 가드.
lsa가 역방향 또는 제자리(lsa.pageid < log_lsa.pageid, 또는 같은 페이지에서lsa.offset <= log_lsa.offset): “loop in the log” 디버그 트레이스,logpb_fatal_error, 그다음LSA_SET_NULL (&lsa); break;. - 누락된 EOF 복구.
lsaNULL,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. - 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. - 디스패치.
log_rv_analysis_record가 트랜잭션 상태를 재건한다(4장);LOG_END_OF_LOGcase는log_rv_analysis_log_end(3.8). - 디스패치 후 잘림.
*did_incom_recovery상승(3.7):end_redo_lsa = prev_lsa— 트리거 레코드는 redo에서 제외;lsanull 처리 후 break. - 자기 루프 assert.
LSA_EQ (end_redo_lsa, &lsa)— 커서가 움직이지 않음:assert_release, NULL 커서로 스캔 중단. - 손상된 페이지 +
LOG_END_OF_LOG→ 정지 (불변 조건 3-C의 후반부). prev_lsa = end_redo_lsa; prev_prev_lsa = prev_lsa;— resetlog 앵커가 끝점보다 한 레코드 뒤를 추적한다.- Page-id 역채움. 앞으로 가리키는
(pageid, NULL_OFFSET)에 오래된 더 작은 pageid가 있으면 → 현재 페이지로 수정 (3.3의 복구와 쌍을 이룸).
불변 조건 3-A (단조 커서). 매 반복마다 커서 (pageid, offset)이 엄격하게 증가한다. 4단계와 9단계가 스캔을 종단시키는 방식으로 강제한다. 위반 시: 분석이 무한 루프에 빠진다.
3.7 시점 지정 정지: stop_at와 LOG_REC_DONETIME
섹션 제목: “3.7 시점 지정 정지: stop_at와 LOG_REC_DONETIME”stop_at은 log_recovery에서 온다 — 일반 재시작에서는 -1(제한 없음), 미디어 크래시에서는 restoredb -d 타임스탬프. 드라이버 자체는 커밋 시각을 직접 읽지 않는다 — log_rv_analysis_complete가 LOG_COMMIT / LOG_ABORT를 처리하며(log_rv_analysis_commit_with_postpone도 LOG_REC_START_POSTPONE의 at_time에 동일 검사를 적용한다). 헤더 뒤 LOG_REC_DONETIME 페이로드를 읽어서, *stop_at != (time_t) (-1)이고 difftime (*stop_at, last_at_time) < 0 — 정지 지점 이후에 찍힌 첫 번째 완료 레코드 — 이면 페이지 커서를 null로 만들고, log_recovery_resetlog (thread_p, &record_header_lsa, prev_lsa)를 호출해 이 커밋 직전에서 로그를 자르며, *did_incom_recovery를 세운다; 3.6의 8단계가 해당 레코드를 제외하고 스캔을 종료한다. 이 last_at_time은 지역 변수이며, 3.2에서 *stop_at으로 에코되는 드라이버의 사본은 -1을 유지 — 현재 비활성.
3.8 log_rv_analysis_log_end, 2PC 재독 꼬리, 그리고 출력
섹션 제목: “3.8 log_rv_analysis_log_end, 2PC 재독 꼬리, 그리고 출력”드라이버 스토리에 속하는 유일한 디스패치 case는 로그의 깔끔한 종료인 log_rv_analysis_log_end다 — logpb_is_page_in_archive로 한 번 분기한다. 활성 로그에서는 EOF 자체의 위치가 log_Gl.hdr.append_lsa가 된다(LOG_RESET_APPEND_LSA (log_lsa) — 새 추가가 EOF 레코드를 덮어씀), next_trid는 소유자로부터 복원되고, 커서는 EOF의 NULL forw_lsa를 취한다 — 두 루프 모두 종료된다(누락된 EOF 복구는 LOG_END_OF_LOG를 면제함). 아카이브 페이지의 EOF는 아카이빙 컷 이전의 구식 잔재다: 헤더는 건드리지 않고 NULL forward가 3.6 3단계를 통과하며 스캔이 계속된다.
2PC 재독 꼬리. 디스패치된 레코드 중에 may_need_synch_checkpoint_2pc를 세운 것이 있으면(체크포인트 시점에 2PC 상태인 트랜잭션을 나열하는 LOG_REC_CHKPT — 4장), 드라이버는 외부 루프 이후 체크포인트 레코드를 재독한다: (1) checkpoint_lsa에서 logpb_fetch_page; 실패 시 치명; (2) chkpt.ntrans 항목으로 이루어진 LOG_INFO_CHKPT_TRANS 배열, log_lsa.offset + size < LOGAREA_SIZE이면 in-page 읽기, 아니면 malloc + logpb_copy_from_log(malloc 실패: 치명); (3) 각 chkpt_trans[i].trid를 logtb_find_tran_index로 resolve; log_2pc_recovery_analysis_info는 아직 LOG_ISTRAN_2PC인 tdes에 대해서만 실행. 3.2의 미디어 크래시 경로는 이 꼬리 이전에 반환 — 잘린 복원에서는 2PC 재구성을 건너뛴다.
log_recovery는 그다음 범위와 카운트에서 ER_LOG_RECOVERY_REDO_STARTED를 방출한다; log_cnt_pages_containing_lsa는 *to_lsa == *from_lsa이면 0을, 아니면 포함적인 to_lsa->pageid - from_lsa->pageid + 1을 반환한다. 앵커 이후에 살아남은 것이 아무것도 없으면, end_redo_lsa는 초기화 값인 start_redo_lsa와 동일한 채로 — 카운트는 정직하게 0이다.
3.9 챕터 요약 — 핵심 정리
섹션 제목: “3.9 챕터 요약 — 핵심 정리”log_recovery_analysis는 페이지 페치 외부 루프와 레코드 스텝 내부 루프의 2단 구조다; 손상 판단은 드라이버가, 레코드 의미론은log_rv_analysis_record(4–5장)가 담당한다.- 페이지 broken 판정은
is_media_crash로 분기한다: 백업 복원은prev_lsa에서log_recovery_resetlog로 잘리고did_incom_recovery를 세우며, 일반 재시작은 치명적 오류다(ER_TDE_CIPHER_IS_NOT_LOADED는 “TDE 키를 로드하라”는 뜻). - 부분 페이지 플러시는 페이지당 한 번의 CRC 검사로 감지하며; 티어는 첫 번째 전체-
0xff4 KB 블록이고,first_corrupted_rec_lsa가 세 가지 레코드 단위 결과로 스캔을 컷오프한다. 손상된 페이지는 종단 조건이다(불변 조건 3-C). log_is_page_of_record_broken(미디어 크래시 전용)은forw_lsa의 타당성을 검증하고 다중 페이지 레코드의 마지막 페이지를 탐색 페치한다; 꼬리가 누락되면end_redo_lsa와prev_lsa를 깨진 레코드 위에 파킹하여 resetlog가 그 위치에서 자른다.- redo 범위는 정직하다(불변 조건 3-B):
end_redo_lsa앞의 모든 것은 읽을 수 있고 완전하며; 경계는 완전히 탐색됐거나, redo가 해당 위치를 읽기 전에 재초기화된다. - 시점 지정 복원은
log_rv_analysis_complete(LOG_REC_DONETIME)에 있고 드라이버에는 없다; 분석은 읽기 전용도 아니다 — 누락된LOG_END_OF_LOG는log_startof_nxrec,forw_lsa패치, 페이지 플러시로 물리적 복구된다.
Chapter 4: 분석 레코드 디스패치와 트랜잭션 테이블 재구성
섹션 제목: “Chapter 4: 분석 레코드 디스패치와 트랜잭션 테이블 재구성”Chapter 3의 드라이버는 읽어 들인 모든 LOG_RECORD_HEADER를 log_rv_analysis_record로 전달한다. 이 챕터에서는 각 분기가 트랜잭션 테이블 상태를 어떻게 재구성하는지, 그리고 append point, next TRANID, MVCCID horizon 같은 전역 카운터가 어떻게 함께 갱신되는지를 추적한다. postpone/sysop 분기는 Chapter 5에서, ARIES 이론은 cubrid-recovery-manager.md에서 다룬다.
4.1 log_rv_analysis_record의 dispatch switch
섹션 제목: “4.1 log_rv_analysis_record의 dispatch switch”log_rv_analysis_record는 순수한 역다중화기(demultiplexer)다: switch (log_type) 하나만 있을 뿐 자체 로직은 없고, 포인터 파라미터는 모두 Chapter 3 드라이버 루프의 상태에 속한다. 모든 LOG_RECTYPE은 정확히 하나의 분기로 떨어진다:
| 레코드 타입 | 핸들러 | 테이블에 미치는 효과 |
|---|---|---|
LOG_UNDOREDO_DATA, LOG_DIFF_UNDOREDO_DATA, LOG_UNDO_DATA, LOG_REDO_DATA, 그리고 그것들의 LOG_MVCC_* 쌍 네 개, LOG_DBEXTERN_REDO_DATA | log_rv_analysis_undo_redo | tail_lsa + undo_nxlsa 전진 (4.3) |
LOG_SAVEPOINT | log_rv_analysis_save_point | 위와 동일, savept_lsa 추가 (4.3) |
LOG_COMPENSATE | log_rv_analysis_compensate | 이미 undo된 작업을 건너뛰도록 undo_nxlsa 재설정 (4.3) |
LOG_COMMIT, LOG_ABORT | log_rv_analysis_complete | tran index 해제, 또는 분석 조기 종료 (4.4) |
LOG_2PC_* 7종 | log_rv_analysis_2pc_* 7개 분기 | 2PC tdes->state 스탬핑 (4.5) |
LOG_START_CHKPT / LOG_END_CHKPT | log_rv_analysis_start_checkpoint / _end_checkpoint | may_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_LOG | log_rv_analysis_log_end | append point + next_trid 재설정 (4.10) |
LOG_DUMMY_CRASH_RECOVERY, LOG_REPLICATION_DATA, LOG_REPLICATION_STATEMENT, LOG_DUMMY_HA_SERVER_STATE, LOG_DUMMY_OVF_RECORD, LOG_DUMMY_GENERIC, LOG_SUPPLEMENTAL_INFO | 없음 — 단순 break | 테이블 효과 없음 |
LOG_SMALLER_LOGREC_TYPE, LOG_LARGER_LOGREC_TYPE, default | 없음 | er_set (ER_LOG_PAGE_CORRUPTED) + assert (false) — “로그가 손상되었을 가능성이 높음” |
반환 코드는 버려진다 — 대부분 (void) 캐스트로 처리되고, sysop-end 및 checkpoint 분기는 단순히 무시한다. 거의 모든 실패는 logpb_fatal_error를 호출해 recovery를 종료한다. 유일한 예외는 end-checkpoint의 sysop 재읽기(4.8 7단계)로, debug 빌드에서는 assert하고 release 빌드에서는 오류를 삼킨다.
4.2 logtb_rv_find_allocate_tran_index — 지연(lazy) TDES 할당자
섹션 제목: “4.2 logtb_rv_find_allocate_tran_index — 지연(lazy) TDES 할당자”거의 모든 분기는 여기서 시작한다(log_tran_table.c): tran_id를 TDES로 매핑하며, 처음 발견할 때 할당한다. 세 가지 분기가 있다: B1 — logtb_is_system_worker_tranid (trid)가 log_system_tdes::rv_get_or_alloc_tdes로 단락(short-circuit)하여 시스템 워커를 테이블 밖에 유지한다. B2 — logtb_find_tran_index 미스: logtb_allocate_tran_index (..., TRAN_UNACTIVE_UNILATERALLY_ABORTED, ...) 후 LSA_COPY (&tdes->head_lsa, log_lsa) 실행; 할당 실패는 logpb_fatal_error + return NULL. B3 — 히트: LOG_FIND_TDES.
불변 조건 — 추정 abort(presumed abort). 분석 중 생성된 모든 TDES는 TRAN_UNACTIVE_UNILATERALLY_ABORTED 상태로 태어나며, head_lsa는 최초 발견 시점이다. 이후 완료 레코드(4.4, 제거)나 2PC/postpone 레코드(상태 업그레이드)만이 이 판정을 바꿀 수 있다. 다른 초기 상태를 부여하면 undo 패스(Chapter 9)가 패자(loser) 트랜잭션을 건너뛰어 그 변경이 디스크에 남는다.
4.3 단순 분기 — undo_redo, save_point, compensate
섹션 제목: “4.3 단순 분기 — undo_redo, save_point, compensate”log_rv_analysis_undo_redo는 아홉 가지 데이터 변경 타입 전체를 처리한다. 비정상 경로는 하나뿐: TDES가 NULL이면 logpb_fatal_error, return ER_FAILED. 그 외에는 LSA_COPY (&tdes->tail_lsa, log_lsa) 후 LSA_COPY (&tdes->undo_nxlsa, &tdes->tail_lsa): tail_lsa는 가장 최근 레코드, undo_nxlsa는 undo가 역방향으로 걷기 시작하는 위치다. 평범한 데이터 레코드에서는 두 값이 일치한다. log_rv_analysis_save_point는 재시작 후 부분 롤백을 위해 LSA_COPY (&tdes->savept_lsa, &tdes->tail_lsa)를 추가한다.
log_rv_analysis_compensate는 LOG_COMPENSATE — 어떤 업데이트가 이미 undo되었다는 증거인 CLR(Compensation Log Record) — 를 처리하며, undo_nxlsa가 tail_lsa와 달라지는 유일한 분기다. 할당자 + NULL-fatal 분기 이후, LOG_REC_COMPENSATE 본문까지 전진(LOG_READ_ADD_ALIGN, LOG_READ_ADVANCE_WHEN_DOESNT_FIT)한 뒤 복사를 하나만 실행한다 — LSA_COPY (&tdes->undo_nxlsa, &compensate->undo_nxlsa) — 이때 tail_lsa는 전진하지 않는다. 복사된 포인터는 보상된 업데이트 이전에 위치하므로 undo는 CLR 자체에서 다시 시작하지 않는다: ARIES의 “이미 undo한 것은 다시 undo하지 않는다(never-undo-an-undo)” 규칙을 포인터 재설정만으로 구현한 것이다.
4.4 log_rv_analysis_complete — commit/abort 마무리
섹션 제목: “4.4 log_rv_analysis_complete — commit/abort 마무리”LOG_COMMIT과 LOG_ABORT는 log_rv_analysis_complete를 공유한다 — 테이블 상태를 제거하는 유일한 분기이며, 두 개의 조기 종료 분기 중 하나다(나머지는 Chapter 5의 log_rv_analysis_commit_with_postpone로, 동일한 stop_at/resetlog 분기를 가진다). 네 가지 분기:
// log_rv_analysis_complete -- src/transaction/log_recovery.c tran_index = logtb_find_tran_index (thread_p, tran_id); /* <- find, never allocate */ // ... condensed: B1 -- if not media crash, goto end; else read LOG_REC_DONETIME -> last_at_time ... if (stop_at != NULL && *stop_at != (time_t) (-1) && difftime (*stop_at, last_at_time) < 0) { /* B2: completion is newer than --until-time */ log_lsa->pageid = NULL_PAGEID; log_recovery_resetlog (thread_p, &record_header_lsa, prev_lsa); *did_incom_recovery = true; return NO_ERROR; /* <- index NOT freed: tran stays a loser */ }end: // ... condensed: B3 -- if tran_index != NULL_TRAN_INDEX, logtb_free_tran_index ... return NO_ERROR; /* B4: never seen before -> nothing to drop */두 가지 비대칭이 있다: 이 함수는 찾을 뿐 할당하지 않는다 — 윈도우 이전에 완료된 트랜잭션은 no-op(B4)이다. 그리고 B2는 index를 유지한다 — commit 레코드에서 로그를 잘라내면 해당 트랜잭션이 소급하여 진행 중인 것으로 처리되어, undo가 롤백한다: 이것이 타임스탬프 시점 복원(restore-to-timestamp)이다.
4.5 7개의 2PC 분기 — 상태 전이 표
섹션 제목: “4.5 7개의 2PC 분기 — 상태 전이 표”구조적으로 동일하다: TDES 할당(NULL: logpb_fatal_error, ER_FAILED), tdes->state 덮어쓰기, tail_lsa 전진. 어느 분기도 undo_nxlsa를 건드리지 않는다. 차이는 스탬핑하는 상태뿐이다:
| 레코드 타입 | 핸들러 | 스탬핑되는 tdes->state |
|---|---|---|
LOG_2PC_PREPARE | log_rv_analysis_2pc_prepare | TRAN_UNACTIVE_2PC_PREPARE |
LOG_2PC_START | log_rv_analysis_2pc_start | TRAN_UNACTIVE_2PC_COLLECTING_PARTICIPANT_VOTES |
LOG_2PC_COMMIT_DECISION | log_rv_analysis_2pc_commit_decision | TRAN_UNACTIVE_2PC_COMMIT_DECISION |
LOG_2PC_ABORT_DECISION | log_rv_analysis_2pc_abort_decision | TRAN_UNACTIVE_2PC_ABORT_DECISION |
LOG_2PC_COMMIT_INFORM_PARTICPS | log_rv_analysis_2pc_commit_inform_particps | TRAN_UNACTIVE_COMMITTED_INFORMING_PARTICIPANTS |
LOG_2PC_ABORT_INFORM_PARTICPS | log_rv_analysis_2pc_abort_inform_particps | TRAN_UNACTIVE_ABORTED_INFORMING_PARTICIPANTS |
LOG_2PC_RECV_ACK | log_rv_analysis_2pc_recv_ack | 변경 없음 — tail_lsa만 전진 |
LOG_2PC_PREPARE는 참여자(participant) 측 레코드이고, 나머지는 코디네이터(coordinator) 레코드다. prepare와 start는 tdes->gtrid = LOG_2PC_NULL_GTRID도 심는다: 이는 본문(gtrid, 참여자, lock)이 아직 읽히지 않았음을 나타내는 sentinel이다 — “redo 패스나 finish_commit_protocol 패스 중에 읽어야 한다”(소스 주석). 4.9에서 소비된다.
4.6 체크포인트 페이로드 구조체
섹션 제목: “4.6 체크포인트 페이로드 구조체”완성된 체크포인트는 두 레코드로 구성된다: 빈 LOG_START_CHKPT 앵커와, 본문이 LOG_REC_CHKPT 헤더, ntrans개의 LOG_INFO_CHKPT_TRANS 항목, 그리고 ntops개의 LOG_INFO_CHKPT_SYSOP 항목으로 이루어진 LOG_END_CHKPT이다(log_record.hpp).
LOG_REC_CHKPT (log_rec_chkpt)는 세 개의 필드를 가진다: redo_lsa — 더티(dirty) 데이터 페이지 중 가장 오래된 recovery LSA로, redo는 반드시 가장 오래된 미반영 변경점에서 시작해야 하기 때문이다(4.8 8단계); ntrans와 ntops — 뒤따르는 두 배열의 항목 수로, 배열은 자기 기술(self-delimiting)이 아니다(ntops는 흔히 0이다).
LOG_INFO_CHKPT_TRANS (log_info_chkpt_trans) — 살아 있는 각 트랜잭션의 직렬화된 TDES 하나:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
isloose_end | tdes->isloose_end로 복사 | 클라이언트 loose end |
trid | 트랜잭션 ID | 할당자의 병합 키 |
state | 스냅샷 시점 상태; TRAN_ACTIVE와 TRAN_UNACTIVE_ABORTED는 TRAN_UNACTIVE_UNILATERALLY_ABORTED로 재매핑, 나머지는 그대로 | 추정 abort; 2PC/postpone 상태는 보존 |
head_lsa | tdes->head_lsa로 복사 | 분석 윈도우 이전일 수 있음 |
tail_lsa | tdes->tail_lsa로 복사 | 체인 재개 지점; 2PC 역방향 탐색 커서 (4.9) |
undo_nxlsa | tdes->undo_nxlsa로 복사 | 체크포인트 이전 CLR 재설정 (4.3) |
posp_nxlsa | tdes->posp_nxlsa로 복사 | postpone 체인 시작점 (Chapter 5) |
savept_lsa | tdes->savept_lsa로 복사 | 체크포인트 이전 savepoint |
tail_topresult_lsa | tdes->tail_topresult_lsa로 복사 | 롤백 시 완료된 sysop 건너뛰기 |
start_postpone_lsa | tdes->rcv.tran_start_postpone_lsa로 복사 | postpone 완료 (Chapter 8) |
user_name | set_system_internal_with_user로 tdes->client에 복사 | loose-end 소유자 |
LOG_INFO_CHKPT_SYSOP (log_info_chkpt_sysop) — postpone을 동반한 커밋(committing with postpone) 중인 sysop만 체크포인팅된다. 진행 중인 일반 sysop은 해당 트랜잭션과 함께 그냥 소멸한다:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
trid | 소유 트랜잭션 | sysop 배열은 평탄(flat); 항목은 id로 결합 |
sysop_start_postpone_lsa | tdes->rcv.sysop_start_postpone_lsa로 복사 | 非null이면 해당 레코드 재읽기 트리거 (4.8 7단계) |
atomic_sysop_start_lsa | tdes->rcv.atomic_sysop_start_lsa로 복사 | atomic sysop abort 구동 (Chapter 8) |
4.7 log_rv_analysis_start_checkpoint와 may_use_checkpoint 가드
섹션 제목: “4.7 log_rv_analysis_start_checkpoint와 may_use_checkpoint 가드”LOG_START_CHKPT 분기는 조건 하나뿐이다 — if (LSA_EQ (log_lsa, start_lsa)) { *may_use_checkpoint = true; } — 그리고 이 조건이 설계의 핵심이다. start_lsa는 분석이 시작된 위치다: 체크포인트가 완료될 때만 갱신되는 log_Gl.hdr.chkpt_lsa(Chapter 3). 플래그는 앵커 start 레코드에 대해서만 활성화되며, 스캔 도중 중간에 만나는 LOG_START_CHKPT에는 활성화되지 않는다 — 그런 스냅샷은 “더 이상 존재하지 않는 내용을 포함할 수 있다”(소스 주석).
stateDiagram-v2
[*] --> 비활성 : 분석 시작, 플래그 false
비활성 --> 활성화됨 : start_lsa에서 LOG_START_CHKPT
비활성 --> 비활성 : 다른 위치의 LOG_START_CHKPT, LSA_EQ 실패
활성화됨 --> 소비됨 : LOG_END_CHKPT, 스냅샷 병합 후 플래그 해제
비활성 --> 비활성 : LOG_END_CHKPT, 가드에서 조기 반환
소비됨 --> 소비됨 : 이후 체크포인트 레코드 무시
Figure 4-1: may_use_checkpoint의 생명주기. 앵커 START와 쌍을 이루는 END만 스냅샷을 병합할 수 있다.
이는 크래시 윈도우 문제에 대한 답이다. START와 END 사이에 크래시 발생: 헤더는 여전히 이전 완성된 체크포인트를 가리키며, 미완성 윈도우의 START는 LSA_EQ를 통과하지 못하고, END는 기록되지 않았다. 범위 내의 두 번째 완성 윈도우(미디어 복구의 경우): 해당 START는 LSA_EQ를 통과하지 못하고, 해당 END는 4.8 가드에서 죽는다.
4.8 log_rv_analysis_end_checkpoint — 스냅샷 병합, 분기별 설명
섹션 제목: “4.8 log_rv_analysis_end_checkpoint — 스냅샷 병합, 분기별 설명”가장 긴 분기; 모든 분기를 설명한다:
- 가드.
if (*may_use_checkpoint == false) return NO_ERROR;— 쌍이 없는 END는 여기서 죽는다. 그 외에는 플래그가 즉시 해제된다: 단발(single-shot). - 앵커 캡처.
LSA_COPY (check_point, log_lsa)로 END의 LSA를 드라이버의checkpoint_lsa에 저장 — run-postpone 분기(Chapter 5)와 4.9에서 사용된다. - 헤더 읽기.
LOG_REC_CHKPT를 값으로 복사한다(chkpt = *tmp_chkpt) — 이후 페이지 전진으로 해당 페이지가 evict될 수 있기 때문이다. - Trans 배열 — 두 가지 분기. 페이지 내(
log_lsa->offset + size < LOGAREA_SIZE)인 경우: 제자리에서 사용; 그 외:malloc+logpb_copy_from_log; malloc 실패는 fatal. chkpt.ntrans항목에 대한 병합 루프 — 먼저 할당자(NULL: 빈 공간,logpb_fatal_error,ER_FAILED), 그리고:
// log_rv_analysis_end_checkpoint -- src/transaction/log_recovery.c logtb_clear_tdes (thread_p, tdes); /* <- wipe what the loop built so far */ if (chkpt_one->state == TRAN_ACTIVE || chkpt_one->state == TRAN_UNACTIVE_ABORTED) { tdes->state = TRAN_UNACTIVE_UNILATERALLY_ABORTED; /* <- presumed-abort remap */ } else { tdes->state = chkpt_one->state; /* <- 2PC / postpone states survive */ } // ... condensed: isloose_end, six LSA_COPYs, rcv.tran_start_postpone_lsa, user name ... if (LOG_ISTRAN_2PC (tdes)) { *may_need_synch_checkpoint_2pc = true; /* <- defer 2PC body reads (4.9) */ }불변 조건 — END 레코드와의 스냅샷 원자성. logtb_clear_tdes는 START와 END 사이의 레코드들로 이미 구축된 상태를 덮어쓴다. 이것이 안전한 이유는 logpb_checkpoint가 테이블을 스냅샷하고 LOG_END_CHKPT를 단일 log_Gl.prior_info.prior_lsa_mutex 보유 하에(prior_lsa_next_record_with_lock) append하기 때문이다: 그 사이에 아무것도 append되지 않으므로, 스냅샷이 START 이후의 모든 것을 대체한다. mutex를 일찍 해제하면 이 병합이 tail_lsa/undo_nxlsa를 조용히 퇴행시켜 undo가 살아있는 변경을 건너뛸 것이다.
6. Trans 영역 해제. free_and_init (area) — sysop 배열을 위해 area를 null로 만든다.
7. chkpt.ntops > 0으로 게이트된 sysop 병합. 4단계와 동일한 페이지-내-vs-malloc 분기. 항목별로: trid로 TDES 할당; tdes->topops.max == 0 || (tdes->topops.last + 1) >= tdes->topops.max인 경우 topops 스택 확장(logtb_realloc_topops_stack)(실패: free, fatal); 두 LSA를 tdes->rcv에 복사. sysop_start_postpone_lsa가 non-null인 경우: topops.last를 -1에서 0으로 증가 — 그렇지 않으면 assert (tdes->topops.last == 0), recovery 중 최대 한 단계 — 그리고 log_read_sysop_start_postpone가 프라이빗 페이지 버퍼에서 해당 레코드를 재읽어 체크포인트 항목이 생략한 topops.stack[last].lastparent_lsa와 .posp_lsa를 채운다. 분석이 오래된 레코드를 재읽는 유일한 지점; 오류 경로는 assert (false); return error_code; — logpb_fatal_error 없음(4.1).
8. Redo 시작점 당기기. if (LSA_LT (&chkpt.redo_lsa, start_redo_lsa)) LSA_COPY (start_redo_lsa, &chkpt.redo_lsa); — redo(Chapter 6)는 가장 오래된 더티 페이지의 recovery LSA에서 시작한다.
9. 최종 free_and_init (area) — sysop 복사본 해제(페이지 내인 경우 no-op), NO_ERROR 반환.
4.9 may_need_synch_checkpoint_2pc — 지연된 2PC 재구성
섹션 제목: “4.9 may_need_synch_checkpoint_2pc — 지연된 2PC 재구성”메인 루프 이후, log_recovery_analysis는 저장된 checkpoint_lsa의 LOG_END_CHKPT를 다시 fetch하고, TDES가 여전히 LOG_ISTRAN_2PC를 만족하는 모든 trans 항목에 log_2pc_recovery_analysis_info (thread_p, tdes, &chkpt_trans[i].tail_lsa) (log_2pc.c)를 호출한다: 스냅샷 시점의 tail_lsa에서 시작하는 prev_tranlsa 역방향 체인 탐색으로, tdes->gtrid == LOG_2PC_NULL_GTRID인 동안 LOG_2PC_PREPARE 본문을, tdes->coord == NULL인 동안 LOG_2PC_START 본문을 읽고, ack를 수집한다. 스냅샷은 “큰 공간 오버헤드 때문에”(소스 주석, 예: lock) 2PC 본문을 생략하며, 이 본문들은 윈도우 이전에 있을 수 있다 — 역방향 탐색만이 이를 복원할 수 있다. 재확인은 스냅샷 이후 완료된 트랜잭션을 건너뛴다.
4.10 LOG_END_OF_LOG, next_trid, MVCCID 복원
섹션 제목: “4.10 LOG_END_OF_LOG, next_trid, MVCCID 복원”트랜잭션별 재구성과 함께 두 가지 전역 상태가 따라간다. 첫째, EOF 분기 — log_rv_analysis_log_end는 하나의 조건이다, if (!logpb_is_page_in_archive (log_lsa->pageid)): 활성 로그의 EOF만 유효하다. 그 안에서 LOG_RESET_APPEND_LSA (log_lsa)는 재시작 후 쓰기가 EOF를 덮어쓰도록 append point를 재설정하며, log_Gl.hdr.next_trid = tran_id는 EOF 레코드 자체의 trid에서 TRANID 카운터를 재시작한다 — 재시작은 로그에서 이미 본 ID를 재발급하지 않는다. 아카이브 내의 EOF는 불완전한 로그를 아카이빙한 아티팩트이므로 건너뛴다; EOF가 발견되지 않는 경우의 복구 경로는 드라이버(Chapter 3) 소관이다.
둘째, MVCCID. 의도적으로 어떠한 분석 분기도 tdes->mvccinfo를 복원하지 않는다 — 재구성된 패자(loser)는 분석 후 MVCCID를 갖지 않는다. 대신 log_recovery_analysis의 마지막 문(그리고 불완전 복구 조기 반환의 마지막 문)은 log_Gl.mvcc_table.reset_start_mvccid () (mvcc_table.cpp)로, log_Gl.hdr.mvcc_next_id에서 활성 MVCCID 비트맵 시작점과 m_current_status_lowest_active_mvccid를 다시 시딩한다: 그보다 낮은 모든 MVCCID는 더 이상 활성이 아닌 것으로 처리된다. Redo는 헤더 값을 정제하며 — 재현된 각 MVCC 레코드는 log_Gl.hdr.mvcc_next_id를 자신의 ID 너머로 밀어낸다 — reset_start_mvccid는 redo 이후(Chapter 6)에 한 번 더 실행된다. 패자의 원래 MVCCID는 undo 중에만 재등장한다: logtb_rv_assign_mvccid_for_undo_recovery가 undo되는 레코드의 rcv->mvcc_id에서 tdes->mvccinfo.id를 설정한다(Chapter 9).
4.11 챕터 요약 — 핵심 정리
섹션 제목: “4.11 챕터 요약 — 핵심 정리”log_rv_analysis_record는 로직이 없는 순수 역다중화기다. 알 수 없는LOG_RECTYPE은 페이지 손상이고, 7개의 dummy/replication 타입은 no-op이다. 핸들러 실패는logpb_fatal_error로 끝난다 — end-checkpoint의 sysop 재읽기는 예외로, release 빌드에서 버려진다.logtb_rv_find_allocate_tran_index는 추정 abort를 강제한다: 트랜잭션은 최초 발견 시TRAN_UNACTIVE_UNILATERALLY_ABORTED로 태어나고, 시스템 워커는 별도의log_system_tdes맵에 산다.undo_nxlsa가tail_lsa와 달라지는 것은 오직log_rv_analysis_compensate뿐으로, CLR에 저장된 포인터로 이미 undo된 작업을 건너뛴다.log_rv_analysis_complete는 찾을 뿐 할당하지 않으며, 테이블 상태를 제거하는 유일한 분기다.stop_at분기는 로그를 자르고 index를 유지한다 — 시점 복원.- 7개의 2PC 분기는 스탬핑되는
TRAN_STATE만 다르다. prepare/start는 사후 루프에서log_2pc_recovery_analysis_info역방향 탐색이 소비하는gtrid = LOG_2PC_NULL_GTRIDsentinel을 심는다. LOG_END_CHKPT는 오직 정확히start_lsa의LOG_START_CHKPT에 의해 활성화된 경우에만 병합된다 — 반쯤 만들어진 또는 추가적인 체크포인트 윈도우는 구조적으로 무시된다.logtb_clear_tdes-후-덮어쓰기 병합은logpb_checkpoint가 테이블을 스냅샷하고 END를 단일prior_lsa_mutex보유 하에 append하기 때문에 안전하다.- 전역 카운터가 함께 간다:
LOG_END_OF_LOG는 append point와next_trid를 재설정한다. MVCCID는 트랜잭션별로 재구성되지 않는다 —reset_start_mvccid가log_Gl.hdr.mvcc_next_id에서 MVCC 테이블을 다시 시딩하고, undo가 패자의 MVCCID를 지연적으로 재부착한다.
Chapter 5: 분석 단계의 Sysop·Postpone 어노테이션 기록
섹션 제목: “Chapter 5: 분석 단계의 Sysop·Postpone 어노테이션 기록”시스템 연산(sysop) 중간, atomic sysop 범위 내, 또는 커밋 시점의 postpone 처리 도중에 크래시가 발생하면 — 이 “지저분한 중간 상태”들은 LOG_RCV_TDES에 다섯 개의 LSA 어노테이션으로 기록된다. 이 어노테이션들은 아래 설명하는 log_rv_analysis_* 핸들러들이 작성하며(드라이버: 3장, 디스패치: 4장), 이후 6장·8장의 복구 단계에서 소비된다. 이론적 배경은 상위 동반 문서 cubrid-recovery-manager.md를 참고한다.
5.1 LOG_RCV_TDES — 복구 어노테이션 블록
섹션 제목: “5.1 LOG_RCV_TDES — 복구 어노테이션 블록”LOG_RCV_TDES(구조체 log_rcv_tdes, log_impl.h)는 다섯 개의 LOG_LSA 필드로 구성되며, 모든 LOG_TDES에 rcv 필드로 내장된다. 복구 단계 바깥에서는 다섯 필드 모두 null 상태를 유지한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
sysop_start_postpone_lsa | 가장 최근의 열린 LOG_SYSOP_START_POSTPONE LSA; log_rv_analysis_sysop_start_postpone이 기록하고 체크포인트에서 복원되며(4장) log_rv_analysis_sysop_end가 초기화 | log_recovery_finish_sysop_postpone(8장)이 이를 읽어 sysop의 postpone 목록을 재개 — end 레코드는 이 LSA를 가리키지 않음 |
tran_start_postpone_lsa | 트랜잭션의 LOG_COMMIT_WITH_POSTPONE LSA; log_rv_analysis_commit_with_postpone 및 구버전 변형이 기록하고 체크포인트에서 복원(4장) | null 여부가 sysop postpone 종료 시 복원할 상태를 결정(5.7); log_recovery_finish_postpone으로 전달됨 |
atomic_sysop_start_lsa | 매칭되지 않은 가장 최근의 LOG_SYSOP_ATOMIC_START LSA; log_rv_analysis_atomic_sysop_start이 기록하고 체크포인트에서 복원되며(4장), atomic 연산이 완료된 것이 확인되면 두 sysop 핸들러가 초기화 | redo 이후에도 set 상태이면 → log_recovery_abort_all_atomic_sysops(8장)가 postpone 재개 전에 이 LSA까지 롤백 |
analysis_last_aborted_sysop_lsa | 가장 최근의 ABORT 타입 LOG_SYSOP_END LSA; 해당 arm에서만 기록 | 논리 redo 건너뜀 범위의 상한(log_recovery_needs_skip_logical_redo, 6장) |
analysis_last_aborted_sysop_start_lsa | 동일한 aborted sysop end의 lastparent_lsa | 동일 건너뜀 범위의 하한 |
flowchart LR
cwp["commit_with_postpone"] --> f1["tran_start_postpone_lsa"]
ssp["sysop_start_postpone"] --> f2["sysop_start_postpone_lsa"]
ats["atomic_sysop_start"] --> f3["atomic_sysop_start_lsa"]
se["sysop_end"] --> f4["analysis_last_aborted_sysop_lsa<br/>+ _start_lsa"]
se -. resets .-> f2
se -. resets .-> f3
f1 --> fp["finish_postpone (8장)"]
f1 --> fsp["finish_sysop_postpone (8장)"]
f2 --> fsp
f3 --> aas["abort_all_atomic_sysops (8장)"]
f4 --> skip["needs_skip_logical_redo (6장)"]
Figure 5-1: 어노테이션 기록자(왼쪽)와 redo 이후 소비자(오른쪽), 접두어 생략.
불변 조건 — 어노테이션은 해당 페이즈가 열려 있는 동안에만 유효하다. 분석 결과 크래시 전에 해당 페이즈가 정상 종료된 것이 확인되면 각 필드는 null로 초기화된다(초기화 가드, 5.7).
atomic_sysop_start_lsa가 잔존하면 → 8장이 이미 커밋된 연산을 롤백하고;sysop_start_postpone_lsa가 잔존하면 → 이미 실행된 postpone 목록이 재실행된다.
5.2 LOG_REC_SYSOP_END와 LOG_SYSOP_END_TYPE
섹션 제목: “5.2 LOG_REC_SYSOP_END와 LOG_SYSOP_END_TYPE”모든 시스템 연산은 LOG_SYSOP_END 레코드로 종료되며, 본문 구조체는 LOG_REC_SYSOP_END(log_record.hpp)이다 — 세 개의 고정 필드, vfid 포인터, 그리고 type으로 분기되는 union으로 구성된다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
lastparent_lsa | sysop 시작 직전 트랜잭션의 마지막 LSA | sysop 범위를 건너뛰는 undo 점프 목표; 어노테이션과 비교해 중첩 순서 판별 |
prv_topresult_lsa | 이전에 완료된 top action의 LSA | sysop 결과를 체인으로 연결해 부분 abort 시 건너뜀 가능(tail_topresult_lsa) |
type | 여섯 가지 LOG_SYSOP_END_TYPE 값 중 하나 | union 해석과 복구 동작 선택 |
vfid | 소유 파일; MVCC undo의 경우 vacuum-info 파일과 일치 | TDE(암호화) 컨텍스트 조회 |
union undo | 논리 undo 페이로드(LOGICAL_UNDO) | 다중 페이지 연산의 undo를 하나의 논리 복구 함수로 처리 |
union mvcc_undo | undo + MVCCID/vacuum 정보(LOGICAL_MVCC_UNDO) | vacuum이 해당 연산의 MVCCID를 확인해야 함 |
union compensate_lsa | 다음 undo LSA(LOGICAL_COMPENSATE) | sysop이 compensation 레코드를 대체; undo가 여기서 재개 |
union run_postpone | postpone_lsa + is_sysop_postpone 플래그(LOGICAL_RUN_POSTPONE) | LOG_RUN_POSTPONE를 대체; 플래그는 어느 postpone 목록의 커서를 전진시킬지 결정(5.7) |
LOG_SYSOP_END_TYPE(enum log_sysop_end_type, log_record.hpp)은 여섯 가지 값을 가진다: LOG_SYSOP_END_COMMIT(“영구 변경”), LOG_SYSOP_END_ABORT(“취소된 시스템 연산”), 그리고 네 가지 LOG_SYSOP_END_LOGICAL_* 변형인 UNDO, MVCC_UNDO, COMPENSATE, RUN_POSTPONE. union은 오직 type에 의해서만 분기되며(LOG_SYSOP_END_TYPE_CHECK로 검증), 각 값의 분석 시점 동작은 5.7에서 상세히 다룬다.
5.3 Postpone 관련 핸들러: LOG_POSTPONE, LOG_DUMMY_HEAD_POSTPONE, LOG_RUN_POSTPONE
섹션 제목: “5.3 Postpone 관련 핸들러: LOG_POSTPONE, LOG_DUMMY_HEAD_POSTPONE, LOG_RUN_POSTPONE”log_rv_analysis_postpone(LOG_POSTPONE)와 log_rv_analysis_dummy_head_postpone(no-op 마커 LOG_DUMMY_HEAD_POSTPONE)은 각각 두 가지 분기를 가진다: logtb_rv_find_allocate_tran_index == NULL일 때의 치명 에러 조기 반환(logpb_fatal_error, ER_FAILED)과, 첫 번째 postpone의 커서 캡처이다. LSA_ISNULL (posp_nxlsa) 조건이면 postpone 핸들러는 tail_lsa를 전진시키기 전에 이전 값을 posp_nxlsa에 복사(“set address early”)하고, dummy-head 핸들러는 먼저 전진한 뒤 캡처해 dummy head 자체를 가리킨다(“set address late”). posp_nxlsa는 log_recovery_find_first_postpone(8장)의 스캔 시작점이다.
log_rv_analysis_run_postpone는 LOG_RUN_POSTPONE(이미 실행되어 redo-log에 기록된 postpone)을 처리한다. 분기:
tdes == NULL→ 치명 에러,ER_FAILED.- 상태가 {
WILL_COMMIT,COMMITTED_WITH_POSTPONE,TOPOPE_COMMITTED_WITH_POSTPONE}에 속하지 않는 경우(TRAN_UNACTIVE_접두어 생략): 체크포인트된 tdes에 대해서는 불가능(SYSTEM ERROR 디버그 로그), 그 외에는 정상; 복구 측은topops.last == -1이면 committed-with-postpone, 아니면 topope-committed로 추정. - 상태가 이제
TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE면 →LSA_SET_NULL (undo_nxlsa): 남은 undo 없음. - 본문 읽기(2장 매크로);
run_posp->ref_lsa— 이 레코드가 실행한LOG_POSTPONE의 LSA — 가 커서를 재설정: topope 상태이면topops.stack[last].posp_lsa, 그 외 두 상태에서는tdes->posp_nxlsa(assert로 확인).
불변 조건 —
posp_nxlsa는 항상 아직 실행된 것으로 알려지지 않은 다음 postpone을 가리킨다.LOG_POSTPONE가 한 번 설정하고; 이후 모든LOG_RUN_POSTPONE가ref_lsa로 전진시킨다. 뒤처지면 → 8장이 동일 postpone을 두 번 실행하고; 너무 앞서면 → 미처리 작업이 조용히 유실된다.
5.4 Postpone을 동반한 트랜잭션 커밋
섹션 제목: “5.4 Postpone을 동반한 트랜잭션 커밋”log_rv_analysis_commit_with_postpone는 LOG_COMMIT_WITH_POSTPONE를 처리한다: 결과는 확정됐으나 deferred 작업이 미완료일 수 있는 상태. 치명 에러 분기 이후 LOG_REC_START_POSTPONE(posp_lsa + at_time)를 읽고 is_media_crash 여부로 분기한다.
// log_rv_analysis_commit_with_postpone -- src/transaction/log_recovery.c if (is_media_crash) { // ... condensed: stop_at test -> resetlog + *did_incom_recovery = true ... } else { tdes->state = TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE; LSA_SET_NULL (&tdes->undo_nxlsa); /* Nothing to undo */ LSA_COPY (&tdes->tail_lsa, log_lsa); tdes->rcv.tran_start_postpone_lsa = tdes->tail_lsa; /* <- annotation write */ LSA_COPY (&tdes->posp_nxlsa, &start_posp->posp_lsa); }media-crash arm은 시점 복구(point-in-time recovery)를 수행한다: stop_at != NULL && *stop_at != (time_t) (-1) && difftime (*stop_at, last_at_time) < 0 — 커밋 시각이 복원 목표 시각을 초과 — 이면 페이지를 해제하고 로그를 잘라낸 뒤(log_recovery_resetlog, 11장) *did_incom_recovery를 설정하며, 해당 트랜잭션은 커밋되지 않은 것으로 처리된다. stop_at 테스트를 통과하지 못하면(또는 stop_at이 NULL/-1이면) media-crash arm은 아무것도 하지 않는다 — 어노테이션 및 상태 갱신은 non-media-crash arm에서만 이루어진다.
log_rv_analysis_commit_with_postpone_obsolete(LOG_COMMIT_WITH_POSTPONE_OBSOLETE, 구 레이아웃 LOG_REC_START_POSTPONE_OBSOLETE — at_time 없음)는 non-media-crash arm만 수행한다 — 타임스탬프도 없고 시점 중단도 없다. 구버전 로그 읽기용으로만 유지되며 “maybe 12.0”에서 제거 예정이다.
5.5 log_rv_analysis_sysop_start_postpone
섹션 제목: “5.5 log_rv_analysis_sysop_start_postpone”LOG_SYSOP_START_POSTPONE는 sysop이 주요 작업을 마치고 자체 postpone 목록을 시작했음을 표시한다. 본문 LOG_REC_SYSOP_START_POSTPONE는 내장된 LOG_REC_SYSOP_END sysop_end(end 레코드에 기록될 내용)와 posp_lsa(sysop의 첫 번째 postpone)로 구성된다. 처리 순서:
- 치명 에러 분기 →
ER_FAILED. tail_lsa/undo_nxlsa전진; 어노테이션 기록:tdes->rcv.sysop_start_postpone_lsa = tdes->tail_lsa.- 내장된 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 페이즈를 시작할 수 없음. - 상태 :=
TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE. - 필요하면
logtb_realloc_topops_stack으로 topops 스택 확장(실패 시ER_OUT_OF_VIRTUAL_MEMORY);topops.last는 반드시-1이어야 하며0으로 증가(assert (false)위반 시);lastparent_lsa와posp_lsa를topops.stack[0]에 복사. 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를 전진시키게 된다.
5.6 log_rv_analysis_atomic_sysop_start
섹션 제목: “5.6 log_rv_analysis_atomic_sysop_start”가장 단순한 핸들러로 LOG_SYSOP_ATOMIC_START를 처리한다 — 두 분기: 치명 에러, 그리고 성공. 성공 분기는 tail_lsa/undo_nxlsa를 전진시킨 후 tdes->rcv.atomic_sysop_start_lsa = *log_lsa를 기록한다(이 레코드는 본문이 없으며 LSA 자체가 페이로드). 이 값이 지워지지 않으면(5.5, 5.7) log_recovery_abort_all_atomic_sysops → log_recovery_abort_atomic_sysop(8장)이 postpone 재개 전에 이 LSA까지 트랜잭션을 롤백한다.
5.7 log_rv_analysis_sysop_end — 복잡한 핵심 핸들러
섹션 제목: “5.7 log_rv_analysis_sysop_end — 복잡한 핵심 핸들러”아직 추정 상태에 있는 트랜잭션의 알 수 없는 종류의 sysop를 닫는다. 프롤로그: 치명 에러 분기; tail_lsa, undo_nxlsa, tail_topresult_lsa 전진; LOG_REC_SYSOP_END 읽기; LOG_SYSOP_END_TYPE_CHECK. 이후 switch문에서 로컬 변수 commit_start_postpone이 이 end 레코드가 열린 sysop-postpone 페이즈도 함께 닫는지 결정한다.
// log_rv_analysis_sysop_end -- src/transaction/log_recovery.c case LOG_SYSOP_END_ABORT: // ... condensed: comment -- abort neither changes state nor finishes a topope postpone ... if (tdes->state == TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE) { LSA_SET_NULL (&tdes->undo_nxlsa); /* no undo */ } tdes->rcv.analysis_last_aborted_sysop_lsa = *log_lsa; /* <- skip-range upper bound */ tdes->rcv.analysis_last_aborted_sysop_start_lsa = sysop_end->lastparent_lsa; /* <- lower bound */ break; case LOG_SYSOP_END_COMMIT: assert (tdes->state != TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE); /* <- falls through to next cases */ case LOG_SYSOP_END_LOGICAL_UNDO: case LOG_SYSOP_END_LOGICAL_MVCC_UNDO: // ... condensed: todo comment ... commit_start_postpone = true; break; case LOG_SYSOP_END_LOGICAL_COMPENSATE: tdes->undo_nxlsa = sysop_end->compensate_lsa; /* <- jump undo over compensated range */ commit_start_postpone = true; break;ABORT arm은 aborted-sysop 추적기 역할을 한다: abort된 범위 안의 LOG_DBEXTERN_REDO_DATA 논리 redo는 크래시 전 롤백이 제거한 상태를 다시 만들어낼 수 있으므로, log_recovery_needs_skip_logical_redo(6장)는 analysis_last_aborted_sysop_start_lsa < lsa < analysis_last_aborted_sysop_lsa 구간의 레코드를 건너뛴다. ABORT end가 올 때마다 필드를 덮어쓴다 — 가장 최근의 aborted sysop 하나만 추적된다.
LOG_SYSOP_END_LOGICAL_RUN_POSTPONE arm: topope-committed 상태에서 run-postpone sysop은 두 postpone 범위 중 어느 쪽에 속할 수 있으며, run_postpone.is_sysop_postpone이 결정한다:
- true(sysop의 postpone):
topops.last < 0이거나 상태가 topope-committed가 아니면 스택을 새로 구성 —max == 0이면 realloc(치명ER_OUT_OF_VIRTUAL_MEMORY),topops.last = 0, 상태를 topope-committed로 강제 설정; 그 후topops.stack[last].posp_lsa = run_postpone.postpone_lsa.commit_start_postpone은 false로 유지 — 페이즈가 계속된다. - false(트랜잭션의 postpone):
posp_nxlsa = run_postpone.postpone_lsa;topops.last != -1이면 assert로 topope 상태 확인, 아니면 상태 :=TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE;undo_nxlsanull 처리;commit_start_postpone = true.
에필로그는 모든 arm에서 실행된다. topope-committed 상태(assert (topops.last == 0))에서 commit_start_postpone이 설정된 경우 sysop postpone 페이즈가 종료된 것이며, 이전 상태는 tran_start_postpone_lsa로 재구성된다: non-null이면 TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE로 복원(assert로 LSA_LE 확인 — sysop이 트랜잭션 postpone 페이즈 내부에서 실행됨); null이면 기본 복구 상태 TRAN_UNACTIVE_UNILATERALLY_ABORTED로 복원. 어느 경우든 topops.last = -1로 설정. commit_start_postpone이 false이면 페이즈가 계속된다(topops.last는 0 유지); topope 상태가 아닌 경우에는 -1로 (재)설정된다.
이어서 두 개의 대칭적 초기화 가드가 실행된다 — postpone 페이즈와 atomic sysop은 어느 쪽이든 먼저 시작될 수 있으며, end 레코드는 나중에 시작된 쪽에 속한다. atomic 가드는 다음 세 조건이 모두 성립할 때만 rcv.atomic_sysop_start_lsa를 null로 초기화한다: (1) non-null 상태; (2) sysop_start_postpone_lsa보다 LSA_GT — atomic 연산이 더 최근에 열린 페이즈; (3) LSA_GT (atomic_sysop_start_lsa, sysop_end->lastparent_lsa). 조건 3은 부활 방지 가드(resurrection guard)다: lastparent_lsa >= atomic_sysop_start_lsa이면, 이 end 레코드는 atomic 마커 이후에 시작된 sysop — 여전히 열린 atomic 연산 내부에 중첩된 sysop — 을 닫는 것이며, 그 end에서 어노테이션을 지우면 아직 미완료인 atomic 연산을 복구가 건너뛰게 된다. 마커보다 앞선 lastparent_lsa를 가진 end 레코드만 — 즉 마커를 포함하는 sysop의 end 레코드만 — atomic 연산이 완료됐음을 증명할 수 있으며 이를 초기화할 수 있다. 거울 이미지 가드는 sysop_start_postpone_lsa에도 같은 방식으로 동작한다.
5.8 챕터 요약 — 핵심 정리
섹션 제목: “5.8 챕터 요약 — 핵심 정리”LOG_RCV_TDES는 모든LOG_TDES에 내장된 다섯 LSA 어노테이션 블록으로, 분석 핸들러(및 4장 체크포인트 복원)가 기록하고 6장·8장이 소비하며, 해당 페이즈가 종료된 것이 확인되면 null로 초기화된다.log_rv_analysis_commit_with_postpone는tran_start_postpone_lsa를 기록하고 미디어 크래시 시 시점 복구 중단 역할도 겸한다; 구버전 변형은 타임스탬프가 없다는 점만 다르다.log_rv_analysis_sysop_start_postpone는sysop_start_postpone_lsa를 기록하고,topops.last를-1에서0으로 전환하며, postponing 중인 sysop 내부에 중첩된 것으로 확인된atomic_sysop_start_lsa를 초기화한다.log_rv_analysis_sysop_end는 여섯 갈래 switch다: ABORT는 상태를 변경하지 않고 건너뜀 범위를 기록하며; COMMIT과 두 LOGICAL_UNDO 변형은 열린 sysop postpone 페이즈를 닫고; LOGICAL_COMPENSATE는 추가로undo_nxlsa를 전환하며; LOGICAL_RUN_POSTPONE는is_sysop_postpone으로 구분한다.- sysop postpone 페이즈가 닫힐 때 이전 상태는
tran_start_postpone_lsa로 재구성된다: non-null →TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE, null →TRAN_UNACTIVE_UNILATERALLY_ABORTED; 이후 초기화 가드가 두 어노테이션을 각각lastparent_lsa와 비교해 자신의 페이즈 어노테이션만 초기화한다. analysis_last_aborted_sysop_start_lsa < lsa < analysis_last_aborted_sysop_lsa는log_recovery_needs_skip_logical_redo가 크래시 전 abort된 sysop 내부의LOG_DBEXTERN_REDO_DATA재실행을 억제하는 방법이다.
Chapter 6: Redo 패스 드라이버와 동기 적용
섹션 제목: “Chapter 6: Redo 패스 드라이버와 동기 적용”log_recovery_redo는 분석 패스(Chapter 3)가 확정한 범위에 걸쳐, 재구성된 트랜잭션 테이블(Chapter 4)을 기반으로 로그를 순방향으로 재생한다 — 드라이버 루프, 그리고 LZ4/XOR 페이로드 조립까지 이어지는 동기 적용 경로를 다룬다. 이론적 배경: “Redo 패스” 동반 문서; 병렬 처리: Chapter 7; 미결 사항: Chapter 8; RV_fun 테이블: Chapter 10.
6.1 Redo 컨텍스트와 레코드별 구조체
섹션 제목: “6.1 Redo 컨텍스트와 레코드별 구조체”log_rv_redo_context는 전체 적용 상태를 담는 구조체다. 생성자에서 두 개의 LOG_ZIP 버퍼를 모두 LOGAREA_SIZE 크기로 사전 할당한다.
| 필드 | 역할 |
|---|---|
m_reader | 전용 로그 커서 (Chapter 2) — 병렬 워커는 각자 하나씩 가져야 함 |
m_redo_zip | redo 페이로드 스크래치+출력 버퍼; rcv.data는 이 버퍼 내부를 가리키며, 레코드마다 malloc하지 않음 |
m_undo_zip | diff undoredo 레코드용 undo 스크래치 — diff redo는 undo 이미지와 XOR함 |
m_end_redo_lsa | const 하드 스톱; 이 값을 넘어선 레코드는 잘린 꼬리이며, §6.4 페이지 LSA assert의 경계값 |
m_reader_fetch_page_mode | 크래시 복구 시 NORMAL(스냅샷 신뢰), 복제 재페치 시 FORCE |
복사 생성자는 메인 생성자에 위임한다 — 복사본들은 아무것도 공유하지 않으며, 이 덕분에 Chapter 7의 워커별 복사가 안전하다. 각 레코드는 log_rv_redo_rec_info<T> 값 스냅샷으로 전달되며, 정확히 세 개의 필드를 가진다: m_start_lsa — 레코드 헤더의 LSA로, 적용 후 페이지에 스탬프되는 멱등성 비교 기준; m_type — 구체적인 LOG_RECTYPE(T 하나가 일반 및 DIFF 레크타입 모두를 처리하며, diff 여부 판단에 필요함); m_logrec — reinterpret_copy_and_add_align으로 만든 타입 바디의 값 복사본으로, 큐에 쌓인 작업은 로그 페이지 포인터를 보유하지 않는다.
디버그 전용인 vpid_lsa_consistency_check(check / cleanup)는 정확히 두 개의 필드를 가진다: mtx — 병렬 redo 워커가 check를 동시에 호출하므로 동기화가 필요 — 와 consistency_check_map, (volid, pageid) 쌍별 최초 확인 LSA를 기록하는 맵(emplace는 기존 키를 덮어쓰지 않음); cleanup은 패스 종료 시 맵을 비운다.
불변식 — 페이지당 LSA 순서. 순서가 어긋난 적용은 갱신을 유실한다. 이는 디버그 빌드(rcv_phase != LOG_RESTARTED)에서 assert ((*map_it).second < a_log_lsa) 로 강제된다 — 각 새 LSA는 해당 페이지의 첫 번째 기록 LSA와 비교된다(쌍별 단조성보다 약한 조건; emplace는 원본 항목을 유지한다).
불변식 — m_redo_zip 버퍼 안정성. rcv.data는 redofun이 반환될 때까지 m_redo_zip.log_data를 참조한다. 스레드당 하나의 컨텍스트, 순차적 조립이라는 구조적 보장으로 강제된다. 조기 재활용 시 redofun은 쓰레기 값을 읽게 된다.
6.2 log_recovery_redo — 초기화와 외부 루프
섹션 제목: “6.2 log_recovery_redo — 초기화와 외부 루프”드라이버는 로그 임계 구역을 해제한다(LOG_CS_EXIT; 꼬리 부분에서 재진입). log_recovery_get_redo_parallel_count — MAX (16, system_core_count) — 는 SERVER_MODE에서(Chapter 7) reusable_jobs와 cublog::redo_parallel의 크기를 결정한다. SA 모드에서는 parallel_recovery_redo가 nullptr로 유지되어 모든 적용이 동기적으로 이루어진다. 루프 진입 전 방어 코드: start_redolsa 오프셋이 페이지 끝에 너무 가까우면 assert (false)를 발생시키고 다음 페이지에서 재개한다. PRM_ID_RECOVERY_PROGRESS_LOGGING_INTERVAL(5초 하한)은 처리된/전체 페이지 수와 예상 잔여 시간을 담은 ER_LOG_RECOVERY_PROGRESS를 주기적으로 출력한다.
외부 루프는 lsa를 담은 페이지를 페치한다. 페치 실패 시 lsa > m_end_redo_lsa이면 정상적인 범위 초과로 goto exit한다. 약속된 범위 내부에서의 실패는 logpb_fatal_error다. 내부 루프는 lsa.pageid == m_reader.get_pageid ()인 동안 레코드를 순회한다. 각 반복에서 헤더를 읽기 전, set_lsa_and_fetch_page로 리더를 (필요 시 복구된) 레코드 lsa에 재위치시킨다.
flowchart TD
A["lsa의 레코드"] --> B{"end_redo_lsa 초과?"}
B -- yes --> Z["null lsa, break"]
B -- no --> C["오프셋이 NULL이면 복구"]
C --> H["lsa에서 재페치, 헤더 읽기, lsa = forw_lsa"]
H --> K{"lsa가 엄격히 전진?"}
K -- no --> L["fatal: 로그 루프 감지"]
K -- yes --> M["log_rtype으로 switch"]
M --> P["pageid 보정"] --> A
Figure 6-1: log_recovery_redo 내부 루프 한 반복의 골격; 아래 설명은 각 분기를 다룬다.
아카이브 페이지 경계 복구 — 불완전하게 아카이브된 레코드는 페이지 헤더 오프셋이나 forw_lsa를 NULL로 남긴다. lsa.offset이 NULL이면 페이지 헤더 오프셋을 사용한다. 그마저도 NULL이면, 아카이브 페이지의 경우 pageid + 1로, 활성 페이지의 경우 진짜 로그 끝(pageid = NULL_PAGEID)으로 이동 후 continue한다. 아카이브 페이지에서 forw_lsa가 NULL이어도 마찬가지로 pageid + 1로 전진한다. 로그 루프 방어 — 다음 lsa가 엄격히 전진하지 않으면 logpb_fatal_error를 발생시켜 무한 루프를 방지한다. switch 후 보정 — 멀티 페이지 바디 이후 lsa.pageid를 리더의 현재 페이지로 갱신하여 이미 소비한 페이지를 재페치하지 않도록 한다.
불변식 — 스캔은 엄격히 전진한다. 모든 경로는 lsa를 앞으로 이동시키거나 null로 만들어 종료한다. 그렇지 않으면 복구가 같은 범위를 영원히 재생하게 된다.
6.3 디스패치 switch — 레코드 타입별 분기
섹션 제목: “6.3 디스패치 switch — 레코드 타입별 분기”헤더 파싱 후, 두 개의 로컬 매크로가 각 타입 분기를 처리한다. BUILD_RECORD_INFO (TEMPLATE_TYPE)은 rcv_lsa, log_rtype, 그리고 reinterpret_copy_and_add_align<TEMPLATE_TYPE> ()로 얻은 바디 복사본을 log_rv_redo_rec_info로 묶는다. INVOKE_REDO_RECORD는 이를 log_rv_redo_record_sync_or_dispatch_async로 전달하며, log_rv_need_sync_redo는 null-VPID 레코드와 RVDK_* rcvindex(Chapter 7에서 열거)를 동기 경로로 강제한다. 모든 분기, 완전 목록:
| 분기 | 동작 |
|---|---|
LOG_UNDOREDO_DATA, LOG_DIFF_UNDOREDO_DATA, LOG_RUN_POSTPONE, LOG_COMPENSATE | 일반 build+invoke (§6.4) |
LOG_UNDO_DATA, LOG_POSTPONE, LOG_SAVEPOINT, 포스트폰 마커(LOG_DUMMY_HEAD_POSTPONE, LOG_COMMIT_WITH_POSTPONE/_OBSOLETE, LOG_SYSOP_START_POSTPONE), 체크포인트, 2PC decision/inform, HA/복제/더미 타입, LOG_SUPPLEMENTAL_INFO, LOG_SYSOP_ATOMIC_START, LOG_END_OF_LOG | 명시적 no-op break |
LOG_MVCC_UNDOREDO_DATA, LOG_MVCC_DIFF_UNDOREDO_DATA | mvcc_next_id를 mvccid 이상으로 bump, mvcc_op_log_lsa = rcv_lsa 설정(vacuum용); invoke |
LOG_MVCC_REDO_DATA | mvcc_next_id만 bump — vacuum은 undo 데이터를 읽음; invoke |
LOG_REDO_DATA | RVVAC_COMPLETE -> logpb_vacuum_reset_log_header_cache; invoke |
LOG_DBEXTERN_REDO_DATA | 페이지 없음(pgptr = NULL, offset = -1); 아래 skip 검사로 게이트; log_rv_redo_record로 적용 |
LOG_2PC_PREPARE | tran/tdes 없으면 break; 있으면 log_2pc_read_prepare로 gtrid 재읽기, TRAN_UNACTIVE_2PC_PREPARE 상태에서만 LOG_2PC_OBTAIN_LOCKS |
LOG_2PC_START | tran이 살아있고 LOG_ISTRAN_2PC이면 coordinator 정보 재구성; 할당 실패 -> fatal + break |
LOG_COMMIT, LOG_ABORT | assert 전용: 완료된 비시스템 트랜잭션은 이미 사라져 있어야 함 |
LOG_MVCC_UNDO_DATA | 부기만 — mvcc_next_id, mvcc_op_log_lsa; 적용하지 않음 |
LOG_SYSOP_END | LOG_SYSOP_END_LOGICAL_MVCC_UNDO -> mvcc_op_log_lsa = rcv_lsa |
default (+LOG_SMALLER/LARGER_LOGREC_TYPE) | er_set (ER_LOG_PAGE_CORRUPTED); forw_lsa가 같은 레코드를 가리키면 lsa를 null로 설정 |
반복 크래시 방어 함수 log_recovery_needs_skip_logical_redo는 세 개의 조기 false 반환 — 레크타입 불일치, NULL_TRAN_INDEX, NULL tdes — 과 하나의 true 경로를 가진다:
// log_recovery_needs_skip_logical_redo -- src/transaction/log_recovery.c if (LSA_LT (&tdes->rcv.analysis_last_aborted_sysop_start_lsa, lsa) && LSA_LT (lsa, &tdes->rcv.analysis_last_aborted_sysop_lsa)) { /* ... condensed: er_log_debug ... */ return true; /* <- 이전 복구가 이미 중단한 sysop 내부에 엄격히 속함 */ }이 창 밖의 LSA는 마지막 return false로 떨어진다. 분석 패스(Chapter 5)가 양 끝점을 기록했으며, 해당 레코드와 보상 레코드는 이미 이전 복구 사이클에서 로그에 기록된 상태다.
꼬리 시퀀스. (SERVER_MODE) parallel_recovery_redo->wait_for_termination_and_stop_execution ()이 모든 비동기 작업을 소진한다; LOG_CS_ENTER; log_Gl.mvcc_table.reset_start_mvccid ()가 MVCC 기준선을 재계산한다; Chapter 8 인계(log_recovery_abort_all_atomic_sysops, log_recovery_finish_all_postpone); 이후 logpb_flush_pages_direct, logpb_flush_header, pgbuf_flush_all. exit: 레이블 — 범위 초과 시의 도착지이기도 함 — 은 curr_rcv_rec_lsa를 null로 만들고, consistency-check cleanup ()을 실행하며, 성능 통계를 보고한다.
6.4 log_rv_redo_record_sync — 페이지 고정, 추출, 적용
섹션 제목: “6.4 log_rv_redo_record_sync — 페이지 고정, 추출, 적용”// log_rv_redo_record_sync -- src/transaction/log_recovery_redo.hpp // ... condensed: debug-only vpid_lsa_consistency_check.check (rcv_vpid, m_start_lsa) ... const LOG_DATA &log_data = log_rv_get_log_rec_data<T> (record_info.m_logrec); LOG_RCV rcv; if (!log_rv_fix_page_and_check_redo_is_needed (thread_p, rcv_vpid, rcv, log_data.rcvindex, record_info.m_start_lsa, redo_context.m_end_redo_lsa)) { // ... condensed: assert (rcv.pgptr == nullptr) ... return; /* <- 페이지가 사라졌거나 변경이 이미 디스크에 있음 */ } scope_exit unfix_rcv_pgptr { [&thread_p, &rcv] () { pgbuf_unfix_and_init_after_check (thread_p, rcv.pgptr); } }; /* <- 모든 exit 경로에서 unfix */ // ... condensed: rcv field extractors; payload assembly ... rvfun::fun_t redofunc = log_rv_get_fun<T> (record_info.m_logrec, log_data.rcvindex);요약된 꼬리 부분: 페이로드 조립 오류 -> logpb_fatal_error + return(scope_exit이 여전히 unfix 수행); null이 아닌 redofunc는 perfmon_counter_timer_raii_tracker(PSTAT_LOG_REDO_FUNC_EXEC) 하에 실행되며, 실패 시 -> logpb_fatal_error; null redofunc -> er_log_debug 경고만; null이 아닌 rcv.pgptr에는 pgbuf_set_lsa로 m_start_lsa를 스탬프한다.
게이트키퍼 log_rv_fix_page_and_check_redo_is_needed는 세 가지 결과를 낸다: (1) VPID가 null이 아닌데 log_rv_redo_fix_page가 null을 반환 — assert (log_is_in_crash_recovery ()), return false, 해제된 페이지 건너뜀; (2) 페이지는 고정됐지만 rcv_lsa <= *pgbuf_get_lsa (rcv.pgptr) — pgbuf_unfix_and_init, return false, 변경이 이미 디스크에 있음(또한 페이지 LSA가 end_redo_lsa를 넘으면 assert로 거부); (3) 그 외 return true, null-VPID 폴스루 포함(이 경우 rcv.pgptr == nullptr로 페이지 없는 레코드 처리). log_rv_redo_fix_page는 섹터 예약 검사 없이 RECOVERY_PAGE 모드로 페이지를 고정한다 — 섹터 테이블이 병렬로 재생되므로 페이지가 일시적으로 해제된 것처럼 보일 수 있다. 검사 비용이 건너뜀으로 얻는 이득보다 크며, NULL 반환은 assert_release 대상이다(“복구를 불가능하게 만들기 때문에 심각한 문제”).
불변식 — 페이지 LSA를 통한 redo 멱등성. rcv_lsa <= page LSA이면 건너뛰고, 적용 후 m_start_lsa를 스탬프한다. 스탬프를 깨면 이후 모든 크래시에서 비멱등적 redo가 이중 적용된다.
여섯 개의 extractor 템플릿 패밀리 — 기본형은 static_assert (sizeof (T) == 0)으로 직접 인스턴스화 불가 — 는 여섯 가지 레코드 형태를 하나의 범용 루틴으로 평탄화한다. 예외 케이스: log_rv_get_fun<LOG_REC_COMPENSATE>는 RV_fun[rcvindex].undofun을 반환한다(소스 코드 주석 “yes, undo”) — CLR의 redo 페이로드는 undo 이미지이므로, 재생 시 undo 함수를 실행한다. 이는 ARIES의 역사 반복(history repeating) 원칙이다(동반 문서: “Compensation log records”).
T | _data / _vpid / _offset 출처 | _mvccid | _redo_length | log_rv_get_fun |
|---|---|---|---|---|
LOG_REC_MVCC_UNDOREDO | undoredo.data | mvccid | undoredo.rlength | redofun |
LOG_REC_UNDOREDO | data | MVCCID_NULL | rlength | redofun |
LOG_REC_MVCC_REDO | redo.data | mvccid | redo.length | redofun |
LOG_REC_REDO | data | MVCCID_NULL | length | redofun |
LOG_REC_RUN_POSTPONE | data | MVCCID_NULL | length | redofun |
LOG_REC_COMPENSATE | data | MVCCID_NULL | length | undofun |
6.5 페이로드 조립 — 압축 해제, diff, 전달
섹션 제목: “6.5 페이로드 조립 — 압축 해제, diff, 전달”log_rv_get_log_rec_redo_data<T>가 페이로드를 디코딩한다. 네 개의 단일 이미지 특수화(LOG_REC_MVCC_REDO, LOG_REC_REDO, LOG_REC_RUN_POSTPONE, LOG_REC_COMPENSATE)는 undo 데이터 없이 log_rv_get_unzip_and_diff_redo_log_data를 호출한다. LOG_REC_MVCC_UNDOREDO는 내장된 undoredo 멤버를 log_rv_redo_rec_info<LOG_REC_UNDOREDO>로 재포장하고 위임한다. LOG_REC_UNDOREDO만 분기한다 — T가 아닌 m_type 기준: DIFF 레크타입 두 종(need_diff_with_undo)에 대해서는 먼저 log_rv_get_unzip_log_data로 undo 이미지를 m_undo_zip에 압축 해제하고(오류 시 fatal + return), 정렬 후 m_undo_zip.data_length / .log_data를 전달한다. 그 외에는 불필요한 undo 이미지를 건너뛰고(m_reader.skip (GET_ZIP_LEN (ulength)), 오류 시 fatal + ER_FAILED), 정렬 후 (0, nullptr)을 전달한다.
log_rv_get_unzip_log_data는 하나의 이미지를 완전하게 디코딩한다. 길이 필드의 부호 비트가 압축 플래그다 — MAKE_ZIP_LEN이 로깅 시점에 0x80000000을 설정하고, ZIP_CHECK로 검사하며, GET_ZIP_LEN으로 비트를 제거한다. 위의 skip 경로도 GET_ZIP_LEN을 거친다. is_zip = ZIP_CHECK (length); does_fit_in_current_page인 이미지는 페이지 버퍼를 직접 참조(alias)하고, 걸쳐 있는 이미지는 copy_from_log로 힙 복사한다. 압축된 경우 -> log_unzip(실패: fatal + ER_FAILED); 비압축 -> log_zip_realloc_if_needed(실패 시 fatal) + memcpy. 마지막으로, fits 케이스에서는 add_align, copy 케이스에서는 copy_from_log가 이미 리더를 전진시켰으므로 align ()만 호출한다.
log_rv_get_unzip_and_diff_redo_log_data는 그 위에 diff를 적용한다: 호출자의 redo_unzip에 log_rv_get_unzip_log_data를 수행한 뒤(실패: fatal + ER_FAILED), if (is_zip)이고 undo_length > 0 && undo_data != nullptr일 때만 un-diff — log_diff (undo_length, undo_data, redo_unzip.data_length, redo_unzip.log_data) — 를 수행하고, m_redo_zip 저장소를 빌려 rcv->length / rcv->data를 전달한다. is_zip 게이트가 동작하는 이유는 diff redo가 항상 압축 상태로만 존재하기 때문이다: 추가 시점에 XOR은 스크래치 복사본 위에서 실행되며, DIFF 레크타입은 is_redo_zip일 때만 설정된다. 압축에 실패하면 원본 un-diff 데이터를 비트 클리어 상태로 기록한다. log_unzip은 저장된 원본 길이 프리픽스 log_zip을 읽고, buf_size <= 0을 거부하며, log_zip_realloc_if_needed가 대상 버퍼를 확장하지 못하면 실패하고, LZ4 압축 해제 후 unzip_len == buf_size일 때만 성공한다 — 짧거나 음수면 잘림이 아닌 손상이다. log_diff는 MIN (undo_length, redo_length) 바이트에 걸쳐 *(p++) ^= *(q++)를 수행한다 — XOR은 자기 역원이므로 하나의 루틴이 양방향 모두를 처리한다.
페이지 없는 쌍둥이 log_rv_redo_record(LOG_DBEXTERN_REDO_DATA 분기)는 fix/skip 게이트 없이 같은 조립-후-호출 시퀀스를 실행한다: 페이로드 실패 -> fatal + return; redofun 실패 -> fatal; redofun == NULL -> 디버그 경고; rcv->pgptr != NULL -> pgbuf_set_lsa(여기서는 pgptr가 NULL이므로 무의미).
6.6 챕터 요약 — 핵심 정리
섹션 제목: “6.6 챕터 요약 — 핵심 정리”log_rv_redo_context는 전체 redo 상태를 담는다 — 하나의 로그 커서와rcv.data가 빌려 쓰는 두 개의 사전 할당된LOG_ZIP버퍼; share-nothing 복사본이 Chapter 7 워커를 가능하게 한다.- switch는
mvcc_next_id,mvcc_op_log_lsa(undo 데이터를 가진 레코드에 한함),RVVAC_COMPLETE리셋, 논리 redo skip 창도 함께 관리한다. - 멱등성:
rcv_lsa <= page LSA이면 건너뛰고, 적용 후m_start_lsa를 스탬프한다.log_rv_redo_fix_page는 의도적으로 해제된 페이지를 허용한다. - 여섯 개의 extractor 패밀리가 여섯 가지 레코드 형태를 하나의 적용 루틴으로 평탄화한다.
log_rv_get_fun<LOG_REC_COMPENSATE>는 undofun을 반환한다 — CLR은 undo로 재생된다. - 부호 비트 하나가 압축을 인코딩한다(
MAKE_ZIP_LEN/ZIP_CHECK/GET_ZIP_LEN); diff redo는 항상 압축 상태로만 존재하므로,log_diff(XOR)는is_zip일 때만 실행된다. - 루프 이후의 모든 처리는
wait_for_termination_and_stop_execution이 병렬 redo를 소진할 때까지 실행되지 않는다. 그 이후에야reset_start_mvccid, Chapter 8 마무리, 플러시가 수행된다.
Chapter 7: 병렬 Redo 인프라
섹션 제목: “Chapter 7: 병렬 Redo 인프라”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);#endifSA_MODE에서는 cublog 클래스들이 빈 더미로 컴파일된다. Figure 7-1은 모든 분기 경로를 정리한다. 판별 술어는 다음과 같다:
// log_rv_need_sync_redo -- src/transaction/log_recovery.c if (VPID_ISNULL (&a_rcv_vpid)) { return true; /* <- no target page to hash */ } switch (a_rcvindex) { case RVDK_NEWVOL: // ... condensed: RVDK_FORMAT, RVDK_INITMAP, RVDK_EXPAND_VOLUME, RVDK_VOLHEAD_EXPAND ... return true; /* <- see Inv 7-A */ case RVDK_RESERVE_SECTORS: // ... condensed: RVDK_UNRESERVE_SECTORS ... return true; /* <- "may be changed to async" */ default: return false; }Invariant 7-A (동기 레코드는 happens-before 배리어). 메인 스레드는 sync 레코드를 적용한 뒤 이후 job을 푸시한다. 새로운 볼륨의 페이지는 그 이후 레코드에만 등장하므로, 어떤 워커도 아직 생성되지 않은 볼륨의 페이지를 fix하는 상황은 발생하지 않는다.
flowchart TD
A["레코드"] --> B{"SERVER_MODE?"}
B -- "아니오 (SA)" --> S1["동기 적용"]
B -- 예 --> C{"인프라 null?"}
C -- 예 --> S1
C -- 아니오 --> D{"log_rv_need_sync_redo"}
D -- "null VPID 또는 RVDK 볼륨/섹터" --> S1
D -- false --> E["blocking_pop + set_record_info"]
E --> G["add: VPID 해시로 고정 태스크에 배정"]
Figure 7-1: 디스패치 분기 경로.
7.2 크기 조정과 생성 — redo_parallel
섹션 제목: “7.2 크기 조정과 생성 — redo_parallel”log_recovery_redo는 REGISTER_WORKERPOOL로 풀 수요를 등록하고, 순방향 스캔 전에 한 번 생성한다: reusable_jobs.initialize (count)와 new cublog::redo_parallel (count, false, MAX_LSA, redo_context)를 순서대로 호출한다. false와 MAX_LSA는 모니터링을 비활성화한다(7.8). 워커 수 계산:
// log_recovery_get_redo_parallel_count -- src/transaction/log_recovery.c const int num_cpus = cubthread::system_core_count (); const int minimum_threads_to_redo = 16; /* <- "determined experimentally" */ return MAX (minimum_threads_to_redo, num_cpus);최소값 16은 소형 머신에서 과구독(oversubscription)을 유발하지만, 워커는 I/O 바운드이므로 실제로는 합리적이다. 생성자는 do_init_worker_pool(워커 수 = 슬롯 수 = a_task_count), 이어서 do_init_tasks와 모니터를 초기화한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_task_count | VPID 빈닝(binning) 모듈러스 | 생성 시 고정됨 (Inv 7-B) |
m_pool_entry_manager | TT_RECOVERY 엔트리 팩토리 | 워커에 실제 엔트리 필요 |
m_task_state_bookkeeping | 활성 태스크 비트셋 | 무한 대기 (7.7) |
m_worker_pool | 워커 풀 포인터 | OS 스레드 소유 |
m_redo_tasks | vector<unique_ptr<redo_task>> | 소유자 관리; perf 통계 유지 |
m_vpid_hash | std::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)를 초과했을 때만 알림을 보낸다.
7.4 redo_task::execute
섹션 제목: “7.4 redo_task::execute”.cpp-private cubthread::task인 redo_task:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_task_idx | 0..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_lsa | atomic<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_rtype | set_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 버퍼를 소유한다.
7.6 reusable_jobs_stack — 재활용
섹션 제목: “7.6 reusable_jobs_stack — 재활용”| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_flush_push_at_count | PARALLEL_REDO_REUSABLE_JOBS_FLUSH_BACK_COUNT (ONE_K) | ~1024번 retire마다 mutex 한 번 |
m_job_pool | PARALLEL_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_values | std::bitset<256>, 태스크별 비트 | set_active/set_inactive는 이전 상태 단언 |
m_values_mtx / m_values_cv | 가드 + 웨이크업 | wait_for_termination은 m_values.none ()까지 대기 |
워커 풀 자체의 대기는 “하드코딩된 최대 대기 시간(60초)” 후 단언하지만, 이 전용 bookkeeping은 무한정 대기한다. 태스크는 생성자에서 비트를 설정하므로, 아직 시작되지 않은 태스크를 조기 대기가 놓치는 일이 없다. 종료 절차:
// redo_parallel::wait_for_termination_and_stop_execution -- src/transaction/log_recovery_redo_parallel.cpp for (auto &redo_task: m_redo_tasks) { redo_task->notify_adding_finished (); } m_task_state_bookkeeping.wait_for_termination (); // ... condensed: assert every task is_idle ... m_worker_pool->stop_execution (); // ... condensed: get_manager ()->destroy_worker_pool ...redo_task::retire는 no-op으로(“자기 파괴 방지”), 태스크별 perf 통계가 이후에도 읽힐 수 있다. WAIT_FOR_PARALLEL은 최후 태스크 대기 시간을 측정한다. 양 끝에서 순서를 단언한다: push_job은 !m_adding_finished를, ~redo_parallel은 활성 태스크 없음과 null 풀을 단언한다 — 소멸 전에 이 블로킹 호출은 반드시 완료되어야 한다.
7.8 min_unapplied_log_lsa_monitoring
섹션 제목: “7.8 min_unapplied_log_lsa_monitoring”크래시 복구 시에는 비활성(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_lsa | m_calculate_mtx 하에 보관된 마지막 전역 최솟값 | 대기자가 비교하는 기준값 |
m_calculate_mtx / m_calculate_cv / m_terminate_calculation / m_calculate_thread | 계산 스레드 플러밍 | 가드, cv, 종료 플래그, 스레드 |
calculate는 메인 스레드 LSA와 태스크 마커들을 비교하여 최솟값을 구하며, 유휴 상태인 MAX_LSA 마커는 건너뛴다. wait_past_target_log_lsa는 두 가지 종료 경로를 가진다: a_target_lsa < m_calculated_log_lsa일 때의 비잠금(unlocked) 빠른 경로, 그렇지 않으면 notify_all(10 ms 낮잠을 깨움)로 킥하고 최솟값이 목표를 넘을 때까지 블로킹한다. redo_parallel::wait_past_target_lsa와 set_main_thread_unapplied_log_lsa는 포워더(forwarder)다.
7.9 perf_stats
섹션 제목: “7.9 perf_stats”perf_stats(log_recovery_redo_perf.hpp)는 cubperf 위에 올려진 nullable 래퍼다:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
m_definition | cubperf::statset_definition에 대한 const 참조 | 슬롯 이름/타입 (모두 COUNTER_AND_TIMER) |
m_stats_set | cubperf::statset *, 비활성 시 nullptr | 단일 분기점: 모든 메서드가 이를 확인 |
활성화는 측면별로 이루어진다(perf_stats_is_active_for_main / ..._for_async); do_not_record_t는 비활성 인스턴스를 생성한다. time_and_increment (id)는 이전 호출 이후 경과 시간을 누적한다:
- 메인 스레드:
FETCH_PAGE,READ_LOG,REDO_OR_PUSH_{PREP, DO_SYNC, POP_REUSABLE_DIRECT/_WAIT, DO_ASYNC},COMMIT_ABORT,WAIT_FOR_PARALLEL,FINALIZE. - 워커:
PARALLEL_POP,PARALLEL_SLEEP(실제로는 증가되지 않음),PARALLEL_EXECUTE,PARALLEL_RETIRE.
redo_parallel::log_perf_stats는 각 워커의 통계셋과 요소별 평균을 로깅한다 — EXECUTE 대 POP 비율로 포화도를 파악하고, DIRECT 대 WAIT 비율로 풀 스로틀링 여부를 확인할 수 있다.
7.10 챕터 요약 — 핵심 정리
섹션 제목: “7.10 챕터 요약 — 핵심 정리”- 디스패처 분기는 세 가지다: SA_MODE 항상 동기; null 인프라 또는
log_rv_need_sync_redo(null VPID, 볼륨 연산, 섹터 reserve/unreserve)에 의한 강제 동기; 재활용된 job의 비동기 디스패치. - Invariant 7-A는 강제 동기 레코드를 happens-before 배리어로 만든다. Invariant 7-B(
hash(VPID) % m_task_count고정, 순서대로 푸시, FIFO 드레인)는 페이지 락 없이 페이지 단위 LSA 순서를 보장한다. - 워커 수는
MAX (16, cores)로, I/O 바운드 워커를 위한 실험적 하한값이다. 종료 시 전용 비트셋을 사용하여 풀의 60초 단언을 우회한다. - Job은
(vpid, lsa, rectype)을 담으며,redo_job_impl::execute는 태스크 전용log_rv_redo_context로 로그 페이지를 재인출해 동기 적용 경로로 합류한다. 워커 루프는 오류 반환값을 버린다. reusable_jobs_stack은 ONE_M개의 job을 재활용한다 — 락 없는 pop, ONE_K 단위 플러시백, 보존 단언(Inv 7-C). 느린 경로는 백프레셔 역할을 한다.min_unapplied_log_lsa_monitoring과perf_stats는 각각 복제와 진단을 위해 제공된다.
Chapter 8: 원자적 시스템 오퍼레이션 중단 및 Postpone 완료
섹션 제목: “Chapter 8: 원자적 시스템 오퍼레이션 중단 및 Postpone 완료”Redo(6~7장)는 크래시 당시 상태를 정확히 재현하지만, undo로 넘어가기 전에 반드시 정리해야 할 두 가지 미결 사항이 남는다: 열린 원자적 시스템 오퍼레이션(atomic sysop), 그리고 postpone이 끝나지 않은 채 커밋된 트랜잭션/sysop이다. log_recovery_redo의 꼬리 단계가 이 두 가지를 처리한다. postpone/sysop 개념 자체는 상위 문서(cubrid-recovery-manager.md)를 참고한다.
8.1 Redo 꼬리 단계에서의 위치
섹션 제목: “8.1 Redo 꼬리 단계에서의 위치”두 단계 모두, 병렬 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_lsa | LOG_SYSOP_END가 기록되지 않은 채 커밋-with-postpone 상태인 sysop의 LOG_SYSOP_START_POSTPONE | 해당 레코드에 재실행할 LOG_REC_SYSOP_END가 내장되어 있음(8.6). |
tran_start_postpone_lsa | 트랜잭션의 LOG_COMMIT_WITH_POSTPONE | 8.6의 (c)/(d) 분기 구분, 8.7의 중단 경계 |
atomic_sysop_start_lsa | 매칭되지 않은 마지막 LOG_SYSOP_ATOMIC_START; NULL이 아니면 원자 연산 도중에 크래시 발생 | 8.4의 롤백 대상: 하나의 단위로 undo할 로그 접미사 |
analysis_last_aborted_sysop_lsa | 분석 단계에서 중단된 마지막 sysop의 종료 LSA | 6장 스킵 윈도우의 상한 경계 |
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.hppstruct 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 부모 — 불가능한 케이스로 단언 — 대 트랜잭션).
8.4 원자적 Sysop 중단
섹션 제목: “8.4 원자적 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 >= undo_nxlsa?"}
G3 -- yes --> R3["LSA 초기화 후 return"]
G3 -- no --> G4{"TOPOPE 및 start postpone<br/>> atomic start?"}
G4 -- yes --> N1["원자 연산 내 중첩 postpone:\n먼저 완료"]
G4 -- no --> G5{"TOPOPE?"}
G5 -- yes --> N2["sysop postpone 중 원자 연산:\n지금 중단"]
G5 -- no --> N3["단독 실행"]
N1 --> RB["시작 페이지 fetch,\nprev = prev_tranlsa"]
N2 --> RB
N3 --> RB
RB --> ERR{"fetch 실패?"}
ERR -- yes --> F["logpb_fatal_error"]
ERR -- no --> SIM["log_sysop_start,\nlastparent_lsa = prev,\nlog_sysop_abort"]
SIM --> DONE["atomic_sysop_start_lsa 초기화"]
Figure 8-1: log_recovery_abort_atomic_sysop의 모든 분기.
중첩 케이스는 8.5와 순서를 맞춘다. sysop_start_postpone_lsa > atomic_sysop_start_lsa이면 원자 연산 내부에서 커밋-with-postpone sysop이 발생한 것이다 — postpone을 먼저 완료한 뒤 중단한다. 반대로 TOPOPE 케이스는 sysop의 postpone 도중에 원자 연산이 시작된 것이다 — 지금 중단하고 8.5에서 postpone을 완료한다. 소스 주석에는 이 두 가지 크래시 시나리오가 번호를 붙여 명시되어 있다.
롤백은 undo를 직접 호출하지 않고 런타임 sysop을 시뮬레이션한다. 소스 주석에서 lastparent_lsa 덮어쓰기를 “hack last parent”라 부르는 이유가 여기에 있다. 새 sysop의 롤백 경계를 LOG_SYSOP_ATOMIC_START의 prev_tranlsa로 설정함으로써 log_sysop_abort가 그 이후의 모든 것을 보상하고 중단 LOG_SYSOP_END를 기록한다.
불변 조건 8-B — 원자적 잔재 없음. 반환 시 모든 TDES의 atomic_sysop_start_lsa는 NULL_LSA 상태여야 한다. 각 종료 경로는 그것이 NULL임을 확인하거나, 초기화하거나, logpb_fatal_error로 종료한다. 이후 단계는 절반만 열린 원자적 파일 연산이 존재하지 않음을 전제할 수 있다.
8.5 트랜잭션 Postpone 완료
섹션 제목: “8.5 트랜잭션 Postpone 완료”TDES별로 log_recovery_finish_postpone이 실행된다: (1) tdes == NULL || trid == NULL_TRANID 가드에서 반환; (2) 항상 log_recovery_finish_sysop_postpone(8.6) 호출 — TOPOPE_COMMITTED_WITH_POSTPONE 상태를 해소하며 경우에 따라 COMMITTED_WITH_POSTPONE으로 승격; (3) 상태에 따라 분기:
// log_recovery_finish_postpone -- src/transaction/log_recovery.c if (tdes->state == TRAN_UNACTIVE_WILL_COMMIT || tdes->state == TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE) { if (tdes->state == TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE) { /* make sure to abort interrupted logical postpone. */ log_recovery_abort_interrupted_sysop (thread_p, tdes, &tdes->rcv.tran_start_postpone_lsa); LSA_SET_NULL (&tdes->undo_nxlsa); } /* <- committed: nothing left to undo */ /* ... find_first_postpone -> log_do_postpone -> log_complete ... */ } else if (tdes->state == TRAN_UNACTIVE_COMMITTED) { /* log_complete + free index only; postpones already done */ }TRAN_UNACTIVE_WILL_COMMIT은 커밋은 기록되었으나 postpone 시작은 미기록된 상태다. COMMITTED_WITH_POSTPONE는 중단된 논리적 run postpone을 먼저 중단시킨다(8.7). 생략된 본문: log_recovery_find_first_postpone(8.8), 결과가 NULL이 아니면 log_do_postpone(8.9), 이후 로컬 트랜잭션(tdes->coord == NULL)에 한해 log_complete가 LOG_COMMIT EOT를 추가하고 TRAN_UNACTIVE_COMMITTED로 전환한 뒤 logtb_free_tran_index로 슬롯을 해제한다(2PC: 11장). 시스템 TDES는 단계 (2)만 통과하며, 완료 불가능한 interrupted sysop은 TDES를 TRAN_UNACTIVE_UNILATERALLY_ABORTED(분기 (d))로 남겨 undo(9장) 대상이 된다.
8.6 log_recovery_finish_sysop_postpone
섹션 제목: “8.6 log_recovery_finish_sysop_postpone”TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE 상태에서만 실행된다. 분석 단계에서 정확히 하나의 topops 항목이 push되었다(assert (tdes->topops.last == 0)). 순서: rcv.sysop_start_postpone_lsa 기준으로 중단된 논리적 run postpone 중단(8.7); topops.stack[last].posp_lsa를 시작점으로 첫 번째 미실행 postpone 탐색(8.8); log_do_postpone(8.9); log_read_sysop_start_postpone으로 start-postpone 레코드 재독취(실패 시 assert_release로 포기); 미리 작성된 end를 log_sysop_end_recovery_postpone으로 추가. 네 가지 결과:
// log_recovery_finish_sysop_postpone (outcomes) -- src/transaction/log_recovery.c if (sysop_start_postpone.sysop_end.type == LOG_SYSOP_END_LOGICAL_RUN_POSTPONE) { if (sysop_start_postpone.sysop_end.run_postpone.is_sysop_postpone) { /* (a) sysop postpone during sysop postpone? should not happen! */ assert (false); tdes->state = TRAN_UNACTIVE_UNILATERALLY_ABORTED; tdes->undo_nxlsa = tdes->tail_lsa; } else { /* (b) logical run postpone during transaction postpone */ tdes->state = TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE; LSA_SET_NULL (&tdes->undo_nxlsa); tdes->posp_nxlsa = sysop_start_postpone.sysop_end.run_postpone.postpone_lsa; } } else if (!LSA_ISNULL (&tdes->rcv.tran_start_postpone_lsa)) { /* (c) sysop nested in transaction postpone phase */ tdes->state = TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE; } else { /* (d) standalone: hand the rest to undo (Ch 9) */ tdes->state = TRAN_UNACTIVE_UNILATERALLY_ABORTED; tdes->undo_nxlsa = tdes->tail_lsa; }(b)는 이 sysop이 실행한 postpone 이후 부모의 postpone을 재개한다(8.3); (b)/(c)는 동일한 호출 내에서 8.5의 분기로 fall-through한다; (d)는 TDES를 undo 대기 상태로 전환한다. 방어적 클램프가 assert_release 하에서 topops.last를 -1로 초기화한다.
8.7 log_recovery_abort_interrupted_sysop — 역방향 스캔
섹션 제목: “8.7 log_recovery_abort_interrupted_sysop — 역방향 스캔”Postpone 실행 자체가 논리적 run postpone sysop(파일 삭제/해제)을 사용할 수 있으며, sysop 도중 크래시가 발생하면 재개 전에 중단해야 할 단편이 남는다. tdes->undo_nxlsa부터 postpone_start_lsa까지 undo 체인을 역방향으로 순회한다.
- 조기 반환:
undo_nxlsa가 NULL이거나<= postpone_start_lsa이면 중단할 것이 없다. - 레코드별 처리(페이지 fetch 실패 시
logpb_fatal_error, 반환):LOG_RUN_POSTPONE— 물리적 run postpone 완료: 중지,last_parent_lsa = iter_lsa.LOG_SYSOP_END—type == LOG_SYSOP_END_LOGICAL_RUN_POSTPONE이면 마찬가지로 중지; 그렇지 않으면sysop_end->lastparent_lsa로 hop하여 중첩 sysop 전체를 건너뜀.- 그 외 —
prev_lsa = logrec_head.prev_tranlsa; postpone-start 타입은 assert로 금지.
- 루프 소진 —
assert (LSA_EQ (&iter_lsa, postpone_start_lsa)); 중단된 sysop이 첫 번째 postpone 액션이었다:last_parent_lsa = *postpone_start_lsa.
이후 8.4의 시뮬레이션-sysop 트릭을 stack[last].lastparent_lsa = last_parent_lsa로 적용한다. 마지막으로 완료된 run postpone 이후의 모든 것이 보상되고, 이미 완료된 것들은 그대로 유지된다.
8.8 log_recovery_find_first_postpone — run-postpone 경로
섹션 제목: “8.8 log_recovery_find_first_postpone — run-postpone 경로”분석 후 tdes->posp_nxlsa는 중의적이다. 분석 단계는 스캔한 모든 LOG_RUN_POSTPONE의 run_posp->ref_lsa로 이를 전진시키는데, 이는 마지막 확인된 postpone이다. 아무것도 실행되지 않았다면 LOG_COMMIT_WITH_POSTPONE의 첫 번째 postpone을 가리킨다(4장). 전방 스캔 한 번으로 이 중의성을 해소한다. 가드: 크래시 복구 외부이거나 세 가지 postpone 상태 중 어느 것도 아닌 경우 — assert (0), ER_FAILED; start_postpone_lsa가 NULL인 경우 — NO_ERROR, NULL 결과. 스캔은 log_do_postpone의 중첩-top 범위 순회 및 페이지 fetch 오류 경로(8.9)를 재사용하며, 이 trid만 검사한다.
LOG_RUN_POSTPONE의ref_lsa == start_postpone_lsa— 후보가 실행됨:start_postpone_lsa_wasapplied설정 후 종료.LOG_SYSOP_END_LOGICAL_RUN_POSTPONE타입의LOG_SYSOP_END—run_postpone.postpone_lsa에도 같은 검사를 수행(논리적 run postpone은LOG_RUN_POSTPONE을 기록하지 않음).LOG_POSTPONE— 후보가 아닌 첫 번째 항목을next_postpone_lsa에 기록.LOG_END_OF_LOG/ NULL-offset — 8.9와 동일한 아카이브 경계 페이지 전진.
꼬리 처리: 후보가 실행되지 않았으면 ret_lsa = start_postpone_lsa; 실행되었으면 ret_lsa = next_postpone_lsa(다음 LOG_POSTPONE; 남은 것이 없으면 NULL).
8.9 log_do_postpone — 공유 전방 실행기
섹션 제목: “8.9 log_do_postpone — 공유 전방 실행기”런타임 커밋 시 postpone을 실행하는 루틴이 여기서 재실행된다. log_get_next_nested_top이 중첩 sysop 범위 스택을 구성하고, 외부 루프는 범위의 시작까지 탐색, 종료 이후 재시작, 또는 start_seek_lsa == nxtop_range->end_lsa일 때 tdes->tail_lsa까지 실행하고 중지하는 세 가지 방식으로 각 범위를 탐색한다. 이는 커밋 또는 중단 여부와 무관하게 모든 완료된 중첩 sysop의 내부를 건너뛴다. 해당 LOG_POSTPONE 레코드는 포함 postpone 단계가 아닌 sysop에 속하기 때문이다. LOG_SYSOP_END_LOGICAL_RUN_POSTPONE 종료 레코드만 스캔 범위 내에 남으며(log_get_next_nested_top은 해당 범위를 한 레코드 앞에서 종료하여 end 레코드 자체가 처리될 수 있도록 한다), 두 전방 스캐너는 페이지 fetch 오류 경로를 공유한다. logpb_fetch_page 실패 시 logpb_fatal_error를 발생시키고 end 레이블로 점프하여 힙에 할당된 nxtop_stack을 해제한다.
범위 내 디스패치: 일반 데이터/더미/복제 타입은 무시; LOG_POSTPONE는 log_run_postpone_op로 즉시 실행 — 실패 시 goto end; LOG_COMMIT_WITH_POSTPONE(및 _OBSOLETE, LOG_SYSOP_START_POSTPONE, 2PC 시작 레코드들)는 forward_lsa를 NULL로 설정 — postpone 구간 종료; LOG_SYSOP_END는 start_seek_lsa에서만 허용되며 그 외에는 잘못된 범위로 디버그 로그에 기록된다. log_run_postpone_op는 LOG_REC_REDO 페이로드를 읽어(페이지 경계를 넘어 복사; OOM 시 logpb_fatal_error) log_execute_run_postpone을 호출한다. redo 함수를 적용하고 새 LOG_RUN_POSTPONE을 기록한다 — 두 번째 크래시가 발생해도 8.8이 소비할 경로만 연장될 뿐이다.
불변 조건 8-C — postpone은 정확히 한 번 실행. posp_nxlsa 경로, 8.8의 applied 검사, 그리고 각 실행 시 새로 기록되는 LOG_RUN_POSTPONE이 조합되어 크래시 횟수에 무관하게 각 LOG_POSTPONE이 정확히 한 번만 실행됨을 보장한다. 이미 실행된 LSA로 log_do_postpone을 시드하면 페이지 해제와 같은 비멱등적 redo가 이중 적용된다.
8.10 챕터 요약 — 핵심 정리
섹션 제목: “8.10 챕터 요약 — 핵심 정리”- 병렬 풀이 소진된 뒤 redo 꼬리 단계는 두 가지 정리를 수행한다: 열린 원자적 sysop 중단, 이후 미완 postpone 완료(불변 조건 8-A). 입력은
tdes->rcv(LOG_RCV_TDES)와LOG_RUN_POSTPONE경로다. - 두 드라이버 모두 일반 TDES 슬롯 순회 후
log_system_tdes::map_all_tdes로 시스템 TDES를 처리하며,log_rv_simulate_runtime_worker로 각 트랜잭션을 가장한다. - 롤백은 sysop을 시뮬레이션한다 —
log_sysop_start,lastparent_lsa덮어쓰기,log_sysop_abort— 순서는 8.4의 중첩 케이스 분기에 따른다. log_recovery_finish_sysop_postpone은 내장된LOG_REC_SYSOP_END를 재실행하여 트랜잭션 postpone 경로로 진입하거나 undo를 위한TRAN_UNACTIVE_UNILATERALLY_ABORTED로 전환한다.- 완료된 각 워커 TDES는
log_complete (LOG_COMMIT)으로TRAN_UNACTIVE_COMMITTED상태로 종료된다 — 2PC 참여자는 예외(11장).
Chapter 9: Undo 패스와 보상 레코드
섹션 제목: “Chapter 9: Undo 패스와 보상 레코드”Redo 패스(6장)는 패자(loser) 트랜잭션의 변경 사항도 그대로 남겨 두었다. Undo 패스는 그것들을 되돌린다. CLR 이론은 companion 문서(cubrid-recovery-manager.md)에서 다루므로, 이 장에서는 log_recovery_undo와 log_rv_undo_record의 각 분기, 그리고 롤백을 크래시 재시작 가능하게 만드는 sysop 브래킷을 집중적으로 살펴본다.
9.1 Undo 패스의 레코드 구조체
섹션 제목: “9.1 Undo 패스의 레코드 구조체”네 가지 구조체(log_rec_undo, log_rec_mvcc_undo, log_rec_compensate, log_rec_sysop_end)는 모두 log_record.hpp에 정의되어 있으며, 로그 페이지에서 직접 읽힌다.
LOG_REC_UNDO — LOG_UNDO_DATA의 본문:
| 필드 | 역할 |
|---|---|
data (LOG_DATA) | rcvindex + volid/pageid/offset: RV_fun 디스패치와 페이지 fix를 위한 단일 로케이터; NULL vpid이면 RCV_IS_LOGICAL_LOG를 유발 |
length | undo 이미지의 바이트 수; ZIP_CHECK 플래그를 포함 |
LOG_REC_MVCC_UNDO — LOG_MVCC_UNDO_DATA의 본문:
| 필드 | 역할 |
|---|---|
undo (LOG_REC_UNDO) | 내포된 일반 undo — 완전한 상위집합; 처리 루틴은 &mvcc_undo->undo를 추출 |
mvccid | 기록자의 MVCCID; undo 중 재활성화되어 해당 버전이 보이지 않게 유지됨 |
vacuum_info (LOG_VACUUM_INFO) | prev_mvcc_op_log_lsa 체인 + vfid — vacuum이 MVCC op 레코드를 연결하는 리스트; undo 패스는 이를 건너뜀 |
LOG_REC_COMPENSATE — LOG_COMPENSATE(CLR)의 본문:
| 필드 | 역할 |
|---|---|
data (LOG_DATA) | 보상의 redo를 위한 로케이터 + rcvindex — CLR은 redo 전용이며 redofun으로 재실행됨 |
undo_nxlsa | 보상된 레코드 이전에 캡처된 다음 undo 대상 — ARIES의 UndoNxtLSA; 재시작 시 이미 완료된 undo를 건너뜀 (9.3 분기 4) |
length | after-image 길이 |
LOG_REC_SYSOP_END — LOG_SYSOP_END의 본문; type으로 키를 갖는 union:
| 필드 | 역할 |
|---|---|
lastparent_lsa | sysop 이전의 마지막 LSA — undo가 이 지점으로 점프; 커밋된 sysop은 재차 undo되지 않음 |
prv_topresult_lsa | 이전에 완료된 top action — 중첩 sysop 체이닝 (5장) |
type (LOG_SYSOP_END_TYPE) | union 판별자 — 하나의 레코드에 여섯 가지 종료 형식 |
vfid | 영향받는 페이지들의 파일 — 후행 undo 데이터의 TDE 결정에 사용 |
undo (union) | LOGICAL_UNDO용 LOG_REC_UNDO — 소유 트랜잭션이 abort될 때 sysop 자체의 undo 레시피 |
mvcc_undo (union) | LOGICAL_MVCC_UNDO용 LOG_REC_MVCC_UNDO — 위와 동일하나 MVCCID 포함 |
compensate_lsa (union) | LOGICAL_COMPENSATE의 재개 지점 — 브래킷 자체가 보상이었던 경우 |
run_postpone (union) | LOGICAL_RUN_POSTPONE용 postpone_lsa + is_sysop_postpone — 분석 단계의 쌍둥이 (5장); undo는 이것을 절대 만나지 않는다고 assert |
9.2 log_recovery_undo — 사전 패스와 패자 선별
섹션 제목: “9.2 log_recovery_undo — 사전 패스와 패자 선별”LOG_RECOVERY_UNDO_PHASE 하에서 log_recovery에 의해 호출된다. 사전 패스(pre-pass)는 더 이상 undo할 것이 없는 패자를 제거한다: TRAN_UNACTIVE_UNILATERALLY_ABORTED 또는 TRAN_UNACTIVE_ABORTED 상태이면서 undo_nxlsa가 NULL인 TDES는 크래시 전에 롤백을 완료했지만 LOG_ABORT가 디스크에 기록되지 않은 것이다 — log_complete (… LOG_ABORT, LOG_DONT_NEED_NEWTRID, LOG_NEED_TO_WRITE_EOT_LOG)가 지금 이를 기록하고, logtb_free_tran_index가 슬롯을 해제한다. 시스템 TDES는 EOT가 필요 없으므로 log_system_tdes::rv_delete_all_tdes_if가 undo_nxlsa가 NULL인 모든 시스템 항목을 삭제한다.
패자 선별은 logtb_rv_read_only_map_undo_tdes (log_tran_table.c)가 수행한다: read 모드 TR_TABLE_CS 하에서 해당 두 상태의 비시스템 슬롯 전체에 펑터를 맵핑하고, 이어서 log_system_tdes::map_all_tdes로 시스템 워커에도 적용한다 — max-scan 람다가 max_undo_lsa를 산출한다(log_find_unilaterally_largest_undo_lsa도 max-scan을 중복 수행하지만 현재 호출처는 없다). 드라이버는 undo_unzip_ptr = log_zip_alloc (LOGAREA_SIZE)를 할당하고 선택적 진행 타이머를 설정한 후 LOG_CS를 빠져나와 그림 9-1과 같은 루프를 수행한다(fetch는 LOG_CS_FORCE_USE; 할당·fetch 실패는 fatal).
불변조건 (전역 내림차순 undo 순서). 매 반복에서 모든 패자에 걸쳐 가장 큰
undo_nxlsa인max_undo_lsa를 undo한다 — 레코드마다 재계산된다. 모든 분기는 커서를 엄격히 뒤로 이동시킨다(prev_tranlsa, CLR의undo_nxlsa, 또는 sysop의lastparent_lsa). 내부의while (max_undo_lsa.pageid == log_lsa.pageid)는 이전 페이지를 fetch하기 전에 현재 페이지를 소진한다; 커서가 앞으로 이동하면 라이브락이 발생한다.
flowchart TD
A["완료된 패자 정리"] --> B["max_undo_lsa = 최대 undo_nxlsa"]
B --> C{NULL?}
C -- yes --> Z["unzip 버퍼 해제, LOG_CS_ENTER,<br/>로그 + 헤더 + 데이터 페이지 flush"]
C -- no --> D["페이지 fetch; 같은 pageid 동안:<br/>tdes 확인, log_rtype에 따라 분기"]
D --> G{prev_tranlsa NULL?}
G -- yes --> H["체인 완료: log_complete +<br/>logtb_free_tran_index 또는 rv_delete_tdes"]
G -- no --> I["undo_nxlsa = prev_tranlsa"]
H --> B
I --> B
그림 9-1: 드라이버 루프.
TDES 확인은 logtb_is_system_worker_tranid에 따라 분기한다: 워커는 log_system_tdes::rv_get_tdes (NULL이면 assert); 일반 트랜잭션은 logtb_find_tran_index + LOG_FIND_TDES — 조회 실패 시(분석 단계에서 등록되지 않은 trid) logtb_free_tran_index_with_undo_lsa가 해당 undo_nxlsa를 보유한 슬롯을 정리하고 레코드를 건너뛴다. if (tran_index != NULL_TRAN_INDEX && tdes != NULL)이 switch의 게이트가 된다; 워커 경로에서 tran_index는 낡은 값이므로 tdes만 의미가 있다.
9.3 레코드 타입 switch — 모든 분기
섹션 제목: “9.3 레코드 타입 switch — 모든 분기”모든 분기에 앞서 무조건 LSA_COPY (&tdes->undo_nxlsa, &prev_tranlsa)가 실행된다 — 이 순서 자체가 핵심이다:
불변조건 (커서가 undo 실행 전에 먼저 이동한다).
log_append_compensate는 자신이 기록하는 CLR에tdes->undo_nxlsa를 복사한다; 드라이버가 이미prev_tranlsa로 이동시켰으므로 CLR은 다음으로 undo할 레코드를 가리킨다. 순서를 바꾸면 롤백 도중 크래시 발생 시 동일한 undo가 두 번 재실행된다.
- UNDOREDO 계열 (
LOG_UNDOREDO_DATA,LOG_DIFF_UNDOREDO_DATA,LOG_MVCC_*쌍둥이) — MVCC 형식은LOG_REC_MVCC_UNDOREDO를 읽고rcv.mvcc_id를 설정하며, 일반 형식은LOG_REC_UNDOREDO에MVCCID_NULL을 사용한다; 내포된LOG_DATA+ulength로rcv를 채우고log_rv_undo_record를 호출한다. DIFF는 redo에서만 의미가 있다. LOG_MVCC_UNDO_DATA/LOG_UNDO_DATA—LOG_REC_MVCC_UNDO/LOG_REC_UNDO와undo->length를 사용하는 동일한 구조.- 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. LOG_COMPENSATE—LSA_COPY (&prev_tranlsa, &compensate->undo_nxlsa). 실제 작업 없음 — 커서가 크래시 전에 이미 undo된 모든 레코드를 뛰어넘는다.LOG_SYSOP_END—sysop_end->type에 따라:LOGICAL_UNDO/LOGICAL_MVCC_UNDO: 커밋된 브래킷이 자체 undo 레시피를 포함한다.sysop_end->undo(또는mvcc_undo.undo+rcv.mvcc_id)로rcv를 채운다;log_rv_undo_record실행 전에prev_tranlsa와tdes->undo_nxlsa모두lastparent_lsa로 이동하므로, 보상 기록이 sysop 전체를 건너뛴다(rcv_lsa는 갱신되지 않으므로 진단 메시지에 낡은 LSA가 출력될 수 있다).LOGICAL_COMPENSATE:prev_tranlsa = sysop_end->compensate_lsa— 브래킷이 보상했던 레코드 이전으로 재개.- default (
COMMIT,ABORT):prev_tranlsa = sysop_end->lastparent_lsa;assert는LOGICAL_RUN_POSTPONE이 undo 단계에 도달하지 않음을 문서화한다(8장).
- 종단/불법 타입 (
LOG_RUN_POSTPONE,LOG_COMMIT*트리오,LOG_SYSOP_START_POSTPONE,LOG_ABORT, 체크포인트 및 2PC 레코드,LOG_DUMMY_CRASH_RECOVERY,LOG_END_OF_LOG)과 default 분기(손상된 타입 →ER_LOG_PAGE_CORRUPTED) — 분석 단계에 오류가 있었음을 의미한다:assert (false)이후, 릴리스 빌드는 해당 TDES를 절단한다 —tdes->mvccinfo.id를 초기화하고,log_system_tdes::rv_delete_tdes(워커) 또는log_complete (… LOG_ABORT …)+logtb_free_tran_index를 수행하며,tdes = NULL로 설정하여 에필로그가 이를 건너뛰게 한다.
에필로그(if (tdes != NULL)): NULL prev_tranlsa는 체인의 끝을 의미한다 — tdes->mvccinfo.id를 초기화하고, 사전 패스에서와 같이 rv_delete_tdes(워커) 또는 log_complete + logtb_free_tran_index를 수행한다(#ifdef CCI_XA 빌드는 TRAN_UNACTIVE_2PC_PREPARE 상태에서 completion을 건너뜀). 그렇지 않으면 prev_tranlsa를 tdes->undo_nxlsa에 다시 기록하여, 분기 4-5가 리다이렉트했을 수 있는 커서를 재확인한다. 루프 종료 후: unzip 버퍼를 해제하고, LOG_CS에 재진입하며, 로그·헤더·데이터 페이지를 force-flush한다.
log_complete 내부에서 업데이터는 log_append_abort_log + log_change_tran_as_completed와 unlock_global_oldest_visible_mvccid를 받고, 업데이트가 없는 패자(LSA_ISNULL (&tdes->tail_lsa))는 상태만 전환된다.
9.4 log_rv_undo_record — 단일 undo 단계의 모든 분기
섹션 제목: “9.4 log_rv_undo_record — 단일 undo 단계의 모든 분기”런타임 log_rollback_rec의 복구 단계 쌍둥이; log_rv_simulate_runtime_worker / log_rv_end_simulation으로 동작을 시뮬레이션하며 페이지 잠금은 없다. 디스패치 전 처리: (1) 유효한 rcv->mvcc_id는 logtb_rv_assign_mvccid_for_undo_recovery로 재활성화된다; (2) RCV_IS_LOGICAL_LOG (rcv_vpid, rcvindex) — NULL vpid이거나 논리적 rcvindex이면 rcv->pgptr = NULL로 남기고, 그렇지 않으면 pgbuf_fix가 무조건 write latch를 획득한다(실패 시 assert 후 허용); (3) ZIP_CHECK (rcv->length)가 압축 플래그를 제거한다; 이미지는 페이지에 맞으면 로그 페이지에서 별칭(alias)으로 참조되고, 그렇지 않으면 malloc + logpb_copy_from_log로 복사되며, 압축된 이미지는 log_unzip으로 undo_unzip_ptr에 풀린다(할당/unzip 실패는 fatal — 6장의 log_rv_get_unzip_log_data / log_rv_get_unzip_and_diff_redo_log_data 쌍둥이와 동일). 이어서 if (rcv->pgptr != NULL || RCV_IS_LOGICAL_LOG (…)) 하에서:
// log_rv_undo_record -- src/transaction/log_recovery.c if (rcvindex == RVBT_MVCC_INCREMENTS_UPD) { /* nothing to do during recovery */ } else if (rcvindex == RVBT_MVCC_NOTIFY_VACUUM || rcvindex == RVES_NOTIFY_VACUUM) { /* nothing to do */ } else if (rcvindex == RVBT_LOG_GLOBAL_UNIQUE_STATS_COMMIT) { /* <- in-memory only: undo on every restart, cannot compensate */ error_code = (*RV_fun[rcvindex].undofun) (thread_p, rcv); assert (error_code == NO_ERROR); } else if (RCV_IS_LOGICAL_COMPENSATE_MANUAL (rcvindex)) { /* <- undofun logs its own compensation */ LSA_COPY (&rcv->reference_lsa, &tdes->undo_nxlsa); error_code = (*RV_fun[rcvindex].undofun) (thread_p, rcv); // ... condensed ... logpb_fatal_error on failure; optional b-tree trace } else if (!RCV_IS_LOGICAL_LOG (rcv_vpid, rcvindex)) { /* <- PHYSICAL undo: CLR first, then apply before-image */ log_append_compensate (thread_p, rcvindex, rcv_vpid, rcv->offset, rcv->pgptr, rcv->length, rcv->data, tdes); error_code = (*RV_fun[rcvindex].undofun) (thread_p, rcv); // ... condensed ... logpb_fatal_error on failure } else { /* <- LOGICAL undo: bracket in a system operation */ save_state = tdes->state; LSA_COPY (&rcv->reference_lsa, &tdes->undo_nxlsa); log_sysop_start (thread_p); (void) (*RV_fun[rcvindex].undofun) (thread_p, rcv); log_sysop_end_logical_compensate (thread_p, &rcv->reference_lsa); tdes->state = save_state; }페이지 fix에 실패한 물리적 레코드(guard의 else 경로)도 log_append_compensate에서 pgptr = NULL로 CLR을 기록하여 체인의 재시작 가능성을 유지한다 — 그리고 해당 볼륨을 명시하는 ER_LOG_MAYNEED_MEDIA_RECOVERY를 발생시킨다; undofun은 건너뛰고 복구는 계속된다(log-and-skip). end: 레이블에서 영역을 해제하고, 페이지 fix를 풀고, log_rv_end_simulation을 호출한다.
불변조건 (모든 undo 단계는 실행 전 또는 실행 중에 기록된다). 물리적 undo는
undofun전에 CLR을 기록한다; 논리적 undo는 먼저log_sysop_start를 열어 모든 페이지 변경이 브래킷 내에 포함되도록 하고,log_sysop_end_logical_compensate로compensate_lsa = rcv->reference_lsa를 설정하여 봉인한다. 브래킷 내부에서 크래시: 분석 단계가 sysop을 abort하고(8장), undo는 원래 레코드에서 재개된다. 봉인 후 크래시:LOGICAL_COMPENSATE분기가compensate_lsa로 점프한다. 어느 경우든 논리적 undo는 정확히 한 번 실행된다.
recovery.h는 수동(manual) 집합을 정의한다: 더 넓은 RCV_IS_LOGICAL_COMPENSATE_MANUAL (추가로 RVFL_ALLOC, RVFL_USER_PAGE_MARK_DELETE, RVPGBUF_DEALLOC, RVFL_TRACKER_HEAP_REUSE, RVHF_LOB_REMOVE_DIR, RVFL_TRACKER_UNREGISTER) 안에 포함된 RCV_IS_BTREE_LOGICAL_LOG(열 개의 RVBT_* 객체 수준 연산). 이들의 undofun은 저장된 rcv->reference_lsa로 log_append_compensate_with_undo_nxlsa를 호출해 페이지 수준 보상을 직접 추가한다 — b-tree undo는 보상 전에 페이지 분할이나 병합을 수행할 수 있으므로 별도의 브래킷은 불필요하다.
9.5 log_append_compensate — CLR 기록자
섹션 제목: “9.5 log_append_compensate — CLR 기록자”log_append_compensate와 log_append_compensate_with_undo_nxlsa는 log_append_compensate_internal (log_manager.c)을 래핑한다; 후자는 b-tree 케이스를 위해 명시적 undo_nxlsa를 전달하고, 전자는 NULL을 전달한다:
// log_append_compensate_internal -- src/transaction/log_manager.c // ... condensed ... node = prior_lsa_alloc_and_copy_data (.., LOG_COMPENSATE, ..); NULL -> silent return LSA_COPY (&prev_lsa, &tdes->undo_nxlsa); /* <- next record to undo, saved */ compensate = (LOG_REC_COMPENSATE *) node->data_header; // ... condensed ... fill compensate->data; store the undo_nxlsa parameter // into compensate->undo_nxlsa if non-NULL (b-tree override), else prev_lsa start_lsa = prior_lsa_next_record (thread_p, node, tdes); // ... condensed ... pgbuf_set_lsa (pgptr, start_lsa) when pgptr != NULL /* Go back to our undo link */ LSA_COPY (&tdes->undo_nxlsa, &prev_lsa); /* <- CLR must not become next undo target */분기 처리: prior_lsa_alloc_and_copy_data 실패 시 조용히 반환한다 — undo는 기록 없이 진행되며, undo_nxlsa가 해당 레코드를 지나쳐 이동하지 않았으므로 재크래시 시 단순히 다시 undo된다(before-image를 재적용하는 것은 무해하다). NULL pgptr(미디어 경로, 9.4)은 TDE 마킹과 pgbuf_set_lsa를 건너뛴다; pgbuf_set_lsa 실패 시 assert 후 반환한다. 마지막 줄이 핵심이다: prior_lsa_next_record가 tail_lsa와 함께 undo_nxlsa를 앞으로 끌어당기므로, prev_lsa를 복원하여 롤백 커서를 CLR 뒤에 유지한다 — 헤더 주석에서 명시하듯 CLR은 “절대 undo되지 않는다.”
9.6 챕터 요약 — 핵심 정리
섹션 제목: “9.6 챕터 요약 — 핵심 정리”- 사전 패스는
undo_nxlsa가 NULL인 패자를 제거한다 —log_complete가 누락된LOG_ABORT를 기록하고,logtb_free_tran_index가 슬롯을 해제하며,rv_delete_all_tdes_if가 완료된 시스템 TDES를 정리한다. - 드라이버는 항상 전역적으로 가장 큰
undo_nxlsa를 undo한다(logtb_rv_read_only_map_undo_tdes로 레코드마다 재계산): 엄격히 역방향으로 진행되는 페이지 단위 스윕. tdes->undo_nxlsa는 undo 실행 전에 이동하므로 모든 CLR이 올바른 재개 지점을 가진다;log_append_compensate_internal은 추가 후 이를 복원하여 CLR이 절대 undo되지 않도록 한다 — undo가 undo를 undo하지 않으므로 패스는 반복 크래시에 걸쳐 멱등성(idempotent)을 갖는다.LOG_COMPENSATE와LOG_SYSOP_END는 undo 중에 순수한 커서 리다이렉션이다(compensate->undo_nxlsa,lastparent_lsa/compensate_lsa) — 크래시된 롤백은 중복 없이 재개된다.log_rv_undo_record는RCV_IS_LOGICAL_LOG에 따라 분기한다: 물리적 undo = CLR 후undofun; 논리적 undo = sysop 브래킷(log_sysop_start부터log_sysop_end_logical_compensate)으로 분석 단계가 미완료 시 abort하고 undo는 봉인 시 건너뛴다;RCV_IS_LOGICAL_COMPENSATE_MANUALundofun은 수동으로 보상을 기록한다.- 유일하게 허용되는 실패는 fix 불가능한 데이터 페이지뿐이다 — CLR은 (
pgptr = NULL로) 여전히 기록되고ER_LOG_MAYNEED_MEDIA_RECOVERY가 발생한다; 그 외 모든 것은logpb_fatal_error다. CLR이 디스크에 있는 상태에서 절반만 적용된 undo는 다음 재시작 시 거짓말을 하기 때문이다.
Chapter 10: RV_fun 디스패치 테이블
섹션 제목: “Chapter 10: RV_fun 디스패치 테이블”redo, undo, 보상 재실행, logdump 출력 — 이 모든 경로는 결국 하나의 전역 배열로 귀결된다: recovery.c의 RV_fun[]이다. 6장, 7장, 9장의 드라이버들은 힙이나 B-트리의 의미론을 알지 못한다. 그들이 아는 것은 오직 올바른 함수 포인터를 찾는 방법뿐이다. 이 챕터는 엔트리 레이아웃, 인덱스-위치 동치 불변식, NULL 암(arm), 공유 packed-change 기계장치를 다룬다. 이론적 배경은 상위 레벨 동반 문서(“CUBRID의 ARIES”, “복구 함수 디스패치”)에서 다룬다.
10.1 rvfun 엔트리와 테이블 구조
섹션 제목: “10.1 rvfun 엔트리와 테이블 구조”각 슬롯은 struct rvfun(recovery.h)이다:
// rvfun -- src/transaction/recovery.hstruct 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_RCVINDEX | rv_check_rvfuns가 슬롯 위치와 비교 |
recv_string | 출력용 이름 ("RVHF_INSERT") | logdump 및 치명적 오류 시 rv_rcvindex_string으로 조회 |
undofun | 롤백, undo 패스 (9장), LOG_COMPENSATE의 redo | CLR 페이로드는 undo 방향; NULL = undo 데이터를 로그에 기록하지 않음 |
redofun | redo 패스 (6/7장), run-postpone (8장) | NULL = redo 데이터를 로그에 기록하지 않음 (undo 전용 논리 레코드) |
dump_undofun | 디버그 출력기, undo 페이로드 | logdump 전용, log_dump_data로 호출 |
dump_redofun | 디버그 출력기, redo 페이로드 | NULL = 페이로드 포맷 없음 |
RV_fun[]은 집합(aggregate) 초기화자로 구성되며, LOG_RCVINDEX마다 리터럴 하나씩, RVDK_NEWVOL(undo 암 NULL — 볼륨 생성은 redo 전용)부터 RVHF_LOB_REMOVE_DIR까지 이어진다. 암은 흔히 거울 쌍으로 구성된다(RVDK_UNRESERVE_SECTORS: undo는 disk_rv_reserve_sectors, redo는 disk_rv_unreserve_sectors).
flowchart LR
REDO["redo + run-postpone Ch 6-8"] --> R["redofun"]
CLR["LOG_COMPENSATE replay"] --> U["undofun"]
UNDO["undo + rollback Ch 9"] --> U
DUMP["logdump"] --> D["dump arms"]
Figure 10-1: 각 rvfun 암의 소비자. 보상(compensate) 재실행은 undofun으로 교차된다.
교차 배선은 log_rv_get_fun<LOG_REC_COMPENSATE>(log_recovery_redo.hpp)에 명시되어 있다: 함수 본문이 return RV_fun[rcvindex].undofun; — 주석은 // yes, undo. 따라서 RVBT_RECORD_MODIFY_COMPENSATE는 btree_rv_redo_record_modify를 undofun으로 등록하고 redo는 NULL로 둔다. CLR 페이로드가 redo 형식이며 undofun으로만 재실행되기 때문이다.
10.2 인덱스-위치 동치 불변식
섹션 제목: “10.2 인덱스-위치 동치 불변식”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()을 참조하도록 안내하고 있다.)
10.3 NULL 암과 폴리싱 위치
섹션 제목: “10.3 NULL 암과 폴리싱 위치”NULL 암은 복구 시점의 폴백이 아니라 로깅에 관한 계약이다: 이 rcvindex를 가진 레코드는 해당 방향의 데이터를 절대 포함하지 않는다. 강제는 append 시점, CUBRID_DEBUG 블록 안(log_manager.c)에서 이루어진다: log_append_undoredo_crumbs는 두 암 모두 비-NULL임을 assert하고, log_append_undo_crumbs는 undofun만, log_append_redo_crumbs는 redofun만 확인한다. rollback은 assert (RV_fun[rcvindex].undofun != NULL)을 추가한다. 복구 시점에는 log_rv_redo_record만 방어적이다 — NULL redofun이면 경고 로그를 남기는 데 그친다 — 반면 log_rv_undo_record는 NULL 검사 없이 암을 호출한다: append 시점의 계약이 유일한 안전망이다.
dump 암은 (FILE *, int length, void *data)를 받는다 — 페이지 없이 페이로드만 — logdump가 오프라인으로 실행되기 때문이다. 출력기는 범용(log_rv_dump_char, log_rv_dump_hexa)이거나 서브시스템별 디코더(disk_rv_dump_hdr)다.
10.4 인덱스 패밀리와 RCV_IS_* 매크로 오버레이
섹션 제목: “10.4 인덱스 패밀리와 RCV_IS_* 매크로 오버레이”접두사가 소유 서브시스템을 나타낸다. append-only 규칙에 따라 후기 추가분은 패밀리와 무관하게 124–129에 흩어진다: RVDK_* 0–9 (disk) · RVFL_* 10–32, 128 (file mgr; 128 = TDE) · RVHF_* 33–53, 126, 129 (heap) · RVOVF_* 54–57 (overflow) · RVEH_* 58–65 (ext hash) · RVBT_* 66–91, 124–125 (b-tree; 124–125 = online index) · RVCT_* 92–96 (catalog) · RVLOG_* 97 (log no-op) · RVREPL_* 98–103 (replication; HA 전송용, 페이지 복구 아님) · RVVAC_* 104–117 (vacuum) · RVES_* 118 (external storage) · RVLOC_* 119 (locator dummy) · RVPGBUF_* 120–123, 127 (page buffer; 127 = TDE).
RCV_IS_* 매크로들(1장)은 두 번째 축이다: 인덱스 값이 함수를 선택하고, 인덱스의 매크로 소속이 log_rv_undo_record의 여섯 단계 사다리(9장)에서 호출 주변의 프로토콜을 선택한다. RCV_IS_LOGICAL_COMPENSATE_MANUAL에 속하는 인덱스(RCV_IS_BTREE_LOGICAL_LOG가 공급)는 tdes->undo_nxlsa로부터 rcv->reference_lsa를 미리 적재하고 undofun이 자체 CLR을 로깅한다. RCV_IS_LOGICAL_LOG를 통과하지 못하는 인덱스는 드라이버 측에서 log_append_compensate가 먼저 실행된다. RCV_IS_LOGICAL_RUN_POSTPONE_MANUAL은 postpone 유사체다(8장).
불변식(매크로/테이블 정합성): RCV_IS_* 매크로에 명시된 모든 인덱스는 해당 매크로가 라우팅하는 프로토콜에 맞는 내부 로깅을 수행하는 암을 유지해야 한다. 이를 기계적으로 검사하는 장치는 없다. RCV_IS_LOGICAL_COMPENSATE_MANUAL에 속한 인덱스의 undofun이 CLR을 로깅하지 않으면 undo_nxlsa가 같은 레코드를 가리키는 상태가 유지된다: 무한 롤백 루프, 또는 다음 재시작 시 CLR 누락으로 인한 크래시다.
10.5 packed partial-change 미니 포맷
섹션 제목: “10.5 packed partial-change 미니 포맷”슬롯 페이지 엔트리 다수는 전체 레코드 대신 스플라이스(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가 복원하는 바이트 수 |
| payload | new data | old 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_at | spage_delete |
LOG_RV_RECORD_DELETE (0x4000) | 레코드 삭제됨 | spage_delete | spage_insert_at |
LOG_RV_RECORD_UPDATE_ALL (0xC000) | 전체 교체 | spage_update | spage_update (암별 페이로드) |
LOG_RV_RECORD_UPDATE_PARTIAL (0x0000) | 스플라이스 체인 | 순방향 스플라이스 후 spage_update | 역방향 스플라이스 후 spage_update |
// log_rv_record_modify_internal -- src/transaction/log_recovery.c INT16 flags = rcv->offset & LOG_RV_RECORD_MODIFY_MASK; PGSLOTID slotid = rcv->offset & (~LOG_RV_RECORD_MODIFY_MASK); if ((!is_undo && LOG_RV_RECORD_IS_INSERT (flags)) || (is_undo && LOG_RV_RECORD_IS_DELETE (flags))) { /* ... condensed: unpack type byte + body; spage_insert_at ... */ } else if ((!is_undo && LOG_RV_RECORD_IS_DELETE (flags)) || (is_undo && LOG_RV_RECORD_IS_INSERT (flags))) { /* ... condensed: spage_delete ... */ } else if (LOG_RV_RECORD_IS_UPDATE_ALL (flags)) { /* ... condensed: unpack type + body; spage_update ... */ } else { assert (LOG_RV_RECORD_IS_UPDATE_PARTIAL (flags)); // ... condensed: spage_get_record (..., COPY); /* <- splice on a private copy */ // log_rv_undoredo_record_partial_changes (..., is_undo); spage_update ... } pgbuf_set_dirty (thread_p, rcv->pgptr, DONT_FREE); /* <- every success path lands here */ return NO_ERROR;네 개의 암은 상호 배타적이고 전체를 포괄한다. 실패 경로는 모두 dirty 마크 이전에 assert_release (false) + ER_FAILED를 거치므로, 실패한 암이 반만 적용된 페이지를 광고하는 일은 없다. (한 가지 흠: UPDATE_PARTIAL 암의 실패한 spage_update는 error_code를 반환하지만 — 그 시점엔 여전히 NO_ERROR — assert되기는 하나 전파되지 않는다.) log_rv_redo_record_modify / log_rv_undo_record_modify는 is_undo를 각각 false/true로 바인딩하는 한 줄짜리 래퍼로, 테이블에 두 개의 구별된 포인터를 제공한다.
B-트리 인덱스 RVBT_RECORD_MODIFY_UNDOREDO / _NO_UNDO / _COMPENSATE는 범용 래퍼 대신 btree_rv_redo_record_modify / btree_rv_undo_record_modify(btree.c)를 등록한다. 내부의 btree_rv_record_modify_internal은 이 사다리를 복제하되(더 넓은 BTREE_RV_FLAGS_MASK, 동일한 log_rv_undoredo_record_partial_changes 호출), 노드 헤더 관리가 추가된다.
10.8 등록 절차 — 새 LOG_RCVINDEX 추가하기
섹션 제목: “10.8 등록 절차 — 새 LOG_RCVINDEX 추가하기”- 열거자를
LOG_RCVINDEX의 맨 아래에 추가하고,RV_LAST_LOGID를 재지정한다. 재번호 매기기는 절대 금지 — 값이 온디스크 로그에 영구 저장된다. - 대응하는
rvfun리터럴을RV_fun[]의 마지막 슬롯에 추가한다. string은 열거자 이름 그대로. - 로깅 방식에 따라 암을 선택한다: undoredo에는 둘 다, redo/postpone 전용에는 redo만, 논리 undo 또는 보상 재실행에는 undo만(§10.1의 교차 배선).
- 해당 인덱스가 수동 보상/postpone 프로토콜을 필요로 하면 올바른
RCV_IS_*매크로에 추가하고 그 프로토콜을 구현한다(§10.4 불변식). - assert를 켠 채로 빌드하고 부팅한다:
rv_check_rvfuns가 유일한 기계적 검사다. 건너뛰거나 잘못 배치된 슬롯은 여기서 치명적 오류로 죽는다. 릴리즈 빌드는 오류 슬롯 이후의 모든 레코드에 잘못된 함수를 적용한다.
10.9 챕터 요약 — 핵심 정리
섹션 제목: “10.9 챕터 요약 — 핵심 정리”RV_fun[]은 검사 없는 배열 로드로 온디스크 rcvindex를 함수 포인터에 매핑한다.recv_index == position이라는 불변식은 디버그 시작 시rv_check_rvfuns만이 검사하며, 모든 패스에서 하중을 지탱한다. 열거형은 번호가 로그에 영구 보존되므로 append-only다.- NULL 암은 로깅 계약을 인코딩하며 append 시점에 폴리싱된다. 복구는 대부분 이를 신뢰한다.
- redo 패스는
LOG_COMPENSATE를undofun슬롯으로 재실행한다. 따라서 compensate 전용 인덱스는 redo 방향 함수를undofun으로 등록한다. RCV_IS_*매크로는log_rv_undo_record(9장)의undofun호출을 감싸는 보상/postpone 프로토콜을 선택한다.- packed splice 포맷은 방향에 무관하다 — undo 패커가 size 바이트를 사전 스왑하고 old data를 저장한다 — 따라서 하나의 인터프리터가 redo는 순방향으로, undo는 언와인드 역방향으로 재실행한다.
log_rv_record_modify_internal은 그 위에 네 단계 사다리를 쌓으며, insert/delete 암은 undo 아래서 스왑된다. B-트리는 범용 래퍼를 등록하는 대신 이 기계장치를 복제한다.
Chapter 11: 특수 경로
섹션 제목: “Chapter 11: 특수 경로”주 크래시-재시작 생애주기 외의 경로: 특정 시점 복원, 로그 절단, 복원 후 아카이브/볼륨 폐기, 추가 지점 수리, 실행 컨텍스트 shim, 그리고 2PC 인계.
11.1 stopat 계약
섹션 제목: “11.1 stopat 계약”두 log_recovery 매개변수가 모든 것을 제어한다: ismedia_crash와 stopat (상위 문서, “Restart orchestrator”):
// log_recovery -- src/transaction/log_recovery.c if (ismedia_crash != false) { /* Media crash, we may have to start from an older checkpoint... check disk headers */ (void) fileio_map_mounted (thread_p, (bool (*)(THREAD_ENTRY *, VOLID, void *)) log_rv_find_checkpoint, &rcv_lsa); } else { // ... condensed ... if (stopat != NULL) { *stopat = -1; /* <- normal restart: point-in-time target forcibly disabled */ } }불변 조건 — 불완전 복구는 미디어 크래시 경로에서만 발생한다. 일반 재시작은 stopat을 무효화하며, 미디어 복구 범위 밖에서 깨진 페이지가 발견되면 절단이 아닌 치명적 오류로 처리된다 — 그렇지 않으면 분석이 커밋된 트랜잭션을 조용히 소멸시킬 수 있기 때문이다. log_rv_find_checkpoint는 복원된 볼륨들 중 가장 오래된 체크포인트를 선택한다.
분석 시점에 로그를 절단하는 세 가지 트리거가 있으며, 모두 *did_incom_recovery = true를 설정한 채 log_recovery_resetlog로 수렴한다:
-
목표 시점보다 새로운 커밋/중단. 미디어 크래시가 없는 경우,
log_rv_analysis_complete는goto end로 점프해 트랜잭션 인덱스를 해제한다(챕터 4). 미디어 크래시 경로에서는LOG_REC_DONETIME을 읽고,*stop_at이 설정된 상태에서difftime (*stop_at, last_at_time) < 0이면 보유 중인 페이지를 해제하고(log_lsa->pageid = NULL_PAGEID), 레코드의 헤더 LSA를 기준으로log_recovery_resetlog를 호출한다 — 새 로그는 너무 새로운 레코드 이전에서 끝난다 — 그리고NO_ERROR를 반환한다. -
목표 시점보다 새로운 commit-with-postpone.
log_rv_analysis_commit_with_postpone는if (is_media_crash)분기 내에서LOG_REC_START_POSTPONE.at_time에 동일한difftime검사를 수행한다;else분기는 챕터 5의 일반적인 처리이다. 미디어 분기에서 시간 검사를 통과하면 아무것도 하지 않는다. -
물리적으로 깨진 페이지.
log_recovery_analysis루프에서logpb_fetch_page가 실패하면, 미디어 분기는last_at_time을*stop_at에 저장하고, 마지막 트랜잭션의tail_lsa/undo_nxlsa를log_rec->prev_tranlsa로 되감으며(반쪽만 기록된 레코드는 undo되지 않는다), 이전 페이지를 다시 가져오고(실패 시 치명적: “reset log is impossible”),prev_lsa/prev_prev_lsa로log_recovery_resetlog를 호출하고,log_Gl.mvcc_table.reset_start_mvccid()를 재설정한 후 반환한다. 미디어 크래시가 아닌 분기는 치명적 오류다 —er_errid()가ER_TDE_CIPHER_IS_NOT_LOADED일 때는 TDE 전용 메시지가 표시된다.
네 번째, 좀 더 완만한 수리: 레코드의 forward LSA가 NULL이면서 log_rtype != LOG_END_OF_LOG이고 *did_incom_recovery == false일 때, 분석 루프는 log_Gl.hdr.append_lsa를 인자로 log_startof_nxrec(§11.5)을 호출하고, log_rec->forw_lsa를 패치하며, 페이지를 재기록(logpb_write_page_to_disk)하고, 같은 블록에서 log_Gl.hdr.next_trid = tran_id를 설정한다; 실패 시 추가 지점은 end_redo_lsa로 폴백한다(“we may destroy a record”).
11.2 log_recovery_resetlog — 절단과 재초기화
섹션 제목: “11.2 log_recovery_resetlog — 절단과 재초기화”log_recovery_resetlog는 역사를 다시 쓰는 유일한 함수이다. LOG_CS_OWN_WRITE_MODE와 NULL이 아닌 new_prev_lsa를 단언한 후 여섯 단계를 실행한다:
- 현재 내용을 플러시한다.
log_Gl.append.vdes != NULL_VOLDES이고 추가 페이지가 보유 중이면:logpb_flush_pages_direct+logpb_invalid_all_append_pages. - 새 추가 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);- 헤더 상태를 초기화한다.
chkpt_lsa = append_lsa(절단된 꼬리가 새 체크포인트가 된다),is_shutdown = false,logpb_invalidate_pool. - 두 가지 처리 방식.
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를 낮춘다. - 추가 페이지를 재초기화한다.
logpb_fetch_start_append_page; 성공 시 2단계에서 저장한 이미지를 가져온 버퍼에memcpy하고, 더티로 표시한 후 직접 플러시한다.logpb_fetch_start_append_page가 실패하면 복원 및 플러시를 조용히 건너뛴다 — 오류가 발생하지 않는다 — 그리고 마무리는 계속 진행된다. - 마무리.
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이나 아카이버가 절단된 페이지를 뒤쫓게 된다.
11.3 log_recovery_notpartof_archives
섹션 제목: “11.3 log_recovery_notpartof_archives”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는 호출자에게 맡겨진다.
11.4 log_recovery_notpartof_volumes
섹션 제목: “11.4 log_recovery_notpartof_volumes”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가 볼륨 정보 파일을 재구축한다.
11.5 log_startof_nxrec
섹션 제목: “11.5 log_startof_nxrec”log_startof_nxrec은 다음 레코드가 어디서 시작하는지를 답한다. 분석 단계에서(§11.1, 수리 4) 마지막 레코드의 forw_lsa가 NULL이지만 레코드 자체는 완전할 때 이 함수를 사용한다. 분기:
- NULL 입력 LSA → NULL 반환;
logpb_fetch_page실패 →goto error.lsa->offset == NULL_OFFSET(아카이브가 레코드 중간에서 절단된 페이지) →log_pgptr->hdr.offset, 즉 페이지가 알고 있는 첫 번째 레코드를 채택한다; 그래도 NULL이면 오류. canuse_forwaddr == true→log_rec->forw_lsa를 취한다; NULL이지만 페이지가 아카이브에 있다면, 다음 레코드는pageid + 1에만 있을 수 있다(불완전한 레코드가 아카이브되고 나중에 완성된 경우). 그래도 NULL이면 수동 스캔으로 넘어간다.- 수동 스캔:
LOG_RECORD_HEADER를 지나switch(type)으로 타입별 헤더와 모든 가변 페이로드를 건너뛴다 — undo/redo 이미지(GET_ZIP_LEN디코딩), postpone/compensate 길이, 체크포인트 배열, savepoint 이름, 2PC 및 복제 페이로드, sysop 계열의 조건부 내장 undo 이미지(챕터 5); 고정 크기 마커는 그냥break. 에필로그LOG_READ_ADVANCE_WHEN_DOESNT_FIT은 다른 레코드 헤더가 맞지 않을 때 다음 페이지로 올림한다. - 두 가지 특이점:
LOG_SUPPLEMENTAL_INFO에는break가 없어 마커 그룹으로 폴스루한다 — 해당 케이스들이 아무것도 하지 않기 때문에 현재는 무해하다;LOG_END_OF_LOG는assert(false)— end-of-log 이후 레코드를 요청하는 호출자는 없다.
11.6 simulate/end shim
섹션 제목: “11.6 simulate/end shim”Undo, postpone, sysop-abort 코드는 thread_p->tran_index 또는 연결된 시스템 tdes의 현재 트랜잭션을 기대한다. 복구 스레드는 LOG_SYSTEM_TRAN_INDEX를 소유하면서 다른 트랜잭션의 체인을 순회하므로, tdes별 작업마다 shim 쌍으로 감싼다:
// log_rv_simulate_runtime_worker -- src/transaction/log_recovery.c if (tdes->is_active_worker_transaction ()) { thread_p->tran_index = tdes->tran_index; /* <- runtime code now sees this tdes as "mine" */ // ... condensed ... (SA_MODE: mirror via LOG_SET_CURRENT_TRAN_INDEX) } else if (tdes->is_system_worker_transaction ()) { log_system_tdes::rv_simulate_system_tdes (tdes->trid); /* <- attach system tdes to thread */ } else { assert (false); }
// log_rv_end_simulation -- src/transaction/log_recovery.c thread_p->reset_system_tdes (); thread_p->tran_index = LOG_SYSTEM_TRAN_INDEX; /* <- unconditional restore */ // ... condensed ... (SA_MODE: mirror restore)두 shim 모두 SA-mode 전역 미러를 유지한다(#if defined (SA_MODE) 아래 LOG_SET_CURRENT_TRAN_INDEX). 시스템 워커 트랜잭션(챕터 4에서 재구축한 log_system_tdes 집합)의 경우 rv_simulate_system_tdes가 systb_System_tdes에서 trid를 찾아(미스 시 단언) set_system_tdes로 설치한다.
불변 조건 — 모든 simulate는 반드시 end와 쌍을 이루며, 트랜잭션 사이에 스레드는 LOG_SYSTEM_TRAN_INDEX로 복귀한다. log_rv_undo_record(챕터 9)는 end: 레이블 이후 쌍을 닫으므로 오류 경로도 스레드를 복원한다; log_recovery_finish_all_postpone와 log_recovery_abort_all_atomic_sysops(챕터 8)는 tdes별 람다로 감싸고 진입 시 tran_index == LOG_SYSTEM_TRAN_INDEX를 단언한다. end가 빠지면 오래된 시스템 tdes가 연결된 채로 남아 엉뚱한 트랜잭션을 위해 로깅하게 된다.
11.7 2PC 인계
섹션 제목: “11.7 2PC 인계”undo와 (필요 시) log_recovery_notpartof_volumes 이후, 드라이버는 분산 미결 항목의 수를 세다:
// log_recovery -- src/transaction/log_recovery.c (void) logtb_set_num_loose_end_trans (thread_p); /* Try to finish any 2PC blocked transactions */ if (log_Gl.trantable.num_coord_loose_end_indices > 0 || log_Gl.trantable.num_prepared_loose_end_indices > 0) { log_Gl.rcv_phase = LOG_RECOVERY_FINISH_2PC_PHASE; // ... condensed ... log_2pc_recovery (thread_p); /* Check number of loose end transactions again.. */ // ... condensed ... (reset rcv_tdes, re-bind tran index) (void) logtb_set_num_loose_end_trans (thread_p); }logtb_set_num_loose_end_trans는 TR_TABLE_CS_ENTER 하에서 두 카운터를 0으로 초기화하고, 유효한 trid를 가진 모든 비시스템 tdes를 logtb_set_loose_end_tdes로 순회한다: LOG_ISTRAN_2PC_PREPARE는 isloose_end를 설정하고 num_prepared_loose_end_indices를 증가시키며(in-doubt 참여자; 락을 유지한다); LOG_ISTRAN_2PC_IN_SECOND_PHASE 또는 TRAN_UNACTIVE_2PC_COLLECTING_PARTICIPANT_VOTES는 num_coord_loose_end_indices를 증가시킨다(코디네이터가 자신의 결정을 재구동한다). 드라이버는 반환된 합계가 아닌 두 전역 변수를 기준으로 판단한다.
log_2pc_recovery는 테이블을 순회하면서 tdes == NULL, NULL_TRANID, !LOG_ISTRAN_2PC(tdes)를 건너뛰고 tdes->state에 따라 분기한다: 투표 수집 중이면 미결정 코디네이터를 중단시키고, 중단/커밋 결정이면 그 결정을 재실행하며, TRAN_UNACTIVE_WILL_COMMIT / TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE는 [[fallthrough]]로 참여자에게 통보한다 — 로컬 postpone은 이미 완료되어 있다(챕터 8). 투표 메커니즘은 2PC 문서와 상위 문서의 “Transaction table with loose-end annotations”에서 다루며, 여기서는 인계 조건만 중요하다: 분석 이후 코디네이터 또는 prepared loose end가 남아 있을 때만 네 번째 페이즈가 실행된다. 결정을 받지 못한 prepared 참여자는 in-doubt 상태로 합당하게 남을 수 있다 — 따라서 assert-zero가 아닌 재집계를 수행한다.
11.8 챕터 요약 — 핵심 정리
섹션 제목: “11.8 챕터 요약 — 핵심 정리”- 불완전 복구는 미디어 크래시에서만 발생한다: 일반 재시작은
*stopat = -1을 강제하고, 미디어 복구 범위 밖에서 깨진 페이지는 절단이 아닌 치명적 오류로 처리한다. - 로그를 절단하는 세 가지 트리거가 있다 —
stopat보다 새로운 완료 또는 commit-with-postpone, 또는 읽을 수 없는 페이지 — 모두log_recovery_resetlog로 수렴한다; end-of-log 누락은log_startof_nxrec으로 패치한다. log_recovery_resetlog는 하나의LOG_CS구역 안에서 모든 위치 관련 헤더 필드를 재작성하고, 아카이브 제거는log_recovery_notpartof_archives에 위임한다.- 볼륨 폐기는 두 단계로 이루어지며
db_creation으로 동일성을 검사한다; 버퍼 페이지는 unformat 전에 무효화된다; 마운트 실패는 건너뛴다. log_startof_nxrec은 레코드 타입별로 길이를 하나씩 순회한다 — 새 페이로드 레이아웃이 추가되면 새switch분기가 필요하다;LOG_SUPPLEMENTAL_INFO의break누락은 우연히 무해할 뿐이다.- simulate/end shim은 런타임 코드가 수정 없이 실행될 수 있도록 스레드를 worker 또는 시스템 tdes에 바인딩한다; 쌍 구조는 구조적이며, 두 shim 모두 SA-mode 미러를 유지한다.
LOG_RECOVERY_FINISH_2PC_PHASE는 코디네이터 또는 prepared loose end가 살아남을 때만 실행된다; prepared 참여자는 in-doubt 상태로 남을 수 있다.
이 리비전 시점의 위치 힌트
섹션 제목: “이 리비전 시점의 위치 힌트”아래 줄 번호는 2026-06-23 시점에 관찰한 값이다. 심볼 이름이 정식 앵커이며 줄 번호는 시간이 지나면 어긋나는 힌트다.
| Symbol | File | Line |
|---|---|---|
vacuum_notify_server_crashed | src/query/vacuum.c | 7570 |
btree_rv_record_modify_internal | src/storage/btree.c | 29757 |
NULL_OFFSET | src/storage/storage_common.h | 49 |
RECORD_REPLACE_DATA | src/storage/storage_common.h | 231 |
log_2pc_recovery_analysis_info | src/transaction/log_2pc.c | 2029 |
log_2pc_recovery | src/transaction/log_2pc.c | 2303 |
LOG_RV_RECORD_MODIFY_MASK | src/transaction/log_append.hpp | 139 |
LOG_PAGE_INIT_VALUE | src/transaction/log_common_impl.h | 46 |
log_zip | src/transaction/log_compress.c | 45 |
log_unzip | src/transaction/log_compress.c | 112 |
log_diff | src/transaction/log_compress.c | 176 |
log_zip_realloc_if_needed | src/transaction/log_compress.c | 203 |
log_zip_alloc | src/transaction/log_compress.c | 238 |
log_zip_free | src/transaction/log_compress.c | 279 |
GET_ZIP_LEN | src/transaction/log_compress.h | 36 |
ZIP_CHECK | src/transaction/log_compress.h | 39 |
log_zip | src/transaction/log_compress.h | 53 |
LOG_ISTRAN_2PC | src/transaction/log_impl.h | 173 |
LOG_HAS_LOGGING_BEEN_IGNORED | src/transaction/log_impl.h | 190 |
log_rcv_tdes | src/transaction/log_impl.h | 458 |
log_recvphase | src/transaction/log_impl.h | 625 |
log_cs_access_mode | src/transaction/log_impl.h | 923 |
log_initialize_internal | src/transaction/log_manager.c | 1100 |
log_append_compensate | src/transaction/log_manager.c | 2985 |
log_append_compensate_with_undo_nxlsa | src/transaction/log_manager.c | 3011 |
log_append_compensate_internal | src/transaction/log_manager.c | 3047 |
log_sysop_end_recovery_postpone | src/transaction/log_manager.c | 4024 |
log_complete | src/transaction/log_manager.c | 5653 |
log_rollback_record | src/transaction/log_manager.c | 7349 |
log_get_next_nested_top | src/transaction/log_manager.c | 8023 |
log_do_postpone | src/transaction/log_manager.c | 8237 |
log_run_postpone_op | src/transaction/log_manager.c | 8481 |
log_execute_run_postpone | src/transaction/log_manager.c | 8543 |
log_read_sysop_start_postpone | src/transaction/log_manager.c | 9962 |
LOGPB_IS_ARCHIVE_PAGE | src/transaction/log_page_buffer.c | 155 |
logpb_page_has_valid_checksum | src/transaction/log_page_buffer.c | 523 |
logpb_fetch_page | src/transaction/log_page_buffer.c | 1739 |
logpb_copy_page | src/transaction/log_page_buffer.c | 1871 |
logpb_read_page_from_file | src/transaction/log_page_buffer.c | 2003 |
logpb_fetch_start_append_page | src/transaction/log_page_buffer.c | 2504 |
logpb_page_get_first_null_block_lsa | src/transaction/log_page_buffer.c | 3190 |
logpb_is_page_in_archive | src/transaction/log_page_buffer.c | 4994 |
logpb_copy_from_log | src/transaction/log_page_buffer.c | 6532 |
logpb_checkpoint | src/transaction/log_page_buffer.c | 6877 |
logpb_page_check_corruption | src/transaction/log_page_buffer.c | 11508 |
log_reader | src/transaction/log_reader.hpp | 36 |
log_reader::set_lsa_and_fetch_page | src/transaction/log_reader.hpp | 162 |
LOG_READ_ALIGN | src/transaction/log_reader.hpp | 315 |
log_rec_undo | src/transaction/log_record.hpp | 176 |
log_vacuum_info | src/transaction/log_record.hpp | 192 |
log_rec_mvcc_undo | src/transaction/log_record.hpp | 211 |
log_rec_compensate | src/transaction/log_record.hpp | 262 |
log_sysop_end_type | src/transaction/log_record.hpp | 285 |
log_rec_sysop_end | src/transaction/log_record.hpp | 305 |
log_rec_sysop_start_postpone | src/transaction/log_record.hpp | 328 |
log_rec_chkpt | src/transaction/log_record.hpp | 345 |
log_info_chkpt_trans | src/transaction/log_record.hpp | 354 |
log_info_chkpt_sysop | src/transaction/log_record.hpp | 372 |
log_rv_undo_record | src/transaction/log_recovery.c | 163 |
log_rv_redo_record | src/transaction/log_recovery.c | 430 |
log_rv_fix_page_and_check_redo_is_needed | src/transaction/log_recovery.c | 494 |
log_rv_need_sync_redo | src/transaction/log_recovery.c | 541 |
log_rv_find_checkpoint | src/transaction/log_recovery.c | 579 |
log_rv_get_unzip_log_data | src/transaction/log_recovery.c | 609 |
log_rv_get_unzip_and_diff_redo_log_data | src/transaction/log_recovery.c | 699 |
log_recovery | src/transaction/log_recovery.c | 736 |
log_rv_analysis_undo_redo | src/transaction/log_recovery.c | 965 |
log_rv_analysis_dummy_head_postpone | src/transaction/log_recovery.c | 1000 |
log_rv_analysis_postpone | src/transaction/log_recovery.c | 1042 |
log_rv_analysis_run_postpone | src/transaction/log_recovery.c | 1086 |
log_rv_analysis_compensate | src/transaction/log_recovery.c | 1181 |
log_rv_analysis_commit_with_postpone | src/transaction/log_recovery.c | 1230 |
log_rv_analysis_commit_with_postpone_obsolete | src/transaction/log_recovery.c | 1315 |
log_rv_analysis_sysop_start_postpone | src/transaction/log_recovery.c | 1365 |
log_rv_analysis_atomic_sysop_start | src/transaction/log_recovery.c | 1472 |
log_rv_analysis_complete | src/transaction/log_recovery.c | 1509 |
log_rv_analysis_sysop_end | src/transaction/log_recovery.c | 1612 |
log_rv_analysis_start_checkpoint | src/transaction/log_recovery.c | 1797 |
log_rv_analysis_end_checkpoint | src/transaction/log_recovery.c | 1830 |
log_rv_analysis_save_point | src/transaction/log_recovery.c | 2077 |
log_rv_analysis_2pc_prepare | src/transaction/log_recovery.c | 2114 |
log_rv_analysis_2pc_start | src/transaction/log_recovery.c | 2153 |
log_rv_analysis_2pc_commit_decision | src/transaction/log_recovery.c | 2190 |
log_rv_analysis_2pc_abort_decision | src/transaction/log_recovery.c | 2224 |
log_rv_analysis_2pc_commit_inform_particps | src/transaction/log_recovery.c | 2258 |
log_rv_analysis_2pc_abort_inform_particps | src/transaction/log_recovery.c | 2293 |
log_rv_analysis_2pc_recv_ack | src/transaction/log_recovery.c | 2328 |
log_rv_analysis_log_end | src/transaction/log_recovery.c | 2355 |
log_rv_analysis_record | src/transaction/log_recovery.c | 2378 |
log_is_page_of_record_broken | src/transaction/log_recovery.c | 2518 |
log_recovery_analysis | src/transaction/log_recovery.c | 2587 |
log_recovery_needs_skip_logical_redo | src/transaction/log_recovery.c | 3153 |
log_recovery_get_redo_parallel_count | src/transaction/log_recovery.c | 3197 |
log_recovery_redo | src/transaction/log_recovery.c | 3251 |
BUILD_RECORD_INFO | src/transaction/log_recovery.c | 3468 |
INVOKE_REDO_RECORD | src/transaction/log_recovery.c | 3471 |
log_recovery_abort_interrupted_sysop | src/transaction/log_recovery.c | 3960 |
log_recovery_finish_sysop_postpone | src/transaction/log_recovery.c | 4064 |
log_recovery_finish_postpone | src/transaction/log_recovery.c | 4174 |
log_recovery_finish_all_postpone | src/transaction/log_recovery.c | 4243 |
log_recovery_abort_all_atomic_sysops | src/transaction/log_recovery.c | 4280 |
log_recovery_abort_atomic_sysop | src/transaction/log_recovery.c | 4317 |
log_recovery_undo | src/transaction/log_recovery.c | 4418 |
log_recovery_notpartof_archives | src/transaction/log_recovery.c | 4997 |
log_unformat_ahead_volumes | src/transaction/log_recovery.c | 5100 |
log_recovery_notpartof_volumes | src/transaction/log_recovery.c | 5132 |
log_recovery_resetlog | src/transaction/log_recovery.c | 5221 |
log_startof_nxrec | src/transaction/log_recovery.c | 5414 |
log_recovery_find_first_postpone | src/transaction/log_recovery.c | 5793 |
log_rv_undoredo_partial_changes_recursive | src/transaction/log_recovery.c | 6048 |
log_rv_undoredo_record_partial_changes | src/transaction/log_recovery.c | 6144 |
log_rv_redo_record_modify | src/transaction/log_recovery.c | 6173 |
log_rv_undo_record_modify | src/transaction/log_recovery.c | 6191 |
log_rv_record_modify_internal | src/transaction/log_recovery.c | 6210 |
log_rv_pack_redo_record_changes | src/transaction/log_recovery.c | 6310 |
log_rv_pack_undo_record_changes | src/transaction/log_recovery.c | 6352 |
log_rv_redo_fix_page | src/transaction/log_recovery.c | 6390 |
log_rv_simulate_runtime_worker | src/transaction/log_recovery.c | 6417 |
log_rv_end_simulation | src/transaction/log_recovery.c | 6438 |
log_cnt_pages_containing_lsa | src/transaction/log_recovery.c | 6449 |
log_find_unilaterally_largest_undo_lsa | src/transaction/log_recovery.c | 6470 |
vpid_lsa_consistency_check::check | src/transaction/log_recovery_redo.cpp | 28 |
log_rv_redo_context::log_rv_redo_context | src/transaction/log_recovery_redo.cpp | 52 |
log_rv_redo_context | src/transaction/log_recovery_redo.hpp | 33 |
log_rv_redo_rec_info | src/transaction/log_recovery_redo.hpp | 53 |
log_rv_get_log_rec_data | src/transaction/log_recovery_redo.hpp | 112 |
log_rv_get_log_rec_mvccid | src/transaction/log_recovery_redo.hpp | 163 |
log_rv_get_log_rec_vpid | src/transaction/log_recovery_redo.hpp | 206 |
log_rv_get_log_rec_redo_length | src/transaction/log_recovery_redo.hpp | 273 |
log_rv_get_log_rec_offset | src/transaction/log_recovery_redo.hpp | 316 |
log_rv_get_fun | src/transaction/log_recovery_redo.hpp | 359 |
log_rv_get_fun<LOG_REC_COMPENSATE> | src/transaction/log_recovery_redo.hpp | 396 |
log_rv_get_fun | src/transaction/log_recovery_redo.hpp | 396 |
log_rv_get_log_rec_redo_data | src/transaction/log_recovery_redo.hpp | 457 |
vpid_lsa_consistency_check | src/transaction/log_recovery_redo.hpp | 558 |
log_rv_redo_record_sync | src/transaction/log_recovery_redo.hpp | 587 |
redo_task | src/transaction/log_recovery_redo_parallel.cpp | 99 |
redo_task::execute | src/transaction/log_recovery_redo_parallel.cpp | 221 |
redo_parallel::add | src/transaction/log_recovery_redo_parallel.cpp | 626 |
redo_parallel::wait_for_termination_and_stop_execution | src/transaction/log_recovery_redo_parallel.cpp | 635 |
redo_parallel::wait_past_target_lsa | src/transaction/log_recovery_redo_parallel.cpp | 728 |
redo_job_impl::execute | src/transaction/log_recovery_redo_parallel.cpp | 752 |
reusable_jobs_stack::blocking_pop | src/transaction/log_recovery_redo_parallel.cpp | 868 |
redo_parallel | src/transaction/log_recovery_redo_parallel.hpp | 55 |
task_active_state_bookkeeping | src/transaction/log_recovery_redo_parallel.hpp | 100 |
min_unapplied_log_lsa_monitoring | src/transaction/log_recovery_redo_parallel.hpp | 131 |
redo_job_base | src/transaction/log_recovery_redo_parallel.hpp | 215 |
redo_job_impl | src/transaction/log_recovery_redo_parallel.hpp | 269 |
reusable_jobs_stack | src/transaction/log_recovery_redo_parallel.hpp | 306 |
log_rv_redo_record_sync_or_dispatch_async | src/transaction/log_recovery_redo_parallel.hpp | 382 |
perf_stats | src/transaction/log_recovery_redo_perf.hpp | 105 |
log_system_tdes::rv_simulate_system_tdes | src/transaction/log_system_tran.cpp | 174 |
log_system_tdes::map_all_tdes | src/transaction/log_system_tran.cpp | 253 |
log_system_tdes::rv_delete_all_tdes_if | src/transaction/log_system_tran.cpp | 265 |
log_system_tdes::rv_delete_tdes | src/transaction/log_system_tran.cpp | 281 |
logtb_rv_find_allocate_tran_index | src/transaction/log_tran_table.c | 1056 |
logtb_rv_assign_mvccid_for_undo_recovery | src/transaction/log_tran_table.c | 1115 |
logtb_free_tran_index | src/transaction/log_tran_table.c | 1202 |
logtb_free_tran_index_with_undo_lsa | src/transaction/log_tran_table.c | 1281 |
logtb_set_loose_end_tdes | src/transaction/log_tran_table.c | 4124 |
logtb_set_num_loose_end_trans | src/transaction/log_tran_table.c | 4170 |
logtb_rv_read_only_map_undo_tdes | src/transaction/log_tran_table.c | 4204 |
mvcctable::reset_start_mvccid | src/transaction/mvcc_table.cpp | 600 |
RV_fun | src/transaction/recovery.c | 54 |
rv_rcvindex_string | src/transaction/recovery.c | 857 |
rv_check_rvfuns | src/transaction/recovery.c | 872 |
LOG_RCVINDEX | src/transaction/recovery.h | 36 |
log_rcv | src/transaction/recovery.h | 197 |
rvfun | src/transaction/recovery.h | 221 |
RCV_IS_BTREE_LOGICAL_LOG | src/transaction/recovery.h | 241 |
RCV_IS_LOGICAL_COMPENSATE_MANUAL | src/transaction/recovery.h | 253 |
RCV_IS_LOGICAL_RUN_POSTPONE_MANUAL | src/transaction/recovery.h | 261 |
RCV_IS_LOGICAL_LOG | src/transaction/recovery.h | 267 |
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.