(KO) CUBRID Log Manager — 코드 수준 심층 분석
이 문서의 위치: 상위 분석서
cubrid-log-manager.md가 설계 의도와 이론적 배경을 다룬다면, 이 문서는 코드 수준에서 모든 분기와 필드를 추적하는 심층 분석서다. 각 챕터는 독립적으로 읽을 수 있지만, 순서대로 읽으면 log record 한 건이 커널 안에서 거치는 전체 생애주기를 따라갈 수 있다.
목차:
Chapter 1: 자료구조 전체 지도
섹션 제목: “Chapter 1: 자료구조 전체 지도”log record는 호출자에서 디스크까지 세 계층에 걸쳐 재인코딩된다.
호출자 입력 (log_data_addr, log_crumb), staging 계층
(log_prior_node, log_prior_lsa_info, log_append_info), 그리고
on-disk 계층 (log_hdrpage, log_page, log_header,
log_arv_header)이 그것이다. 본체, 즉 log_rec_header에서 파생되는
log_rec_* 패밀리는 세 계층 전체를 변형 없이 그대로 거쳐 간다. 이 장은
field 수준의 지도다. 계층 간 이동은 이후 장에서 추적한다.
연결: WAL 이론(LSA가 모든 것을 정렬하는 이유, log가 data page보다 먼저 디스크에 도달해야 하는 이유)은 상위 레벨 companion 문서인
cubrid-log-manager.md에 있다. 이 장은 그 규칙이 작동하는 구조체를 기술하며, 규칙 자체를 다루지 않는다.
1.1 주소 기본 단위 — log_lsa
섹션 제목: “1.1 주소 기본 단위 — log_lsa”log sequence address (LSA) 는 논리적 page id와 페이지 내 바이트 offset을 64비트 비트 필드로 압축한다.
// log_lsa -- src/transaction/log_lsa.hppstruct log_lsa{ std::int64_t pageid:48; /* Log page identifier : 6 bytes length */ std::int64_t offset:16; /* Offset in page. :16 of int64 (not short) for alignment */ // ... condensed: is_null(), is_max(), set_null(); ordering compares pageid then offset ...};| 필드 | 역할 | 존재 이유 |
|---|---|---|
pageid (48b) | 무한 log 내의 논리적 page id | 제한 없는 append-only 페이지 시퀀스 |
offset (16b) | area[] 내의 바이트 offset | int64의 :16으로 8바이트에 맞춤 |
불변식 — LSA 전순서(total ordering). operator<는 pageid를 먼저,
다음으로 offset을 비교하므로 LSA는 단조 증가하는 WAL clock이 된다.
모든 before/after 판단과 durability 결정이 LSA 비교에 의존한다. 이 순서를
잃으면 WAL 규칙(Ch 7)과 recovery replay에서 무엇을 redo할지 결정할 수
없다.
센티널/심: NULL_LSA = {-1,-1} (set_null()은 두 필드를 모두 기록),
MAX_LSA = {(1<<47)-1,(1<<15)-1}, 그리고 LSA_* 매크로
(LSA_COPY, LSA_SET_NULL, LSA_ISNULL, LSA_EQ/LE/LT/GE/GT,
LSA_AS_ARGS) — 레거시 C 컴파일을 위한 연산자 인라인 래퍼.
1.2 record header — log_rec_header
섹션 제목: “1.2 record header — log_rec_header”on-disk의 모든 record는 고정 크기의 log_rec_header로 시작한다. 이
헤더는 record를 물리적 체인과 트랜잭션별 체인에 연결한다.
// log_rec_header -- src/transaction/log_record.hppstruct log_rec_header{ LOG_LSA prev_tranlsa; /* prev record of SAME transaction */ LOG_LSA back_lsa, forw_lsa; /* physically prev / next record */ TRANID trid; LOG_RECTYPE type;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
prev_tranlsa | 동일 트랜잭션의 이전 record | Undo가 한 트랜잭션을 거슬러 올라감 |
back_lsa | 물리적 이전 record | 역방향 log scan |
forw_lsa | 물리적 다음 record | Redo 전방 scan; 후속 record가 확정될 때까지 NULL_LSA (Ch 4) |
trid | 소유 트랜잭션 id | 뒤섞인 스트림을 역다중화 |
type | LOG_RECTYPE 판별자 | tagged-union 태그; payload struct 선택 |
불변식 — header가 이중 연결 물리 체인을 구성한다. 연속된 A, B에
대해: B.back_lsa == addr(A) 이고 A.forw_lsa == addr(B) 이다.
back_lsa는 record 생성 시 설정되고, forw_lsa는 후속 record의 LSA가
확정된 후에야 설정된다 — 따라서 tail에서 체인은 잠시 반쪽만 열린 상태다.
두 값이 어긋나면 undo와 redo scan이 서로 다른 record 집합을 훑게 되어
recovery가 깨진다.
1.3 타입 태그 — log_rectype
섹션 제목: “1.3 타입 태그 — log_rectype”type이 나타내는 enum은 명시적 번호 부여와 append-only 방식을
따른다. 폐기된 값은 삭제하지 않고 #if 0으로 감싸두므로, on-disk 정수
의미가 절대 변하지 않는다.
// log_rectype -- src/transaction/log_record.hpp (condensed)enum log_rectype{ LOG_SMALLER_LOGREC_TYPE = 0, /* lower-bound check */#if 0 LOG_CLIENT_NAME = 1, /* Obsolete -- hole preserved */#endif LOG_UNDOREDO_DATA = 2, LOG_UNDO_DATA = 3, LOG_REDO_DATA = 4, // ... LOG_COMMIT=17, LOG_SYSOP_END=20, LOG_ABORT=22, ... LOG_MVCC_UNDOREDO_DATA = 46, LOG_MVCC_UNDO_DATA = 47, LOG_MVCC_REDO_DATA = 48, LOG_MVCC_DIFF_UNDOREDO_DATA = 49, LOG_SYSOP_ATOMIC_START = 50, LOG_DUMMY_GENERIC = 51, /* dummy used for flush */ LOG_SUPPLEMENTAL_INFO = 52, LOG_LARGER_LOGREC_TYPE /* upper-bound check */};불변식 — 센티널 경계와 안정적인 wire 값. LOG_SMALLER_LOGREC_TYPE (0) 과 LOG_LARGER_LOGREC_TYPE 가 유효 범위를 감싼다. 이 정수는
디스크에 저장되므로 번호가 재사용되지 않으며 (#if 0 구멍이 이를
보장한다), 분류 매크로(LOG_IS_UNDO_RECORD_TYPE,
LOG_IS_REDO_RECORD_TYPE, LOG_IS_UNDOREDO_RECORD_TYPE,
LOG_IS_MVCC_OP_RECORD_TYPE)가 type을 읽는다.
1.4 recovery 데이터 로케이터 — log_data
섹션 제목: “1.4 recovery 데이터 로케이터 — log_data”undo/redo payload는 log_data를 포함하여 변경이 적용되는 data volume
내 위치를 명시한다. 이것은 log의 주소가 아니라 recovery 좌표다.
// log_data -- src/transaction/log_record.hppstruct log_data { LOG_RCVINDEX rcvindex; PAGEID pageid; PGLENGTH offset; VOLID volid; };| 필드 | 역할 | 존재 이유 |
|---|---|---|
rcvindex | recovery dispatch 테이블의 인덱스 | 해당 바이트에 적용할 rv* 함수를 선택 |
pageid | 대상 data page id | refix할 페이지 |
offset | 해당 페이지 내 offset/slot | 변경이 적용될 위치 |
volid | 대상 페이지의 volume id | 볼륨 간 pageid 중복을 해소 |
1.5 payload 패밀리 — undo/redo 및 MVCC 변형
섹션 제목: “1.5 payload 패밀리 — undo/redo 및 MVCC 변형”type 태그는 페이지에서 header 뒤에 오는 payload 하나를 선택하며,
모두 log_data를 기반으로 한다.
// log_rec_undoredo / undo / redo -- src/transaction/log_record.hppstruct log_rec_undoredo { LOG_DATA data; int ulength, rlength; };struct log_rec_undo { LOG_DATA data; int length; };struct log_rec_redo { LOG_DATA data; int length; };log_rec_undoredo는 ulength+rlength를 가지며(두 블롭의 길이를
구분), log_rec_undo는 length 하나(undo 이미지만, 논리적 undo),
log_rec_redo는 length 하나(redo 이미지만, 페이지 물리적 redo)를
가진다. MVCC 변형은 이것들을 감싸고 MVCC id와 vacuum 북키핑을 덧붙인다.
// MVCC payload wrappers -- src/transaction/log_record.hppstruct log_rec_mvcc_undoredo { LOG_REC_UNDOREDO undoredo; MVCCID mvccid; LOG_VACUUM_INFO vacuum_info; };struct log_rec_mvcc_undo { LOG_REC_UNDO undo; MVCCID mvccid; LOG_VACUUM_INFO vacuum_info; };struct log_rec_mvcc_redo { LOG_REC_REDO redo; MVCCID mvccid; }; /* no vacuum_info */| struct | 감싸는 대상 | 추가 내용 | 존재 이유 |
|---|---|---|---|
log_rec_mvcc_undoredo | log_rec_undoredo | mvccid, vacuum_info | MVCC 연산을 vacuum이 추적 |
log_rec_mvcc_undo | log_rec_undo | mvccid, vacuum_info | MVCC delete 계열 연산 |
log_rec_mvcc_redo | log_rec_redo | mvccid 만 | 순수 redo는 vacuum할 버전을 만들지 않음 |
log_vacuum_info는 undo MVCC record가 가지는 back-pointer다.
// log_vacuum_info -- src/transaction/log_record.hppstruct log_vacuum_info { LOG_LSA prev_mvcc_op_log_lsa; VFID vfid; };| 필드 | 역할 | 존재 이유 |
|---|---|---|
prev_mvcc_op_log_lsa | 이전 MVCC-op record의 LSA | vacuum이 log 순서대로 이 체인을 순회 |
vfid | 변경이 속한 파일 | 삭제/재사용된 파일을 감지하고 객체 종류를 판단 |
1.6 staging node — log_prior_node
섹션 제목: “1.6 staging node — log_prior_node”append 경로는 record를 log_prior_node로 구체화하여 prior list에 연결한다.
prior list는 중심적인 staging 구조다(Ch 3–5).
// log_prior_node -- src/transaction/log_append.hppstruct log_prior_node{ LOG_RECORD_HEADER log_header; LOG_LSA start_lsa; bool tde_encrypted; int data_header_length; char *data_header; int ulength; char *udata; int rlength; char *rdata; LOG_PRIOR_NODE *next;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
log_header | 내장된 log_rec_header | 페이지에 복사됨; back_lsa/forw_lsa는 연결 시점에 채워짐 |
start_lsa / tde_encrypted | 할당된 LSA; 암호화 플래그 | LSA는 페이지 offset 대비 검증; 플래그는 drain 시 hdr.flags를 구동 |
data_header_length / data_header | log_rec_* struct의 길이 + 버퍼 | 가변 데이터와 분리되어 직렬화 |
ulength/udata, rlength/rdata | undo / redo 바이트의 길이 + 버퍼 | 두 이미지, 압축 가능 |
next | 다음 노드 | drain을 기다리는 노드의 순서 |
불변식 — prior node는 자신의 힙 버퍼를 소유한다. data_header,
udata, rdata는 각각 독립적으로 malloc된다. 길이가 0이면 포인터가
사용되지 않는다는 의미다. drain은 페이지 버퍼에 복사한 후 이를 해제한다.
누수나 이중 해제는 힙을 오염시킨다.
1.7 prior-list 앵커 — log_prior_lsa_info
섹션 제목: “1.7 prior-list 앵커 — log_prior_lsa_info”전체 prior list의 인메모리 앵커로, LSA 커서·리스트 head/tail·직렬화 mutex를 모두 담는다.
// log_prior_lsa_info -- src/transaction/log_append.hppstruct log_prior_lsa_info{ LOG_LSA prior_lsa; LOG_LSA prev_lsa; LOG_PRIOR_NODE *prior_list_header; LOG_PRIOR_NODE *prior_list_tail; INT64 list_size; /* bytes */ LOG_PRIOR_NODE *prior_flush_list_header; std::mutex prior_lsa_mutex;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
prior_lsa | 다음에 할당할 LSA | 단조 증가 할당 커서; record 크기만큼 전진 (Ch 4) |
prev_lsa | 마지막으로 append된 record의 LSA | 다음 노드의 back_lsa를 채움 |
prior_list_header / prior_list_tail | drain 대기 리스트의 head / tail | drain 시작점; O(1) append |
list_size | staging된 총 바이트 수 | flusher가 drain 시점을 결정 |
prior_flush_list_header | 분리된 flush 서브리스트의 head | drain이 여기서 가져가므로 producer는 계속 append 가능 |
prior_lsa_mutex | 위 모든 필드를 보호하는 mutex | LSA 할당 + 연결을 원자적으로 처리 |
불변식 — prior_lsa_mutex가 LSA 할당을 직렬화한다. prior_lsa
전진과 노드 연결이 하나의 lock 획득 아래 이루어지므로 두 record가 동일한
LSA를 공유하지 않으며, 리스트 순서가 LSA 순서와 일치한다. 이를 분리하면
drain된 페이지의 순서가 뒤집힌다.
1.8 on-disk append 커서 — log_append_info
섹션 제목: “1.8 on-disk append 커서 — log_append_info”디스크 방향 append 지점 — 열린 log 파일, 고정된 페이지, 그리고 아직 디스크에 기록되지 않은 가장 낮은 LSA.
// log_append_info -- src/transaction/log_append.hppstruct log_append_info{ int vdes; std::atomic<LOG_LSA> nxio_lsa; /* Lowest LSA NOT yet written to disk (WAL) */ LOG_LSA prev_lsa; LOG_PAGE *log_pgptr; bool appending_page_tde_encrypted; // ... condensed: get_nxio_lsa(), set_nxio_lsa() ...};| 필드 | 역할 | 존재 이유 |
|---|---|---|
vdes | 활성 log volume의 OS fd | 페이지 쓰기 대상 |
nxio_lsa | Atomic — 아직 flush되지 않은 가장 낮은 LSA | WAL watermark; reader/flusher가 prior mutex 없이 경쟁 |
prev_lsa | 버퍼에 마지막으로 append된 record | staging prev_lsa의 drain 측 미러 |
log_pgptr | 현재 고정된 log 페이지 | drain 대상; 페이지 경계에서 교체됨 (Ch 6) |
appending_page_tde_encrypted | 현재 페이지를 암호화해야 함 | 노드의 tde_encrypted를 페이지로 전달 |
불변식 — nxio_lsa는 WAL durability watermark다. LSA < nxio_lsa인
record는 디스크에 있고, >= nxio_lsa인 record는 없다. flusher와 WAL
검사가 동시에 접근하므로 std::atomic이며, 반드시 get_nxio_lsa()/
set_nxio_lsa()를 통해서만 접근한다. torn read가 발생하면 data page가
log보다 먼저 flush될 수 있다(Ch 7).
1.9 호출자 입력 — log_data_addr와 log_crumb
섹션 제목: “1.9 호출자 입력 — log_data_addr와 log_crumb”호출자 (heap/btree 연산)가 append API에 전달하는 것으로, 위의 모든 구조체는 이로부터 유도된다.
// log_data_addr / log_crumb -- src/transaction/log_append.hppstruct log_crumb { int length; const void *data; };struct log_data_addr { const VFID *vfid; PAGE_PTR pgptr; PGLENGTH offset; };| struct / 필드 | 역할 | 존재 이유 |
|---|---|---|
log_crumb.length / .data | 연속된 호출자 데이터 한 조각 | 호출자는 배열을 전달하여 분산된 버퍼를 수집 |
log_data_addr.vfid | 페이지가 속한 파일, 또는 NULL | 파일/TDE 컨텍스트; log_data.volid/pageid는 pgptr에서 추출, vfid에서 오지 않음 |
log_data_addr.pgptr | 고정된 data page 포인터 | volid/pageid를 추출하여 log_data에 기록 |
log_data_addr.offset | 변경의 offset/slot | log_data.offset이 됨; 상위 비트는 LOG_RV_RECORD_* 플래그 |
1.10 on-disk 페이지 구조 — log_hdrpage와 log_page
섹션 제목: “1.10 on-disk 페이지 구조 — log_hdrpage와 log_page”물리적 log 페이지는 log_hdrpage와 가변 길이 area[]로 구성된다.
area[1]은 struct-hack 패턴이다 — sizeof로 크기를 측정하면 안 되며,
LOG_PAGESIZE를 사용해야 한다.
// log_hdrpage / log_page -- src/transaction/log_storage.hppstruct log_hdrpage { LOG_PAGEID logical_pageid; PGLENGTH offset; short flags; int checksum; };struct log_page { LOG_HDRPAGE hdr; char area[1]; }; /* area is flexible */| 필드 | 역할 | 존재 이유 |
|---|---|---|
logical_pageid | 무한 시퀀스 내의 page id | log_lsa.pageid와 일치; 읽기 시 동일성 확인 |
offset | 여기서 시작하는 첫 번째 record의 offset | 이전 페이지가 손상된 경우 복구 앵커 |
flags | TDE 비트 (..._ENCRYPTED_AES/ARIA) | LOG_IS_PAGE_TDE_ENCRYPTED가 마스크를 확인 |
checksum | 페이지 전체의 CRC32 | torn page 감지 |
log_page.hdr | 위의 header | 고정 페이지 접두부 |
log_page.area[] | record 바이트 | LOG_PAGESIZE로 크기 결정 |
불변식 — LOG_PAGEID -9는 header 페이지다.
LOGPB_HEADER_PAGE_ID = -9는 log_header를 담으며, log record를 전혀
가지지 않고, 모든 archive에 복제된다. 코드는 절대 pageid -9에 일반
record를 기록해서는 안 된다.
1.11 volume header — log_header와 log_arv_header
섹션 제목: “1.11 volume header — log_header와 log_arv_header”log_header는 페이지 -9에 있는 마스터 제어 블록이다. 역할별로 묶은
모든 멤버:
| 필드 그룹 | 필드들 | 역할 |
|---|---|---|
| 식별 / 안전성 | magic, db_creation, db_release, db_compatibility, db_iopagesize, db_logpagesize, db_charset | 호환되지 않는 빌드/페이지 크기의 log 거부 |
| Append 커서 | append_lsa, fpageid, eof_lsa | 영속적인 append 위치, 슬롯 1의 pageid, log 끝 |
| Recovery | chkpt_lsa, smallest_lsa_at_last_chkpt | recovery가 시작하는 가장 낮은 LSA |
| 트랜잭션 / MVCC | next_trid, mvcc_next_id, mvcc_op_log_lsa, oldest_visible_mvccid, newest_block_mvccid, vacuum_last_blockid, does_block_need_vacuum | 다음에 할당할 id; vacuum의 진행 상황 |
| Archive | nxarv_pageid, nxarv_phy_pageid, nxarv_num, last_arv_num_for_syscrashes, last_deleted_arv_num, npages | Ch 10의 archiving을 구동 |
| Backup | bkup_level0_lsa/1/2, bkinfo[] | 레벨별 증분 backup 앵커 |
| HA / 생명주기 | ha_server_state, ha_file_status, ha_promotion_time, is_shutdown, was_active_log_reset, has_logging_been_skipped, db_restore_time, mark_will_del | replication 상태; 정상 종료 플래그 |
| 정렬 / 기타 | dummy, dummy3, dummy4, vol_creation, avg_ntrans, avg_nlocks, was_copied, prefix_name, perm_status_obsolete | dummy*는 패딩; vol_creation 시각; avg_* 크기 힌트; was_copied는 복사된 DB를 초기화; prefix_name은 log 접두사; perm_status_obsolete는 레거시 |
log_arv_header는 각 archive 파일에 찍히는 더 작은 header다.
// log_arv_header -- src/transaction/log_storage.hppstruct log_arv_header{ char magic[CUBRID_MAGIC_MAX_LENGTH]; INT32 dummy; INT64 db_creation; INT64 vol_creation; TRANID next_trid; DKNPAGES npages; LOG_PAGEID fpageid; int arv_num; INT32 dummy2;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
magic | 파일 타입 magic | 파일 인식 + 정상성 확인 |
db_creation / vol_creation | 생성 타임스탬프 | archive를 DB/volume에 대응 |
next_trid | archive 시점의 다음 trid | recovery 컨텍스트 |
npages | 이 archive의 페이지 수 | 페이지 범위 경계 |
fpageid | 물리적 슬롯 1의 논리적 pageid | 물리 → 논리 페이지 변환 |
arv_num | archive 시퀀스 번호 | log_header.nxarv_num 체인과 일치 |
dummy, dummy2 | 정렬 패딩 | on-disk 레이아웃 안정화 |
1.12 struct 관계도
섹션 제목: “1.12 struct 관계도”flowchart TB
subgraph CALLER["호출자 입력"]
DADDR["log_data_addr"]
CRUMB["log_crumb[]"]
end
subgraph STAGE["Staging 계층 (메모리)"]
PLINFO["log_prior_lsa_info"]
NODE["log_prior_node"]
AINFO["log_append_info"]
end
subgraph REC["Record 본체 (모든 계층)"]
HDR["log_rec_header"]
PAY["log_rec_*\n+ MVCC wrappers"]
end
subgraph DISK["On-disk 계층"]
PAGE["log_page"]
HPAGE["log_hdrpage"]
LHDR["log_header (page -9)"]
AHDR["log_arv_header"]
end
DADDR --> NODE
CRUMB --> NODE
PLINFO -->|owns list of| NODE
NODE -->|embeds| HDR
NODE -->|serializes| PAY
PAY -->|embeds| LDATA["log_data"]
PLINFO -->|drains to| AINFO
AINFO -->|fixes / writes| PAGE
PAGE -->|hdr is| HPAGE
LHDR -->|append_lsa to| PAGE
LHDR -->|nxarv_* feed| AHDR
Figure 1-1 — 세 계층에 걸쳐 record의 구조체들이 어떻게 연결되는지 보여주는 다이어그램.
1.13 포인터 관계 요약
섹션 제목: “1.13 포인터 관계 요약”수정자가 일관성을 유지해야 하는 LSA/포인터 엣지:
- Physical chain —
log_rec_header.forw_lsa/back_lsa;prev_tranlsa. - Staging 할당자 —
log_prior_lsa_info.prior_lsa/prev_lsa. - Durability watermark —
log_append_info.nxio_lsa. - Vacuum chain —
log_vacuum_info.prev_mvcc_op_log_lsa.
1.14 Chapter 요약 — 핵심 정리
섹션 제목: “1.14 Chapter 요약 — 핵심 정리”- log record는 세 계층을 거친다 — 호출자 입력, staging (
log_prior_lsa_info가 앵커 역할을 하는log_prior_node,log_append_info를 통해 drain), on-disk (log_header아래의log_page). 본체(log_rec_header+log_rec_*payload)는 변하지 않는 상수다. log_lsa는 48:16 비트 필드 clock이며, 그 전순서가 모든 durability 결정의 기반이다.log_rec_header는 각 record를 물리적 이중 연결 체인과 트랜잭션별 체인에 연결하고,type은 append-only·구멍 보존 방식의log_rectype을 판별한다.- MVCC 래퍼는
mvccid와 (undo에만)log_vacuum_info를 추가한다. redo 래퍼는vacuum_info를 생략한다 — 순수 redo는 vacuum할 버전을 만들지 않기 때문이다. prior_lsa_mutex는 LSA 할당과 연결을 원자적으로 만들고,nxio_lsa는 atomic WAL watermark다. 이 두 동시성 불변식이 append 경로를 지탱한다. on-disk 페이지는 pageid -9를log_header용으로 예약하며,log_header의nxarv_*가log_arv_header를 구동한다.
Chapter 2: 초기화와 메모리
섹션 제목: “Chapter 2: 초기화와 메모리”독자가 가져야 할 질문은 이것이다. record를 append하기 전에, prior list·page-buffer pool·flush 북키핑·전역 log 상태는 어떻게 초기화되고 할당되는가? prior list의 개념적 역할 — prior list가 무엇을 위한 것인지, WAL이 왜 ring을 요구하는지 — 은 companion 문서 cubrid-log-manager.md의 “The append pipeline”, “Durability” 절을 참고하라. 이 장은 bring-up 메커니즘을 다룬다. 누가 malloc하는지, 각 필드의 초기값이 무엇인지, 어떤 teardown이 무엇을 해제하는지가 주제다. 진입점은 두 개이며, 둘 다 LOG_CS 아래에서 실행되고, 이미 인스턴스가 마운트되어 있으면 먼저 log_final을 호출한다.
log_create_internal— DB 생성 시 단 한 번 실행된다. active-log volume을 포맷하고, 페이지-9에 최초의LOG_HEADER를 기록하고, 빈 append 페이지를 flush한 뒤, pool을 다시 해제한다. 살아남는 라이브 상태는 없다.log_initialize_internal— 모든 재시작 / SA boot 시 실행된다. 기존 active log를 마운트하고, 페이지-9를 읽고, pool을 활성 상태로 유지하며, 제어를 recovery에 넘긴다.
2.1 전역 싱글턴: log_global / log_Gl
섹션 제목: “2.1 전역 싱글턴: log_global / log_Gl”모든 것은 프로세스 전역 싱글턴 하나인 log_Gl (struct log_global)에
매달린다. log_global::log_global(log_global.c)이 정적 초기화 시
기본 생성자로 만들며, bring-up은 멤버를 채우는 작업이지 이를 할당하는
작업이 아니다.
// log_global -- src/transaction/log_impl.h (condensed; #if SERVER_MODE members noted in the table)struct log_global { TRANTABLE trantable; LOG_APPEND_INFO append; LOG_PRIOR_LSA_INFO prior_info; LOG_HEADER hdr; LOG_ARCHIVES archive; LOG_PAGEID run_nxchkpt_atpageid; LOG_LSA chkpt_redo_lsa; DKNPAGES chkpt_every_npages; LOG_RECVPHASE rcv_phase; LOG_LSA rcv_phase_lsa; LOG_PAGE *loghdr_pgptr; LOG_FLUSH_INFO flush_info; LOG_GROUP_COMMIT_INFO group_commit_info; logwr_info *writer_info; /* the ONLY heap member of the ctor: new logwr_info() */ BACKGROUND_ARCHIVING_INFO bg_archive_info; mvcctable mvcc_table; GLOBAL_UNIQUE_STATS_TABLE unique_stats_table; // #if SERVER_MODE: flushed_lsa_lower_bound, chkpt_lsa_lock, backup_in_progress; #else: final_restored_lsa};생성자는 모든 LSA 타입 필드를 NULL_LSA로 초기화하고, flush_info를
{0, 0, NULL, PTHREAD_MUTEX_INITIALIZER}로 시드하고, prior_info의
생성자를 실행하며(§2.5), 유일한 힙 할당으로 writer_info를 new한다.
전체 필드:
| 필드 | 역할 | 존재 이유 / 생성자 초기값 |
|---|---|---|
trantable | 트랜잭션별 LOG_TDES 테이블 | area == NULL이 “초기화되지 않음” 센티널; logtb_define_trantable_log_latch가 크기를 결정. |
append | 라이브 append 커서 (vdes, log_pgptr, prev_lsa, atomic nxio_lsa) | prior node가 페이지로 drain되는 곳; Ch 4-5. |
prior_info | 인메모리 prior list head/tail + LSA 커서 | LSA 할당을 디스크 레이아웃에서 분리; Ch 3-5. |
hdr | on-disk LOG_HEADER의 RAM 내 사본 (append_lsa/eof_lsa가 여기에 위치) | 페이지 -9를 반복 읽는 것을 피함. |
archive | 현재 archive 디스크립터 캐시 | 원하는 페이지가 archive로 넘어갔을 때 사용. |
run_nxchkpt_atpageid | 다음 checkpoint가 발동될 page id | create/init 중에는 NULL_PAGEID; init 끝에서 재계산. |
flushed_lsa_lower_bound / chkpt_lsa_lock | SERVER_MODE 전용: flush 조정 LSA + checkpoint-LSA mutex | NULL_LSA / PTHREAD_MUTEX_INITIALIZER. |
chkpt_redo_lsa / chkpt_every_npages | redo 시작 LSA + checkpoint 빈도 | NULL_LSA / INT_MAX (후자는 PRM_ID_LOG_CHECKPOINT_NPAGES에서). |
rcv_phase / rcv_phase_lsa | Recovery 단계 + 해당 LSA | LOG_RECOVERY_ANALYSIS_PHASE / NULL_LSA; log_final이 단계를 초기화. |
backup_in_progress / final_restored_lsa | #if 쌍: SERVER backup 플래그 vs SA 마지막 복원 LSA | 빌드마다 하나; false / NULL_LSA. |
loghdr_pgptr | header I/O용 LOG_PAGESIZE 스크래치 페이지 | log_initialize_internal에서 malloc하는 전역 버퍼, log_final에서 해제 — create 경로의 동명 지역 변수(§2.2)와 구분됨. |
flush_info | toflush[] + 카운터 + mutex | flush 시 디스크에 밀어낼 dirty append 페이지; §2.4. |
group_commit_info | group commit용 mutex+cond | committer들이 fsync를 묶을 수 있게 함. |
writer_info | HA log-writer 상태 | 생성자에서만 new; ~log_global에서 delete. |
bg_archive_info | 백그라운드 archiving 디스크립터 | PRM_ID_LOG_BACKGROUND_ARCHIVING이 켜져 있으면 init 마지막에 초기화. |
mvcc_table / unique_stats_table | MVCC snapshot 테이블 / 전역 고유 인덱스 통계 | 기본 생성자 / GLOBAL_UNIQUE_STATS_TABLE_INITIALIZER. |
graph TD
subgraph logGl["log_Gl (LOG_GLOBAL singleton)"]
A["append : LOG_APPEND_INFO<br/>vdes, log_pgptr, prev_lsa, nxio_lsa"]
P["prior_info : LOG_PRIOR_LSA_INFO<br/>prior_lsa, prev_lsa, list head/tail"]
F["flush_info : LOG_FLUSH_INFO<br/>toflush[], max/num_toflush, mutex"]
end
PB["log_Pb (LOG_PB_GLOBAL_DATA)<br/>buffers[], pages_area, header_page"]
F -- "toflush[] points into" --> PB
A -- "log_pgptr points into" --> PB
Figure 2-1 — 전역 싱글턴과 별도로 선언된 page-buffer 전역 변수 log_Pb의 관계.
2.2 log_create_internal — 최초 bring-up
섹션 제목: “2.2 log_create_internal — 최초 bring-up”LOG_CS_ENTER 아래에서 실행된다. 모든 분기:
- 잔류 상태 확인:
trantable.area != NULL→log_final(§2.7). umask;logpb_initialize_pool(§2.3)이 ring을 할당. 오류 →goto error.logpb_initialize_log_names가log_Name_active등을 구성. 오류 →goto error.logpb_initialize_header (&log_Gl.hdr, ...)가 RAM 내 header를 채움 (페이지 수,db_logpagesize = LOG_PAGESIZE). 오류 →goto error.logpb_create_header_page가 page--9버퍼를 스택 지역loghdr_pgptr에 할당 — 이것은log_create_internal내부에 선언된 변수로, §2.1의 전역log_Gl.loghdr_pgptr이 아니다. create 경로는 재시작 경로의 I/O 버퍼와 분리된 스크래치 페이지를 사용한다.fileio_format이 active-log 파일을 생성한다.vdes == NULL_VOLDES,logpb_fetch_start_append_page실패, 지역loghdr_pgptr == NULL중 하나라도 발생하면goto error:
// log_create_internal -- src/transaction/log_manager.c log_Gl.append.vdes = fileio_format (thread_p, db_fullname, log_Name_active, ...); if (log_Gl.append.vdes == NULL_VOLDES || logpb_fetch_start_append_page (thread_p) != NO_ERROR || loghdr_pgptr == NULL) goto error; /* <- any one failure unwinds the whole pool */- 빈 append 페이지를 dirty로 표시;
logpb_flush_pages_direct가 end-of-log 마크를 기록. - RAM 내
hdr을 지역loghdr_pgptr->area에memcpy;logpb_flush_page가 페이지-9를 기록 (오류 →goto error;CUBRID_DEBUG아래에서는 읽어서assert확인). log_pgptr초기화, 언마운트, volume-info/log-info 파일 생성,logpb_add_volume으로 active + backup-info volume 등록.- 정상 종료:
logpb_finalize_pool,LOG_CS_EXIT,NO_ERROR.
error: 레이블은 동일하게 logpb_finalize_pool + LOG_CS_EXIT를 실행한다 (설정되지 않았으면 ER_FAILED 반환). create는 라이브 pool을 남기지 않는다.
불변식 — 페이지
-9는 log 기하 구조의 단일 출처다. 새로운LOG_HEADER가 처음부터 기록되는 유일한 장소다. 이후 모든 boot는 여기서 읽어간다. 8단계의memcpy+ 동기logpb_flush_page가 이를 보장한다. 그 flush가 조용히 실패하면, 재시작 시 잘못된 기하 구조(db_logpagesize,fpageid)를 읽어 재포맷하거나 마운트를 거부한다.
2.2b log_initialize_internal — 재시작 bring-up
섹션 제목: “2.2b log_initialize_internal — 재시작 bring-up”초기 비계는 공유하지만, 마운트 지점에서 갈라진다. 여기서는 페이지 -9를
읽고, pool을 유지하며, recovery로 디스패치한다. 순서에 따른 모든 분기:
- 클린 상태 확인:
trantable.area != NULL → log_final. - log-names 초기화:
logpb_initialize_log_names실패는 단순 전파가 아닌 치명적 오류 (logpb_fatal_error후goto error). loghdr_pgptrmalloc: 전역log_Gl.loghdr_pgptr(페이지--9I/O 버퍼,logpb_fetch_header/logpb_flush_header용);NULL→ 치명적 +goto error.log_final(§2.7)과error:레이블에서 해제.- Pool 초기화:
logpb_initialize_pool(§2.3); 오류 →goto error. fileio_mount가NULL_VOLDES반환 시 두 경로로 나뉜다 — 미디어 충돌 (ismedia_crash != false): 근사 header를 합성한다 (logpb_initialize_header로 기하 구조 설정 후, 아래 강제 필드들이 모든 것을 미체크포인트 상태로 표시,LOG_RESET_APPEND_LSA로prior_info에 동기화,chkpt_lsanull 처리,nxarv_*최대화); 아닐 경우error_code = ER_IO_MOUNT_FAIL; goto error:// log_initialize_internal -- src/transaction/log_manager.clog_Gl.hdr.fpageid = LOGPAGEID_MAX; log_Gl.hdr.append_lsa.pageid = LOGPAGEID_MAX;log_Gl.hdr.append_lsa.offset = 0; LOG_RESET_APPEND_LSA (&log_Gl.hdr.append_lsa);vdes가 NULL이 아닌 경우:logpb_fetch_header (&log_Gl.hdr)가 실제 페이지-9를 미러에 읽어들임.hdr.chkpt_lsa→chkpt_redo_lsa로 복사.restore_slave분기 (ismedia_crash && r_args && r_args->restore_slave): HA slave restore를 위해db_creation,smallest_lsa_at_last_chkpt,append_lsa를r_args로 복사.- 접두사 이름 불일치:
strcmp(hdr.prefix_name, prefix_logname) != 0→ER_LOG_INCOMPATIBLE_PREFIX_NAME(알림) 후 계속 진행. - 페이지 크기 불일치 → 재귀 재초기화:
hdr.db_iopagesize != IO_PAGESIZE || hdr.db_logpagesize != LOG_PAGESIZE→db_set_page_size,logpb_finalize_pool, 언마운트,LOG_CS_EXIT,logtb_define_trantable_log_latch재실행 후log_initialize_internal재귀 호출 후 반환 — 버퍼가 올바른 크기로 재건됨 (§2.8 참조). - 호환성 검사 (
rel_get_disk_compatible,rel_is_log_compatible)가 호환되지 않는 버전에서goto error;logtb_define_trantable_log_latch(-1)이 라이브 trantable을 구성;fileio_map_mounted가 log가 이 DB에 속하는지 확인 (아닐 경우 trantable 해제 +goto error). - Recovery 디스패치:
init_emergency == false && (hdr.is_shutdown == false || ismedia_crash)→ 이전 실행 충돌 →log_recovery. 아닐 경우 클린/긴급 boot →logpb_fetch_start_append_page, EOF record를 읽어LOG_RESET_PREV_LSA(&eof->back_lsa)로prev_lsa시드,is_shutdown = false설정,logpb_flush_header. - Prior/append LSA assert + reset (§2.5 참조):
rcv_phase = LOG_RESTARTED설정 후,append.prev_lsa/hdr.append_lsa가prior_info와 다르면 방어적assert(0)+ 재reset;chkpt_every_npages,run_nxchkpt_atpageid재계산, bg-archiving 활성화,LOG_CS_EXIT, 반환.
error: 레이블은 마운트되어 있으면 vdes 언마운트, loghdr_pgptr free_and_init, LOG_CS_EXIT, logpb_fatal_error — 재시작 실패는 abort다.
2.3 logpb_initialize_pool — page-buffer ring
섹션 제목: “2.3 logpb_initialize_pool — page-buffer ring”ring은 log_Gl 내부가 아니라 별도의 전역 변수인 log_Pb
(LOG_PB_GLOBAL_DATA 타입)에 위치한다.
// log_pb_global_data / log_buffer -- src/transaction/log_page_buffer.cstruct log_pb_global_data { LOG_BUFFER *buffers; LOG_PAGE *pages_area; LOG_BUFFER header_buffer; LOG_PAGE *header_page; int num_buffers; LOGPB_PARTIAL_APPEND partial_append; };struct log_buffer { volatile LOG_PAGEID pageid; volatile LOG_PHY_PAGEID phy_pageid; bool dirty; LOG_PAGE *logpage; };LOG_PB_GLOBAL_DATA: buffers (디스크립터 배열), pages_area (num_buffers * LOG_PAGESIZE의 슬랩 하나), header_buffer/header_page (페이지--9 디스크립터 + 배킹 페이지), num_buffers, partial_append (flush에 걸친 record 분할 상태, Ch 6). 페이지별 디스크립터 LOG_BUFFER:
| 필드 | 역할 | 존재 이유 |
|---|---|---|
pageid | 상주 log-sequence 페이지의 논리적 id | NULL_PAGEID = 비어있음; 조회가 이 값을 키로 사용. volatile — lock 없이 읽힘. |
phy_pageid | active-log 파일의 물리적 offset | 변환 캐시로, 각 flush마다 logpb_to_physical_pageid를 건너뜀. |
dirty | 페이지가 디스크와 다름 | 슬롯을 toflush[]에 추가할지 결정. |
logpage | 공유 pages_area 슬랩 내의 포인터 | 작은 디스크립터를 LOG_PAGESIZE payload에서 분리. |
분기 완전 기술 (LOG_CS_OWN_WRITE_MODE assert):
log_append_init_zip(§2.6) — 압축 컨텍스트가 ring보다 먼저 올라옴.logpb_Initialized이면logpb_finalize_pool(재진입 안전), 그 후assert pages_area == NULL.num_buffers = prm_get_integer_value (PRM_ID_LOG_NBUFFERS).malloc buffers.NULL→er_set+ER_OUT_OF_VIRTUAL_MEMORY반환 (해제할 pool 없음).malloc pages_area(num_buffers * LOG_PAGESIZE).NULL→free_and_init(buffers), 반환.- 슬랩을
LOG_PAGE_INIT_VALUE로memset; 루프에서logpb_initialize_log_buffer (&buffers[i], pages_area + i*LOG_PAGESIZE)가 디스크립터 i를 슬랩 슬롯 i에 연결하고,pageid = phy_pageid = NULL_PAGEID,dirty = false, 페이지 header를 (logical_pageid = NULL_PAGEID,offset = NULL_OFFSET,flags = 0)으로 설정. malloc header_page(LOG_PAGESIZE하나);NULL→ 이전 두 할당 모두 해제 후 반환. 페이지-9용 상주 슬롯인header_buffer에 연결 (LOGPB_HEADER_PAGE_ID == -9).logpb_initialize_flush_info(§2.4). 오류 →goto error.partial_append.status = LOGPB_APPENDREC_SUCCESS; 정렬된 스크래치 페이지 포인터 설정.logpb_Initialized = true;pthread_*_init으로 chkpt-lsa lock, group-commit cond/mutex, writer_info cond/mutex 초기화;writer_info->is_init = true.NO_ERROR반환.
error: 레이블은 logpb_finalize_pool 후 logpb_fatal_error (abort) — pool 초기화 실패는 치명적이다. 이는 단순 ER_OUT_OF_VIRTUAL_MEMORY를 전파하는 초기 malloc 반환과 다르다.
불변식 — 디스크립터 배열과 페이지 슬랩은 같은 길이이며 함께 해제된다.
buffers[i].logpage는 항상pages_area + i*LOG_PAGESIZE를 가리킨다.logpb_locate_page는(log_pg - pages_area) / LOG_PAGESIZE로 인덱스를 복원하고 왕복을assert로 검증한다. 두malloc사이에서num_buffers가 달라지면 해당 산술 연산이 범위를 벗어나 접근한다.
flowchart TD
S["init_zip; finalize_pool if re-entrant"] --> N["num_buffers = PRM_ID_LOG_NBUFFERS"]
N --> B{"malloc buffers?"}
B -- no --> E1["return ER_OUT_OF_VIRTUAL_MEMORY"]
B -- yes --> P{"malloc pages_area?"}
P -- no --> E2["free buffers; return"]
P -- yes --> Hp{"malloc header_page?"}
Hp -- no --> E3["free buffers+pages; return"]
Hp -- yes --> Fi{"init_flush_info?"}
Fi -- no --> Err["goto error: finalize_pool; fatal_error"]
Fi -- yes --> Done["init mutexes; Initialized=true; NO_ERROR"]
Figure 2-2 — logpb_initialize_pool의 분기 지도. 모든 할당 실패 경로를 보여준다.
2.4 logpb_initialize_flush_info — dirty 페이지 명단
섹션 제목: “2.4 logpb_initialize_flush_info — dirty 페이지 명단”LOG_FLUSH_INFO (log_Gl.flush_info에 내장)는 flush가 디스크에 밀어내야
할 append 페이지 목록이다.
// log_flush_info -- src/transaction/log_impl.hstruct log_flush_info { int max_toflush; int num_toflush; LOG_PAGE **toflush;#if defined(SERVER_MODE) pthread_mutex_t flush_mutex;#endif};| 필드 | 역할 | 존재 이유 |
|---|---|---|
max_toflush | 용량, num_buffers - 1로 설정 | 슬롯 하나가 예약되어 있어 (header는 별도 flush) 명단이 num_buffers - 1을 초과하지 않음. |
num_toflush | 스테이징된 페이지의 실제 개수 | 여기서 0으로 초기화되고, 각 flush 후에도 0으로 재설정. |
toflush | page id 오름차순으로 정렬된 LOG_PAGE* 배열 | num_buffers 포인터로 calloc; 정렬되어 있어 writev가 연속 I/O를 수행. |
flush_mutex | (SERVER_MODE) 명단 변경을 직렬화 | log-flush thread와 committer 양쪽이 접근. |
logpb_initialize_flush_info: toflush != NULL이면 먼저 logpb_finalize_flush_info를 호출(재진입)한 후 toflush == NULL을 assert; max_toflush = num_buffers - 1, num_toflush = 0 설정; toflush를 num_buffers 포인터로 calloc (여분 슬롯은 무해한 여유분); NULL이면 ER_OUT_OF_VIRTUAL_MEMORY er_set; pthread_mutex_init — 할당 실패 시에도 실행하여 해당 오류 코드를 반환하며, 이를 호출자가 goto error로 처리. logpb_finalize_flush_info는 역으로 처리한다: toflush != NULL이면 lock, free_and_init(toflush), 카운터 초기화, unlock, pthread_mutex_destroy; 이미 NULL이면 no-op (중복 호출 안전).
2.5 prior_lsa_info 생성자 — prior list 시드
섹션 제목: “2.5 prior_lsa_info 생성자 — prior list 시드”LOG_PRIOR_LSA_INFO는 인메모리 prior list의 head다 (호출자의 append 요청과
페이지 버퍼 사이의 staging 영역; Ch 3-5).
// log_prior_lsa_info -- src/transaction/log_append.hppstruct log_prior_lsa_info { LOG_LSA prior_lsa; LOG_LSA prev_lsa; LOG_PRIOR_NODE *prior_list_header; LOG_PRIOR_NODE *prior_list_tail; INT64 list_size; LOG_PRIOR_NODE *prior_flush_list_header; std::mutex prior_lsa_mutex; log_prior_lsa_info (); };| 필드 | 역할 | 존재 이유 |
|---|---|---|
prior_lsa | 다음 append될 노드가 받을 LSA | mutex 아래에서 전진시키면 디스크를 건드리지 않고 LSA를 단조 순서로 발급. |
prev_lsa | 이전에 append된 노드의 LSA | 새 노드가 역방향 체인/undo를 위해 back_lsa를 저장할 수 있게 함. |
prior_list_header / prior_list_tail | FIFO head (drain이 소비) / tail (O(1) append) | drain (Ch 5)이 head를 읽음; 새 노드는 tail에 연결. |
list_size | 큐의 바이트 수 | drainer/flusher가 밀어낼 시점을 결정. |
prior_flush_list_header | flush로 이미 승격된 서브리스트 | ”append됨”과 “flush 중”을 분리. |
prior_lsa_mutex | 서브시스템 전체의 핫 lock | 모든 LSA 할당이 여기서 직렬화. |
생성자는 모든 것을 비어있는 상태로 시드한다. 실제 LSA 시드는
log_initialize_internal로 미뤄지며, 그곳에서 복원된 header LSA를
append와 prior_info 양쪽에 복사한다.
// log_prior_lsa_info ctor / LOG_RESET_*_LSA -- src/transaction/log_append.cpplog_prior_lsa_info::log_prior_lsa_info () // every member: NULL_LSA / NULL / 0 / default mutex : prior_lsa (NULL_LSA), prev_lsa (NULL_LSA), prior_list_header (NULL), prior_list_tail (NULL) , list_size (0), prior_flush_list_header (NULL), prior_lsa_mutex () { }void LOG_RESET_APPEND_LSA (const LOG_LSA *lsa) // header drives prior_lsa{ log_Gl.hdr.append_lsa = *lsa; log_Gl.prior_info.prior_lsa = *lsa; }void LOG_RESET_PREV_LSA (const LOG_LSA *lsa){ log_Gl.append.prev_lsa = *lsa; log_Gl.prior_info.prev_lsa = *lsa; }불변식 — 초기화 종료 시
prior_info.prior_lsa == hdr.append_lsa이고prior_info.prev_lsa == append.prev_lsa.log_initialize_internal은 값이 다르면assert(0)을 발생시키고 재reset한다 (if (!LSA_EQ (&log_Gl.hdr.append_lsa, &log_Gl.prior_info.prior_lsa)) { assert (0); LOG_RESET_APPEND_LSA (...); }와 대칭적인prev_lsa검사). 값이 어긋나면 첫 번째로 append되는 record가 커서가 기록하는 위치와 다른 LSA를 받아 back-chain이 오염된다.
2.6 log_append_init_zip / log_append_final_zip — 압축 컨텍스트
섹션 제목: “2.6 log_append_init_zip / log_append_final_zip — 압축 컨텍스트”LOG_ZIP은 (역)압축 스크래치 버퍼다: struct log_zip { LOG_ZIP_SIZE_T data_length = 0; LOG_ZIP_SIZE_T buf_size = 0; char *log_data = nullptr; }; (log_compress.h).
| 필드 | 역할 | 존재 이유 |
|---|---|---|
data_length | 현재 보유한 바이트 수 | log_zip/log_unzip 후 결과 길이. |
buf_size | log_data의 용량 | log_zip_realloc_if_needed가 늘린다; record마다 re-malloc를 피함. |
log_data | (역)압축 버퍼 | LZ4 출력을 담음; log_zip_alloc(IO_PAGESIZE)로 크기 설정. |
log_append_init_zip은 모드와 PRM_ID_LOG_COMPRESS에 따라 분기한다:
- 압축 비활성화 →
log_Zip_support = false, 반환. SERVER_MODE:log_Zip_support = true; 버퍼는 스레드별로, 첫 사용 시 지연 할당 —log_append_get_zip_undo/_redo가if (thread_p->log_zip_undo == NULL) thread_p->log_zip_undo = log_zip_alloc (IO_PAGESIZE);를 수행.- SA-mode: 프로세스 전역 스태틱
log_zip_undo/log_zip_redo두 개와IO_PAGESIZE * 2의log_data_ptr스크래치를 할당. 어느 것이라도NULL이면 →log_Zip_support = false로 설정하고 할당된 것들을 각각if로 해제. 아닐 경우log_Zip_support = true.
log_append_final_zip은 반대로 처리한다: !log_Zip_support이면 반환; SERVER_MODE에서는 아무것도 안 함(스레드별 버퍼는 스레드 엔트리와 함께 소멸); SA-mode에서는 log_zip_undo/log_zip_redo/log_data_ptr을 해제. logpb_finalize_pool에서 호출되므로(§2.7) zip 해제는 pool 해제에 묶인다.
불변식 —
log_Zip_support가 단일 게이트다. 모든 호출자는 개별 버퍼 포인터가 아닌 이 값으로 분기한다. init은 어떤 부분 할당 실패가 있어도false로 설정하므로 반쪽만 할당된 컨텍스트는 절대 사용되지 않는다.
2.7 Teardown: log_final과 logpb_finalize_pool
섹션 제목: “2.7 Teardown: log_final과 logpb_finalize_pool”log_final은 정상 종료이자 create/init이 앞서 호출하는 재진입 방지 함수다. 분기 완전 기술:
- 서버 daemon과 시스템 트랜잭션 종료;
LOG_CS_ENTER;rcv_phase초기화. trantable.area == NULL→ 초기화되지 않음; 종료.- 아닐 경우
!logpb_is_pool_initialized()→ trantable만 있음;logtb_undefine_trantable, 종료. - 아닐 경우
append.vdes == NULL_VOLDES→ pool은 있지만 volume 없음;logpb_finalize_pool+logtb_undefine_trantable, 종료. - 아닐 경우 모든 활성 트랜잭션을 abort (
log_abort),anyloose_ends추적; 디스크에 flush (logpb_flush_pages_direct+pgbuf_flush_all+fileio_synchronize_all). - Header 분기:
!anyloose_ends && error_code == NO_ERROR이면hdr.is_shutdown = true설정 후chkpt_lsa = append_lsa스냅 (클린 — 재시작이 recovery를 건너뜀). 아닐 경우logpb_checkpoint. logpb_flush_header,logpb_finalize_pool,logtb_undefine_trantable, bg-archive + active volume 언마운트,free_and_init(loghdr_pgptr),LOG_CS_EXIT.
logpb_finalize_pool은 (log_final과 create/init 오류 경로에서) 멱등적이다 — !logpb_Initialized이면 바로 반환한다. 초기화되어 있으면 bring-up의 역순으로 해제한다: append 커서 초기화 (log_pgptr = NULL, nxio_lsa/prev_lsa = NULL_LSA, prior_info에 미러링), buffers/pages_area/header_page를 free_and_init, num_buffers = 0, logpb_Initialized = false, logpb_finalize_flush_info (§2.4), chkpt + group-commit lock 소멸, writer info 정리, 마지막으로 log_append_final_zip (§2.6) — zip을 마지막에 해제하는 것은 init의 zip-먼저 패턴을 반영한다. 이렇게 해야 진행 중인 append(스레드별 LOG_ZIP을 건드리는)가 버퍼보다 오래 살아남지 않는다.
2.8 LOGAREA_SIZE / LOG_PAGESIZE 관계
섹션 제목: “2.8 LOGAREA_SIZE / LOG_PAGESIZE 관계”LOG_PAGE는 LOG_PAGESIZE 바이트다 (storage_common.h의 db_Log_page_size). 첫 SSIZEOF(LOG_HDRPAGE) 바이트는 페이지 header이고, 나머지가 record 영역이다: #define LOGAREA_SIZE (LOG_PAGESIZE - SSIZEOF(LOG_HDRPAGE)) (log_impl.h).
이 상수는 모든 record 배치를 제약한다. append 매크로(LOG_APPEND_ALIGN, LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT)는 append_lsa.offset을 LOGAREA_SIZE와 비교하여 오버플로 시 logpb_next_append_page를 호출한다. LOG_PRIOR_LSA_LAST_APPEND_OFFSET()도 마찬가지로 LOGAREA_SIZE를 반환하므로, prior list와 page-buffer 측이 페이지 끝 위치에 대해 일치한다(페이지 경계 통과는 Chapter 6). 초기화 관점에서 핵심은 이것이다. 기하 구조는 header의 db_logpagesize로 고정되며, 실행 중인 LOG_PAGESIZE와 검증된다.
불변식 —
db_logpagesize는 실행 중인LOG_PAGESIZE와 반드시 같아야 한다. §2.2b 9단계에서 추적했듯,log_initialize_internal은db_iopagesize != IO_PAGESIZE || db_logpagesize != LOG_PAGESIZE를 검사한다. 불일치 시db_set_page_size, pool 종료, 언마운트 후 재귀적으로 자신을 다시 호출하여 올바른 크기로 버퍼를 재할당한다. 그렇지 않으면LOGAREA_SIZE가 잘못된 페이지 크기를 기반으로 계산되어 record가 물리적 페이지 경계를 넘어 걸치게 된다.
2.9 Chapter 요약 — 핵심 정리
섹션 제목: “2.9 Chapter 요약 — 핵심 정리”- 두 진입점, 서로 다른 생존 기간.
log_create_internal은 포맷하고, 페이지-9를 기록하고, pool을 종료한다 (라이브 상태 없음);log_initialize_internal은 마운트하고, 페이지-9를 읽고, pool을 활성 상태로 유지하며, recovery를 실행한다. - 재시작은 더 풍부한 분기 트리를 가진다 (§2.2b): 치명적 log-names 경로, 전역
loghdr_pgptrmalloc,fileio_mount NULL_VOLDES분기 (미디어 충돌 header 합성LOGPAGEID_MAXvsER_IO_MOUNT_FAIL),logpb_fetch_header,restore_slave복사, 허용되는 접두사 불일치, 재귀 페이지 크기 재초기화, recovery/클린 디스패치. - 두 개의 전역 변수.
log_Gl(append/prior/header/flush) vs 별도의 ringlog_Pb;flush_info.toflush[]와append.log_pgptr은log_Pb내부를 가리킨다. - ring은 두 개의 병렬 할당이다 —
LOG_BUFFER[]+pages_area슬랩 하나; 디스크립터 i ↔ 슬랩 슬롯 i, 포인터 산술로 복원. Flush 용량은num_buffers - 1. - prior list는 비어있는 상태로 시작하며, LSA는 header에서 시드된다 —
LOG_RESET_APPEND_LSA/LOG_RESET_PREV_LSA로append와prior_info양쪽에 적용; init이 양쪽의 일치를assert한다. - 압축은 모드에 따라 다르다: SA-mode는 프로세스 전역
LOG_ZIP스태틱, server-mode는 스레드별 지연 할당;log_Zip_support가 단일 게이트이며 부분 실패 시false. - Teardown은 bring-up의 역순이다. flush-info와 zip을 마지막에 해제;
log_final의is_shutdown = true분기가 다음 boot에서 recovery를 건너뛸 수 있게 한다.
Chapter 3: 호출자 요청으로부터 Prior Node 만들기
섹션 제목: “Chapter 3: 호출자 요청으로부터 Prior Node 만들기”엔진이 페이지를 수정할 때는 반드시 log_append_* API를 호출한다. 그 변경 내용이 디스크에 닿으려면 먼저 prior node가 되어야 한다. Prior node란 완전히 구성된 log record를 담는 힙 할당 구조체 LOG_PRIOR_NODE를 말한다. 이 챕터가 답하는 질문은 이것이다: 호출자 튜플 (rcvindex, addr, undo_data, redo_data)가 주어졌을 때, LSA를 받기 전에 완전한 LOG_PRIOR_NODE는 어떻게 만들어지는가?
이 단계의 핵심 성질은 하나다. prior-list mutex 바깥에서 전부 실행된다 — 할당, 헤더 크기 결정, 페이로드 복사, 압축이 모두 호출자의 메모리 위에서 이루어진다. node가 완성된 다음에야 Chapter 4의 prior_lsa_next_record가 prior_lsa_mutex를 잡고, LSA를 찍고, node를 list에 연결한다 (함께 읽어야 할 내용: single-writer pipeline).
3.1 append API 표면 — crumb 위에 얹힌 얇은 래퍼
섹션 제목: “3.1 append API 표면 — crumb 위에 얹힌 얇은 래퍼”진입점인 log_append_undoredo_data, log_append_undo_data, log_append_redo_data, 그리고 *2 / *_recdes 변형들은 모두 호출자의 연속 버퍼 하나를 LOG_CRUMB 하나로 포장하고 crumbs API에 위임한다.
// log_append_undoredo_data -- src/transaction/log_manager.cLOG_CRUMB undo_crumb, redo_crumb;assert (0 == undo_length || undo_data != NULL); /* <- zero length must mean NULL data */undo_crumb.data = undo_data; undo_crumb.length = undo_length; // ... redo_crumb the same ...log_append_undoredo_crumbs (thread_p, rcvindex, addr, 1, 1, &undo_crumb, &redo_crumb);// inside log_append_undoredo_crumbs: type from rcvindex alone:LOG_RECTYPE rectype = LOG_IS_MVCC_OPERATION (rcvindex) ? LOG_MVCC_UNDOREDO_DATA : LOG_UNDOREDO_DATA;LOG_CRUMB는 (length, data) 쌍이다. *2 변형은 (vfid, pgptr, offset)으로 LOG_DATA_ADDR을 합성하고, _recdes 변형은 RECDES를 감싼다. LOG_IS_MVCC_OPERATION은 MVCC heap/btree 연산과 RVES_NOTIFY_VACUUM에 대해 참이며, undo-only 경로는 LOG_(MVCC_)UNDO_DATA를, redo-only 경로는 *REDO*를 선택한다. 이 rectype은 이후 모든 크기 결정의 분기 키가 된다. node를 구성하기 전에 log_append_*_crumbs는 다섯 단계의 guard chain을 실행한다(Figure 3-1). 각 guard는 독립적인 early return이므로, 다섯 가지를 모두 통과한 뒤에야 node 구성에 들어간다.
flowchart TB
B{"log_No_logging?"} -- yes --> B1["log_skip_logging; return"]
B -- no --> D{"LOG_FIND_TDES == NULL?"}
D -- yes --> D1["ER_LOG_UNKNOWN_TRANINDEX; return"]
D -- no --> E{"not sysop AND not active AND not aborted?"}
E -- yes --> E1["return, log nothing"]
E -- no --> F{"log_can_skip_undo_logging?"}
F -- yes --> F1["append redo crumbs only; return"]
F -- no --> G["prior_lsa_alloc_and_copy_crumbs"]
G --> H{"node == NULL?"} -- yes --> H1["return"]
H -- no --> I["TDE encrypt; prior_lsa_next_record (Ch 4)"]
Figure 3-1 — log_append_undoredo_crumbs의 guard chain. undo를 건너뛸 수 있으면 redo-only append로 축소된다. log_append_undo_crumbs는 조용히 건너뛰고(redo fallback 없음), log_append_redo_crumbs는 log_can_skip_redo_logging을 사용한다.
3.2 LOG_PRIOR_NODE — 구성의 목표
섹션 제목: “3.2 LOG_PRIOR_NODE — 구성의 목표”// struct log_prior_node -- src/transaction/log_append.hppstruct log_prior_node { LOG_RECORD_HEADER log_header; LOG_LSA start_lsa; /* for assertion */ bool tde_encrypted; int data_header_length; char *data_header; int ulength; char *udata; int rlength; char *rdata; LOG_PRIOR_NODE *next;};| 필드 | 역할 | 존재 이유 |
|---|---|---|
log_header | .type만 여기서 설정된다. | record 식별자 / 분기 키. LSA 링크는 Chapter 4에서 mutex 아래 채워진다. |
start_lsa | 최종 LSA. 여기서는 미설정 — /* for assertion */. | prior_lsa_next_record(Chapter 4)가 할당한다. MVCC vacuum-header assertion에서만 읽힌다. node는 아직 log 위치가 없으므로 구성 도중 읽는 것은 버그다. |
tde_encrypted | log page에 TDE 암호화가 필요한지 여부. | 할당 시 false, prior_set_tde_encrypted가 올린다. page 경계 암호화(Chapter 6)를 주도한다. |
data_header_length / data_header | 바이트 크기 + 채워진 LOG_REC_*를 담는 별도 malloc. | rectype에서 sizeof(LOG_REC_*)로 결정된다. 별도 버퍼이므로 drain(Chapter 5)이 헤더와 데이터를 독립적으로 복사할 수 있다. |
ulength / udata | 저장된 undo 길이(상위 비트 = 압축됨) + undo 바이트의 힙 복사본. | node는 페이로드를 소유해야 한다. 호출자 버퍼는 반환 후 해제될 수 있다. drain은 정확히 ulength 바이트만 복사한다. |
rlength / rdata | 위와 동일, redo용. | redo 페이로드 소유권과 길이. |
next | 리스트 포인터. | 여기서는 NULL. Chapter 4가 prior_list_tail에 추가될 때 설정한다. |
완성된 node는 세 개의 독립적인 malloc으로 구성된다 — node 자체, data_header(LOG_REC_UNDOREDO 또는 LOG_REC_MVCC_UNDOREDO), 그리고 각 페이로드 복사본. 이로써 node는 완전히 자체 소유가 된다. next, start_lsa, log_header LSA 링크는 Chapter 4까지 비어 있다.
불변식 — node는 페이로드를 값으로 소유한다. udata/rdata는 항상 새로 malloc된 복사본이며(memcpy 항상 수행됨), 호출자 버퍼의 별칭이 아니다. 이 조건이 깨지면 Chapter 5의 비동기 drain이 해제된 메모리를 읽게 된다.
3.3 할당 dispatch — prior_lsa_alloc_and_copy_crumbs
섹션 제목: “3.3 할당 dispatch — prior_lsa_alloc_and_copy_crumbs”prior_lsa_alloc_and_copy_crumbs는 node를 malloc하고, 모든 구성 필드를 0으로 초기화하고, log_header.type을 설정한 뒤 dispatch한다.
// prior_lsa_alloc_and_copy_crumbs -- src/transaction/log_append.cppnode->log_header.type = rec_type; node->tde_encrypted = false; /* ... all payload fields zeroed ... */switch (rec_type) { case LOG_UNDOREDO_DATA: ... case LOG_MVCC_REDO_DATA: /* all 8 undo/redo families */ error = prior_lsa_gen_undoredo_record_from_crumbs (thread_p, node, rcvindex, addr, ...); break; default: assert_release (false); error = ER_FAILED; break; /* <- crumbs path is undo/redo only */}오류가 발생하면 data_header, udata, rdata, 그리고 node 자체를 순서대로 해제하고 NULL을 반환한다. 호출자(§3.1)는 NULL을 “조용히 포기”로 처리한다.
형제 함수 prior_lsa_alloc_and_copy_data는 non-crumb 계열(postpone, compensate, commit, sysop, 2PC)을 처리한다. 이 함수의 switch는 undo/redo 케이스를 assert_release(false)로 라우팅하고 나머지는 prior_lsa_gen_record, prior_lsa_gen_postpone_record 등으로 보낸다. 즉, 두 allocator가 타입 공간을 분할한다: undo/redo 데이터에는 crumbs, 제어 record에는 plain copy.
prior_lsa_gen_record는 Chapter 8–10에서 commit/abort/sysop node에 쓰이는 plain-copy builder다. 이 함수는 압축도, MVCC 스탬핑도 하지 않는다 — 크기를 결정하고, 할당하고, 선택적 undo blob을 복사할 뿐이며, 헤더 내용은 호출자가 채운다. 세 분기는 다음과 같다.
| 분기 | 효과 |
|---|---|
switch (rec_type) | Dummy/decision record(LOG_DUMMY_HEAD_POSTPONE, LOG_2PC_*_DECISION, LOG_START_CHKPT, LOG_SYSOP_ATOMIC_START)는 length==0 && data==NULL을 assert하고 data_header_length == 0을 유지한다. 제어 record는 data_header_length = sizeof(LOG_REC_*)를 설정한다(예: LOG_COMMIT/LOG_ABORT → LOG_REC_DONETIME, LOG_SYSOP_END → LOG_REC_SYSOP_END). default는 0으로 둔다. |
if (data_header_length > 0) | 헤더를 malloc한다(디버그 빌드에서는 memset). 실패 시 ER_OUT_OF_VIRTUAL_MEMORY를 발생시키고 즉시 반환한다 — udata 복사는 시도하지 않는다. |
if (length > 0) | 선택적 undo blob을 prior_lsa_copy_undo_data_to_node로 복사하며, 해당 함수의 오류 코드를 전파한다. 그렇지 않으면 NO_ERROR를 반환한다. |
3.4 prior_lsa_gen_undoredo_record_from_crumbs — 핵심 builder
섹션 제목: “3.4 prior_lsa_gen_undoredo_record_from_crumbs — 핵심 builder”이 builder는 네 단계로 실행된다(Figure 3-2). crumb 길이를 합산하고, 각 side의 zip scratch(log_append_get_zip_undo/_redo)를 가져온 뒤 타입별 플래그를 설정한다. LOG_IS_UNDOREDO_RECORD_TYPE이면 has_undo와 has_redo 모두 설정하고 두 scratch(또는 길이가 0인 side)가 필요하다. LOG_IS_REDO_RECORD_TYPE이면 has_redo와 zip_redo가 필요하고, 그 외에는 UNDO에 zip_undo가 필요하다 — 이 모두는 log_Zip_support와 &&로 연결되어 can_zip을 결정한다.
그런 다음 (선택적으로) 압축하고, 타입별 헤더를 크기 결정 및 malloc하고, 로컬 포인터를 하위 필드에 조준하고, 공유 LOG_DATA를 채우고, 페이로드를 복사한다. 포인터 조준은 fall-through switch로 수행된다. 각 MVCC arm은 추가적인 mvccid_p/vacuum_info_p를 잡은 뒤 [[fallthrough]]로 non-MVCC arm에 진입하여 공유 length/data 포인터를 설정한다 — UNDO는 ulength_p만, REDO는 rlength_p만, UNDOREDO는 둘 다 설정한다.
// prior_lsa_gen_undoredo_record_from_crumbs -- src/transaction/log_append.cppcase LOG_MVCC_UNDOREDO_DATA: case LOG_MVCC_DIFF_UNDOREDO_DATA: /* MVCC arm: extra ptrs, then fall through */ vacuum_info_p = &mvcc_undoredo_p->vacuum_info; mvccid_p = &mvcc_undoredo_p->mvccid; [[fallthrough]];case LOG_UNDOREDO_DATA: case LOG_DIFF_UNDOREDO_DATA: /* shared: aim both length ptrs + log_data_p */ data_header_ulength_p = &undoredo_p->ulength; ... log_data_p = &undoredo_p->data; break;공유 LOG_DATA는 addr에서 채워진다: rcvindex, offset, 그리고 pgbuf_get_vpid_ptr을 통한 (pageid, volid) — addr->pgptr == NULL(논리 logging)이면 NULL_PAGEID/NULL_VOLID.
flowchart TB
M["Phase 1: 길이 합산, zip scratch 획득, has_undo/has_redo/can_zip 계산"] --> Z{"can_zip AND\n어느 side >= 임계값?"}
Z -- yes --> ZB["Phase 2: log_diff + log_zip; 둘 다 압축되면 타입을 *_DIFF_*로 변경"]
Z -- no --> HSZ["Phase 3a: 타입별 헤더 크기 결정"]
ZB --> HSZ
HSZ --> MAL{"malloc data_header OK?"}
MAL -- no --> ERR["ER_OUT_OF_VIRTUAL_MEMORY; goto error"]
MAL -- yes --> PTR["Phase 3b: fall-through switch로 포인터 조준, LOG_DATA 채우기, MVCCID/vacuum 스탬핑"]
PTR --> CP["Phase 4: udata/rdata 복사 (압축 또는 raw)"]
CP --> RET["return NO_ERROR"]
ERR --> RETE["return error_code"]
Figure 3-2 — prior_lsa_gen_undoredo_record_from_crumbs의 제어 흐름. 모든 분기는 return NO_ERROR 또는 error: 레이블에 도달하며, error: 레이블은 data_header/udata/rdata를 해제한다.
3.5 압축 분기 — 경계는 페이지가 아닌 node
섹션 제목: “3.5 압축 분기 — 경계는 페이지가 아닌 node”CUBRID는 record 단위(prior node 단위)로 압축하며, log page 단위로는 압축하지 않는다. 그래서 압축은 LSA나 page가 결정되기 전, 구성 단계에서 이루어진다. 압축된 바이트는 ulength/rlength에 크기가 기록되고 node에 복사되므로 Chapter 6의 page 경계 로직은 미압축 데이터를 볼 일이 없다. 두 개의 전역 변수가 이를 제어하고, scratch는 side별 LOG_ZIP이다.
// src/transaction/log_append.cpp ; src/transaction/log_compress.hbool log_Zip_support = false; /* <- master toggle, from prm */int log_Zip_min_size_to_compress = 255; /* <- per-side threshold (bytes) */struct log_zip { LOG_ZIP_SIZE_T data_length = 0; LOG_ZIP_SIZE_T buf_size = 0; char *log_data = nullptr; };LOG_ZIP은 결과 하나를 세 필드에 담는다: log_data는 출력 버퍼(prior_lsa_copy_*_data_to_node가 여기서 memcpy), data_length는 생성된 길이(MAKE_ZIP_LEN이 헤더에 인코딩하는 값; 크기가 줄지 않으면 raw), buf_size는 log_zip_alloc이 설정한 용량(IO_PAGESIZE + LZ4 bound)이어서 record마다 재할당하지 않아도 된다. Scratch는 log_append_get_zip_undo/_redo를 통해 가져온다: SERVER_MODE에서는 per-thread로 thread_p->log_zip_undo에 위치하며 lazily log_zip_alloc된다. 독립 실행 환경에서는 file-static 싱글톤이다. thread_p가 NULL이고 thread_get_thread_entry_info로도 해결이 안 되면 getter는 NULL을 반환하고, zip_* != NULL 조건에 의해 해당 side의 can_zip이 false가 된다.
압축 블록과 길이를 기록하는 복사는 하나의 단위로 실행된다.
// prior_lsa_gen_undoredo_record_from_crumbs -- src/transaction/log_append.cppif (can_zip && (ulength >= log_Zip_min_size_to_compress || rlength >= log_Zip_min_size_to_compress)) { if (ulength >= thr && rlength >= thr) { (void) log_diff (ulength, undo_data, rlength, redo_data); /* <- redo diffed against undo */ is_undo_zip = log_zip (zip_undo, ulength, undo_data); is_redo_zip = log_zip (zip_redo, rlength, redo_data); if (is_redo_zip) is_diff = true; } else { if (ulength >= thr) is_undo_zip = log_zip (zip_undo, ulength, undo_data); if (rlength >= thr) is_redo_zip = log_zip (zip_redo, rlength, redo_data); }}if (is_diff) node->log_header.type = is_mvcc_op ? LOG_MVCC_DIFF_UNDOREDO_DATA : LOG_DIFF_UNDOREDO_DATA;// ... after header sized/aimed, undo arm (redo symmetric): ...if (is_undo_zip) { *data_header_ulength_p = MAKE_ZIP_LEN (zip_undo->data_length); /* <- sets 0x80000000 */ error_code = prior_lsa_copy_undo_data_to_node (node, zip_undo->data_length, (char *) zip_undo->log_data);} else if (has_undo) { *data_header_ulength_p = ulength; error_code = prior_lsa_copy_undo_crumbs_to_node (node, num_ucrumbs, ucrumbs); }결과는 네 가지다: 어느 side도 임계값을 넘지 못하면 건너뜀; 둘 다 크면 log_diff가 redo를 undo의 차분으로 재작성하고 둘 다 압축되어 redo가 압축됐으면 타입이 *_DIFF_*로 바뀜; 하나만 크면 그 side만 압축되고 diff 없음; log_zip이 false를 반환하면 raw로 복사됨. MAKE_ZIP_LEN(len)은 len | 0x80000000이며, recovery는 GET_ZIP_LEN/ZIP_CHECK로 이를 벗겨낸다.
불변식 — 헤더 길이가 압축 상태를 인코딩한다. 어느 side가 압축됐는지는 헤더 length 필드의 부호 비트에만 기록된다. MAKE_ZIP_LEN 없이 압축된 페이로드를 쓰면 recovery에 압축된 바이트가 그대로 전달되어 page가 손상된다. 같은 arm에서 is_*_zip과 MAKE_ZIP_LEN을 함께 처리함으로써 둘이 서로 어긋나는 일이 없도록 보장한다.
복사 함수 prior_lsa_copy_undo_data_to_node(_redo_ 버전도 동일)는 length 바이트를 malloc하고(length <= 0 || data == NULL이면 NO_ERROR, 실패 시 ER_OUT_OF_VIRTUAL_MEMORY), memcpy하고, node->ulength를 설정한다. crumb copier는 한 번 malloc한 뒤 각 crumb를 memcpy한다. 어느 쪽이든 node->ulength/rlength에는 저장된 길이가 담긴다.
3.6 MVCC 식별자 스탬핑
섹션 제목: “3.6 MVCC 식별자 스탬핑”MVCC 타입의 경우, 포인터 switch에서 mvccid_p/vacuum_info_p가 NULL이 아닌 채로 남겨지므로 두 가지 추가 작업이 실행된다. MVCCID는 현재 TDES에서 가져오며, 가장 안쪽 sub-transaction id를 우선한다.
// prior_lsa_gen_undoredo_record_from_crumbs -- src/transaction/log_append.cppif (mvccid_p != NULL) { tdes = LOG_FIND_CURRENT_TDES (thread_p); if (tdes == NULL || !MVCCID_IS_VALID (tdes->mvccinfo.id)) { assert_release (false); error_code = ER_FAILED; goto error; /* <- MVCC op needs an MVCCID */ } else if (!tdes->mvccinfo.sub_ids.empty ()) *mvccid_p = tdes->mvccinfo.sub_ids.back (); /* nested sysop */ else *mvccid_p = tdes->mvccinfo.id;}vacuum_info_p에는 파일 id(addr->vfid)가 들어간다. RVES_NOTIFY_VACUUM이면 NULL이고, 그 외에는 assert_release(false)다. prev_mvcc_op_log_lsa는 여기서 NULL로 설정된다 — 이 값은 Chapter 4의 prior_lsa_next_record_internal에서 완성되는데, LSA가 결정된 뒤에야 해당 record를 vacuum chain에 연결할 수 있기 때문이다. 이 두 필드와 start_lsa가, 호출자 튜플이 아니라 transaction/log 상태에 의존하는 유일한 필드들이다. 두 record 레이아웃은 다음과 같다.
// struct log_rec_undoredo / log_rec_mvcc_undoredo -- src/transaction/log_record.hppstruct log_rec_undoredo { LOG_DATA data; int ulength; int rlength; };struct log_rec_mvcc_undoredo { LOG_REC_UNDOREDO undoredo; MVCCID mvccid; LOG_VACUUM_INFO vacuum_info; };각 필드의 역할은 이렇다: data(LOG_DATA 트리플 rcvindex/pageid/offset과 volid)는 recovery가 dispatch하고 바이트를 찾는 기준이다. ulength/rlength는 저장된 길이(상위 비트 = 압축됨)다. MVCC 변형은 undoredo를 임베드하여 non-MVCC reader가 코드를 공유하게 하고, 추가로 mvccid(writer의 id, vacuum과 가시성 판단용)와 vacuum_info(prev_mvcc_op_log_lsa 역방향 링크 + 소유 vfid; 역방향 링크는 Chapter 4에서 채워진다)를 더한다.
3.7 Chapter 요약 — 핵심 정리
섹션 제목: “3.7 Chapter 요약 — 핵심 정리”- 공개
log_append_*API는 얇다 — 버퍼를LOG_CRUMB으로 감싸고log_append_*_crumbs에 위임하며, 해당 함수가rcvindex에서LOG_RECTYPE을 결정하고 다섯 단계의 guard chain을 먼저 실행한다. - 구성은 lock-free이고 자체 소유적이다 — 모든 작업이
prior_lsa_mutex바깥에서 이루어지며, node는 세 개의 malloc(node,data_header, 페이로드 복사본)을 소유하므로 비동기 drain이 절대로 호출자 메모리를 건드리지 않는다. - 두 allocator가 타입 공간을 분할한다 —
..._crumbs→prior_lsa_gen_undoredo_record_from_crumbs(undo/redo 데이터용);..._data→prior_lsa_gen_record(제어 record용). 후자의 세 분기는 헤더 크기를 결정하고(dummy/decision 타입은 0),ER_OUT_OF_VIRTUAL_MEMORY로 bail하며, 선택적 undo blob을 복사한다. - 핵심 builder는 측정 → 압축 → 타입별 헤더 크기+채우기 → 페이로드 복사 순서로 실행된다.
[[fallthrough]]switch가 UNDO/REDO/UNDOREDO 형태에 걸쳐 non-MVCC 레이아웃을 공유한다. - 압축은 페이지가 아닌 node 단위다 —
log_Zip_support와 255바이트 임계값으로 제어되며 per-threadLOG_ZIPscratch를 사용한다(NULL thread_p이면 압축 없음). 양쪽이 모두 크면log_diff가 발동하고 타입이*_DIFF_*로 바뀔 수 있다. 압축/raw 선택은MAKE_ZIP_LEN을 통해 길이의 최상위 비트에만 기록된다. - MVCC record는 TDES에서 MVCCID + vacuum info를 가져온다(sub-id 우선).
prev_mvcc_op_log_lsa와start_lsa는 Chapter 4에서 LSA가 할당될 때까지 NULL/빈 상태로 남는다 — 구성 도중start_lsa를 읽는 것은 버그다.
Chapter 4: LSA 할당과 Prior List 부착
섹션 제목: “Chapter 4: LSA 할당과 Prior List 부착”Chapter 3에서 우리는 페이로드가 채워진 완전한 LOG_PRIOR_NODE를 손에 쥐고 있지만, log에서의 위치는 아직 알 수 없다. 이 챕터는 node에 LSA를 할당하고, 짧은 mutex 보호 임계 구역 안에서 prior-list tail에 연결하는 과정을 다룬다. CUBRID가 record를 in-memory prior list에 단계적으로 쌓는 이유는 cubrid-log-manager.md의 “prior list” 절을 참고한다. 그 효과는 다음과 같다.
불변식 4-A (LSA 순서 = mutex 획득 순서). 엔진이 발급하는 모든 LSA는 단조 증가하며, 두 thread가 LSA를 받는 순서는 정확히
prior_info.prior_lsa_mutex를 획득한 순서와 같다. Mutex는 O(1) 분량의 포인터/offset 업데이트를 위해서만 유지되며 — I/O도, 할당도 없다.
4.1 여기에 등장하는 구조체들
섹션 제목: “4.1 여기에 등장하는 구조체들”세 구조체가 만난다: node(Chapter 1; 이 챕터에서 쓰는 필드만), 전역 커서, 그리고 내장된 on-disk record 헤더.
log_prior_node (이 챕터에서 쓰는 필드)
섹션 제목: “log_prior_node (이 챕터에서 쓰는 필드)”| 필드 | 역할 | 존재 이유 |
|---|---|---|
log_header | LOG_RECORD_HEADER — page에서 record 앞에 물리적으로 위치하는 바이트들 | recovery가 순회하는 네 개의 linkage LSA + trid + type을 담는다 |
start_lsa | 이 node에 할당된 LSA | 호출자에게 record의 식별자로 반환됨. node가 drain될 때 교차 검증된다 |
tde_encrypted | 이 node를 담는 page에 TDE 암호화가 필요한지 여부 | prior_set_tde_encrypted로 설정됨. page 할당/flush 시 읽힌다 |
data_header_length | data_header의 바이트 길이 | data-header 영역의 offset 전진량을 결정한다 |
data_header | 타입별 record 헤더(예: LOG_REC_SYSOP_END) | 매칭된 type arm에서 캐스트하여 MVCC/sysop 하위 필드를 읽는다 |
ulength / udata | undo 페이로드 길이 / 버퍼 | ulength>0이면 undo 데이터 영역의 offset 전진이 발동된다 |
rlength / rdata | redo 페이로드 길이 / 버퍼 | rlength>0이면 redo 데이터 영역의 offset 전진이 발동된다 |
next | 다음 prior node로 이어지는 단방향 포인터 | 이 node가 새 tail이 될 때 설정된다 |
log_rec_header (LOG_RECORD_HEADER) — 전체 필드
섹션 제목: “log_rec_header (LOG_RECORD_HEADER) — 전체 필드”물리적 record 헤더이며, prior_lsa_start_append/prior_lsa_end_append의 존재 이유는 거의 전적으로 이 헤더를 채우는 것이다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
prev_tranlsa | 같은 transaction의 이전 log record | 전체 log를 스캔하지 않고 한 transaction의 record를 역방향으로 순회할 수 있게 한다(undo/rollback) |
back_lsa | 이전 물리적 record(어느 transaction이든) | recovery가 전역 log를 역방향으로 순회할 수 있게 한다 |
forw_lsa | 다음 물리적 record | analysis/redo가 순방향으로 순회할 수 있게 한다. 이 record의 크기가 확정된 후에야 알 수 있으므로 prior_lsa_end_append에서 채워진다 |
trid | 이 record를 소유하는 transaction id | recovery가 transaction별로 record를 그룹화한다. tdes->trid에서 설정됨 |
type | LOG_RECTYPE(예: LOG_COMMIT, LOG_SYSOP_END) | prior_lsa_next_record_internal의 모든 타입별 분기에서 dispatch 키로 사용된다 |
log_prior_lsa_info (전역 커서; log_Gl.prior_info) — 전체 필드
섹션 제목: “log_prior_lsa_info (전역 커서; log_Gl.prior_info) — 전체 필드”| 필드 | 역할 | 존재 이유 |
|---|---|---|
prior_lsa | 다음에 할당할 LSA — 이동하는 커서 | 모든 node가 이 값을 start_lsa로 복사한다. offset 헬퍼가 node의 바이트를 반영하면서 전진시킨다 |
prev_lsa | prior stream에 마지막으로 추가된 record의 LSA | 새 node의 back_lsa가 되고, 이후 새 node를 가리키도록 업데이트된다 |
prior_list_header | 단방향 prior list의 head | drain 측(Chapter 5)이 head에서 소비한다 |
prior_list_tail | prior list의 tail | 새 node가 O(1)로 여기 연결된다 |
list_size | 아직 flush되지 않고 단계적으로 쌓인 바이트 수 | logpb_get_memsize()와 비교하여 강제 flush 여부를 결정한다 |
prior_flush_list_header | drain 중인 분리된 리스트의 head | 리스트가 drain을 위해 분리될 때 설정된다(Chapter 5) |
prior_lsa_mutex | 전체 할당을 직렬화하는 std::mutex | 획득 순서가 LSA 순서를 정의하는 단일 lock(불변식 4-A) |
flowchart LR
subgraph G["log_Gl.prior_info (LOG_PRIOR_LSA_INFO)"]
PL["prior_lsa<br/>(next LSA cursor)"]
PV["prev_lsa<br/>(last record)"]
H["prior_list_header"]
T["prior_list_tail"]
M["prior_lsa_mutex"]
end
N["new LOG_PRIOR_NODE<br/>start_lsa, log_header, next"]
PL -- "copied into" --> N
PV -- "copied into log_header.back_lsa" --> N
T -- "->next = node, then tail = node" --> N
M -. "guards all of the above" .- G
Figure 4-1. 커서가 node에 식별자와 linkage를 제공하고, 그 후 node를 새 tail로 받아들인다.
4.2 진입점: with_lock과 LOG_PRIOR_LSA_LOCK enum
섹션 제목: “4.2 진입점: with_lock과 LOG_PRIOR_LSA_LOCK enum”공개 진입점은 두 개이고, 공유 본체는 하나다. 차이는 호출자가 이미 prior_lsa_mutex를 쥐고 있는지 여부뿐이다.
// prior_lsa_next_record / _with_lock -- src/transaction/log_append.cppprior_lsa_next_record (THREAD_ENTRY *thread_p, LOG_PRIOR_NODE *node, log_tdes *tdes){ return prior_lsa_next_record_internal (thread_p, node, tdes, LOG_PRIOR_LSA_WITHOUT_LOCK); }
prior_lsa_next_record_with_lock (THREAD_ENTRY *thread_p, LOG_PRIOR_NODE *node, log_tdes *tdes){ return prior_lsa_next_record_internal (thread_p, node, tdes, LOG_PRIOR_LSA_WITH_LOCK); }with_lock 인자는 아래 두 값 중 하나다(enum에 소스 주석이 없으며, 아래 주석은 설명을 위한 것이다).
// LOG_PRIOR_LSA_LOCK -- src/transaction/log_append.hppenum LOG_PRIOR_LSA_LOCK{ LOG_PRIOR_LSA_WITHOUT_LOCK = 0, // internal locks/unlocks the mutex itself LOG_PRIOR_LSA_WITH_LOCK = 1 // caller already holds the mutex};_with_lock 변형은 호출자가 여러 record를 인터리빙 없이 방출할 수 있게 해준다. mutex를 한 번 잡고, _with_lock을 반복 호출하면 된다. 일반 변형은 단일 record의 일반적인 경로다.
4.3 prior_lsa_next_record_internal — 모든 분기를 따라가는 분석
섹션 제목: “4.3 prior_lsa_next_record_internal — 모든 분기를 따라가는 분석”본체는 세 단계로 이루어진다: lock + prior_lsa_start_append(4.4); 6개 arm의 타입 dispatch 사다리(아래 표); offset 전진 + prior_lsa_end_append(4.5) + tail 연결 + 조건부 unlock-and-flush. 프레임과 tail 연결 코드를 원문 그대로 인용한다(두 연결 arm 모두 연속 할당이 아닌 두 개 문장임에 주목하라).
// prior_lsa_next_record_internal -- src/transaction/log_append.cpp if (with_lock == LOG_PRIOR_LSA_WITHOUT_LOCK) { log_Gl.prior_info.prior_lsa_mutex.lock (); } prior_lsa_start_append (thread_p, node, tdes); // <- start_lsa + 헤더 linkage 할당 (4.4) LSA_COPY (&start_lsa, &node->start_lsa); // <- 전진 전에 스냅샷 // ... vacuum-produce guard + 6-arm type dispatch ladder (tables below) ... log_prior_lsa_append_advance_when_doesnot_fit (node->data_header_length); log_prior_lsa_append_add_align (node->data_header_length); if (node->ulength > 0) { prior_lsa_append_data (node->ulength); } if (node->rlength > 0) { prior_lsa_append_data (node->rlength); } prior_lsa_end_append (thread_p, node); // <- forw_lsa 확정 (4.5)
if (log_Gl.prior_info.prior_list_tail == NULL) { log_Gl.prior_info.prior_list_header = node; // <- 빈 리스트: node가 head ... log_Gl.prior_info.prior_list_tail = node; // <- ... 이자 tail } else { log_Gl.prior_info.prior_list_tail->next = node; // <- O(1) tail 연결 (두 문장) log_Gl.prior_info.prior_list_tail = node; } log_Gl.prior_info.list_size += (sizeof (LOG_PRIOR_NODE) + node->data_header_length + node->ulength + node->rlength); if (with_lock == LOG_PRIOR_LSA_WITHOUT_LOCK) { log_Gl.prior_info.prior_lsa_mutex.unlock (); // <- flush 결정 전에 해제 // ... condensed: if list_size >= logpb_get_memsize() -> force-flush fork (see callout) ... } tdes->num_log_records_written++; return start_lsa;사다리 앞에는 vacuum-produce guard가 있다. LOG_ISRESTARTED()이고 log_Gl.hdr.does_block_need_vacuum인 상태에서 start_lsa가 mvcc_op_log_lsa 대비 새 vacuum block id로 넘어갔으면 vacuum_produce_log_block_data를 호출한다(이전 block id가 정확히 하나 뒤라고 assert). 크래시 복구 중에는 완전히 건너뛴다.
6개 arm의 타입 dispatch 사다리. node->log_header.type에 대한 상호 배타적 if/else if 체인이다. 모든 할당은 방금 스냅샷된 start_lsa와 함께 mutex 아래서 이루어진다 — 이것이 캡처된 LSA가 일관성을 유지하는 이유다(Chapter 8–9).
| # | 매칭되는 type | 조건 | 동작 |
|---|---|---|---|
| 1 | LOG_MVCC_UNDO_DATA, LOG_MVCC_UNDOREDO_DATA, LOG_MVCC_DIFF_UNDOREDO_DATA, 또는 (LOG_SYSOP_END && ((LOG_REC_SYSOP_END *)data_header)->type == LOG_SYSOP_END_LOGICAL_MVCC_UNDO) | — | 중첩 sub-branch로 vacuum_info/mvccid 해석; vacuum_info->prev_mvcc_op_log_lsa = log_Gl.hdr.mvcc_op_log_lsa; prior_update_header_mvcc_info(start_lsa, mvccid) 호출(4.6) |
| 2 | LOG_SYSOP_START_POSTPONE | assert (LSA_ISNULL (rcv.sysop_start_postpone_lsa)) | rcv.sysop_start_postpone_lsa = start_lsa; lastparent_lsa < rcv.atomic_sysop_start_lsa이면 null 처리; tdes->state = TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE(checkpoint 정확성을 위해 mutex 아래서) |
| 3 | LOG_SYSOP_END | — | atomic_sysop_start_lsa가 non-null이고 lastparent_lsa < 이면 null 처리; sysop_start_postpone_lsa에도 동일한 검사/null 처리 |
| 4 | LOG_COMMIT_WITH_POSTPONE 또는 LOG_COMMIT_WITH_POSTPONE_OBSOLETE | — | rcv.tran_start_postpone_lsa = start_lsa |
| 5 | LOG_SYSOP_ATOMIC_START | assert (LSA_ISNULL (rcv.atomic_sysop_start_lsa)) | rcv.atomic_sysop_start_lsa = start_lsa |
| 6 | LOG_COMMIT 또는 LOG_ABORT | assert (commit_abort_lsa.is_null ()) | commit_abort_lsa = start_lsa |
중첩 3-way MVCC sub-branch(arm 1 내부) — vacuum_info/mvccid를 담는 구조체를 선택한다.
| Sub-arm | 조건 | vacuum_info / mvccid 출처 |
|---|---|---|
| a | type == LOG_MVCC_UNDO_DATA | (LOG_REC_MVCC_UNDO *) node->data_header → &mvcc_undo->vacuum_info, mvcc_undo->mvccid |
| b | type == LOG_SYSOP_END | &((LOG_REC_SYSOP_END *) node->data_header)->mvcc_undo → &mvcc_undo->vacuum_info, mvcc_undo->mvccid |
| c | 그 외(LOG_MVCC_UNDOREDO_DATA / LOG_MVCC_DIFF_UNDOREDO_DATA, assert됨) | (LOG_REC_MVCC_UNDOREDO *) node->data_header → &mvcc_undoredo->vacuum_info, mvcc_undoredo->mvccid |
arm 1–6 중 어느 것도 매칭되지 않으면(일반적인 데이터 record의 경우) 사다리는 no-op이 되고 제어는 바로 offset 전진으로 넘어간다.
연결: Unlock-then-flush 분기.
WITHOUT_LOCK만 여기서 unlock하며,list_size >= logpb_get_memsize()검사는 mutex 바깥에서 이루어진다.SERVER_MODE에서는 크래시 복구 중이 아니면 flush daemon을 깨우고 1ms 대기하며, 복구 중에는LOG_CS아래에서 동기적으로logpb_prior_lsa_append_all_list를 실행한다. SA 모드(#else)는 항상 동기적이다. drain에 대한 내용은 Chapter 5에서 다룬다.
flowchart TD
A["enter internal"] --> B{"WITHOUT_LOCK?"}
B -- yes --> C["lock prior_lsa_mutex"]
B -- no --> D["prior_lsa_start_append"]
C --> D
D --> E["start_lsa 스냅샷"]
E --> VG{"vacuum-produce guard"}
VG --> F["6개 arm 타입 dispatch 사다리\n매칭 없으면 no-op"]
F --> G["data_header advance + add_align"]
G --> H{"ulength>0?"}
H -- yes --> I["append_data ulength"]
H -- no --> J{"rlength>0?"}
I --> J
J -- yes --> K["append_data rlength"]
J -- no --> L["end_append: forw_lsa 설정"]
K --> L
L --> M{"tail == NULL?"}
M -- yes --> N["header = tail = node"]
M -- no --> O["tail->next = node; tail = node"]
N --> P["list_size += 점유 크기"]
O --> P
P --> Q{"WITHOUT_LOCK?"}
Q -- yes --> R["unlock; 필요시 강제 flush"]
Q -- no --> S["num_log_records_written++; return"]
R --> S
Figure 4-2. 모든 여섯 dispatch arm을 포함한 prior_lsa_next_record_internal의 분기 완전 제어 흐름.
4.4 prior_lsa_start_append — LSA 할당과 역방향 체인 구성
섹션 제목: “4.4 prior_lsa_start_append — LSA 할당과 역방향 체인 구성”여기서 node의 식별자가 탄생한다.
// prior_lsa_start_append -- src/transaction/log_append.cpp log_prior_lsa_append_advance_when_doesnot_fit (sizeof (LOG_RECORD_HEADER)); // <- 헤더가 page 경계에 걸쳐선 안 된다 node->log_header.trid = tdes->trid; LSA_COPY (&node->start_lsa, &log_Gl.prior_info.prior_lsa); // <- LSA 할당, 전진 전에 수행 (불변식 4-C) if (tdes->is_system_worker_transaction () && !tdes->is_under_sysop ()) { LSA_SET_NULL (&node->log_header.prev_tranlsa); // <- worker, sysop 없음: per-tran 체인 없앰 LSA_SET_NULL (&tdes->head_lsa); LSA_SET_NULL (&tdes->tail_lsa); } else { LSA_COPY (&node->log_header.prev_tranlsa, &tdes->tail_lsa); // 이 transaction의 마지막 record에 연결 LSA_COPY (&tdes->tail_lsa, &log_Gl.prior_info.prior_lsa); // 이 record가 이제 tran tail이다 if (LSA_ISNULL (&tdes->head_lsa)) { LSA_COPY (&tdes->head_lsa, &tdes->tail_lsa); } // tran의 첫 번째 record LSA_COPY (&tdes->undo_nxlsa, &log_Gl.prior_info.prior_lsa); // rollback 시 다음에 undo할 record } LSA_COPY (&node->log_header.back_lsa, &log_Gl.prior_info.prev_lsa); // <- 물리적 역방향 링크 (어느 tran이든) LSA_SET_NULL (&node->log_header.forw_lsa); // <- 아직 알 수 없다 (end_append에서 확정) LSA_COPY (&log_Gl.prior_info.prev_lsa, &log_Gl.prior_info.prior_lsa); // <- prev_lsa가 이제 이 record를 가리킨다 log_prior_lsa_append_add_align (sizeof (LOG_RECORD_HEADER)); // <- 헤더 바이트를 반영transaction-chain 분기: system worker(예: vacuum)는 rollback 체인을 소유하지 않으므로, sysop 아래에 있지 않은 worker record는 prev_tranlsa/head_lsa/tail_lsa를 null로 만든다. 나머지는 이전 tail에 연결하고 tail을 업데이트한다. 물리적 back_lsa = prev_lsa 링크는 transaction에 무관하다. 이후 prev_lsa는 이 record를 가리키도록 전진한다. forw_lsa는 여기서 null로 설정되고 end_append에서 확정된다.
불변식 4-B. Sysop 아래에 있지 않은 system-worker record는 null
prev_tranlsa를 가진다. 이 조건이 깨지면 recovery가 존재하지 않는 transaction 체인을 순회하게 된다.
불변식 4-C (전진 전에 start).
start_lsa는add_align이 커서를 전진시키기 전에 읽히므로, record의 첫 번째 바이트를 가리킨다. 헤더 fit guard가 먼저 실행되기 때문에 그 첫 번째 바이트는 헤더를 담을 수 있는 page에 있다.
4.5 prior_lsa_end_append — forw_lsa 확정
섹션 제목: “4.5 prior_lsa_end_append — forw_lsa 확정”data-header, undo, redo 영역이 모두 반영되면 커서는 이 record의 첫 번째 바이트 다음 위치에 있다 — 즉, 다음 record의 시작점이자 이 record의 forw_lsa다. 두 헬퍼는 forw_lsa를 읽기 전에 실행된다: 정렬 후 다음 헤더가 들어맞지 않으면 다음 page로 넘긴다. 따라서 forw_lsa는 항상 다음 헤더가 합법적으로 위치할 수 있는 위치를 가리키며, 모든 forw_lsa는 헤더 중간에 걸침 없이 다음 record의 start_lsa와 일치한다.
// prior_lsa_end_append -- src/transaction/log_append.cppstatic voidprior_lsa_end_append (THREAD_ENTRY *thread_p, LOG_PRIOR_NODE *node){ log_prior_lsa_append_align (); // <- 다음 record 시작점으로 정렬 log_prior_lsa_append_advance_when_doesnot_fit (sizeof (LOG_RECORD_HEADER)); // <- 다음 헤더도 들어맞아야 한다 LSA_COPY (&node->log_header.forw_lsa, &log_Gl.prior_info.prior_lsa);}4.6 prior_update_header_mvcc_info — vacuum block 장부 정리
섹션 제목: “4.6 prior_update_header_mvcc_info — vacuum block 장부 정리”사다리의 arm 1에서 호출된다. vacuum이 어느 block에 MVCC 작업이 있는지 알 수 있도록 전역 log 헤더의 실행 중 MVCC-block 요약을 유지한다.
// prior_update_header_mvcc_info -- src/transaction/log_append.cpp assert (MVCCID_IS_VALID (mvccid)); if (!log_Gl.hdr.does_block_need_vacuum) // <- 이 block의 첫 번째 MVCC record { log_Gl.hdr.oldest_visible_mvccid = log_Gl.mvcc_table.get_global_oldest_visible (); log_Gl.hdr.newest_block_mvccid = mvccid; } else { // ... condensed: sanity asserts on oldest/newest/block id ... if (log_Gl.hdr.newest_block_mvccid < mvccid) // <- 이후 record: 최고 watermark만 올린다 { log_Gl.hdr.newest_block_mvccid = mvccid; } } log_Gl.hdr.mvcc_op_log_lsa = record_lsa; // <- 두 분기 모두: 최신 MVCC op 위치 log_Gl.hdr.does_block_need_vacuum = true;block의 첫 번째 MVCC record는 oldest_visible_mvccid를 MVCC table에서 씨드로 심는다. 이후 record들은 newest_block_mvccid만 올린다(생략된 else는 block id가 mvcc_op_log_lsa와 일치하는지 assert한다). 두 arm 모두 mvcc_op_log_lsa = record_lsa로 설정하고 block을 더티 표시한다. mutex 아래에서 실행되기 때문에 완전히 순서화된 LSA 스트림과 일관성을 유지한다.
4.7 offset 헬퍼들 — prior_lsa가 record 점유 공간을 어떻게 전진하는가
섹션 제목: “4.7 offset 헬퍼들 — prior_lsa가 record 점유 공간을 어떻게 전진하는가”세 개의 static 함수가 log_Gl.prior_info.prior_lsa를 전진시키며, 각 영역을 소비한다. 모두 LOGAREA_SIZE 바이트 page 영역 내의 0 기반 offset을 대상으로 동작한다(앞부분 assert (... offset >= 0) 라인은 생략).
// offset helpers -- src/transaction/log_append.cppstatic void log_prior_lsa_append_align (){ log_Gl.prior_info.prior_lsa.offset = DB_ALIGN (log_Gl.prior_info.prior_lsa.offset, DOUBLE_ALIGNMENT); if ((size_t) log_Gl.prior_info.prior_lsa.offset >= (size_t) LOGAREA_SIZE) // <- 정렬로 page 끝을 넘은 경우 { log_Gl.prior_info.prior_lsa.pageid++; log_Gl.prior_info.prior_lsa.offset = 0; }}static void log_prior_lsa_append_advance_when_doesnot_fit (size_t length){ if ((size_t) log_Gl.prior_info.prior_lsa.offset + length >= (size_t) LOGAREA_SIZE) // <- 영역이 들어맞지 않음 { log_Gl.prior_info.prior_lsa.pageid++; log_Gl.prior_info.prior_lsa.offset = 0; }}static void log_prior_lsa_append_add_align (size_t add){ log_Gl.prior_info.prior_lsa.offset += (add); // <- 영역의 바이트를 소비 log_prior_lsa_append_align (); // <- 소비 후 정렬 (다음 page로 넘어갈 수 있음)}advance_when_doesnot_fit만 분기를 가진다 — 헤더가 경계에 걸치지 않도록 하는 사전 검사다. advance_when_doesnot_fit(N) 후 add_align(N)을 순서대로 쌍으로 사용하면, 먼저 영역 N이 들어맞음을 보장하고 그 다음 소비한다. page 경계를 걸치는 페이로드(prior_lsa_append_data)는 Chapter 6에서 다룬다.
4.8 prior_set_tde_encrypted — 암호화 표시
섹션 제목: “4.8 prior_set_tde_encrypted — 암호화 표시”LSA 경로와 별개로, 민감한 record를 위해 node에 대해 호출된다.
// prior_set_tde_encrypted -- src/transaction/log_append.cpp if (!tde_is_loaded()) // <- cipher가 사용 가능해야 한다 { er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, ER_TDE_CIPHER_IS_NOT_LOADED, 0); return ER_TDE_CIPHER_IS_NOT_LOADED; // <- 오류 분기 } tde_er_log ("prior_set_tde_encrypted(): rcvindex = %s\n", rv_rcvindex_string (recvindex)); node->tde_encrypted = true; // <- 유일한 상태 변경 return NO_ERROR;분기는 두 가지다: cipher가 로드되지 않았으면 오류를 기록하고 ER_TDE_CIPHER_IS_NOT_LOADED를 반환하며 node는 건드리지 않는다. 그 외에는 node->tde_encrypted = true로 뒤집는다. 이 플래그는 나중에 해당 node를 담는 page가 할당/flush될 때 읽힌다 — LSA 할당에는 관여하지 않기 때문에 prior_lsa_start_append의 일부가 아닌 독립 setter로 존재한다. (조회 측: 단순한 prior_is_tde_encrypted.)
4.9 Chapter 요약 — 핵심 정리
섹션 제목: “4.9 Chapter 요약 — 핵심 정리”- 하나의 mutex가 순서를 정의한다.
prior_lsa_mutex는 record 하나당 한 번(또는_with_lock으로 여러 번에 걸쳐 한 번) 획득된다. 획득 순서가 곧 LSA 순서다(불변식 4-A). 하나의 lock 아래에서 읽고 전진하므로 공유 또는 순서가 뒤집힌 LSA는 없으며 별도 카운터도 필요 없다. prior_lsa_start_append가 탄생의 순간이다.prior_lsa를 전진 전에start_lsa로 복사하고(불변식 4-C),trid를 설정하고,prev_tranlsa/back_lsa를 구성하고,forw_lsa를 null로 만든다.- transaction 체인은 worker 여부에 따라 분기한다. Sysop 아래에 있지 않은 worker record는 null
prev_tranlsa/head_lsa/tail_lsa를 갖는다(불변식 4-B). 나머지는 체인에 연결하고tail_lsa/head_lsa/undo_nxlsa를 업데이트한다. - 6개 arm 타입 사다리가 lock 아래에서
start_lsa를 캡처한다. MVCC-undo(중첩 3-way 선택 →prior_update_header_mvcc_info),SYSOP_START_POSTPONE(tdes->state도 뒤집음),SYSOP_END,COMMIT_WITH_POSTPONE(_OBSOLETE),SYSOP_ATOMIC_START,COMMIT/ABORT가 각각 LSA를tdes->rcv.*/commit_abort_lsa에 저장한다. forw_lsa는 마지막에 확정된다.prior_lsa_end_append가 record를 지나 정렬하고 다음 헤더의 fit을 보장하므로,forw_lsa는 다음 record의start_lsa와 일치한다.- Offset 헬퍼들이 record 점유 공간을 소비한다.
advance_when_doesnot_fit은 fit을 사전 검사하고(유일한 분기),add_align은 소비 후 정렬하며,align은DOUBLE_ALIGNMENT로 반올림하고 page를 넘긴다. - 비용이 큰 작업은 lock 바깥에서 이루어진다. O(1) 연결과
list_size증가로 임계 구역이 끝난다. flush 검사와 flush 실행은 unlock 이후에 수행된다.
Chapter 5: Prior List를 페이지 버퍼로 비우기
섹션 제목: “Chapter 5: Prior List를 페이지 버퍼로 비우기”Chapter 4는 prior_lsa_next_record가 LSA를 연결해 둔 log_prior_node 체인을 남겼다. 이 노드들에는 주소가 확정되어 있을 뿐, 실제 LOG_PAGE 프레임에 복사된 바이트는 아직 없다. 이 챕터는 리스트를 떼어내어 LSN 순서로 순회하고 각 노드를 페이지 버퍼에 직렬화하는 단일 writer drain 과정을 추적한다. 페이지 경계 처리는 Chapter 6(logpb_next_append_page)에 맡기고, WAL 규칙은 Chapter 7(동반 문서 cubrid-log-manager.md)에서 다룬다.
5.1 두 직렬화 계층
섹션 제목: “5.1 두 직렬화 계층”잠금 두 개, 역할 두 가지. prior_lsa_mutex 는 appender들 사이를 직렬화한다. LSA 스탬프와 링크 시점에만 잠깐 보유하며(Chapter 4), 페이지 버퍼는 보호하지 않는다. LOG_CS write 모드는 appender와 페이지 버퍼 writer 사이를 직렬화한다. 모든 drain 함수는 assert (LOG_CS_OWN_WRITE_MODE (thread_p))로 시작한다.
불변식 — 단일 writer drain. drain은 호출자가
LOG_CSwrite 모드를 보유한 상태에서 실행된다. drain하는 스레드가 둘이면append_lsa.offset과log_pgptr가 원자적으로 갱신되지 못해 레코드가 서로 뒤섞인다.LOG_CS_OWN_WRITE_MODEassert는 이 규칙 위반을 즉시 치명적 오류로 만든다. 아래의 모든 struct 표는 이 조건을 전제한다.
핵심 전환 지점은 detach다. writer는 prior_lsa_mutex 아래에서 리스트를 잘라내고 헤더를 null로 초기화한다. 이후 새로 들어오는 appender들은 빈 리스트에 붙이기 시작하고, 떼어낸 리스트는 잠금 없이 drain된다.
flowchart TD A1["prior_lsa_next_record\nprior_lsa_mutex를 잠깐 보유"] D1["logpb_prior_lsa_append_all_list"] D2["prior_lsa_mutex 아래에서 리스트 detach\nheader/tail/list_size 초기화"] D3["logpb_append_prior_lsa_list\nLSN 순서로 노드 순회"] D4["노드마다 logpb_append_next_record 호출"] D5["LOG_PAGE 프레임에 바이트 복사\ndirty 표시 후 노드 해제"] A1 -->|"prior_list에 연결"| D2 D1 --> D2 --> D3 --> D4 --> D5
Figure 5-1 — 두 직렬화 계층과 detach 핸드오프.
5.2 drain이 읽고 쓰는 struct
섹션 제목: “5.2 drain이 읽고 쓰는 struct”log_prior_node — drain의 기본 단위 (log_append.hpp)
섹션 제목: “log_prior_node — drain의 기본 단위 (log_append.hpp)”| 필드 | 역할 | 존재 이유 |
|---|---|---|
log_header | logpb_start_append가 그대로 복사하는 LOG_RECORD_HEADER | 디스크 상의 레코드 헤더 |
start_lsa | append 시점에 append_lsa와 반드시 일치해야 함 | LSN 순서 오염 감지 |
tde_encrypted | 목적 페이지가 TDE 암호화 대상임을 표시 | appending_page_tde_encrypted 설정에 사용 |
data_header_length | data_header의 바이트 길이 | 헤더 복사 크기 결정 |
data_header | 레코드 타입별 고정 헤더 페이로드 | LOG_RECORD_HEADER 뒤에 오는 부분 |
ulength / udata | undo 세그먼트의 길이/포인터 | rollback 이미지 |
rlength / rdata | redo 세그먼트의 길이/포인터 | recovery 이미지 |
next | 다음 노드 링크 | LSN 순서 순회에 사용 |
불변식 — 노드 순서 = LSN 순서.
prior_lsa_mutex아래에서 꼬리에 이어 붙이기 때문에next순회는 정확히 LSN 오름차순이 된다.logpb_append_next_record는LSA_EQ (&node->start_lsa, &log_Gl.hdr.append_lsa)로 각 노드를 재확인하며, 불일치가 생기면logpb_fatal_error를 호출한다.
LOG_PAGE / log_hdrpage — 목적 프레임 (log_storage.hpp)
섹션 제목: “LOG_PAGE / log_hdrpage — 목적 프레임 (log_storage.hpp)”log_page는 { LOG_HDRPAGE hdr; char area[1]; } 구조이고, log_hdrpage는 프레임별 헤더다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
hdr.logical_pageid | log 내에서 이 프레임의 식별자 | 페이지를 물리 슬롯에 매핑 |
hdr.offset | 이 페이지에서 첫 레코드가 시작하는 offset | logpb_start_append가 한 번 설정; 복구 시 salvage 가능 |
hdr.flags | TDE 암호화 플래그 | logpb_set_tde_algorithm이 기록 |
hdr.checksum | 페이지의 CRC32 | flush 시 계산됨 (Chapter 7) |
area | 헤더+페이로드가 memcpy되는 영역 | LOG_APPEND_PTR() = area + append_lsa.offset |
LOG_BUFFER — dirty 비트를 관리하는 프레임 래퍼 (log_page_buffer.c)
섹션 제목: “LOG_BUFFER — dirty 비트를 관리하는 프레임 래퍼 (log_page_buffer.c)”| 필드 | 역할 | 존재 이유 |
|---|---|---|
pageid (volatile) | 래핑된 프레임의 논리 페이지 id | flush 대상 검증 |
phy_pageid (volatile) | active log 내의 물리 페이지 id | 논리 페이지를 디스크 슬롯에 매핑 |
dirty (bool) | “아직 flush되지 않은 변경이 있음” | logpb_set_dirty가 올리고, flusher가 내림 (Chapter 7) |
logpage (LOG_PAGE*) | 버퍼링된 페이로드에 대한 역방향 포인터 | logpb_get_log_buffer가 LOG_PAGE*로부터 래퍼를 복원 |
log_append_info — 단일 writer의 커서 상태 (log_append.hpp)
섹션 제목: “log_append_info — 단일 writer의 커서 상태 (log_append.hpp)”| 필드 | 역할 | 존재 이유 |
|---|---|---|
vdes | active log 볼륨 디스크립터 | flush 대상; drain 중 변경 없음 |
nxio_lsa (atomic) | 아직 디스크에 없는 최소 LSN | WAL 프런티어 (Chapter 7) |
prev_lsa | 마지막으로 완전히 append된 레코드의 주소 | logpb_start_append가 back_lsa == prev_lsa를 확인한 후 전진 |
log_pgptr | 현재 고정된 append 페이지 프레임 | LOG_APPEND_PTR()이 log_pgptr->area에 씀 |
appending_page_tde_encrypted | 현재 채워지는 페이지에 TDE 필요 | 노드의 tde_encrypted에서 페이지별로 전파 |
불변식 — back_lsa 체인.
logpb_start_append는 각 헤더 앞에서back_lsa == prev_lsa를 assert한다. 디스크의 역방향 체인이 끊기면 프로세스가 fatal 종료된다.
5.3 logpb_prior_lsa_append_all_list — detach 후 drain
섹션 제목: “5.3 logpb_prior_lsa_append_all_list — detach 후 drain”// logpb_prior_lsa_append_all_list -- src/transaction/log_page_buffer.cintlogpb_prior_lsa_append_all_list (THREAD_ENTRY * thread_p){ LOG_PRIOR_NODE *prior_list; assert (LOG_CS_OWN_WRITE_MODE (thread_p)); /* <- 단일 writer 불변식 */
log_Gl.prior_info.prior_lsa_mutex.lock (); prior_list = prior_lsa_remove_prior_list (thread_p); /* <- detach */ log_Gl.prior_info.prior_lsa_mutex.unlock (); /* <- 뮤텍스 조기 해제 */
if (prior_list != NULL) { // ... condensed: perfmon stats ... logpb_append_prior_lsa_list (thread_p, prior_list); /* <- drain, 뮤텍스 없이 */ } return NO_ERROR;}prior_lsa_remove_prior_list가 실제 detach다. drain 중 prior list 헤더를 변경하는 유일한 함수다.
// prior_lsa_remove_prior_list -- src/transaction/log_page_buffer.cstatic LOG_PRIOR_NODE *prior_lsa_remove_prior_list (THREAD_ENTRY * thread_p){ LOG_PRIOR_NODE *prior_list; assert (LOG_CS_OWN_WRITE_MODE (thread_p)); prior_list = log_Gl.prior_info.prior_list_header; log_Gl.prior_info.prior_list_header = NULL; /* <- header/tail/size 초기화: */ log_Gl.prior_info.prior_list_tail = NULL; /* 이후 appender들은 빈 리스트에 붙음 */ log_Gl.prior_info.list_size = 0; return prior_list;}prior_list == NULL이면 drain을 건너뛰고, 그렇지 않으면 뮤텍스를 복사 이전에 해제한다. 이로써 appender를 막는 구간이 포인터 세 번 쓰기로 줄어든다.
5.4 logpb_append_prior_lsa_list — 순회 및 해제
섹션 제목: “5.4 logpb_append_prior_lsa_list — 순회 및 해제”떼어낸 리스트는 prior_flush_list_header(별도 슬롯)에 임시로 두고, 새로 재구성되는 prior_list_header는 건드리지 않는다. 이후 노드를 하나씩 drain한다.
// logpb_append_prior_lsa_list -- src/transaction/log_page_buffer.cstatic intlogpb_append_prior_lsa_list (THREAD_ENTRY * thread_p, LOG_PRIOR_NODE * list){ LOG_PRIOR_NODE *node; assert (log_Gl.prior_info.prior_flush_list_header == NULL); /* <- 동시 drain 불가 */ log_Gl.prior_info.prior_flush_list_header = list;
while (log_Gl.prior_info.prior_flush_list_header != NULL) { node = log_Gl.prior_info.prior_flush_list_header; log_Gl.prior_info.prior_flush_list_header = node->next; /* <- 복사 전 전진 */ logpb_append_next_record (thread_p, node); /* <- 복사 */
if (node->data_header != NULL) free_and_init (node->data_header); if (node->udata != NULL) free_and_init (node->udata); if (node->rdata != NULL) free_and_init (node->rdata); free_and_init (node); /* <- 노드 생존 종료 */ } return NO_ERROR;}각 세그먼트는 non-NULL일 때만 해제한다. 노드는 세그먼트 중 일부만 가질 수 있기 때문이다. 헤드는 복사 이전에 전진하므로 next가 NULL인 순간 루프가 자연스럽게 끝난다. assert (prior_flush_list_header == NULL)는 flush 리스트가 겹치지 않음을 강제한다. 이는 LOG_CS가 보장하는 단일 writer 불변식에서 자연스럽게 따라 나오는 결과다.
5.5 logpb_append_next_record — 노드 하나, 헤더 + 페이로드
섹션 제목: “5.5 logpb_append_next_record — 노드 하나, 헤더 + 페이로드”// logpb_append_next_record -- src/transaction/log_page_buffer.cstatic intlogpb_append_next_record (THREAD_ENTRY * thread_p, LOG_PRIOR_NODE * node){ if (!LSA_EQ (&node->start_lsa, &log_Gl.hdr.append_lsa)) logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "logpb_append_next_record"); /* <- LSN 순서 */
if (log_Gl.flush_info.num_toflush + 1 >= log_Gl.flush_info.max_toflush) logpb_flush_all_append_pages (thread_p); /* <- 이 레코드 앞에서 조기 flush */
log_Gl.append.appending_page_tde_encrypted = prior_is_tde_encrypted (node); logpb_start_append (thread_p, &node->log_header); /* LOG_RECORD_HEADER 기록 */
if (node->data_header != NULL) { LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT (thread_p, node->data_header_length); /* 헤더를 한 페이지에 */ logpb_append_data (thread_p, node->data_header_length, node->data_header); } if (node->udata != NULL) logpb_append_data (thread_p, node->ulength, node->udata); if (node->rdata != NULL) logpb_append_data (thread_p, node->rlength, node->rdata);
logpb_end_append (thread_p, &node->log_header); log_Gl.append.appending_page_tde_encrypted = false; /* 다음 노드를 위해 초기화 */ return NO_ERROR;}눈에 잘 띄지 않는 분기는 조기 flush(num_toflush + 1 >= max_toflush)다. 레코드 조립 중간이 아닌 지금 flush함으로써 Chapter 7의 부분 append 상태 기계(LOGPB_APPENDREC_*)가 레코드 중간에 발동되는 일을 막는다. data_header는 한 페이지 안에 유지되도록 미리 전진하지만, udata/rdata는 페이지를 넘어갈 수 있다. 모든 분기는 Figure 5-2에 정리되어 있다.
flowchart TD
S["logpb_append_next_record 진입"] --> C1{"start_lsa == append_lsa ?"}
C1 -->|아니오| F["logpb_fatal_error"]
C1 -->|예| C2{"flush 리스트가 거의 찼는가 ?"}
C2 -->|예| FL["logpb_flush_all_append_pages"]
C2 -->|아니오| H
FL --> H["tde 플래그 설정\nlogpb_start_append: 헤더 기록"]
H --> C3{"data_header ?"}
C3 -->|예| DH["ADVANCE_WHEN_DOESNOT_FIT\nappend_data 헤더"]
C3 -->|아니오| C4
DH --> C4{"udata ?"}
C4 -->|예| UD["append_data udata"]
C4 -->|아니오| C5
UD --> C5{"rdata ?"}
C5 -->|예| RD["append_data rdata"]
C5 -->|아니오| E
RD --> E["logpb_end_append\ntde 플래그 초기화"]
Figure 5-2 — logpb_append_next_record의 분기 완전 흐름.
5.6 logpb_start_append — 레코드 헤더 스탬프
섹션 제목: “5.6 logpb_start_append — 레코드 헤더 스탬프”// logpb_start_append -- src/transaction/log_page_buffer.cstatic voidlogpb_start_append (THREAD_ENTRY * thread_p, LOG_RECORD_HEADER * header){ LOG_RECORD_HEADER *log_rec; // ... condensed: assert, perfmon, ADVANCE_WHEN_DOESNOT_FIT (헤더 연속 배치) ... if (!LSA_EQ (&header->back_lsa, &log_Gl.append.prev_lsa)) logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "logpb_start_append"); /* <- back-chain 확인 */
if (log_Gl.append.appending_page_tde_encrypted && !LOG_IS_PAGE_TDE_ENCRYPTED (log_Gl.append.log_pgptr)) { // ... condensed: 페이지에 TDE 알고리즘 기록 ... logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); }
log_rec = (LOG_RECORD_HEADER *) LOG_APPEND_PTR (); *log_rec = *header; /* <- 헤더 복사 */ // ... condensed: hdr.offset == NULL_OFFSET이면 이 페이지의 첫 레코드 offset 설정 ...
if (log_rec->type == LOG_END_OF_LOG) { LSA_COPY (&log_Gl.hdr.eof_lsa, &log_Gl.hdr.append_lsa); logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); } else { LSA_COPY (&log_Gl.append.prev_lsa, &log_Gl.hdr.append_lsa); /* prev_lsa 전진 */ LOG_APPEND_SETDIRTY_ADD_ALIGN (thread_p, sizeof (LOG_RECORD_HEADER)); /* dirty + 전진 + 정렬 */ log_Pb.partial_append.status = LOGPB_APPENDREC_IN_PROGRESS; }}분기가 두 가지다. hdr.offset == NULL_OFFSET일 때는 해당 페이지의 첫 레코드 offset을 한 번만 설정한다. type 분기는 LOG_END_OF_LOG(EOF 센티널, Chapter 7)를 prev_lsa/IN_PROGRESS를 건드리지 않는 플레이스홀더 경로로 보내고, else는 체인을 IN_PROGRESS 상태로 전진시킨다.
5.7 logpb_append_data — 정렬된 바이트 복사
섹션 제목: “5.7 logpb_append_data — 정렬된 바이트 복사”// logpb_append_data -- src/transaction/log_page_buffer.cstatic voidlogpb_append_data (THREAD_ENTRY * thread_p, int length, const char *data){ int copy_length; char *ptr, *last_ptr; if (length == 0 || data == NULL) return; /* <- 빈 세그먼트: 아무것도 하지 않음 */
LOG_APPEND_ALIGN (thread_p, LOG_DONT_SET_DIRTY); /* 정렬, dirty 아직 아님 */ ptr = LOG_APPEND_PTR (); last_ptr = LOG_LAST_APPEND_PTR (); /* = area + LOGAREA_SIZE */
if ((ptr + length) >= last_ptr) /* <- 현재 페이지에 맞지 않음 */ { while (length > 0) { if (ptr >= last_ptr) { logpb_next_append_page (thread_p, LOG_SET_DIRTY); /* Chapter 6 */ ptr = LOG_APPEND_PTR (); last_ptr = LOG_LAST_APPEND_PTR (); } copy_length = (ptr + length >= last_ptr) ? CAST_BUFLEN (last_ptr - ptr) : length; memcpy (ptr, data, copy_length); ptr += copy_length; data += copy_length; length -= copy_length; log_Gl.hdr.append_lsa.offset += copy_length; /* 복사한 바이트만큼 전진 */ } } else /* <- 전부 맞음 */ { memcpy (ptr, data, length); log_Gl.hdr.append_lsa.offset += length; } LOG_APPEND_ALIGN (thread_p, LOG_SET_DIRTY); /* 다음 append를 위한 정렬 + dirty 표시 */}경계 넘기 경로는 페이지 끝까지 복사한 뒤 logpb_next_append_page(Chapter 6)를 호출하고, length == 0이 될 때까지 반복한다. logpb_append_crumbs는 scatter-gather 형제 함수로 같은 맞음/넘기 논리를 사용하지만, drain 경로에는 등장하지 않는다.
불변식 — 커서는 복사한 바이트를 정확히 추적한다.
append_lsa.offset은 모든 경로에서memcpy한 바이트만큼 정확히 전진한다. 이 값이 틀어지면 다음LOG_APPEND_PTR()이 잘못된 바이트를 가리켜 레코드가 겹친다. 두LOG_APPEND_ALIGN호출은 올림 처리만 한다.
5.8 logpb_end_append — 레코드 닫기, 앞 방향 연결
섹션 제목: “5.8 logpb_end_append — 레코드 닫기, 앞 방향 연결”// logpb_end_append -- src/transaction/log_page_buffer.cstatic voidlogpb_end_append (THREAD_ENTRY * thread_p, LOG_RECORD_HEADER * header){ // ... condensed: 정렬 + ADVANCE_WHEN_DOESNOT_FIT으로 커서를 다음 슬롯에 위치 ... assert (LSA_EQ (&header->forw_lsa, &log_Gl.hdr.append_lsa)); /* <- forw_lsa = 다음 슬롯 */
if (!LSA_EQ (&log_Gl.append.prev_lsa, &log_Gl.hdr.append_lsa)) logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); /* 커서가 prev에서 벗어났으면 dirty */
if (log_Pb.partial_append.status == LOGPB_APPENDREC_IN_PROGRESS) ; /* 정상: 그냥 통과 */ else if (log_Pb.partial_append.status == LOGPB_APPENDREC_PARTIAL_FLUSHED_END_OF_LOG) { log_Pb.partial_append.status = LOGPB_APPENDREC_PARTIAL_ENDED; logpb_flush_all_append_pages (thread_p); /* 올바른 버전으로 재flush */ } else assert_release (false); /* 유효하지 않은 상태 */
log_Pb.partial_append.status = LOGPB_APPENDREC_SUCCESS; /* 레코드가 안정적으로 완성됨 */}커서가 재조정되고 forw_lsa assert(back_lsa와 짝을 이룸)가 확인된 뒤 상태 기계가 분기한다. IN_PROGRESS는 그냥 통과한다. PARTIAL_FLUSHED_END_OF_LOG(강제 flush가 EOF 센티널로 교체한 경우)는 실제 레코드를 재flush한다(Chapter 7). else는 assert_release(false)다. 모두 SUCCESS로 끝난다.
불변식 — 레코드 브래킷.
logpb_start_append(IN_PROGRESS)부터logpb_end_append(SUCCESS)까지 정확히 레코드 하나가 쓰기 중 상태다. IN_PROGRESS를 보는 강제 flush는 불완전한 레코드를 잡은 것임을 안다. SUCCESS는 페이지를 flush해도 안전하다는 의미다. 이 브래킷이 깨지면 절반만 쓴 레코드가 표시 없이 디스크에 도달한다.
5.9 챕터 요약 — 핵심 정리
섹션 제목: “5.9 챕터 요약 — 핵심 정리”- 잠금 두 개, 역할 두 가지.
prior_lsa_mutex는 appender들 사이를 직렬화하고,LOG_CS는 appender와 단일 writer 사이를 직렬화한다. - Detach 후 drain. 뮤텍스 아래에서 header/tail/
list_size를 초기화하고 뮤텍스를 해제한 다음 복사한다. - LSN 순서, 즉시 해제. 각 노드는
logpb_append_next_record로 복사된 직후 세그먼트와 함께 해제된다. - 세 개의 assert가 체인을 증명한다.
back_lsa==prev_lsa,forw_lsa==append_lsa,start_lsa==append_lsa— 이 중 하나가 틀리면 fatal 종료다. - 커서는 정직하게 유지된다.
logpb_append_data는 복사한 바이트만큼 정확하게append_lsa.offset을 전진시킨다. - Dirty, 아직 flush 아님.
logpb_set_dirty는LOG_BUFFER::dirty만 뒤집는다. flush/checksum/WAL은 Chapter 7에서 처리한다. - 경계 넘기는 미룬다. 모든
logpb_next_append_page호출은 Chapter 6에 위임된다.
Chapter 6: 로그 페이지 경계 넘기
섹션 제목: “Chapter 6: 로그 페이지 경계 넘기”Chapter 5의 drain 루프는 prior 노드의 바이트를 log_Gl.append.log_pgptr에 조각조각 스트리밍한다. 실행 중인 offset log_Gl.hdr.append_lsa.offset이 페이지의 사용 가능한 한계(LOGAREA_SIZE)에 도달하면 appender는 꽉 찬 페이지를 봉인하고 flush 대기열에 등록한 뒤 새 논리 페이지를 얻어야 한다.
독자가 가져야 할 질문은 이것이다. 레코드가 맞지 않을 때 어떤 일이 일어나고, 꽉 찬 페이지가 대기열에 들어가는 동시에 새 페이지를 어떻게 가져오는가? 스트림 중간의 답은 logpb_next_append_page이고, 첫 페이지 부트스트랩은 logpb_fetch_start_append_page와 그 간소화 버전 logpb_fetch_start_append_page_new다. 모두 logpb_create_page / logpb_locate_page를 통해 버퍼 프레임을 얻고 헤더를 초기화한다. WAL과 append/flush 분리는 상위 동반 문서에서 다루고, flush 내구성은 Chapter 7에서 다룬다. prior 측 대응 함수 log_prior_lsa_append_advance_when_doesnot_fit(Chapter 4)은 바이트가 존재하기 전에 페이지 꼬리를 넘어 주소 공간을 예약한다. 이 챕터는 그 주소에 해당하는 물리 프레임을 가져오는 과정을 다룬다.
6.1 누가 경계 넘기를 발동하는가
섹션 제목: “6.1 누가 경계 넘기를 발동하는가”appender는 레코드 조립 코드에서 logpb_next_append_page를 직접 호출하지 않는다. 이 결정은 두 매크로가 담당하며, 둘 다 log_Gl.hdr.append_lsa.offset을 LOGAREA_SIZE와 비교한다(정렬/전진 산술은 Chapter 4 / Chapter 5 내용이다). LOG_APPEND_ALIGN은 조각 이후 DOUBLE_ALIGNMENT로 올림한 offset이 한계에 도달하면 넘기를 수행한다. LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT(length)는 offset + length가 넘칠 경우 쓰기 이전에 넘겨서 조각이 다음 페이지에 온전히 들어가도록 한다.
이 “이후 vs 이전” 분리가 logpb_next_append_page가 current_setdirty를 인자로 받는 이유다. LOG_APPEND_ALIGN(LOG_APPEND_SETDIRTY_ADD_ALIGN에서 LOG_SET_DIRTY로 호출됨)은 이미 떠나는 페이지를 dirty로 만든 상태다. ADVANCE 매크로는 바이트를 쓰기 전에 넘기므로 아직 dirty한 것이 없다. 그러므로 둘 다 LOG_DONT_SET_DIRTY를 전달하고, logpb_next_append_page 내부의 봉인 분기는 핫 경로에서 실행되지 않는다. 이 분기는 페이지를 사전에 봉인하지 않은 직접 호출자를 위해 존재한다.
logpb_set_dirty는 페이지의 버퍼 프레임에서 boolean 하나를 뒤집는다.
// logpb_set_dirty -- src/transaction/log_page_buffer.cvoidlogpb_set_dirty (THREAD_ENTRY * thread_p, LOG_PAGE * log_pgptr){ LOG_BUFFER *bufptr; bufptr = logpb_get_log_buffer (log_pgptr); /* <- 페이지 주소로부터 프레임 복원 */ // ... condensed ... bufptr->dirty = true;}불변식 (detach 전 dirty): append 바이트를 받은 페이지는 log_Gl.append.log_pgptr가 다른 곳을 가리키기 전에 반드시 bufptr->dirty여야 한다. 모든 쓰기 경로는 offset이 LOGAREA_SIZE에 도달하기 전에 LOG_APPEND_SETDIRTY_ADD_ALIGN(LOG_APPEND_ALIGN을 LOG_SET_DIRTY로 호출)을 실행한다. 이것이 지켜지지 않으면 꽉 찬 페이지가 toflush[]에 dirty 표시 없이 들어가고 flusher가 이를 건너뛰어 commit된 레코드가 유실된다.
6.2 경계 지점의 struct들
섹션 제목: “6.2 경계 지점의 struct들”log_append_info는 appender의 고정 커서이고, log_page / log_hdrpage는 물리 페이지 레이아웃이며, log_flush_info는 flusher로 넘기는 대기열이다.
log_append_info (log_append.hpp) — 전역 하나, log_Gl.append.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
vdes | active log의 볼륨 디스크립터 (fd) | 결국 fileio_write의 대상 |
nxio_lsa | atomic<LOG_LSA>: 아직 디스크에 없는 최소 LSA | WAL 경계; fetch 헬퍼(6.5)가 재조정, Chapter 7이 읽음 |
prev_lsa | 마지막으로 append된 레코드의 LSA | logpb_start_append가 back_lsa == prev_lsa 확인; 논리값이라 경계 넘기 후에도 그대로 유지됨 (6.7) |
log_pgptr | 현재 고정된 append 페이지 | 경계 넘기가 null로 만든 후 재조정하는 포인터 |
appending_page_tde_encrypted | append 중간에 생성되는 페이지도 TDE 암호화 필요 | 레코드의 암호화 결정을 레코드 중간에 새로 생성된 페이지에 전파 (6.6) |
log_hdrpage (log_storage.hpp) — 모든 log 페이지 앞에 있는 헤더.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
logical_pageid | LOG_PAGEID: 무한 log에서 페이지의 주소 | 독자/flusher가 프레임이 어떤 논리 페이지인지 알게 함 |
offset | PGLENGTH: 이 페이지에서 첫 번째 전체 레코드의 바이트 offset | 이전 페이지가 손상된 경우 recovery 시 salvage 앵커 |
flags | short 비트 필드; 현재는 TDE 비트만 사용 | LOG_HDRPAGE_FLAG_ENCRYPTED_AES/_ARIA 보유; logpb_set_tde_algorithm으로 설정 |
checksum | int: 페이지의 CRC32 | 일관성 확인; 생성 시 memset 쓰레기값이고, logpb_writev_append_pages가 fileio_write 직전에 계산 (6.4) |
log_page (log_storage.hpp): LOG_HDRPAGE hdr 다음에 char area[1](레코드 영역, LOGAREA_SIZE 크기). sizeof를 사용하지 말고 LOG_PAGESIZE를 사용한다.
log_flush_info (log_impl.h) — 경계 넘기가 push하는 대기열; 전역 하나, log_Gl.flush_info.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
max_toflush | toflush의 용량 | 대기열이 찰 때 flush를 강제하는 임계값 |
num_toflush | 대기 중인 페이지 수 | 경계 넘기마다 flush_mutex 아래에서 증가 |
toflush | LOG_PAGE **: flush를 기다리는 순서 있는 페이지들 | 핸드오프 목록; 배열 순서 = flush 순서 |
flush_mutex | 위 세 항목에 대한 mutex (SERVER_MODE) | Log Flush Thread와 appender가 대기열을 안전하게 공유할 수 있게 함 |
graph TD
subgraph append_cursor
AI["log_append_info<br/>log_Gl.append"]
AI -->|log_pgptr| PG["LOG_PAGE (현재)"]
AI -->|appending_page_tde_encrypted| TDE["TDE 결정"]
end
PG -->|hdr| HDR["log_hdrpage<br/>logical_pageid / offset / flags / checksum"]
subgraph flush_queue
FI["log_flush_info<br/>log_Gl.flush_info"]
FI -->|toflush num_toflush| Q["LOG_PAGE *[] 순서 있음"]
end
PG -.->|경계 넘기 시 enqueue| Q
Figure 6-1 — struct 관계: 경계 넘기는 log_pgptr를 새 LOG_PAGE로 재조정하고 toflush[]에 페이지를 push한다.
6.3 logpb_next_append_page: 분기 완전 설명
섹션 제목: “6.3 logpb_next_append_page: 분기 완전 설명”// logpb_next_append_page -- src/transaction/log_page_buffer.c assert (LOG_CS_OWN_WRITE_MODE (thread_p)); /* (진입) LOG CS write-exclusive 보유 */ if (current_setdirty == LOG_SET_DIRTY) { logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); } /* (A) 이전 페이지 봉인 */ log_Gl.append.log_pgptr = NULL; /* (B) detach; (C) pageid++, offset=0 */ log_Gl.hdr.append_lsa.pageid++; log_Gl.hdr.append_lsa.offset = 0; if (LOGPB_AT_NEXT_ARCHIVE_PAGE_ID (log_Gl.hdr.append_lsa.pageid)) { logpb_archive_active_log (thread_p); } /* (D) 아카이브되지 않은 슬롯으로 전환 */ if (LOGPB_IS_FIRST_PHYSICAL_PAGE (log_Gl.hdr.append_lsa.pageid)) { log_Gl.hdr.fpageid += LOGPB_ACTIVE_NPAGES; logpb_flush_header (thread_p); } /* (E) 사이클 완료 */ log_Gl.append.log_pgptr = logpb_create_page (thread_p, log_Gl.hdr.append_lsa.pageid); /* (F) */ if (log_Gl.append.log_pgptr == NULL) { logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "log_next_append_page"); return; } /* (G) */ if (log_Gl.append.appending_page_tde_encrypted) /* (H) TDE 전파 — 6.6 참조 */ { /* ... logpb_set_tde_algorithm + logpb_set_dirty ... */ } rv = pthread_mutex_lock (&flush_info->flush_mutex); flush_info->toflush[flush_info->num_toflush++] = log_Gl.append.log_pgptr; /* (I) 새 페이지 enqueue */ need_flush = (flush_info->num_toflush >= flush_info->max_toflush); /* (J) 대기열 찼는가? */ pthread_mutex_unlock (&flush_info->flush_mutex); if (need_flush) { logpb_flush_all_append_pages (thread_p); } /* (K) 강제 flush, mutex 밖에서 */Figure 6-2는 모든 분기를 추적한다. 비직관적인 분기가 두 가지 있다. (B): detach와 (F) 사이에는 현재 append 페이지가 없지만, 진입 시 assert로 확인한 write-exclusive LOG_CS가 있기 때문에 다른 appender가 이 공백을 관찰하는 일은 없다. (I): enqueue되는 페이지는 방금 꽉 찬 페이지가 아니라 새로 비어 있는 페이지다. 꽉 찬 페이지는 그 페이지 자신이 태어날 때 이미 대기열에 들어갔으므로, 모든 페이지는 toflush[]에 정확히 한 번만 들어간다. 나머지(D archive 전환 → Chapter 10, E 링 전환 헤더 갱신, G fatal-NULL)는 코드 발췌와 흐름도에 표시되어 있다.
flowchart TD
S["진입, LOG CS write 보유"] --> A{"current_setdirty == LOG_SET_DIRTY?"}
A -->|예| A1["logpb_set_dirty(이전 페이지)"]
A -->|아니오| B
A1 --> B["log_pgptr = NULL; pageid++; offset = 0"]
B --> D{"LOGPB_AT_NEXT_ARCHIVE_PAGE_ID?"}
D -->|예| D1["logpb_archive_active_log"]
D -->|아니오| E
D1 --> E{"LOGPB_IS_FIRST_PHYSICAL_PAGE?"}
E -->|예| E1["fpageid += ACTIVE_NPAGES; logpb_flush_header"]
E -->|아니오| F
E1 --> F["log_pgptr = logpb_create_page(pageid)"]
F --> G{"log_pgptr == NULL?"}
G -->|예| G1["logpb_fatal_error -> return"]
G -->|아니오| H{"appending_page_tde_encrypted?"}
H -->|예| H1["set_tde_algorithm; set_dirty"]
H -->|아니오| I
H1 --> I["flush_mutex 잠금; toflush[num++] = 새 페이지"]
I --> J{"num_toflush >= max_toflush?"}
J -->|예| J1["need_flush = true"]
J -->|아니오| K
J1 --> K["flush_mutex 해제"]
K --> L{"need_flush?"}
L -->|예| L1["logpb_flush_all_append_pages"]
L -->|아니오| Z["return"]
L1 --> Z
Figure 6-2 — logpb_next_append_page 제어 흐름, 모든 분기.
6.4 프레임 획득: NEW_PAGE에 대한 logpb_locate_page
섹션 제목: “6.4 프레임 획득: NEW_PAGE에 대한 logpb_locate_page”logpb_create_page(thread_p, pageid)는 return logpb_locate_page (thread_p, pageid, NEW_PAGE);다. logpb_locate_page는 논리 pageid를 버퍼 프레임에 매핑하고, NEW_PAGE의 경우 헤더를 제자리에서 초기화한다. 디스크는 건드리지 않는다. 중요한 분기들을 보자.
// logpb_locate_page -- src/transaction/log_page_buffer.c index = logpb_get_log_buffer_index (pageid); /* 링 해시 -> 프레임 슬롯; 잘못된 index -> NULL */ log_bufptr = &log_Pb.buffers[index]; if (log_bufptr->pageid != NULL_PAGEID && log_bufptr->pageid != pageid) { /* 프레임에 다른 페이지가 있음 */ if (log_bufptr->dirty == true) { assert_release (false); /* dirty를 victim으로 삼으면 안 됨 */ ... } log_bufptr->pageid = NULL_PAGEID; /* 무효화 */ } if (log_bufptr->pageid == NULL_PAGEID) { if (fetch_mode == NEW_PAGE) { memset (log_bufptr->logpage, LOG_PAGE_INIT_VALUE, LOG_PAGESIZE); /* 0xff 채우기 */ log_bufptr->logpage->hdr.logical_pageid = pageid; /* (1) */ log_bufptr->logpage->hdr.offset = NULL_OFFSET; /* (2) */ log_bufptr->logpage->hdr.flags = 0; /* (3) TDE 비트 초기화 */ } else /* OLD_PAGE */ { if (logpb_read_page_from_file (...) != NO_ERROR) { return NULL; } } } else { assert (fetch_mode == OLD_PAGE); /* 프레임에 이미 이 페이지가 있음 */ }세 번의 헤더 쓰기가 새 페이지를 초기화한다. (1) logical_pageid = pageid는 이 프레임이 해당 논리 페이지임을 확정한다. (2) offset = NULL_OFFSET — 아직 레코드가 시작하지 않았으며, logpb_start_append가 나중에 첫 레코드 offset으로 덮어쓴다. (3) flags = 0은 이전의 TDE 비트를 지운다. 그래서 (H)가 알고리즘을 다시 적용해야 한다. checksum은 여기서 설정하지 않는다. logpb_writev_append_pages가 페이지마다 fileio_write 직전에 logpb_set_page_checksum을 호출하여 log_pgptr->hdr.checksum = checksum_crc32;로 채운다.
불변식 (링 슬롯당 프레임 하나): assert_release (false)는 appender가 새 append 페이지를 위해 dirty 프레임을 절대 방출해서는 안 된다는 것을 코드로 표현한 것이다. 링은 슬롯의 이전 점유자가 재사용 전에 flush되도록 크기가 정해진다. 이것이 위반되면 flusher가 안전하다고 믿었던 페이지를 덮어쓰게 되어 log가 조용히 손상된다.
6.5 두 개의 공개 진입점
섹션 제목: “6.5 두 개의 공개 진입점”logpb_next_append_page는 스트림 중간 경계 넘기를 처리한다. 두 개의 공개 함수는 append 세션의 첫 페이지를 처리하며, log_pgptr를 부트스트랩하고 WAL을 재고정한다.
// logpb_fetch_start_append_page -- src/transaction/log_page_buffer.c PAGE_FETCH_MODE flag = OLD_PAGE; if ((log_Gl.hdr.append_lsa.pageid == FIRST_LOG_PAGEID) /* NDEBUG: ==0; else PRM_ID_FIRST_LOG_PAGEID */ && (log_Gl.hdr.append_lsa.offset == 0)) { flag = NEW_PAGE; } /* 비어 있는 log: 읽기 생략 */ if (log_Gl.append.log_pgptr != NULL) { logpb_invalid_all_append_pages (thread_p); } /* 오래된 append 페이지: 폐기 */ log_Gl.append.log_pgptr = logpb_locate_page (thread_p, log_Gl.hdr.append_lsa.pageid, flag); if (log_Gl.append.log_pgptr == NULL) { return ER_FAILED; } log_Gl.append.set_nxio_lsa (log_Gl.hdr.append_lsa); /* (*) WAL 경계 재고정 */ // ... 6.3과 동일한 flush_mutex enqueue: toflush[num_toflush++] = log_pgptr; 임계값 -> need_flush ... if (need_flush) { logpb_flush_pages_direct (thread_p); } /* 주의: _all_append가 아니라 direct */스트림 중간 경로와 구별되는 분기가 두 가지 있다. flag 선택: 비어 있는 log(pageid가 PRM_ID_FIRST_LOG_PAGEID와 같고 offset이 0)는 읽기 없이 NEW_PAGE로 가져온다. 그렇지 않으면 OLD_PAGE로 디스크 페이지를 읽어온다. 예를 들어 재시작 후 절반만 찬 마지막 페이지를 이어받는 경우다. 오래된 페이지 폐기: 진입 시 log_pgptr이 non-NULL이면 logpb_invalid_all_append_pages를 실행한다. (*)는 WAL을 재고정하는 줄이다. nxio_lsa가 현재 append 위치로 이동하여 “이 아래는 모두 내구성이 있다”를 선언한다. 스트림 중간의 logpb_next_append_page는 nxio_lsa를 건드리지 않는다. 세션 내에서 경계는 실제로 페이지가 flush될 때만 이동하기 때문이다(Chapter 7).
// logpb_fetch_start_append_page_new -- src/transaction/log_page_buffer.c log_Gl.append.log_pgptr = logpb_locate_page (thread_p, log_Gl.hdr.append_lsa.pageid, NEW_PAGE); if (log_Gl.append.log_pgptr == NULL) { return NULL; } /* 호출자가 NULL 처리 */ log_Gl.append.set_nxio_lsa (log_Gl.hdr.append_lsa); return log_Gl.append.log_pgptr;_new는 간소화 버전이다. 항상 NEW_PAGE이고, 오래된 페이지 확인이 없으며, enqueue와 flush 임계값 처리도 없다. log 생성/포맷처럼 새 첫 페이지가 필요하지만 flush를 직접 관리하는 호출자를 위한 함수다. 재사용 전에 알아야 할 사항은 toflush[] 부기를 생략한다는 점이다. 나머지 두 함수는 이 처리를 모두 수행한다.
6.6 경계를 넘는 TDE 플래그 전파
섹션 제목: “6.6 경계를 넘는 TDE 플래그 전파”암호화 결정은 레코드에 속하지만, log_Gl.append.appending_page_tde_encrypted를 통해 페이지 단위로 적용된다. logpb_append_next_record(Chapter 5)가 이 플래그의 생존 주기를 소유한다. prior_is_tde_encrypted (node)에서 플래그를 설정하고, logpb_end_append 이후 false로 초기화한다. 플래그가 true인 동안, 조립 중 접근하는 페이지에 TDE 비트를 다시 기록하는 지점이 두 군데 있다. 지점 (H)는 logpb_next_append_page 내부(6.3)로, 막 생성된 페이지에 logpb_set_tde_algorithm을 호출한 뒤 logpb_set_dirty를 실행한다. 두 번째 지점은 logpb_start_append에 있으며, 중복 적용을 방지하는 guard가 있다.
// logpb_start_append -- src/transaction/log_page_buffer.c if (log_Gl.append.appending_page_tde_encrypted) { if (!LOG_IS_PAGE_TDE_ENCRYPTED (log_Gl.append.log_pgptr)) /* 멱등성 guard */ { TDE_ALGORITHM tde_algo = (TDE_ALGORITHM) prm_get_integer_value (PRM_ID_TDE_DEFAULT_ALGORITHM); logpb_set_tde_algorithm (thread_p, log_Gl.append.log_pgptr, tde_algo); logpb_set_dirty (thread_p, log_Gl.append.log_pgptr); } }logpb_set_tde_algorithm은 hdr.flags에 쓴다(암호화 마스크를 지우고 알고리즘 비트를 OR한다). logpb_locate_page가 생성 시 hdr.flags를 0으로 설정하기 때문에(6.4 단계 3), 새 페이지는 처음에 암호화되지 않은 상태이고 (H)가 이를 다시 기록한다. 뒤따르는 logpb_set_dirty가 없으면 이 비트가 유실될 수 있다. logpb_start_append의 LOG_IS_PAGE_TDE_ENCRYPTED guard는 (H)가 이미 처리한 페이지를 다시 기록하는 것을 막는다.
불변식 (암호화는 레코드를 따라 페이지를 넘는다): 레코드 R이 TDE 암호화 대상이라면, R이 접촉하는 모든 페이지 — R 중간에 할당된 페이지 포함 — 는 0이 아닌 TDE 플래그를 가져야 한다. 이는 R의 조립 전체에서 appending_page_tde_encrypted가 true로 유지되고, 새 페이지마다 (H) 재기록이 실행됨으로써 보장된다. 이것이 깨지면 암호화된 레코드의 일부가 평문으로 기록된다. logpb_writev_append_pages는 쓰기 시점에 페이지마다 LOG_IS_PAGE_TDE_ENCRYPTED를 확인하므로, 플래그가 없는 페이지는 조용히 암호화를 건너뛴다.
6.7 경계 넘기 후 살아남는 것들
섹션 제목: “6.7 경계 넘기 후 살아남는 것들”log_Gl.append.prev_lsa는 논리값이라 페이지에 상대적이지 않다. 그래서 logpb_next_append_page는 이 값을 건드리지 않는다. prev_lsa는 새로 넘어간 페이지에서 레코드가 시작되더라도 마지막으로 append된 레코드를 계속 가리킨다. 덕분에 logpb_start_append는 header->back_lsa == prev_lsa를 검증할 수 있다. prev_lsa는 오직 logpb_start_append 내부(LSA_COPY (&log_Gl.append.prev_lsa, &log_Gl.hdr.append_lsa))에서만 전진하며, 페이지 fetch 시에는 변경되지 않는다. 이 버퍼 측 이음새는 Chapter 4의 prior 측 log_prior_lsa_append_advance_when_doesnot_fit과 짝을 이룬다. 양쪽 모두 같은 LOGAREA_SIZE 임계값을 기준으로 경계를 계산하므로 예약된 주소와 실체화된 프레임이 절대 어긋나지 않는다.
6.8 챕터 요약 — 핵심 정리
섹션 제목: “6.8 챕터 요약 — 핵심 정리”- 스트림 중간 경계 넘기는 함수 하나가 담당한다.
logpb_next_append_page는 꽉 찬 페이지를 선택적으로 봉인하고,log_pgptr를 null로 만들고,append_lsa.pageid를 전진시키고, 새 프레임을 생성하고, 페이지를 대기열에 넣는다. 이 모든 것이 write-heldLOG_CS아래에서 이루어진다. - 경계 넘기 시 enqueue되는 페이지는 꽉 찬 페이지가 아니라 새 페이지다. 모든 페이지는 자신이 태어날 때 정확히 한 번
toflush[]에 들어간다. NEW_PAGE생성은 헤더 필드 세 개를 쓸 뿐, checksum은 쓰지 않는다.logpb_locate_page는logical_pageid,offset = NULL_OFFSET,flags = 0을 설정하고 본문을0xff로 memset한다.checksum은fileio_write직전logpb_set_page_checksum이 계산한다.- 생성 시
flags = 0이기 때문에 TDE를 다시 기록해야 한다.appending_page_tde_encrypted를 건드리는 지점은 세 군데다.logpb_append_next_record에서 설정/초기화하고,logpb_next_append_page(H)와logpb_start_append에서 재기록하여 암호화된 레코드가 페이지 경계를 넘어도 암호화 상태를 유지한다. - 임계값 flush는
flush_mutex아래에서 결정되고, 밖에서 실행된다.num_toflush >= max_toflush가need_flush를 설정하고,logpb_flush_all_append_pages는 mutex 해제 이후에 실행된다. logpb_fetch_start_append_pagevs_new. 전자는NEW_PAGE/OLD_PAGE를 선택하고, 오래된 페이지를 폐기하고, enqueue하고,nxio_lsa를 재고정한다._new는 항상NEW_PAGE이며 자체적으로 flush를 관리하는 호출자를 위해 enqueue/임계값 처리를 생략한다.- 경계 넘기는 Chapter 4와 짝을 이루는 버퍼 측 동작이다. prior 측은 바이트가 존재하기 전에 꼬리를 넘어 주소 공간을 예약하고, 이 챕터는 그 주소에 해당하는 프레임을 가져온다. 둘 다
LOGAREA_SIZE를 기준으로 삼기 때문에 예약 주소와 실체화된 프레임이 항상 일치한다.
Chapter 7: Flush 내구성과 WAL 규칙
섹션 제목: “Chapter 7: Flush 내구성과 WAL 규칙”이 장은 하나의 질문에 답한다. 더티 log page는 어떻게 안정적인 저장소에 도달하는가, nxio_lsa는 어떻게 전진하는가, 그리고 group commit과 WAL 불변식은 어떻게 협력하여 recovery의 정확성을 유지하는가? 3~6장에서는 prior list를 구성하고, 그것을 page buffer로 drain하며(5장), page 경계를 넘어갔다(6장). 이 모든 과정은 휘발성 메모리 안에서 이루어졌다. 이 장에서는 그 바이트들이 비로소 디스크에 닿는다.
WAL의 개념적 배경과 log가 data page보다 먼저 영속되어야 하는 이유는 companion 문서 cubrid-log-manager.md의 “Write-Ahead Logging” 절에 있다. 이 장은 이론이 아니라 그것을 강제하는 코드를 추적한다.
7.1 내구성 상태를 보유하는 세 가지 구조체
섹션 제목: “7.1 내구성 상태를 보유하는 세 가지 구조체”내구성 조율은 세 struct를 중심으로 이루어진다. log_append_info(log_append.hpp)는 워터마크를 소유하고, log_flush_info(log_impl.h)는 스캔할 page 목록을 관리하며, log_buffer(log_page_buffer.c)는 per-page 슬롯으로서 flusher가 dirty 비트를 지우는 단위가 된다.
log_append_info는 { int vdes; std::atomic<LOG_LSA> nxio_lsa; LOG_LSA prev_lsa; LOG_PAGE *log_pgptr; bool appending_page_tde_encrypted; }를 보유한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
vdes | 모든 fileio_write/fileio_synchronize에 전달되는 active log fd. | flusher는 어느 fd에 쓰고 fsync할지 알아야 한다. |
nxio_lsa | 내구성 워터마크 — 아직 디스크에 강제 기록되지 않은 page의 최솟값 LSA. lock-free 읽기를 위해 atomic으로 선언된다. | ”record X는 영속되었는가?”(group commit)와 “data page 쓰기 전에 flush해야 하는가?”(WAL) 두 질문 모두에 답한다. |
prev_lsa | 마지막으로 append된(버퍼 내) record의 LSA. nxio_lsa(마지막으로 flush된 LSA)와 구별된다. | flusher가 부분 record 상황(nxio_lsa.pageid == prev_lsa.pageid)을 감지하여 조기에 검증하지 않도록 한다. |
log_pgptr | 현재 새 record를 위해 고정된 append page. | flush가 num_toflush를 리셋한 후 toflush[0]으로 다시 설정된다. |
appending_page_tde_encrypted | 다음 page에 TDE 암호화가 필요한지 여부. | 암호화 결정을 append 시점에서 write 시점까지 전달한다. |
접근자는 단순하다. get_nxio_lsa는 nxio_lsa.load ()이고 set_nxio_lsa는 nxio_lsa.store ()이다. atomicity 자체가 계약이다.
log_flush_info는 { int max_toflush; int num_toflush; LOG_PAGE **toflush; pthread_mutex_t flush_mutex; }를 보유한다(flush_mutex는 SERVER_MODE에서만 존재).
| 필드 | 역할 | 존재 이유 |
|---|---|---|
max_toflush | 배열 용량. num_toflush == max_toflush가 되면 버퍼가 꽉 찬 것으로 간주하며 log_buffer_full_count가 증가한다. | 배치 크기를 제한한다. 목록이 가득 찬 이벤트는 6장의 partial-append 경로를 유발한다. |
num_toflush | 대기 중인 page 수. < 1이면 flush할 대상이 없다. | 루프 경계값이며, flush 후 0으로 리셋되고 live append page로 다시 채워진다. |
toflush | pageid 오름차순으로 정렬된 LOG_PAGE* 배열. | 연속성 스캔이 이 배열을 순회하며 인접한 page들을 하나의 writev로 합친다. |
flush_mutex | 배열을 concurrent producer(5장 drain)와 flusher 사이에서 보호한다. | flush 본문 전체에 걸쳐 유지된다 — 스캔, nxio page 쓰기, fileio_synchronize까지 모두 이 mutex를 쥔 채 실행된다. Phase-1의 num_toflush 확인만 별도의 짧은 획득으로 처리된다. group-commit 대기는 gc_cond/gc_mutex(§7.5)라는 별개의 lock을 사용한다. |
log_buffer는 { volatile LOG_PAGEID pageid; volatile LOG_PHY_PAGEID phy_pageid; bool dirty; LOG_PAGE *logpage; }를 보유한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
pageid | 논리 page id. 슬롯이 재활용되므로 volatile로 선언된다. | flusher는 bufptr->pageid == toflush[i]->hdr.logical_pageid를 assertion으로 확인하여 슬롯이 여전히 해당 page를 보유하는지 검증한다. |
phy_pageid | active log 볼륨 내 물리 offset. | 쓰기 대상은 phy_pageid + i이며, 연속성 판단에는 pageid+1이 아니라 phy_pageid+1이 필요하다. |
dirty | flush되지 않은 변경 사항이 있음을 나타낸다. | 스캔의 주요 필터이며, 쓰기 성공 직후에만 지워진다. |
logpage | page 바이트 전체(헤더 + 영역). | fileio_write와 암호화에 전달되는 실제 데이터다. |
flowchart LR FI["LOG_FLUSH_INFO<br/>toflush[ ], num_toflush"] LB["LOG_BUFFER<br/>pageid, phy_pageid, dirty, logpage"] AI["log_append_info<br/>nxio_lsa, prev_lsa, log_pgptr"] FI -->|"toflush[i] resolves to"| LB LB -->|"dirty pages written, then"| AI AI -->|"nxio_lsa.pageid flushed LAST"| FI
Figure 7-1 — flusher가 세 내구성 구조체 사이를 순환하는 방식.
불변식 — 내구성. LSA L의 commit record는
get_nxio_lsa () > L인 경우에만 영속된 것으로 간주한다(워터마크가 L의 page를 지나쳐야 한다).logpb_flush_all_append_pages는fileio_synchronize가 반환된 후, 그리고nxio_lsapage가 마지막으로 기록된 후에만nxio_lsa를 전진시킨다. fsync 전에 워터마크를 앞당기면, 크래시 발생 시 commit된 트랜잭션의 log가 디스크에 없는 상황이 만들어지고 recovery는 그것을 잃게 된다.
7.2 logpb_flush_all_append_pages — 내구성 엔진
섹션 제목: “7.2 logpb_flush_all_append_pages — 내구성 엔진”append page를 기록하고 nxio_lsa를 전진시키는 유일한 함수다. LOG_CS write 모드 아래에서 실행되며(assert (LOG_CS_OWN_WRITE_MODE)), 1 = flush 완료, 0 = 할 일 없음, < 0 = 오류를 반환한다.
Phase 1 — flush 여부 결정. 짧은 flush_mutex 획득 후 본문 시작 전에 해제한다. 두 가지 조건이 need_flush = false와 return 0을 발동한다. num_toflush < 1(목록이 빈 경우)과 num_toflush == 1 && !logpb_is_dirty (toflush[0])이 그것이다. 단일 클린 page에 대한 단축 회로는 타이머 기반 idle flush가 변경되지 않은 end-of-log 마커를 불필요하게 재기록하는 상황을 막는다.
Phase 2 — end-of-log 마커 배치. log_Pb.partial_append.status(6장의 LOGPB_APPENDREC_* enum)에 따라 분기하고, 이후 본문 나머지를 위해 flush_mutex를 다시 획득한다.
IN_PROGRESS— record가 절반만 append된 상태다. 헤더 page를 별도로 복사하고, 슬롯의dirty를 지운 다음, 진행 중인 헤더를LOG_END_OF_LOG로 덮어쓰고 그 복사본을logpb_write_page_to_disk로 기록한다. status는PARTIAL_FLUSHED_END_OF_LOG로 전환된다. page가 이미 버퍼를 떠난 경우는 fatal →goto error.PARTIAL_FLUSHED_END_OF_LOG— flush를 이어 받아 계속 진행한다. 로그를 남기고 fall through.PARTIAL_ENDED/SUCCESS— 정상 경우.eofrecord를 만들고append_lsa를 전진시키지 않은 채로logpb_start_append를 호출한다(나중에 덮어쓰인다).- 그 외 —
assert_release (false)→goto error.
Phase 3 — 2단계 연속 run 스캔. flush_mutex를 쥔 채 실행되며, 규칙은 nxio_lsa page를 마지막에 flush한다는 것이다. while (true) 루프는 1단계(클린이거나 nxio page가 아닌 더티 page까지 건너뛰기, 없으면 종료)와 2단계(run 확장)를 번갈아 수행한다. 2단계에는 run을 종료하고 skip 단계로 돌아가게 하는 네 가지 break 조건이 있다.
// logpb_flush_all_append_pages (step-2 run conditions) -- src/transaction/log_page_buffer.cif (!bufptr->dirty) break; /* <- clean stops run */if (bufptr->pageid == log_Gl.append.get_nxio_lsa ().pageid) break; /* <- nxio last */if (prv_bufptr->pageid + 1 != bufptr->pageid) break; /* <- logical gap */if (prv_bufptr->phy_pageid + 1 != bufptr->phy_pageid) break;/* <- physical gap */[idxflush, i) 범위의 run은 logpb_writev_append_pages로 전달된다. NULL 반환은 fatal(goto error)이다. 그렇지 않으면 need_sync = true로 설정되고, 각 page의 dirty는 쓰기가 non-NULL을 반환한 후에만 지워진다.
Phase 4 — nxio_lsa page를 마지막에 flush. 완전한 record를 보유하는지 여부에 따라 분기한다.
// logpb_flush_all_append_pages (nxio page) -- src/transaction/log_page_buffer.cif (log_Pb.partial_append.status == LOGPB_APPENDREC_SUCCESS || nxio_lsa.pageid != log_Gl.append.prev_lsa.pageid) /* complete -> write it */ { /* assert_release pageid match and dirty, else goto error */ logpb_write_page_to_disk (thread_p, bufptr->logpage, bufptr->pageid); need_sync = true; bufptr->dirty = false; }else { /* skip: nxio page holds an incomplete record, defer until complete */ }Phase 5 — fsync. 여전히 flush_mutex를 쥔 상태에서 실행된다. need_sync가 설정되어 있고 PRM_ID_SUPPRESS_FSYNC 샘플링 탈출 조건이 허용하는 경우(escape == 0이거나 total_sync_count % escape == 0)에 한해 fileio_synchronize (thread_p, log_Gl.append.vdes, log_Name_active, false)를 호출한다. NULL_VOLDES 반환은 fatal → goto error.
Phase 6 — nxio_lsa 전진. 다시 partial_append.status에 따라 분기한다.
LOGPB_APPENDREC_PARTIAL_ENDED— 원래 record 헤더를 복원하고 재기록한 뒤 다시 fsync를 수행한 후set_nxio_lsa (log_Gl.hdr.append_lsa)를 호출한다. status는PARTIAL_FLUSHED_ORIGINAL로 전환된다.LOGPB_APPENDREC_PARTIAL_FLUSHED_END_OF_LOG— 아직 검증할 수 없다.set_nxio_lsa (log_Gl.append.prev_lsa)로 한 record 이전까지만 설정한다.LOGPB_APPENDREC_SUCCESS—set_nxio_lsa (log_Gl.hdr.append_lsa).- 그 외 —
assert_release (false)→goto error.
이후 목록을 리셋한다(num_toflush = 0). log_Gl.append.log_pgptr != NULL이면 해당 live page를 toflush[0]으로 다시 설정한다. flush_mutex를 해제하고 1을 반환한다. error: 레이블은 flush_mutex가 아직 보유 중이면 해제하고 logpb_fatal_error를 호출한다. 모든 오류 경로는 복구 불가능하다.
flowchart TD
A["enter (LOG_CS write)"] --> B{num_toflush?}
B -->|"< 1, or 1+clean"| Z0["return 0"]
B -->|flushable| C{partial_append.status}
C -->|IN_PROGRESS| D["overwrite EOL, write copy"]
C -->|SUCCESS/PARTIAL_ENDED| E["start_append EOL marker"]
D --> F["scan toflush: skip clean,<br/>collect dirty run, writev, clear dirty"]
E --> F
F --> H{nxio page holds<br/>partial record?}
H -->|no| I["write nxio page LAST"]
H -->|yes| K
I --> K{need_sync?}
K -->|yes| L["fileio_synchronize"]
K -->|no| M
L --> M["advance nxio_lsa per status,<br/>num_toflush=0, reseed log_pgptr"]
M --> Z1["return 1"]
D -.->|fatal| ERR["logpb_fatal_error"]
L -.->|fail| ERR
Figure 7-2 — logpb_flush_all_append_pages의 완전한 분기 흐름.
7.3 logpb_writev_append_pages — 실제 쓰기
섹션 제목: “7.3 logpb_writev_append_pages — 실제 쓰기”가장 하위 레벨의 write 헬퍼다. 모든 page에 CRC를 찍고(logpb_set_page_checksum, 실패 시 NULL 반환), npages에 걸쳐 per-page 두 분기를 루프로 처리한다.
// logpb_writev_append_pages -- src/transaction/log_page_buffer.cif (LOG_IS_PAGE_TDE_ENCRYPTED (log_pgptr)) /* branch 1: encrypt into enc_pgptr; */ // ... on encrypt failure, turn TDE off for this page ...if (fileio_write (..., log_pgptr, phy_pageid + i, LOG_PAGESIZE, write_mode) == NULL) { /* branch 2: ER_LOG_WRITE_OUT_OF_SPACE / ER_LOG_WRITE */ to_flush = NULL; break; }이름과 달리 실제로는 phy_pageid + i 위치에서 fileio_write를 page 단위로 반복 호출한다. Phase 3의 연속성 보장 덕분에 이 배치는 하나의 순차적 extent가 된다. write_mode는 DWB 환경에서 FILEIO_WRITE_NO_COMPENSATE_WRITE다. 어느 쓰기에서든 to_flush == NULL 반환이 발생하면 호출자에게 fatal 오류로 처리된다.
7.4 동기식 요청 경로
섹션 제목: “7.4 동기식 요청 경로”logpb_flush_pages_direct가 핵심이다. assert (LOG_CS_OWN_WRITE_MODE) 아래에서 logpb_prior_lsa_append_all_list(5장 drain)를 호출한 뒤 logpb_flush_all_append_pages(엔진)를 호출한다. 두 개의 얇은 래퍼가 critical section을 추가한다. logpb_force_flush_pages는 단순히 LOG_CS_ENTER; logpb_flush_pages_direct; LOG_CS_EXIT이고, logpb_force_flush_header_and_pages는 exit 전에 logpb_flush_header(10장)를 추가로 호출한다. 이 래퍼는 checkpoint나 헤더의 eof_lsa/append 필드가 디스크 상태와 일치해야 하는 모든 시점에 사용된다.
7.5 logpb_flush_pages — 네 가지 commit 모드
섹션 제목: “7.5 logpb_flush_pages — 네 가지 commit 모드”logpb_flush_pages (thread_p, flush_lsa)는 commit하는 모든 트랜잭션이 호출하는 진입점이다. !SERVER_MODE에서는 LOG_CS 아래에서 직접 flush한다. SERVER_MODE에서도 두 가지 상황은 직접 flush로 떨어진다.
- 서버가 재시작되지 않은 경우, 또는
flush_lsa가 NULL/ISNULL인 경우 → 직접 flush 후 반환. - daemon을 사용할 수 없는 경우(
!log_is_log_flush_daemon_available ()) → 직접 flush.
이 외의 경우에는 async_commit(PRM_ID_LOG_ASYNC_COMMIT) × group_commit(LOG_IS_GROUP_COMMIT_ACTIVE ())의 2×2 정책에 따라 행동이 결정된다.
// logpb_flush_pages (mode matrix) -- src/transaction/log_page_buffer.c// async group | need_wait need_wakeup_LFT// X X | true true (sync, non-group: wake daemon, wait)// X O | true false (sync, group: just wait)// O X | false true (async: wake daemon, return)// O O | false false (async+group: just return)대기 루프는 group commit의 waiter 측으로, gc_cond에서 잠들어(gc_mutex를 쥔 채) nxio_lsa가 자신의 flush_lsa를 지나칠 때까지 기다린다.
// logpb_flush_pages (group-commit wait) -- src/transaction/log_page_buffer.cif (need_wakeup_LFT == false && pgbuf_has_perm_pages_fixed (thread_p)) need_wakeup_LFT = true; /* <- holding data pages: push daemon to avoid a stall */while (LSA_LT (&nxio_lsa, flush_lsa)) { // re-read nxio_lsa each iteration // ... lock gc_mutex ... if (LSA_GE (&nxio_lsa, flush_lsa)) break; /* <- re-check under lock: already durable */ if (need_wakeup_LFT == true) log_wakeup_log_flush_daemon (); pthread_cond_timedwait (&gc_cond, &gc_mutex, &to); /* 1000ms deadline */ need_wakeup_LFT = true; /* <- after first wait, always nudge daemon */}lock 획득 후 재확인은 lost-wakeup race를 방지한다. 1000ms 타임아웃은 지연 시간에 상한을 둔다. (공유 fsync 메커니즘은 §7.6 / takeaway 4에 있다 — 여기서는 반복하지 않는다.)
미해결 질문 (companion에서 이어짐). group commit 윈도우 정책 — daemon이 fsync 전에 얼마나 오래 배치를 모으는가 — 은
log_get_log_group_commit_interval의looper주기와 on-demandwakeup ()호출로 결정된다. companion 문서는 배치/지연 간 트레이드오프를 미해결 항목으로 표시한다. 이 장은 메커니즘을 기록할 뿐이며 튜닝 기준은 다루지 않는다.
7.6 flush daemon과 group-commit producer 측
섹션 제목: “7.6 flush daemon과 group-commit producer 측”daemon은 log_get_log_group_commit_interval을 looper 주기로 사용하는 cubthread::daemon이다. task 본문인 log_flush_execute(log_manager.c)는 먼저 BO_IS_SERVER_RESTARTED ()와 log_Flush_has_been_requested를 확인하여 하나라도 false이면 일찍 반환한다. 그렇지 않으면 LOG_CS_ENTER; logpb_flush_pages_direct; LOG_CS_EXIT을 한 번 수행하고, 이어서 gc_mutex 아래에서 pthread_cond_broadcast (&gc_cond)를 실행한 뒤 log_Flush_has_been_requested를 지운다. signal이 아닌 broadcast를 사용하므로 flush 한 번으로 flush_lsa <= nxio_lsa를 만족하는 모든 waiter를 깨울 수 있다. producer 측인 log_wakeup_log_flush_daemon은 committer와 WAL 경로에서 호출되며, SERVER_MODE에서만 log_Flush_has_been_requested = true; log_Flush_daemon->wakeup ();을 수행한다. wakeup () 전에 플래그를 먼저 설정하는 이유는, 이미 이터레이션 중인 daemon이 다음 루프에서 요청을 확인하도록 보장하기 위해서다.
stateDiagram-v2 [*] --> Sleeping Sleeping --> Checking : 타이머 틱 또는 wakeup Checking --> Sleeping : 요청 없음\n 반환 Checking --> Flushing : 요청됨\n LOG_CS 진입 Flushing --> Broadcasting : flush_pages_direct 완료\n fsync 1회 Broadcasting --> Sleeping : gc_cond broadcast\n 요청 초기화
Figure 7-3 — log_Flush_daemon 상태 사이클. logpb_flush_pages의 waiter들은 Broadcasting 이후 nxio_lsa 전진을 관찰한다.
7.7 logpb_flush_log_for_wal — read-side WAL 불변식
섹션 제목: “7.7 logpb_flush_log_for_wal — read-side WAL 불변식”data page를 쓰기 전에 page buffer manager가 해당 page의 마지막 수정 LSA를 인수로 전달하며 호출하는 함수다. logpb_need_wal을 이용한 double-checked locking으로 WAL을 강제한다.
// logpb_flush_log_for_wal -- src/transaction/log_page_buffer.cif (logpb_need_wal (lsa_ptr)) /* <- cheap atomic check, no lock */ { LOG_CS_ENTER (thread_p); if (logpb_need_wal (lsa_ptr)) /* <- re-check under LOG_CS, else someone flushed it */ logpb_flush_pages_direct (thread_p); LOG_CS_EXIT (thread_p); assert (LSA_ISNULL (lsa_ptr) || !logpb_need_wal (lsa_ptr)); /* <- post-condition */ }술어 logpb_need_wal (lsa)는 단순히 LSA_LE (&get_nxio_lsa (), lsa)다. log가 *lsa까지 아직 영속되지 않은 경우 true를 반환하므로, 불변식 자체를 코드에서 직접 검증할 수 있다.
불변식 — WAL. LSA L에서 수정된 data page는
logpb_need_wal (L)이 성립하는 동안(nxio_lsa <= L) 기록될 수 없다. buffer manager는 먼저logpb_flush_log_for_wal을 호출하며, 그 사후 조건인assert (!logpb_need_wal (lsa_ptr))가 data page 쓰기 전에 log가 L까지 영속되었음을 보장한다. 이것을 위반하면 redo record의 효과는 디스크에 있지만 record 자체가 없는 상황이 발생하여, recovery가 변경 사항을 재현하거나 되돌릴 수 없게 된다.logpb_need_wal을LOG_CS바깥과 안쪽에서 두 번 확인하는 이유는, 이미 영속된 경우의 불필요한 critical section 진입과 concurrent committer가nxio_lsa를 전진시킨 경우의 중복 flush를 모두 피하기 위해서다.
7.8 장 요약 — 핵심 takeaway
섹션 제목: “7.8 장 요약 — 핵심 takeaway”nxio_lsa는 단 하나의 내구성 워터마크다.log_append_info에 atomic으로 선언되며 아직 기록되지 않은 최솟값 LSA를 나타낸다. “이 commit은 영속되었는가?”와 “이 data 쓰기 전에 flush해야 하는가?”를 동시에 답한다.fileio_synchronize가 성공한 후에만 전진한다.logpb_flush_all_append_pages는nxio_lsapage를 마지막에 flush한다. 2단계 스캔(클린 page 건너뛰기, 연속 더티 page 수집)으로 인접 page를 배치로 묶고, 워터마크 page만 별도로 마지막에 기록한다. 덕분에 새로운 end-of-log는 이전 page들이 디스크에 올라가기 전에는 절대 검증되지 않는다.- 주
flush_mutex는 flush 본문 전체를 커버한다. 스캔, nxio page 쓰기,fileio_synchronize가 모두 이 mutex를 쥔 채 실행된다. Phase-1의num_toflush확인만 별도의 짧은 lock으로 처리된다. group-commit 대기는gc_cond/gc_mutex라는 별개의 lock을 사용한다. - group commit은 하나의 fsync를 여러 committer에게 분산시킨다. waiter들은
gc_cond에서 블록하여gc_mutex아래에서nxio_lsa를 재확인한다. daemon은logpb_flush_pages_direct를 한 번 수행하고broadcast하여flush_lsa가nxio_lsa이하인 모든 committer를 깨운다. - 2×2 commit 매트릭스(
async_commit×group_commit)는 wake-and-wait, just-wait, wake-and-return, just-return 중 하나를 선택한다. non-SERVER 환경과 daemon 미사용 경로는 직접 flush로 대체된다. - WAL은 read-side에서
logpb_flush_log_for_wal로 강제된다.LOG_CS주변에서logpb_need_wal을 두 번 확인하는 방식으로 구현되며, 사후 조건은 어떠한 data page 쓰기 전에도 log가 요청된 LSA까지 영속되었음을 assert로 보장한다. - group commit 윈도우 정책은 미해결 질문으로 남아 있다 (companion에서 이어짐). 메커니즘은 daemon의
looper인터벌과 on-demandwakeup ()이지만, 배치/지연 간 튜닝 기준은 여기서 확정하지 않는다.
Chapter 8: Commit과 Abort 생애주기
섹션 제목: “Chapter 8: Commit과 Abort 생애주기”이 장이 답하는 핵심 질문은 하나다. 트랜잭션 경계 레코드가 Chapter 3-7에서 구축한 prior-list / page-buffer / flush 파이프라인을 그대로 타면서, 어떻게 자신의 내구성을 강제하고 최종 상태 전이와 락 해제를 주도하는가? 경계 레코드가 일반 레코드와 다른 점은 단 두 가지다. LOG_REC_DONETIME 페이로드(undo/redo 데이터 없음)를 담는다는 것, 그리고 내구성 및 상태 기계 규율로 감싸인다는 것이다. 추가(append) 메커니즘 자체는 변하지 않는다. log_commit은 Chapter 3-4의 prior_lsa_alloc_and_copy_data / prior_lsa_next_record와 Chapter 7의 logpb_flush_pages force 경로를 그대로 재사용한다. 이 장은 그 감싸는 구조에 집중하며, recovery 측 재실행은 범위 밖이다(cubrid-recovery-manager.md).
8.1 트랜잭션 경계에서 만나는 세 가지 struct
섹션 제목: “8.1 트랜잭션 경계에서 만나는 세 가지 struct”commit/abort 시점에 세 struct가 교차한다. 디스크립터 log_tdes, 레코드별 헤더 log_rec_header(Chapter 1, 3), 그리고 경계 페이로드 log_rec_donetime이다.
log_rec_donetime — commit/abort 페이로드
섹션 제목: “log_rec_donetime — commit/abort 페이로드”LOG_COMMIT/LOG_ABORT 레코드의 전체 타입별 페이로드다. 알려진 LSA에 이 struct의 존재 자체가 곧 정보다.
// log_rec_donetime — src/transaction/log_record.hppstruct log_rec_donetime{ INT64 at_time; /* Database creation time. For safety reasons */};| 필드 | 역할 | 존재 이유 |
|---|---|---|
at_time | log_append_donetime_internal에서 캡처한 wall-clock time(NULL). | 완료 시각을 포렌식 용도로 기록한다. 주석의 “Database creation time”은 시대착오적 표현이며, 실제로는 종료 시각을 담는다. commit 프로토콜은 이 값을 다시 읽지 않는다. |
불변식 — donetime 레코드의 LSA 자체가 commit 지점이다. 이 레코드는 다른 상태를 전혀 담지 않으므로, 내구성은 “이 LSA를 담은 페이지가 디스크에 있다”는 사실로 환원된다. §8.4의 모든 로직은 클라이언트에게 commit 성공을 알리기 전에 바로 그 조건을 만족시키기 위한 것이다.
log_rec_header — 경계에서의 역할
섹션 제목: “log_rec_header — 경계에서의 역할”제네릭 헤더(Chapter 1에서 전체 설명)는 수정 없이 재사용된다. 경계 레코드에서 특별한 의미를 갖는 필드는 type과 prev_tranlsa뿐이다.
// log_rec_header — src/transaction/log_record.hppstruct log_rec_header{ LOG_LSA prev_tranlsa; /* previous log record for the same transaction */ LOG_LSA back_lsa, forw_lsa; /* physical backward/forward links */ TRANID trid; /* transaction identifier */ LOG_RECTYPE type; /* e.g. LOG_COMMIT, LOG_ABORT */};| 필드 | COMMIT/ABORT 레코드에서의 역할 | 왜 여기서 중요한가 |
|---|---|---|
prev_tranlsa | 트랜잭션별 체인을 닫는다. recovery는 committed 체인을 undo하지 않지만, 링크는 여전히 기록된다. | Chapter 4에서 attach 시점에 tdes->tail_lsa로부터 할당된다. |
type | LOG_COMMIT, LOG_ABORT, 또는 postpone 작업이 남아 있을 때 LOG_COMMIT_WITH_POSTPONE. | recovery 디스패치는 이 값을 기반으로 “이 trid는 완료됨 — undo 금지”를 판단한다. |
back_lsa / forw_lsa | 데이터 레코드와 마찬가지로 prior-list 기계가 할당하는 물리 순서 링크. | analysis 패스가 경계 레코드를 지나 스캔할 수 있게 한다. |
trid | commit/abort 중인 트랜잭션의 id. | recovery는 trid를 기준으로 레코드를 그룹화한다. |
log_tdes — 트랜잭션 디스크립터(경계 관련 필드)
섹션 제목: “log_tdes — 트랜잭션 디스크립터(경계 관련 필드)”log_tdes는 덩치가 크다. 여기서는 commit/abort 경로가 읽거나 쓰는 필드만 다룬다. 전체 struct는 log_impl.h에 있다.
// log_tdes (excerpt) — src/transaction/log_impl.hstruct log_tdes{ int tran_index; TRANID trid; TRAN_STATE state; LOG_LSA head_lsa; LOG_LSA tail_lsa; LOG_LSA undo_nxlsa; LOG_LSA posp_nxlsa; LOG_LSA commit_abort_lsa; LOG_TOPOPS_STACK topops; /* topops.last must be < 0 at the boundary */ void *first_save_entry; bool has_supplemental_log; // ... condensed ...};다섯 개 필드는 두 경로에서 동일하게 동작하므로, 두 열짜리 표 대신 한 번에 설명한다. tran_index(LOG_FIND_THREAD_TRAN_INDEX로 결정되는 테이블 인덱스; log_abort_by_tdes는 이를 실행 thread에 재바인딩한다 — §8.7), trid(log_rec_header.trid에 스탬프되고 logtb_get_new_tran_id로 재활용됨), head_lsa(정보용, 프로토콜이 읽지 않음), topops.last(< 0이어야 하며, 열려 있는 sysop은 버그 — assert(false) 후 외부로 강제 attach), first_save_entry(spage_free_saved_spaces로 해제). 역할이 commit과 abort 간에 다른 필드들은 다음과 같다.
| 필드 | commit에서의 역할 | abort에서의 역할 |
|---|---|---|
state | ACTIVE -> WILL_COMMIT -> (…_WITH_POSTPONE) -> COMMITTED. | 어떤 rollback보다 먼저 바로 ABORTED로. |
tail_lsa | NULL이면 아무것도 건드리지 않은 것 -> donetime 생략(§8.3, §8.5); 아니면 레코드가 연결되는 체인 tail. | 동일한 게이트. |
undo_nxlsa | NULL로 초기화해 WILL_COMMIT 중 발생하는 checkpoint가 낡은 커서를 보지 않도록 한다. | log_rollback이 prev_tranlsa를 따라 역방향으로 순회하는 rollback 커서. |
posp_nxlsa | NULL이 아니면 postpone 대기 중 -> LOG_COMMIT_WITH_POSTPONE(§8.3.1). | 사용 안 함. |
has_supplemental_log | 설정되어 있으면 commit 레코드 앞에 LOG_SUPPLEMENT_TRAN_USER 레코드가 선행된다(CDC), 이후 초기화. | 그냥 초기화. |
commit_abort_lsa | 경계 LSA가 스탬프됨으로써 checkpoint가 완료된 트랜잭션과 활성 트랜잭션을 구분할 수 있게 한다. 스탬프는 여기가 아니라 donetime 노드가 구체화될 때 log_append.cpp의 prior-list append에서 이루어진다. | 동일. |
flowchart TB TDES["log_tdes\nstate, tail_lsa,\nundo_nxlsa, posp_nxlsa"] HDR["log_rec_header\ntype = LOG_COMMIT/ABORT\nprev_tranlsa = tail_lsa"] DT["log_rec_donetime\nat_time"] NODE["LOG_PRIOR_NODE\n(Chapter 3)"] TDES -->|prev_tranlsa = tail_lsa| HDR HDR --> NODE DT -->|node->data_header| NODE NODE -->|prior_lsa_next_record| PL["prior list -> page buffer -> disk"]
Figure 8-1 — log_tdes가 체인 tail을 제공하고, LOG_COMMIT/LOG_ABORT 타입의 log_rec_header가 구성된다. log_rec_donetime은 노드의 data header가 된다. 그 이후부터는 평범한 prior-list 노드와 동일하다.
8.2 log_commit — 진입점과 분기 팬아웃
섹션 제목: “8.2 log_commit — 진입점과 분기 팬아웃”log_commit은 디스크립터를 결정하고, 상태를 검증한 뒤, 2PC 경로 또는 로컬 경로로 분기한다. 모든 분기는 다음과 같다.
// log_commit — src/transaction/log_manager.cif (tdes == NULL) return TRAN_UNACTIVE_UNKNOWN; /* <- fatal: unknown index */if (!LOG_ISTRAN_ACTIVE (tdes) && !LOG_ISTRAN_2PC_PREPARE (tdes) && LOG_ISRESTARTED ()) return tdes->state; /* <- not commitable; no-op */if (tdes->topops.last >= 0) /* <- impossible-but-handled */ { assert (false); while (tdes->topops.last >= 0) log_sysop_attach_to_outer (thread_p); }if (log_2pc_clear_and_is_tran_distributed (tdes)) state = log_2pc_commit (...); /* <- 2PC arm (cubrid-2pc.md) */else /* <- local arm */ { state = log_commit_local (thread_p, tdes, retain_lock, true); state = log_complete (thread_p, tdes, LOG_COMMIT, LOG_NEED_NEWTRID, LOG_ALREADY_WROTE_EOT_LOG); }if (log_No_logging) { /* force pages + data, flush header */ }perfmon_inc_stat (thread_p, PSTAT_TRAN_NUM_COMMITS); /* return state */불변식 — 트랜잭션 경계에서 topops.last < 0이어야 한다. commit과 abort는 열린 system operation이 없어야 한다. 코드는 경고와 debug 단언을 출력한 뒤, log_sysop_attach_to_outer로 열린 sysop을 외부 트랜잭션에 강제로 접어 넣는다. 이를 위반하면 해당 sysop의 레코드가 경계 레코드의 prev_tranlsa 체인에서 누락된다.
8.3 log_commit_local — postpone, append, release, flush
섹션 제목: “8.3 log_commit_local — postpone, append, release, flush”log_commit_local이 실제 작업을 수행하며, 그 순서는 하나의 규칙이 결정한다.
불변식 — 트랜잭션이 unactive 상태에 들어선 이후에는 아무것도 로그에 기록되어서는 안 된다. checkpoint가 트랜잭션을 TRAN_UNACTIVE_WILL_COMMIT으로 스냅샷하고 아직 대기 중인 로그(예: unique statistics)가 남아 있는 상태에서 크래시가 발생하면, recovery는 그 변경 없이 해당 트랜잭션을 commit한다 — 조용한 데이터 손실이다. 따라서 tx_lob_locator_clear와 logtb_complete_mvcc(둘 다 로그를 기록함)는 tdes->state = TRAN_UNACTIVE_WILL_COMMIT 이전에 실행된다.
// log_commit_local — src/transaction/log_manager.ctx_lob_locator_clear (...); logtb_complete_mvcc (thread_p, tdes, true); /* both log -> must precede WILL_COMMIT */tdes->state = TRAN_UNACTIVE_WILL_COMMIT;LSA_SET_NULL (&tdes->undo_nxlsa); /* checkpoint must not see a stale undo cursor */if (!LSA_ISNULL (&tdes->tail_lsa)) /* <- transaction touched data */ { log_tran_do_postpone (thread_p, tdes); /* §8.3.1 — run postpone if any */ if (is_local_tran) { if (... log_does_allow_replication () ...) log_append_repl_info_and_commit_log (...); /* repl+commit, one mutex */ else log_append_commit_log (thread_p, tdes, &commit_lsa); /* plain LOG_COMMIT */ if (retain_lock != true) lock_unlock_all (thread_p); /* <- retain_lock gate */ log_change_tran_as_completed (thread_p, tdes, LOG_COMMIT, &commit_lsa); /* state + force */ } else { /* participant: commit log + unlock deferred to log_complete_for_2pc */ } }else { if (retain_lock != true) lock_unlock_all (thread_p); /* <- read-only: no donetime record */ tdes->state = TRAN_UNACTIVE_COMMITTED; }return tdes->state;replication 경로는 prior_lsa_mutex를 한 번만 잡아 replication 레코드와 commit 레코드가 인접한 LSA를 갖도록 하고, plain 경로는 donetime 레코드만 추가한다. participant 분기(is_local_tran == false)는 commit 레코드 기록과 락 해제 모두를 log_complete_for_2pc로 미룬다(cubrid-2pc.md).
8.3.1 log_tran_do_postpone — LOG_COMMIT_WITH_POSTPONE
섹션 제목: “8.3.1 log_tran_do_postpone — LOG_COMMIT_WITH_POSTPONE”posp_nxlsa가 NULL이 아니면 트랜잭션에 지연된 작업이 있다는 뜻이다. log_tran_do_postpone는 postpone를 실행하기 전에 LOG_COMMIT_WITH_POSTPONE 레코드를 기록하고 force한다(Chapter 9).
// log_tran_do_postpone — src/transaction/log_manager.cif (LSA_ISNULL (&tdes->posp_nxlsa)) return; /* <- nothing to postpone */assert (tdes->topops.last < 0);log_append_commit_postpone (thread_p, tdes, &tdes->posp_nxlsa); /* COMMIT_WITH_POSTPONE + flush */if (tdes->m_log_postpone_cache.do_postpone (*thread_p, tdes->posp_nxlsa)) { perfmon_inc_stat (..., PSTAT_TRAN_NUM_PPCACHE_HITS); return; } /* cache fast-path */log_do_postpone (thread_p, tdes, &tdes->posp_nxlsa); /* scan forward, run LOG_POSTPONE records */log_append_commit_postpone는 state = TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE로 설정하고 즉시 force한다. 이는 postpone가 실행되기 전에 마커가 내구적으로 기록되도록 하기 위한 것이다. 크래시 이후 recovery가 재개할 수 있게 된다. 이후에 기록되는 plain LOG_COMMIT이 트랜잭션을 최종 닫는다.
8.4 내구성 강제 — log_append_commit_log와 WAL force
섹션 제목: “8.4 내구성 강제 — log_append_commit_log와 WAL force”log_append_commit_log는 log_append_donetime_internal 위에 얹힌 얇은 래퍼다. log_append_donetime_internal은 commit과 abort 모두가 donetime 레코드를 구성하는 단일 지점이다.
// log_append_donetime_internal — src/transaction/log_manager.cnode = prior_lsa_alloc_and_copy_data (thread_p, iscommitted, RV_NOT_DEFINED, ...); /* type = LOG_COMMIT/ABORT */if (node == NULL) return; /* <- alloc failed: eot_lsa stays NULL */((LOG_REC_DONETIME *) node->data_header)->at_time = time (NULL); /* the only payload field */lsa = (with_lock == LOG_PRIOR_LSA_WITH_LOCK) /* caller holds prior mutex, else take it */ ? prior_lsa_next_record_with_lock (...) : prior_lsa_next_record (thread_p, node, tdes);LSA_COPY (eot_lsa, &lsa); /* hand the commit LSA back to the caller */iscommitted는 레코드 타입으로도 기능하고, with_lock은 replication 경로가 이미 보유한 mutex를 재사용할 수 있게 한다. 이어서 log_change_tran_as_completed가 내구성 force를 수행한다.
// log_change_tran_as_completed — src/transaction/log_manager.cif (iscommitted == LOG_COMMIT) { tdes->state = TRAN_UNACTIVE_COMMITTED; logpb_flush_pages (thread_p, lsa); } /* <- COMMIT: always force up to commit LSA */else { tdes->state = TRAN_UNACTIVE_ABORTED; /* SERVER_MODE only: */ if (BO_IS_SERVER_RESTARTED () && VOLATILE_ACCESS (log_Gl.run_nxchkpt_atpageid, INT64) == NULL_PAGEID) logpb_flush_pages (thread_p, lsa); /* <- ABORT: force only if checkpoint in flight */}불변식 — commit된 트랜잭션의 commit 레코드는 클라이언트에게 “committed”를 알리기 전에 안정적인 저장 장치에 있어야 한다. logpb_flush_pages(thread_p, lsa)는 Chapter 7의 group-commit 요청이다. 이 함수는 commit하는 thread를 flush daemon의 대기자 집합에 넣고, nxio_lsa >= commit_lsa가 될 때까지 블록한다. 많은 commit 요청자가 하나의 fsync를 공유하는 구조다. commit 경로 전체에서 I/O를 기다리는 유일한 지점이 여기다. abort 분기는 설계상 비대칭이다. 플러시되지 않은 LOG_ABORT를 잃어도 무해하기 때문이다. recovery가 어차피 다시 undo한다. 따라서 abort는 재시작된 서버에서 checkpoint가 진행 중일 때만 force한다. 그렇지 않으면 해당 checkpoint가 recovery에 아직 필요한 아카이브를 회수해 버릴 수 있다.
8.5 log_complete — 최종 상태 전이와 다음 trid
섹션 제목: “8.5 log_complete — 최종 상태 전이와 다음 trid”log_commit과 log_abort 모두 log_complete를 통해 마무리된다. 두 가지 enum 플래그를 전달한다. 누가 EOT 레코드를 이미 기록했는가와 trid를 재활용할 것인가다. commit은 LOG_ALREADY_WROTE_EOT_LOG를 전달하며(else 분기는 단언만 수행함), abort는 LOG_NEED_TO_WRITE_EOT_LOG를 전달한다(if 분기가 LOG_ABORT를 추가함).
// log_complete — src/transaction/log_manager.cif (LSA_ISNULL (&tdes->tail_lsa)) { /* read-only: set COMMITTED/ABORTED; recycle or clear tdes */ }else { if (wrote_eot_log == LOG_NEED_TO_WRITE_EOT_LOG) /* <- abort: write LOG_ABORT now */ { log_append_abort_log (...); log_change_tran_as_completed (..., LOG_ABORT, &abort_lsa); } else assert (iscommitted == LOG_COMMIT && state == TRAN_UNACTIVE_COMMITTED); /* commit already wrote it */ tdes->unlock_global_oldest_visible_mvccid (); /* always */ if (iscommitted == LOG_COMMIT) log_Gl.mvcc_table.reset_transaction_lowest_active (...); /* commit only */ if (get_newtrid == LOG_NEED_NEWTRID) logtb_get_new_tran_id (thread_p, tdes);}if (LOG_ISCHECKPOINT_TIME ()) log_wakeup_checkpoint_daemon (); /* or logpb_checkpoint in SA mode */분기 팬아웃:
tail_lsaNULL. EOT 레코드 없이 상태를 설정하고, trid를 재활용하거나(LOG_NEED_NEWTRID) 강제 초기화한다(logtb_clear_tdes).tail_lsanon-NULL, abort. abort 레코드를 기록하고 §8.4에 따라 force한 뒤 상태를 설정한다.tail_lsanon-NULL, commit. 레코드가 이미 기록되었고 상태가TRAN_UNACTIVE_COMMITTED임을 단언한다.- MVCC 차단 해제(데이터 경로).
unlock_global_oldest_visible_mvccid는 항상 실행되고,reset_transaction_lowest_active는 commit에서만 실행된다(cubrid-mvcc.md). - 다음 trid.
logtb_get_new_tran_id는 해당 인덱스에 새로운trid를 재활용한다. donetime 레코드 자체가 CUBRID의 EOT 마커이며, 별도의 타입이 존재하지 않는다. - checkpoint 기동. append가 임계값을 넘었으면 checkpoint daemon을 깨우거나
logpb_checkpoint를 인라인으로 실행한다(SA mode).
flowchart TD
A["log_commit(tran_index, retain_lock)"] --> B{"tdes NULL?"}
B -->|yes| Z0["return TRAN_UNACTIVE_UNKNOWN"]
B -->|no| C{"active or 2PC-prepared?"}
C -->|no, restarted| Z1["no-op, return tdes.state"]
C -->|yes| D{"topops.last >= 0?"}
D -->|yes| D1["assert false\nattach_to_outer until < 0"]
D -->|no| E{"distributed 2PC?"}
D1 --> E
E -->|yes| E1["log_2pc_commit\nsee cubrid-2pc.md"]
E -->|no| F["log_commit_local"]
subgraph LOCAL["log_commit_local — 엄격한 순서"]
direction TB
F --> G["tx_lob_locator_clear\nlogtb_complete_mvcc\nboth LOG before state change"]
G --> H["state = WILL_COMMIT\nundo_nxlsa = NULL"]
H --> I{"tail_lsa NULL?"}
I -->|yes, read-only| J["unlock unless retained\nstate = COMMITTED"]
I -->|no| K["log_tran_do_postpone"]
K --> K1{"posp_nxlsa set?"}
K1 -->|yes| K2["append COMMIT_WITH_POSTPONE\nforce, then run LOG_POSTPONE"]
K1 -->|no| L
K2 --> L["log_append_commit_log\n+repl info if HA"]
L --> M["lock_unlock_all\nunless retain_lock"]
M --> N["log_change_tran_as_completed\nstate = COMMITTED\nlogpb_flush_pages = group-commit force"]
end
E1 --> O["log_complete"]
J --> O
N --> O
O --> P["MVCC unblock · recycle trid\nkick checkpoint if due"]
P --> Z2["return state"]
Figure 8-2 — commit 제어 흐름. I/O를 기다리는 유일한 지점은 log_change_tran_as_completed 내부의 logpb_flush_pages(§8.4의 group-commit force)이며, 그 이전의 모든 과정은 북키핑이다. 다이어그램이 인코딩하는 두 가지 순서 불변식에 주목하라. tx_lob_locator_clear와 logtb_complete_mvcc가 기록하는 레코드들은 상태가 WILL_COMMIT으로 이동하기 전에 기록되며, tail_lsa가 비어 있으면 레코드 없이 read-only commit으로 단락된다.
8.6 log_abort와 log_abort_local — 경계 이전의 undo
섹션 제목: “8.6 log_abort와 log_abort_local — 경계 이전의 undo”log_abort는 log_commit의 진입 검증을 미러링하되 두 가지 추가 가드를 붙이고, log_abort_local -> log_complete로 분기한다.
// log_abort (excerpt) — src/transaction/log_manager.cif (LOG_HAS_LOGGING_BEEN_IGNORED ()) { er_set (... ER_LOG_CORRUPTED_DB_DUE_NOLOGGING ...); return tdes->state; } /* <- no log to undo */if (!LOG_ISTRAN_ACTIVE (tdes) && !LOG_ISTRAN_2PC_PREPARE (tdes)) return tdes->state; /* <- nothing to abort */// topops.last >= 0 -> same assert+attach salvage as commitstate = log_abort_local (thread_p, tdes, true);state = log_complete (thread_p, tdes, LOG_ABORT, LOG_NEED_NEWTRID, LOG_NEED_TO_WRITE_EOT_LOG);추가된 LOG_HAS_LOGGING_BEEN_IGNORED 가드가 commit과의 핵심 차이다. undo 레코드가 없는 상태에서는 rollback이 불가능하므로 데이터베이스는 손상된 것으로 선언된다. log_abort_local이 log_commit_local과 다른 점은 순서에 있다. 상태를 TRAN_UNACTIVE_ABORTED로 먼저 설정한 뒤 작업을 수행한다.
// log_abort_local — src/transaction/log_manager.ctdes->state = TRAN_UNACTIVE_ABORTED; /* <- set early; rollback logs CLRs, allowed */if (!LSA_ISNULL (&tdes->tail_lsa)) /* <- transaction touched data */ { log_rollback (thread_p, tdes, NULL); /* §8.6.1 — the undo pass */ log_cleanup_modified_class_list (thread_p, tdes, NULL, true, true); /* + free first_save_entry */ }/* both branches: */ logtb_complete_mvcc (thread_p, tdes, false); /* committed=false -> discard mvccid */lock_unlock_all (thread_p); /* <- always release; abort never retains */tx_lob_locator_clear (thread_p, tdes, false, NULL);return tdes->state;flowchart TD
A["log_abort(tran_index)"] --> B{"logging been ignored?"}
B -->|yes| Z0["ER_LOG_CORRUPTED_DB_DUE_NOLOGGING\nreturn — no undo records exist"]
B -->|no| C{"active or 2PC-prepared?"}
C -->|no| Z1["nothing to abort, return"]
C -->|yes| D["topops salvage\nassert + attach_to_outer"]
D --> E["log_abort_local"]
subgraph LOCAL["log_abort_local — 상태를 먼저 설정"]
direction TB
E --> F["state = ABORTED\nset early: rollback may log CLRs"]
F --> G{"tail_lsa NULL?"}
G -->|no| H["log_rollback\nwalk prev_tranlsa backward,\nappend compensating CLRs"]
H --> I["log_cleanup_modified_class_list"]
G -->|yes| J
I --> J["logtb_complete_mvcc false\ndiscard mvccid"]
J --> K["lock_unlock_all\nalways — abort never retains"]
end
K --> L["log_complete LOG_ABORT"]
L --> M{"tail_lsa non-NULL?"}
M -->|yes| N["log_append_abort_log\nlog_change_tran_as_completed\nforce only if checkpoint in flight"]
M -->|no| O["set ABORTED, no EOT record"]
N --> P["recycle trid"]
O --> P
P --> Z2["return ABORTED"]
Figure 8-3 — abort 제어 흐름. Figure 8-2와 대칭을 이루지만 의도적인 비대칭이 두 가지 있다. (1) 상태 우선: log_abort_local은 작업을 수행하기 전에 ABORTED를 설정한다. rollback 패스 자체가 compensating 레코드(CLR)를 기록하며, 이 레코드들은 상태가 전환된 이후에도 허용되어야 하기 때문이다. commit에서 WILL_COMMIT 이후 로그 기록이 금지되는 것과 정반대다. (2) 지연 force: 플러시되지 않은 LOG_ABORT를 잃어도 무해하므로(recovery가 어차피 다시 undo함), 내구성 force는 재시작된 서버에서 checkpoint가 진행 중일 때만 실행된다.
상태를 먼저 설정하는 것이 여기서는 안전하지만 commit에서는 금지된다. 그 이유는 rollback이 compensating log record(CLR) — Chapter 9에서 다루는 redo 전용 레코드 — 를 기록하기 때문이다. 이 레코드들은 트랜잭션이 이미 aborted 상태일 때 기록되도록 설계되어 있다. logtb_complete_mvcc(..., false)는 MVCCID를 폐기하며, log_abort_local은 retain_lock을 무시한다. abort는 항상 lock_unlock_all을 호출한다.
8.6.1 log_rollback — prev_tranlsa를 역방향으로 걷기
섹션 제목: “8.6.1 log_rollback — prev_tranlsa를 역방향으로 걷기”log_rollback은 tdes->undo_nxlsa에서 출발해 체인을 역방향으로 걸으며, 각 undo 이미지를 재적용한다. 레코드 타입별 CLR 생성은 Chapter 9에서 다루며, 여기서 중요한 분기는 커서 규율이다.
// log_rollback (control skeleton) — src/transaction/log_manager.cLSA_COPY (&prev_tranlsa, &tdes->undo_nxlsa); /* start cursor */while (!LSA_ISNULL (&prev_tranlsa) && !isdone) { logpb_fetch_page (...); /* fatal on error */ log_rec = LOG_GET_LOG_RECORD_HEADER (log_pgptr, &log_lsa); LSA_COPY (&prev_tranlsa, &log_rec->prev_tranlsa); /* advance cursor BEFORE undo */ LSA_COPY (&tdes->undo_nxlsa, &prev_tranlsa); /* persist cursor (CLR may move it) */ switch (log_rec->type) { /* ... see Chapter 9 ... */ } }불변식 — undo 커서는 undo를 적용하기 전에 먼저 전진한다. prev_tranlsa와 tdes->undo_nxlsa 모두 undo가 실행되기 전에 log_rec->prev_tranlsa로 이동한다. undo를 적용하면 체인된 CLR이 기록되는데, 아직 전진하지 않은 커서는 해당 레코드를 다시 undo하거나 CLR 자체의 링크를 따라갈 수 있기 때문이다. upto_lsa(여기서는 NULL, log_rollback_to_savepoint에서는 non-NULL)는 부분 rollback을 일찍 중단시키며, xlogtb_reset_wait_msecs(INFINITE_WAIT)은 락 타임아웃을 차단한다. recovery 시점의 재실행은 cubrid-recovery-manager.md에 있다.
8.7 재시작 변형 — log_abort_by_tdes와 log_abort_all_active_transaction
섹션 제목: “8.7 재시작 변형 — log_abort_by_tdes와 log_abort_all_active_transaction”셧다운 또는 크래시 recovery 시에는 트랜잭션을 소유자가 아닌 다른 thread가 abort해야 한다. log_abort_by_tdes는 실행 중인 thread를 피해자의 tran_index에 재바인딩해, log_abort 내부의 모든 LOG_FIND_THREAD_TRAN_INDEX 조회가 올바르게 해석되도록 한 뒤 일반 경로를 재사용한다.
// log_abort_by_tdes — src/transaction/log_manager.c (SERVER_MODE)thread_p->tran_index = tdes->tran_index; /* impersonate the victim's index */pthread_mutex_unlock (&thread_p->tran_index_lock);(void) log_abort (thread_p, tdes->tran_index); /* reuse the normal abort path */log_abort_all_active_transaction은 셧다운 스윕이다. server mode에서는 모든 인덱스를 순회하며 각 활성 트랜잭션에 비동기 abort를 디스패치하고, worker thread가 남아 있지 않을 때까지 반복한다. 디스패치는 직접적이지 않다. css_push_external_task가 log_abort_task_execute를 큐에 넣고, 이 래퍼가 log_abort_by_tdes(&thread_ref, &tdes)를 호출한다.
// log_abort_all_active_transaction (server-mode essence) — src/transaction/log_manager.cif (already_called) return; already_called = 1; /* <- idempotent static guard */loop: repeat_loop = false; for (i = 0; i < log_Gl.trantable.num_total_indices; i++) if (i != LOG_SYSTEM_TRAN_INDEX && (tdes = LOG_FIND_TDES (i)) && tdes->trid != NULL_TRANID) { if (css_count_transaction_worker_threads (...) > 0) repeat_loop = true; /* still busy */ else if (LOG_ISTRAN_ACTIVE (tdes) && !abort_thread_running[i]) { /* exec_f = std::bind (log_abort_task_execute, _1, std::ref (*tdes)); */ css_push_external_task (...); /* -> log_abort_task_execute -> log_abort_by_tdes */ abort_thread_running[i] = 1; repeat_loop = true; } } if (repeat_loop) { thread_sleep (50); if (css_is_shutdown_timeout_expired ()) _exit (0); goto loop; } /* <- give up: hard exit */already_called는 스윕을 한 번만 실행시키며, LOG_SYSTEM_TRAN_INDEX는 건너뛴다. 동작 중인 worker가 있는 트랜잭션은 다음 패스를 강제하고, 타임아웃이 만료되면 _exit(0)으로 종료한다. SA_MODE 분기는 테이블을 순회하며 log_abort를 동기적으로 호출한다.
8.8 장 요약 — 핵심 정리
섹션 제목: “8.8 장 요약 — 핵심 정리”- 경계 레코드는 전체 파이프라인을 재사용한다 — 데이터 레코드와 동일하게 구성/attach되며(Chapter 3-4), 단일 필드
log_rec_donetime의 LSA가 내구적 commit 지점이다. log_commit은 분기하고log_commit_local이 실제 작업을 한다 — postpone, append, unlock, force가 차례로 이루어지며,log_complete는 레코드가 이미 기록된 상태에서(LOG_ALREADY_WROTE_EOT_LOG) 상태만 최종 확정한다.- 순서 규율은 commit 중 checkpoint로부터 보호한다 — 로그를 기록하는 모든 작업은
TRAN_UNACTIVE_WILL_COMMIT이전에 실행된다. - commit은 force하고 abort는 대체로 하지 않는다 —
logpb_flush_pages는 commit에서 항상 실행되며(group-commit, Chapter 7), abort에서는 재시작된 서버에서 checkpoint가 진행 중일 때만 실행된다. - abort는 상태를 먼저 설정한 뒤 undo한다 —
log_rollback은 CLR이 체인에 재진입하지 않도록 undo 전에 커서를 전진시킨다. retain_lock은 commit 전용 옵션이다 — abort는 항상 unlock한다.- 재시작 변형은 재바인딩할 뿐 재구현하지 않는다 —
log_abort_by_tdes는tran_index를 가장해log_abort를 호출하고,log_abort_all_active_transaction은 worker가 소진되거나 타임아웃이_exit(0)을 강제할 때까지log_abort_task_execute를 멱등하게 디스패치한다.
Chapter 9: 시스템 연산 — Postpone와 Compensation
섹션 제목: “Chapter 9: 시스템 연산 — Postpone와 Compensation”시스템 연산(sysop, 또는 “top operation”)은 서버가 상위 사용자 트랜잭션과 독립적으로 commit하거나 abort하는 하위 트랜잭션이다. 인덱스 분할, 파일 할당, 오버플로우 레코드 관리 같은 작업이 여기에 해당한다. 이 챕터에서는 sysop, postponed 액션, 그리고 compensation 레코드가 prior-list 파이프라인(Chapter 3–5)을 어떻게 재사용하면서 동시에 고유한 logical-undo 페이로드를 갖추는지를 추적한다. WAL/postpone/ARIES 이론은 cubrid-log-manager.md의 “System operations”, “Postpone & compensation” 절을 참조한다. 모든 계열은 prior_lsa_alloc_and_copy_data + prior_lsa_next_record를 호출한다. 이 챕터의 핵심은 어떤 헤더가 찍히는가, 그리고 그 과정에서 tdes의 sysop 스택과 LSA 체인(undo_nxlsa, posp_nxlsa, 레벨별 posp_lsa)이 어떻게 변하는가에 있다.
9.1 log_tdes의 sysop 스택
섹션 제목: “9.1 log_tdes의 sysop 스택”log_sysop_start는 로그 레코드를 아무것도 기록하지 않는다. 대신 트랜잭션 디스크립터의 인메모리 스택에 프레임 하나를 푸시한다. 아래 표는 sysop/postpone와 직접 관련된 log_tdes 필드만 정리한 것이며, 전체 약 82개 필드로 구성된 struct는 Chapter 2에서 확인할 수 있다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
topops (LOG_TOPOPS_STACK) | 활성 sysop의 중첩 스택 | last는 현재 깊이, max는 할당된 크기 |
topop_lsa | 가장 최근 sysop의 부모 LSA | appender가 “sysop 안에 있는가”를 빠르게 확인하기 위한 프로브 |
tail_lsa | 이 트랜잭션의 마지막으로 추가된 레코드 LSA | sysop end가 “변경 없음”을 감지할 때 비교하는 상한선 |
undo_nxlsa | 다음으로 undo할 레코드 | CLR이 되감아서 이미 undo된 레코드를 건너뛰게 한다 |
posp_nxlsa | 첫 번째 트랜잭션 레벨 postpone 레코드 | 어떤 sysop 밖에서 추가된 LOG_POSTPONE가 여기에 씨앗을 뿌린다 |
savept_lsa | 마지막 savepoint의 LSA | savepoint 체인; log_abort_partial의 도착 지점 |
tail_topresult_lsa | 마지막 partial commit/abort의 LSA | 모든 sysop-end에 prv_topresult_lsa로 찍힌다 |
state (TRAN_STATE) | 트랜잭션 상태 | 어떤 sysop-end 분기가 합법적인지를 제한한다 |
m_log_postpone_cache | 캐시된 postpone redo + LSA | do_postpone가 메모리에서 재실행할 수 있게 한다 |
rcv.sysop_start_postpone_lsa | 진행 중인 sysop postpone의 복구 앵커 | 크래시 후 sysop의 postpone 단계를 재개하기 위한 기준점 |
rcv.tran_start_postpone_lsa | 트랜잭션 레벨 postpone의 복구 앵커 | 트랜잭션 postpone 단계 재개를 위한 기준점 |
rcv.atomic_sysop_start_lsa | atomic sysop의 복구 앵커 | 중단된 atomic sysop을 단일 단위로 롤백하는 데 사용 |
(rcv.* 멤버는 내장된 log_rcv_tdes 안에 있다.) 스택의 각 프레임은 두 개의 LSA를 갖는 log_topops_addresses이며, 세 개의 접근자 매크로를 통해 읽는다.
// log_topops_addresses -- src/transaction/log_impl.hstruct log_topops_addresses{ LOG_LSA lastparent_lsa; /* The last address of the parent transaction. This is needed for undo of the top * system action */ LOG_LSA posp_lsa; /* The first address of a postpone log record for top system operation. We add this * since it is reset during recovery to the last reference postpone address. */};// LOG_TDES_LAST_SYSOP* -- src/transaction/log_manager.c#define LOG_TDES_LAST_SYSOP(tdes) (&(tdes)->topops.stack[(tdes)->topops.last])#define LOG_TDES_LAST_SYSOP_PARENT_LSA(tdes) (&LOG_TDES_LAST_SYSOP(tdes)->lastparent_lsa)#define LOG_TDES_LAST_SYSOP_POSP_LSA(tdes) (&LOG_TDES_LAST_SYSOP(tdes)->posp_lsa)flowchart LR
subgraph tdes["log_tdes"]
tail["tail_lsa"]
posp["posp_nxlsa (트랜잭션 레벨)"]
undo["undo_nxlsa"]
stk["topops.stack[]"]
end
stk --> f0["[0] lastparent_lsa, posp_lsa"]
stk --> fl["[last] lastparent_lsa, posp_lsa"]
Figure 9-1: sysop 스택과 log_tdes를 통해 연결되는 LSA 앵커들.
불변식 — 부모 LSA는 sysop 본문의 경계를 정한다. sysop이 추가하는 모든 레코드는
tail_lsa > LOG_TDES_LAST_SYSOP_PARENT_LSA(tdes)조건을 만족한다. end 함수는LSA_LE(&tdes->tail_lsa, parent_lsa)조건으로 비어 있는 sysop을 감지한다. 이 불변식이 깨지면 end가 유령 레코드를 기록하거나 필요한 end 마커를 건너뛰어 로그 중첩이 스택과 어긋나게 된다.log_sysop_commit_internal과log_sysop_abort에서 강제한다.
9.2 log_sysop_start와 log_sysop_start_atomic
섹션 제목: “9.2 log_sysop_start와 log_sysop_start_atomic”// log_sysop_start -- src/transaction/log_manager.cif (tdes->topops.max == 0 || (tdes->topops.last + 1) >= tdes->topops.max) /* first-alloc OR full */ if (logtb_realloc_topops_stack (tdes, 1) == NULL) /* OOM: bail, stack unchanged */ { assert (false); tdes->unlock_topop (); return; }// ... condensed: VACUUM_IS_THREAD_VACUUM diagnostic logging only ...tdes->topops.last++; /* <- push */LSA_COPY (&tdes->topops.stack[tdes->topops.last].lastparent_lsa, &tdes->tail_lsa);LSA_COPY (&tdes->topop_lsa, &tdes->tail_lsa);LSA_SET_NULL (&tdes->topops.stack[tdes->topops.last].posp_lsa); /* <- no postpone yet */topops.max == 0 조건은 트랜잭션의 첫 번째 sysop(스택이 아직 없는 경우)을 처리하고, 두 번째 조건은 스택이 꽉 찼을 때 확장한다. 분기는 다음과 같다. (1) tdes == NULL → ER_LOG_UNKNOWN_TRANINDEX 치명 오류 조기 반환, (2) realloc 실패 시 OOM 언락 후 푸시 없이 반환, (3) VACUUM 진단 로그만 처리, (4) 정상 경로는 tail_lsa를 lastparent_lsa에 저장하고 posp_lsa를 null로 초기화한다.
log_sysop_start_atomic은 이 함수를 감싸고 나서, 복구 시 전체 atomic sysop을 하나의 단위로 롤백할 수 있도록 LOG_SYSOP_ATOMIC_START 마커가 하나 존재하는지 확인한다.
// log_sysop_start_atomic -- src/transaction/log_manager.clog_sysop_start (thread_p); /* ... re-fetch tdes, guard ... */if (LSA_ISNULL (&tdes->rcv.atomic_sysop_start_lsa)) /* first atomic level: emit marker */ { node = prior_lsa_alloc_and_copy_data (thread_p, LOG_SYSOP_ATOMIC_START, ...); (void) prior_lsa_next_record (thread_p, node, tdes); }else { assert (tdes->topops.last > 0); /* nested: parent already marked */ assert (LSA_ISNULL (&tdes->rcv.sysop_start_postpone_lsa)); }else 분기는 중첩된 atomic sysop에서 실행된다. 외부 레벨이 atomic_sysop_start_lsa를 소유하므로 내부 sysop은 두 번째 마커 없이 원자성을 상속받는다. assert는 “sysop-postpone가 실행 중인 동안에는 atomic start가 없다”는 규칙을 코드로 표현한 것이다.
9.3 sysop-end 유니온과 여섯 가지 분기
섹션 제목: “9.3 sysop-end 유니온과 여섯 가지 분기”abort를 제외한 모든 end는 log_sysop_commit_internal을 통해 처리되며, 이 함수가 log_rec_sysop_end를 기록한다(주석은 소스 원문 그대로).
// log_rec_sysop_end -- src/transaction/log_record.hppstruct log_rec_sysop_end{ LOG_LSA lastparent_lsa; /* last address before the top action */ LOG_LSA prv_topresult_lsa; /* previous top action (either, partial abort or partial commit) address */ LOG_SYSOP_END_TYPE type; /* end system op type */ const VFID *vfid; /* File where the page belong. ... used to get TDE information. */ union /* other info based on type */ { LOG_REC_UNDO undo; /* undo data for logical undo */ LOG_REC_MVCC_UNDO mvcc_undo; /* undo data for logical undo of MVCC operation */ LOG_LSA compensate_lsa; /* compensate lsa for logical compensate */ struct { LOG_LSA postpone_lsa; bool is_sysop_postpone; } run_postpone; /* run postpone info */ };};| 필드 | 역할 | 존재 이유 |
|---|---|---|
lastparent_lsa | sysop 본문이 시작되는 위치 | 복구 undo가 여기서 멈춘다. 프레임에서 복사된다 |
prv_topresult_lsa | 이전 partial commit/abort의 LSA | 복구가 top result를 역방향으로 탐색할 수 있도록 체인을 잇는다 |
type | 유니온의 어떤 멤버가 유효한가 | append와 복구의 디스패치 키 |
vfid | 영향받은 페이지가 속한 파일 | TDE 키 조회; MVCC vacuum 정보로도 쓰인다 |
undo | LOGICAL_UNDO용 LOG_REC_UNDO 페이로드 | logical undo 재실행을 위한 rcvindex + 길이 |
mvcc_undo | LOGICAL_MVCC_UNDO용 LOG_REC_MVCC_UNDO 페이로드 | mvccid와 vacuum_info를 추가한다 |
compensate_lsa | LOGICAL_COMPENSATE의 undo 건너뛰기 대상 | 이 logical compensation 이후의 next-undo LSA |
run_postpone.postpone_lsa | 원본 LOG_POSTPONE의 LSA | run-postpone을 그 원천에 연결한다 |
run_postpone.is_sysop_postpone | sysop postpone인지 tran postpone인지 구분하는 플래그 | 복구가 어떤 postpone 단계에서 생성됐는지 알아야 한다 |
같은 바이트를 여섯 가지로 해석한다. 각 래퍼(§9.4)는 자신의 type에 해당하는 멤버만 채운다.
type | 활성 멤버 | 생성 함수 |
|---|---|---|
LOG_SYSOP_END_COMMIT | (없음) | log_sysop_commit |
LOG_SYSOP_END_ABORT | (없음) | log_sysop_abort |
LOG_SYSOP_END_LOGICAL_UNDO | undo | log_sysop_end_logical_undo (non-MVCC) |
LOG_SYSOP_END_LOGICAL_MVCC_UNDO | mvcc_undo | log_sysop_end_logical_undo (MVCC) |
LOG_SYSOP_END_LOGICAL_COMPENSATE | compensate_lsa | log_sysop_end_logical_compensate |
LOG_SYSOP_END_LOGICAL_RUN_POSTPONE | run_postpone | log_sysop_end_logical_run_postpone |
9.4 log_sysop_commit_internal — 분기 완전 분석
섹션 제목: “9.4 log_sysop_commit_internal — 분기 완전 분석”호출자가 log_record->type을 설정하면, commit_internal은 state와 type의 조합이 유효한지 검증하고, 대기 중인 postpone를 실행한 뒤, end 레코드를 추가한다(Figure 9-2).
flowchart TD
A["commit_internal(log_record)"] --> B{"tdes == NULL?"}
B -->|yes| Z["assert_release; return"]
B -->|no| C{"빈 sysop\nAND COMMIT 또는 no_logging?"}
C -->|yes| D["posp_lsa NULL 검증\nno-op end"]
C -->|no| F{"switch type"}
F -->|RUN_POSTPONE| G["*_COMMITTED_WITH_POSTPONE 검증\nis_sysop_postpone 설정"]
F -->|COMPENSATE| H["aborting 또는 rv-finish 검증"]
F -->|UNDO / MVCC_UNDO| I["상태 제한 없음"]
F -->|COMMIT| J["postpone 단계 중이면 불가\nrv-finish 제외"]
G --> K
H --> K
I --> K
J --> K["lastparent_lsa, prv_topresult_lsa 채우기"]
K --> M["do_postpone -> append_sysop_end -> tail_topresult_lsa = tail_lsa"]
D --> P["log_sysop_end_final"]
M --> P
Figure 9-2: log_sysop_commit_internal의 모든 분기.
// log_sysop_commit_internal -- src/transaction/log_manager.cassert (log_record->type != LOG_SYSOP_END_ABORT); /* aborts never come here */if ((LSA_ISNULL (&tdes->tail_lsa) || LSA_LE (&tdes->tail_lsa, LOG_TDES_LAST_SYSOP_PARENT_LSA (tdes))) && (log_record->type == LOG_SYSOP_END_COMMIT || log_No_logging)) assert (LSA_ISNULL (&LOG_TDES_LAST_SYSOP (tdes)->posp_lsa)); /* empty COMMIT: nothing to log */else { if (log_record->type == LOG_SYSOP_END_LOGICAL_RUN_POSTPONE) { assert (tdes->state == TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE || tdes->state == TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE); log_record->run_postpone.is_sysop_postpone = /* recovery needs which phase */ (tdes->state == TRAN_UNACTIVE_TOPOPE_COMMITTED_WITH_POSTPONE && !is_rv_finish_postpone); } // ... condensed: COMPENSATE / LOGICAL_UNDO / COMMIT state asserts (see Fig 9-2) ... log_record->lastparent_lsa = *LOG_TDES_LAST_SYSOP_PARENT_LSA (tdes); log_record->prv_topresult_lsa = tdes->tail_topresult_lsa; log_sysop_do_postpone (thread_p, tdes, log_record, data_size, data); /* run postpones */ log_append_sysop_end (thread_p, tdes, log_record, data_size, data); /* emit end */ LSA_COPY (&tdes->tail_topresult_lsa, &tdes->tail_lsa); }log_sysop_end_final (thread_p, tdes); /* always pops the stack */불변식 — end 레코드 type은 트랜잭션 상태와 일치해야 한다.
LOGICAL_RUN_POSTPONE는 오직*_COMMITTED_WITH_POSTPONE상태에서만 허용된다.LOGICAL_COMPENSATE는 aborting 중이거나 복구의 postpone-finish인 경우에만 허용된다. 평범한COMMIT은 복구의is_rv_finish_postpone재진입이 아닌 한 postpone 단계 중에는 절대 허용되지 않는다. 이 규칙이 깨지면 복구가 postpone를 두 번 실행하거나 undo를 건너뛰어 페이지를 손상시킨다.
log_sysop_end_final은 모든 경로에서 실행되므로, 비어 있거나 오류가 발생한 경우에도 topops.last는 감소한다. 네 개의 logical 래퍼는 §9.3의 유니온 멤버를 미리 채운다. log_sysop_end_logical_run_postpone만 is_sysop_postpone를 commit_internal이 state로부터 직접 파생하도록 남겨 둔다.
9.5 log_sysop_abort — 롤백 후 마킹
섹션 제목: “9.5 log_sysop_abort — 롤백 후 마킹”abort는 commit_internal을 거치지 않는다. 직접 롤백하고 ABORT end를 찍는다.
// log_sysop_abort -- src/transaction/log_manager.cif (LSA_ISNULL (&tdes->tail_lsa) || LSA_LE (&tdes->tail_lsa, &LOG_TDES_LAST_SYSOP (tdes)->lastparent_lsa)) { /* No change: empty sysop, nothing to undo or log */ }else { save_state = tdes->state; tdes->state = TRAN_UNACTIVE_ABORTED; /* <- so compensation appends are legal */ log_rollback (thread_p, tdes, LOG_TDES_LAST_SYSOP_PARENT_LSA (tdes)); /* undo body, emits CLRs */ sysop_end.type = LOG_SYSOP_END_ABORT; sysop_end.lastparent_lsa = *LOG_TDES_LAST_SYSOP_PARENT_LSA (tdes); sysop_end.prv_topresult_lsa = tdes->tail_topresult_lsa; log_append_sysop_end (thread_p, tdes, &sysop_end, 0, NULL); LSA_COPY (&tdes->tail_topresult_lsa, &tdes->tail_lsa); tdes->state = save_state; } /* <- restore: sysop abort != tran abort */log_sysop_end_final (thread_p, tdes);state = TRAN_UNACTIVE_ABORTED로 임시 변경하는 것은 핵심 동작이다. 이 상태여야 log_rollback이 CLR을 추가할 수 있고(§9.8), 이후 원래 상태로 복원함으로써 외부 트랜잭션이 영향받지 않는다.
9.6 log_append_postpone — 커밋 이후로 액션 미루기
섹션 제목: “9.6 log_append_postpone — 커밋 이후로 액션 미루기”LOG_POSTPONE는 지금 당장 적용하지 않고 commit 이후에 재실행할 redo-only 액션을 기록한다.
flowchart TD
A["log_append_postpone"] --> B{"log_No_logging?"}
B -->|yes| C["redofun 즉시 실행; return"]
B -->|no| E{"skipredo 또는\nsysop 없고 active/aborted 아님?"}
E -->|yes| F["redofun 즉시 실행;\n!skipredo이면 append_redo_data; return"]
E -->|no| G{"tail_lsa NULL 또는\ncrash point 이전?"}
G -->|yes| H["LOG_DUMMY_HEAD_POSTPONE 추가"]
G -->|no| J
H --> J["LOG_POSTPONE 할당; redo + start_lsa 캐시"]
J --> N{"sysop 안에 있는가?"}
N -->|yes, posp_lsa NULL| O["frame.posp_lsa = tail_lsa"]
N -->|no, posp_nxlsa NULL| P["posp_nxlsa = tail_lsa"]
Figure 9-3: log_append_postpone 분기.
redo를 동기적으로 실행하는 탈출구가 두 개 있다(log_No_logging, 또는 미룰 수 없는 경우 — Figure 9-3). 그 외의 경우에는 레코드가 추가되고, redo와 start LSA가 m_log_postpone_cache에 저장된 뒤 적절한 앵커에 씨앗이 뿌려진다.
// log_append_postpone -- src/transaction/log_manager.cnode = prior_lsa_alloc_and_copy_data (thread_p, LOG_POSTPONE, rcvindex, addr, 0, NULL, length, (char *) data);tdes->m_log_postpone_cache.add_redo_data (*node); /* save before node may be freed */start_lsa = prior_lsa_next_record (thread_p, node, tdes);tdes->m_log_postpone_cache.add_lsa (start_lsa);if (tdes->topops.last >= 0) /* in sysop: seed frame anchor */ { if (LSA_ISNULL (&tdes->topops.stack[tdes->topops.last].posp_lsa)) LSA_COPY (&tdes->topops.stack[tdes->topops.last].posp_lsa, &tdes->tail_lsa); }else if (LSA_ISNULL (&tdes->posp_nxlsa)) /* tran level: seed tran anchor */ LSA_COPY (&tdes->posp_nxlsa, &tdes->tail_lsa);불변식 — 첫 번째 postpone는 정확히 하나의 앵커에 씨앗을 뿌린다. sysop 레벨에서 가장 먼저 나타난
LOG_POSTPONE가 해당 프레임의posp_lsa를 설정하고, 트랜잭션 레벨에서 가장 먼저 나타난 것이posp_nxlsa를 설정한다. 이후의 postpone는LSA_ISNULL가드 덕분에 이 값을 건드리지 않는다. 이 앵커가 postpone 재실행 스캔의 시작점이므로, 덮어쓰면 앞선 postpone가 고아가 된다.
9.7 Postpone 단계: log_sysop_do_postpone, log_do_postpone, log_run_postpone_op
섹션 제목: “9.7 Postpone 단계: log_sysop_do_postpone, log_do_postpone, log_run_postpone_op”대기 중인 postpone가 있는 sysop이 종료될 때, log_sysop_do_postpone는 LOG_SYSOP_START_POSTPONE 마커를 기록한 뒤 재실행을 시작한다. 이 마커의 헤더인 log_rec_sysop_start_postpone(log_record.hpp)는 전체 end 레코드를 저장해 두어 크래시 후 복구가 완료할 수 있게 한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
sysop_end (LOG_REC_SYSOP_END) | “시스템 연산 종료에 사용되는 로그 레코드” | 크래시 후 log_sysop_end_recovery_postpone가 올바른 end를 다시 기록할 수 있도록 한다 |
posp_lsa | ”첫 번째 postpone 연산이 시작되는 주소” | 크래시 후 재실행 스캔이 재개할 위치 |
// log_sysop_do_postpone -- src/transaction/log_manager.cif (LSA_ISNULL (LOG_TDES_LAST_SYSOP_POSP_LSA (tdes))) { return; } /* nothing to postpone */sysop_start_postpone.sysop_end = *sysop_end;sysop_start_postpone.posp_lsa = *LOG_TDES_LAST_SYSOP_POSP_LSA (tdes);log_append_sysop_start_postpone (thread_p, tdes, &sysop_start_postpone, data_size, data);if (tdes->m_log_postpone_cache.do_postpone (*thread_p, *(LOG_TDES_LAST_SYSOP_POSP_LSA (tdes)))) { tdes->state = save_state; return; } /* fast path: replay from memory */log_do_postpone (thread_p, tdes, LOG_TDES_LAST_SYSOP_POSP_LSA (tdes)); /* slow path: scan the log */트랜잭션 레벨에 대응하는 함수는 log_append_commit_postpone이다. 이 함수는 LOG_COMMIT_WITH_POSTPONE를 기록하고(log_rec_start_postpone 헤더, log_record.hpp), 상태를 TRAN_UNACTIVE_COMMITTED_WITH_POSTPONE로 전환하며, commit이 postpone 실행 전에 내구성을 갖도록 flush한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
posp_lsa | 첫 번째 트랜잭션 postpone 연산이 시작되는 주소 | commit 후 재실행 스캔을 위한 앵커; 복구 중에는 마지막 참조 주소로 재설정된다 |
at_time | ”완료 시각. 시간 지정 복구를 위한 정보” | point-in-time / 시간 지정 복구를 위해 commit-postpone 시점을 기록한다 |
log_do_postpone는 느린 경로의 순방향 스캔 함수다. log_get_next_nested_top으로 중첩된 top 본문을 건너뛰고 log_rec->type에 따라 분기한다. LOG_POSTPONE만 재실행을 트리거하고, 시작 마커 그룹(LOG_COMMIT_WITH_POSTPONE[_OBSOLETE], LOG_SYSOP_START_POSTPONE, LOG_2PC_*)은 LSA_SET_NULL(&forward_lsa)로 루프를 종료한다. 데이터/redo/CLR/savepoint 분기는 이미 적용된 것이므로 비활성 상태다. default는 “잘못된 log_rectype”이다. log_run_postpone_op는 redo를 읽어 실행하며, 페이지 경계를 넘는 경우 malloc 실패는 치명 오류로 처리되어 정상 반환하지 않는다.
// log_run_postpone_op -- src/transaction/log_manager.cLSA_COPY (&ref_lsa, log_lsa); /* remember the original postpone LSA */// ... condensed: advance past LOG_RECORD_HEADER + LOG_REC_REDO header ...redo = *((LOG_REC_REDO *) ((char *) log_pgptr->area + log_lsa->offset));if (log_lsa->offset + redo.length < (int) LOGAREA_SIZE) rcv_data = (char *) log_pgptr->area + log_lsa->offset; /* contiguous: point in place */else { area = (char *) malloc (redo.length); /* spans pages: need contiguous copy */ if (area == NULL) { logpb_fatal_error (thread_p, true, ARG_FILE_LINE, "log_run_postpone_op"); return ER_FAILED; } logpb_copy_from_log (thread_p, area, redo.length, log_lsa, log_pgptr); rcv_data = area; }(void) log_execute_run_postpone (thread_p, &ref_lsa, &redo, rcv_data);if (area != NULL) free_and_init (area);ref_lsa는 run-postpone 레코드(log_rec_run_postpone, log_record.hpp)에 기록되어 복구가 어떤 postpone가 이미 실행됐는지 파악할 수 있게 한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
data (LOG_DATA) | “복구 데이터의 위치” (rcvindex, vpid, offset) | postpone가 건드린 페이지와 redo 함수를 특정한다 |
ref_lsa | ”원본 postpone 레코드의 주소” | 두 번째 복구 패스가 이 LSA와 대조해 이미 실행된 postpone를 건너뛴다 |
length | ”redo 데이터의 길이” | redo 복사본의 경계를 정한다 |
log_append_run_postpone는 WILL_COMMIT 또는 *_COMMITTED_WITH_POSTPONE 상태인지 assert하고, 세 필드를 채워 추가하며 페이지 LSA를 설정한다. 이로써 두 번째 복구 패스에서도 이 액션이 멱등성을 갖는다.
9.8 Compensation: log_append_compensate_internal과 undo_nxlsa 되감기
섹션 제목: “9.8 Compensation: log_append_compensate_internal과 undo_nxlsa 되감기”CLR(Compensation Log Record, LOG_COMPENSATE)은 undo의 redo를 기록하여, 크래시 후 undo가 다시 수행되지 않도록 한다. 헤더는 log_rec_compensate(log_record.hpp)이다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
data (LOG_DATA) | “복구 데이터의 위치” (rcvindex, pageid, offset, volid) | 보상 redo가 다시 적용할 페이지의 위치를 나타낸다 |
undo_nxlsa | ”다음으로 undo할 로그 레코드의 주소” | 복구 undo가 여기로 건너뛰어 보상된 레코드를 다시 undo하지 않는다 |
length | ”보상 데이터의 길이” | redo 페이로드의 경계를 정한다 |
// log_append_compensate_internal -- src/transaction/log_manager.cnode = prior_lsa_alloc_and_copy_data (thread_p, LOG_COMPENSATE, rcvindex, NULL, length, (char *) data, 0, NULL);LSA_COPY (&prev_lsa, &tdes->undo_nxlsa); /* remember where we were */compensate = (LOG_REC_COMPENSATE *) node->data_header;// ... condensed: fill compensate->data {rcvindex, pageid, offset, volid}, length ...if (undo_nxlsa != NULL) LSA_COPY (&compensate->undo_nxlsa, undo_nxlsa); /* explicit skip target */else LSA_COPY (&compensate->undo_nxlsa, &prev_lsa); /* default: current link */start_lsa = prior_lsa_next_record (thread_p, node, tdes);if (pgptr != NULL) pgbuf_set_lsa (thread_p, pgptr, &start_lsa); /* TDE/page-LSA only when fixed */LSA_COPY (&tdes->undo_nxlsa, &prev_lsa); /* <- rewind: undo continues from BEFORE this CLR */불변식 — CLR은 redo-only이며 undo 커서를 자신의 위치 이전으로 되감는다. append 이후
tdes->undo_nxlsa는prev_lsa로 재설정되고, CLR 자체의undo_nxlsa는 다음으로 undo해야 할 레코드를 가리킨다. 복구 undo 중 CLR을 만나면undo_nxlsa로 건너뛰고, 이 CLR이 나타내는 undo는 절대 다시 적용하지 않는다. 되감기를 빠뜨리면 페이지가 이중으로 undo된다.pgptr != NULL가드는 페이지를 fix할 수 없는 복구 상황에서 CLR이 기록될 때를 처리한다. 이 경우 TDE와pgbuf_set_lsa는 건너뛴다.
동급 함수인 log_sysop_end_logical_compensate(§9.3)는 sysop-end 레코드의 compensate_lsa를 통해 sysop 단위로 같은 건너뛰기를 수행한다.
9.9 Savepoint와 partial abort
섹션 제목: “9.9 Savepoint와 partial abort”log_append_savepoint는 LOG_SAVEPOINT를 log_rec_savept 헤더와 함께 체인으로 연결한다(log_record.hpp).
| 필드 | 역할 | 존재 이유 |
|---|---|---|
prv_savept | ”이전 savepoint 레코드”의 LSA | log_get_savepoint_lsa가 이름으로 역방향 탐색할 수 있도록 단방향 연결 리스트를 구성한다 |
length | ”Savepoint 이름” 길이 (이름은 레코드 뒤에 붙는다) | 가변 길이 이름 복사의 경계를 정한다 |
// log_append_savepoint -- src/transaction/log_manager.cif (!LOG_ISTRAN_ACTIVE (tdes)) { er_set (... ER_LOG_CANNOT_ADD_SAVEPOINT ...); return NULL; }if (savept_name == NULL) { er_set (... ER_LOG_NONAME_SAVEPOINT ...); return NULL; }node = prior_lsa_alloc_and_copy_data (thread_p, LOG_SAVEPOINT, ..., savept_name, ...);savept = (LOG_REC_SAVEPT *) node->data_header;LSA_COPY (&savept->prv_savept, &tdes->savept_lsa); /* <- link to previous savepoint */(void) prior_lsa_next_record (thread_p, node, tdes);LSA_COPY (&tdes->savept_lsa, &tdes->tail_lsa); /* <- this is now the latest savepoint */분기 순서는 다음과 같다. NULL tdes(치명 오류), 비활성 트랜잭션(ER_LOG_CANNOT_ADD_SAVEPOINT), NULL 이름(ER_LOG_NONAME_SAVEPOINT), 이후 정상 추가.
log_abort_partial은 명명된 savepoint까지 롤백하는 함수다. 내부적으로는 sysop 메커니즘을 재사용하는 방식으로 구현되어 있다. savepoint가 부모 LSA인 가상의 sysop을 만들어 처리한다. 실제 본문에 들어가기 전에 다섯 가지 가드가 실행된다.
tdes == NULL→ER_LOG_UNKNOWN_TRANINDEX,TRAN_UNACTIVE_UNKNOWN반환.LOG_HAS_LOGGING_BEEN_IGNORED ()→ER_LOG_CORRUPTED_DB_DUE_NOLOGGING, 현재state반환.!LOG_ISTRAN_ACTIVE→ 현재state를 조용히 반환.- 이름이 NULL이거나 savepoint를 찾을 수 없는 경우 →
ER_LOG_UNKNOWN_SAVEPOINT,TRAN_UNACTIVE_UNKNOWN반환. - 매달린 sysop이 있는 경우(
topops.last >= 0) → 경고 +assert(false),log_sysop_attach_to_outer로 전부 비운다.
// log_abort_partial -- src/transaction/log_manager.cif (tdes == NULL) { er_set (... ER_LOG_UNKNOWN_TRANINDEX ...); return TRAN_UNACTIVE_UNKNOWN; }if (LOG_HAS_LOGGING_BEEN_IGNORED ()) { er_set (...); return tdes->state; }if (!LOG_ISTRAN_ACTIVE (tdes)) { return tdes->state; }if (savepoint_name == NULL || log_get_savepoint_lsa (...) == NULL) { er_set (... ER_LOG_UNKNOWN_SAVEPOINT ...); return TRAN_UNACTIVE_UNKNOWN; }if (tdes->topops.last >= 0) /* dangling sysops: drain them first */ { er_set (... ER_LOG_HAS_TOPOPS_DURING_COMMIT_ABORT ...); assert (false); while (tdes->topops.last >= 0) log_sysop_attach_to_outer (thread_p); }log_sysop_start (thread_p);LSA_COPY (&tdes->topops.stack[tdes->topops.last].lastparent_lsa, savept_lsa); /* stop at savepoint */// ... condensed: if posp_nxlsa not null, transfer/clamp it into the frame's posp_lsa ...log_sysop_abort (thread_p); /* the actual rollback + CLRs */LSA_COPY (&tdes->savept_lsa, savept_lsa); /* discard newer savepoints */return TRAN_UNACTIVE_ABORTED;Partial abort는 “savepoint부터 현재까지를 아우르는 합성 sysop을 abort한다”는 방식으로 동작한다. 생략된 postpone 앵커 이전 부분은 posp_nxlsa를 프레임의 posp_lsa로 이전하되 savept_lsa로 상한을 잡는다. 이렇게 하면 savepoint 이전에 해당하는 postpone가 소실되지 않는다.
9.10 log_sysop_attach_to_outer — sysop을 부모로 병합
섹션 제목: “9.10 log_sysop_attach_to_outer — sysop을 부모로 병합”sysop은 자신의 상위 범위로 병합될 수 있다. 이때 postpone 앵커만 전달된다.
// log_sysop_attach_to_outer -- src/transaction/log_manager.cif (tdes->topops.last == 0 && (!LOG_ISTRAN_ACTIVE (tdes) || tdes->is_system_transaction ())) { assert_release (false); log_sysop_commit (thread_p); return; } /* nothing to attach to */if (tdes->topops.last - 1 >= 0) /* attach to parent sysop frame */ { if (LSA_ISNULL (&tdes->topops.stack[tdes->topops.last - 1].posp_lsa)) LSA_COPY (&tdes->topops.stack[tdes->topops.last - 1].posp_lsa, &tdes->topops.stack[tdes->topops.last].posp_lsa); }else /* attach to transaction level */ { if (LSA_ISNULL (&tdes->posp_nxlsa)) LSA_COPY (&tdes->posp_nxlsa, &tdes->topops.stack[tdes->topops.last].posp_lsa); }log_sysop_end_final (thread_p, tdes); /* pop, no end record appended */세 개의 분기가 있다. (1) 부착할 대상이 없는 경우 → 실제 commit으로 폴백한다. (2) 부모 sysop이 있는 경우 → 부모에 posp_lsa가 없으면 현재 레벨의 값을 올려 보낸다. (3) 최상위 레벨인 경우 → posp_nxlsa로 올려 보낸다. LOG_SYSOP_END는 기록되지 않으며, sysop의 효과는 부모의 것으로 통합된다.
9.11 챕터 요약 — 핵심 정리
섹션 제목: “9.11 챕터 요약 — 핵심 정리”- Sysop은 스택 프레임으로 시작하며 로그 레코드가 아니다.
log_sysop_start는topops에 푸시하고tail_lsa를lastparent_lsa에 저장한다. 디스크에 남는 첫 번째 흔적은 end 레코드(또는 atomic-start 마커)다. 부모-LSA 불변식(§9.1)이 end 함수가 빈 sysop을 감지하는 수단이다. log_sysop_commit_internal은 여섯 개의 type 기반 분기를 갖는 단일 허브다.log_rec_sysop_end유니온은type에 따라 재해석된다. 이 함수는 분기-상태 조합을 검증하고, postpone를 실행하고, end를 추가하며,tail_topresult_lsa를 체인으로 잇는다.- Abort는 상태 스왑 후 롤백-마킹이다.
log_sysop_abort는TRAN_UNACTIVE_ABORTED로 설정해log_rollback이 CLR을 기록하게 하고,LOG_SYSOP_END_ABORT를 추가한 뒤 외부 상태를 복원한다. - Postpone는 redo를 commit 이후 재실행으로 미루며 앵커는 한 번만 설정된다. 첫 번째
LOG_POSTPONE가posp_lsa(sysop) 또는posp_nxlsa(tran)에 씨앗을 뿌린다. 캐시는 메모리에서,log_do_postpone는 로그에서 재실행하며, 시작 마커를 만나면 중단하고LOG_POSTPONE만 재실행한다. log_run_postpone_op는LOG_RUN_POSTPONE의ref_lsa역방향 포인터를 통해 postpone를 멱등적으로 만든다. 페이지 경계를 넘는malloc실패는 치명 오류다.- Compensation은 redo-only이며 undo 커서를 되감는다.
log_append_compensate_internal은 보상된 레코드를 건너뛰는undo_nxlsa를 가진 CLR을 기록하고tdes->undo_nxlsa를 재설정한다. - Savepoint와 partial abort는 sysop에 편승한다.
log_abort_partial은 다섯 가지 가드를 통과한 뒤 savepoint부터 현재까지를 아우르는 합성 sysop을 만들어log_sysop_abort를 호출한다.log_sysop_attach_to_outer는 end 레코드 없이 sysop을 부모로 병합하며, postpone 앵커만 전달한다.
Chapter 10: Archiving, 헤더 유지보수, 경계 경로
섹션 제목: “Chapter 10: Archiving, 헤더 유지보수, 경계 경로”Chapter 3~9는 레코드 단위의 핫 경로를 추적했다. 이 장은 그 바깥에 있는 요소들을 다룬다. 즉, active log를 archive 볼륨으로 재활용하는 백그라운드 처리, 로그 header의 디스크 내구성 보장, 그리고 경계 레코드 및 손상 검사가 그 대상이다. “active log 대 archives”와 “force-at-commit” 이론은 동반 문서를 참고하라.
10.1 세 가지 header 구조체: 페이지 header, 로그 header, archive header
섹션 제목: “10.1 세 가지 header 구조체: 페이지 header, 로그 header, archive header”모든 로그 페이지는 LOG_HDRPAGE로 시작된다. 논리 페이지 -9 (LOGPB_HEADER_PAGE_ID)에는 LOG_HEADER가 담기고, 모든 archive의 물리적 페이지 0에는 LOG_ARV_HEADER가 담긴다.
flowchart LR hp["active page -9<br/>LOG_HDRPAGE + LOG_HEADER"] -->|"nxarv_pageid"| p0["active data page<br/>LOG_HDRPAGE + records"] p0 -.archived into.-> ap0["archive phy 0<br/>LOG_HDRPAGE + LOG_ARV_HEADER"]
Figure 10-1 — 세 가지 header 구조체와 active-to-archive 복사 관계.
LOG_HDRPAGE — 페이지별 header 접두사. LOG_PAGE는 LOG_HDRPAGE hdr과 char area[1]로 구성된다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
logical_pageid | 무한 로그 내 논리 페이지 ID | 물리적 슬롯과 독립된 식별자. header 페이지는 항상 -9 |
offset | 이 페이지에서 첫 번째 레코드의 오프셋 | 이전 페이지가 손상되어 archive에 없을 때의 복구 앵커 |
flags | TDE 플래그 (..._AES/_ARIA) | 메모리에서 벗어나기 전에 암호화해야 하는 페이지를 표시. header 페이지는 0 |
checksum | 샘플 바이트에 대한 CRC32 | 읽기 시 손상 감지 (10.6절) |
LOG_HEADER — active 로그 마스터 레코드. 페이지 -9의 데이터 영역에 위치하며 log_Gl.hdr로 미러링된다. 모든 멤버를 아래에 나열한다.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
magic | file(1) 매직 | 로그 파일이 아닌 파일에 대한 가드 |
dummy / dummy3 / dummy4 | 정렬 패딩 | 8바이트 정렬 |
db_creation | DB 생성 시각 | 로그를 DB에 연결. LOG_ARV_HEADER로 복사됨 |
vol_creation | active 볼륨 생성 시각 | 진단 / 순서 확인 |
db_release / db_compatibility | 릴리스 문자열, 호환성 부동소수점 | 호환되지 않는 빌드/버전 거부 |
db_iopagesize / db_logpagesize | 생성 시 페이지 크기 | 로그가 기대하는 크기로 DB 실행 |
is_shutdown | 정상 종료 플래그 | Recovery: dismount가 정상적이었는지 여부 |
next_trid | 다음 트랜잭션 ID | replay를 위해 LOG_ARV_HEADER로 복사됨 |
mvcc_next_id | 다음 MVCC ID | MVCC 할당 상한선 |
avg_ntrans / avg_nlocks | 크기 추정치 | 트랜잭션/잠금 테이블 사전 크기 조정 |
npages | active 페이지 수 (header 제외) | active 볼륨 크기 / archive 범위 조정 |
db_charset | DB 문자셋 ID | 마운트 시 문자셋 검사 |
was_copied | 복사된 DB 플래그 | 복사본 대 원본 구분 |
fpageid | active 물리 슬롯 1의 논리 페이지 ID | LOG_ARV_HEADER.fpageid의 active 측 대응 |
append_lsa | 현재 append 위치 | 실제 로그의 상한선 |
chkpt_lsa | Recovery가 재생을 시작하는 최소 LSA | Recovery 시작점. 여기서 durable (10.5절) |
nxarv_pageid | 다음으로 archive할 논리 페이지 | active/archive 경계 (10.2절) |
nxarv_phy_pageid | nxarv_pageid의 물리 슬롯 | logpb_to_physical_pageid 재계산 불필요 |
nxarv_num | 다음 archive 번호 | 다음 _lgarNNN 이름 지정 |
last_arv_num_for_syscrashes | 크래시 recovery를 위한 가장 오래된 archive | 삭제 하한선. -1 = 고정 없음 |
last_deleted_arv_num | 제거된 가장 높은 archive 번호 | 재스캔 없이 삭제 재개 가능 |
bkup_level0/1/2_lsa / bkinfo[] | 레벨별 백업 LSA 및 정보 | 백업 — 백업 장 참조 |
prefix_name | 로그 접두사 이름 | 볼륨 패밀리 이름 지정 |
has_logging_been_skipped | 로깅 건너뜀 플래그 | WAL 우회 구간 표시 |
vacuum_last_blockid | 마지막 vacuum 블록 ID | 삭제 게이팅 (10.4절) |
perm_status_obsolete | 사용 중단된 상태 | 레이아웃 호환성 |
ha_server_state / ha_file_status / ha_promotion_time | HA 상태, 복사 상태, 승격 시각 | 복제 / HA |
eof_lsa | LOG_END_OF_LOG의 LSA | durable 로그 끝. 여기서 durable (10.5절) |
smallest_lsa_at_last_chkpt | 마지막 checkpoint 시의 가장 오래된 dirty LSA | Recovery/vacuum 소급 범위 제한 |
mvcc_op_log_lsa | 마지막 MVCC 연산의 LSA | Vacuum MVCC 앵커 |
oldest_visible_mvccid / newest_block_mvccid | 가장 오래된 가시 MVCCID, 가장 최신 블록 MVCCID | Vacuum 가시성 / 블록 경계 |
db_restore_time | 마지막 복원 시각 | 복원 기록 관리 |
mark_will_del | 삭제 예정 표시 | DB 삭제 기록 관리 |
does_block_need_vacuum | 블록 vacuum 필요 여부 | Vacuum 스케줄링 |
was_active_log_reset | active 로그 초기화 여부 | logpb_archive_active_log에서 초기화됨 |
불변식 — archive 경계. nxarv_pageid는 무엇이 archive되었는지(< 이면 archive에만 존재, >= 이면 여전히 active)를 나타내는 단일 진실의 원천이다. nxarv_phy_pageid는 반드시 logpb_to_physical_pageid(nxarv_pageid)와 같아야 한다. 두 값은 logpb_archive_active_log 끝에서 한 단위로 증가하고, 이후 logpb_flush_header가 이를 원자적으로 durable 상태로 만든다. 두 값이 일치하지 않으면 다음 archive가 잘못된 물리 슬롯을 읽어 시퀀스를 손상시킨다.
LOG_ARV_HEADER — archive 볼륨 하나당 하나씩.
| 필드 | 역할 | 존재 이유 |
|---|---|---|
magic | CUBRID_MAGIC_LOG_ARCHIVE | 잘못된 파일을 archive로 마운트하는 것에 대한 가드 |
dummy | 패딩 | 정렬 |
db_creation | log_Gl.hdr.db_creation에서 복사 | archive를 DB에 연결 |
vol_creation | 기록 시 time(NULL) | 진단 / 순서 확인 |
next_trid | log_Gl.hdr.next_trid에서 복사 | replay 컨텍스트 |
npages | 데이터 페이지 수 (previous-lsa 페이지 제외) | 읽기 범위 제한 |
fpageid | 물리 슬롯 1의 논리 페이지 ID | 이 archive 내 논리-물리 맵 |
arv_num | 이 archive의 번호 | 자기 식별 |
dummy2 | 패딩 | 정렬 |
10.2 logpb_archive_active_log — active 로그를 archive로 전환
섹션 제목: “10.2 logpb_archive_active_log — active 로그를 archive로 전환”active 로그가 가득 찼을 때 LOG_CS 쓰기 모드에서 호출된다. [nxarv_pageid .. prev_lsa.pageid-1] 범위를 새 archive에 복사한 뒤 경계를 앞당긴다. Figure 10-2는 모든 분기를 추적한다.
flowchart TB
start["enter LOG_CS write"] --> wake["wake remove daemon SERVER, or remove-exceed-limit SA"]
wake --> guard{"nxarv_pageid >= append_lsa.pageid ?"}
guard -->|yes, only incomplete page| ret["er_log_debug + return"]
guard -->|no| dis{"archive.vdes open ?"}
dis -->|yes| dismount["dismount old archive"]
dis -->|no| mal["malloc arv hdr page"]
dismount --> mal
mal -->|NULL| err["goto error"]
mal -->|ok| flush["flush_all_append_pages, build hdr"]
flush --> bg{"bg archiving and vdes open ?"}
bg -->|yes| chk["set hdr checksum"]
bg -->|no| fmt["fileio_format new vol"]
fmt -->|NULL_VOLDES| err
fmt --> chk
chk -->|error| err
chk --> wrhdr["write header page phy 0"]
wrhdr -->|NULL| err
wrhdr --> loop["copy loop: read LOGPB_IO_NPAGES, write"]
loop -->|read/write fails| err
loop --> fin{"background archiving ?"}
fin -->|yes| rename["dismount, rename _lgar_t, remount"]
fin -->|no| sync["fileio_synchronize"]
rename -->|fail| err
sync -->|fail| err
rename --> adv["advance nxarv_num/pageid/phy_pageid"]
sync --> adv
adv --> fh["logpb_flush_header"]
fh --> done["cache hdr, log, return"]
err --> fatal["logpb_fatal_error -> exit"]
Figure 10-2 — logpb_archive_active_log의 분기 완전 흐름.
앞부분의 가드 (nxarv_pageid >= append_lsa.pageid → er_log_debug + return)는 빈 범위 요청을 거부한다. 이후 logpb_flush_all_append_pages가 강제로 실행된다. header는 자기 서술적으로 구성되는데 (db_creation/next_trid/fpageid를 log_Gl.hdr에서 복사), 퇴화된 범위에서 음수 npages가 나오지 않도록 last_pageid를 아래와 같이 클램핑한다.
// logpb_archive_active_log -- src/transaction/log_page_buffer.clast_pageid = log_Gl.append.prev_lsa.pageid - 1; /* <- never the live append page */if (last_pageid < arvhdr->fpageid) last_pageid = arvhdr->fpageid; /* <- clamp >= 1 page */arvhdr->npages = (DKNPAGES) (last_pageid - arvhdr->fpageid + 1);복사 루프는 logpb_read_page_from_active_log를 통해 최대 LOGPB_IO_NPAGES (4)개 페이지를 읽어 FILEIO_WRITE_NO_COMPENSATE_WRITE로 저장된 그대로 (TDE 암호화 상태 유지) 기록한다. 읽기 반환값 <= 0이거나 쓰기가 NULL이면 error로 이동한다.
경계 증가가 durable 커밋에 해당한다. 구체적으로는: last_arv_num_for_syscrashes가 아직 -1이면 nxarv_num으로 고정하고 (recovery 하한선 설정), nxarv_num++ 후 nxarv_pageid/nxarv_phy_pageid를 한 단위로 증가시키고, was_active_log_reset = false로 설정한 뒤 logpb_flush_header를 호출한다. error 레이블은 logpb_fatal_error(..., true, ...)를 호출한다. archive 실패는 복구 불가능하므로 서버가 종료된다 (10.7절).
10.3 logpb_write_toflush_pages_to_archive — 백그라운드 archiving
섹션 제목: “10.3 logpb_write_toflush_pages_to_archive — 백그라운드 archiving”PRM_ID_LOG_BACKGROUND_ARCHIVING이 켜져 있을 때, 가득 찬 페이지는 flush 시마다 임시 볼륨(_lgar_t)으로 스트리밍된다. 최종 archive 시에는 이 볼륨을 이름만 바꾸면 된다. bg_archive_info.vdes == NULL_VOLDES || num_toflush <= 1이면 일찍 반환하고, 그렇지 않으면 prev_lsa.pageid 미만의 모든 toflush[] 페이지를 복사한다. 이 과정에서 커서 pageid와 toflush[] 내 다음 bufptr->pageid를 세 가지 분기로 조정한다.
// logpb_write_toflush_pages_to_archive -- src/transaction/log_page_buffer.cif (pageid > bufptr->pageid) { assert_release (...); dismount; return; } /* backwards: never */else if (pageid < bufptr->pageid) { if (logpb_fetch_page (...)) { dismount; return; } } /* gap: fetch */else { log_pgptr = flush_info->toflush[i]; i++; } /* match: use in hand */LOG_IS_PAGE_TDE_ENCRYPTED에 해당하는 페이지는 각각 TDE 암호화된다. 암호화에 실패하면 TDE 플래그를 지우고 평문으로 기록한다 (데이터 누출을 감수하는 트레이드오프). fileio_synchronize는 PRM_ID_PB_SYNC_ON_NFLUSH 페이지마다 한 번씩 실행된다. 쓰기 오류가 발생하면 임시 볼륨을 dismount하고 bg archiving을 포기한다. 이 경우 10.2절의 logpb_archive_active_log는 fileio_format으로 대체한다.
10.4 remove daemon — 오래된 archive의 게이팅 삭제
섹션 제목: “10.4 remove daemon — 오래된 archive의 게이팅 삭제”삭제는 핫 경로에서 발생하지 않는다. 서버 환경에서 logpb_archive_active_log는 daemon을 깨우기만 한다 (log_wakeup_remove_log_archive_daemon이 wakeup()을 비동기적으로 호출). log_remove_log_archive_daemon_task는 주기적으로도 실행된다 (compute_period가 PRM_ID_REMOVE_LOG_ARCHIVES_INTERVAL을 읽어 0이 아니면 타이머 대기, 0이면 wake만). 본체와 SA 경로 모두 logpb_remove_archive_logs_exceed_limit을 호출한다. log_max_archives == INT_MAX (무제한)이거나 !vacuum_is_safe_to_remove_archives() (vacuum 데이터 미로드)이면 0으로 일찍 반환한다. 그 다음 [last_deleted_arv_num + 1, nxarv_num - num_remove_arv_num] 창의 상한을 각 게이트가 MIN으로 클램핑한다.
// logpb_remove_archive_logs_exceed_limit -- src/transaction/log_page_buffer.cif (log_Gl.hdr.last_arv_num_for_syscrashes != -1) /* crash-recovery floor */ last_arv_num_to_delete = MIN (last_arv_num_to_delete, log_Gl.hdr.last_arv_num_for_syscrashes);if (vacuum_first_pageid != NULL_PAGEID && logpb_is_page_in_archive (vacuum_first_pageid)) last_arv_num_to_delete = MIN (last_arv_num_to_delete, min_arv_required_for_vacuum);if (prm_get_integer_value (PRM_ID_SUPPLEMENTAL_LOG)) { /* CDC + flashback gates */ if (logpb_is_page_in_archive (cdc_min_log_pageid_to_keep ())) /* CDC progress */ last_arv_num_to_delete = MIN (last_arv_num_to_delete, min_arv_required_for_cdc); if (flashback_is_needed_to_keep_archive ()) last_arv_num_to_delete = MIN (last_arv_num_to_delete, min_arv_required_for_flashback); }불변식 — 소비자가 자신의 하한선을 넘어 읽지 못한다. archive는 모든 활성 소비자의 최소값보다 번호가 낮을 때만 삭제 가능하다. 관련 소비자는 last_arv_num_for_syscrashes, vacuum, CDC (cdc_min_log_pageid_to_keep — CDC가 소비하지 않은 가장 오래된 페이지, PRM_ID_SUPPLEMENTAL_LOG 조건), flashback, (서버) HA 복사 진행 상황 (logwr_get_min_copied_fpageid, PRM_ID_FORCE_REMOVE_LOG_ARCHIVES가 없을 때)이다. MIN() 체인이 이를 강제한다. 하나의 클램프라도 제거하면 해당 소비자가 삭제된 archive를 참조하게 된다.
이후 max_count가 배치 크기를 제한하고, last_arv_num_to_delete-- (창은 마지막으로 필요한 archive를 제외하는 방식)를 수행한다. >= first_arv_num_to_delete인 경우에만 last_deleted_arv_num을 유지하고 logpb_flush_header를 호출한다. 실제 unlink는 LOG_CS_EXIT 이후 logpb_remove_archive_logs_internal을 통해 실행된다.
10.5 logpb_flush_header — active 로그 header를 durable 상태로 만들기
섹션 제목: “10.5 logpb_flush_header — active 로그 header를 durable 상태로 만들기”위의 모든 경계 변경은 이 함수에서 끝난다. LOG_CS_OWN_WRITE_MODE를 assert하고, loghdr_pgptr이 NULL이면 지연 할당한다 (메모리 부족 시 logpb_fatal_error). 그런 다음 페이지 -9에 스냅샷을 찍어 기록한다.
// logpb_flush_header -- src/transaction/log_page_buffer.clog_hdr = (LOG_HEADER *) (log_Gl.loghdr_pgptr->area);*log_hdr = log_Gl.hdr; /* <- snapshot in-memory header */log_Gl.loghdr_pgptr->hdr.flags = 0; /* <- never TDE-encrypted */logpb_write_page_to_disk (thread_p, log_Gl.loghdr_pgptr, LOGPB_HEADER_PAGE_ID);이 함수가 chkpt_lsa (recovery 시작점)와 eof_lsa (durable 로그 끝)를 durable 상태로 만드는 단 하나의 지점이다. append 페이지는 flush하지 않는다. 해당 flush는 Chapter 7의 WAL flush가 담당한다.
10.6 경계 레코드 및 손상: EOF 마커, dummy, checksum
섹션 제목: “10.6 경계 레코드 및 손상: EOF 마커, dummy, checksum”LOG_END_OF_LOG 배치. logpb_flush_all_append_pages에서 EOF 마커 (eof.type = LOG_END_OF_LOG, null forw_lsa)를 logpb_start_append를 통해 제자리에 추가하여 recovery가 로그 끝을 찾을 수 있도록 한다. 단, append_lsa는 앞당기지 않는다 — 다음 실제 레코드가 이를 덮어쓴다.
LOG_DUMMY_GENERIC 및 기타 dummy. 일부 로그 타입은 페이로드를 갖지 않는다. enum 주석에는 말 그대로 "ridiculous, but flush needs it"이라고 적혀 있다. dummy는 실제 레코드가 경계에 걸쳐 분리되어 어색하게 위치하는 상황에서 페이지를 부분 레코드 header 없이 닫을 수 있도록 flush에 종단/패딩 레코드를 제공한다.
Checksum. logpb_compute_page_checksum은 4096바이트 블록의 head와 tail에서 각각 16바이트를 샘플링하여 연결한 뒤 CRC32를 계산한다. 이때 계산 중에는 hdr.checksum을 0으로 설정하고 이후 복원하여 저장된 checksum이 자신을 검사하지 않도록 한다. logpb_set_page_checksum이 checksum을 저장하고, logpb_page_has_valid_checksum이 재계산하여 비교하며, logpb_page_check_corruption은 *is_page_corrupted = !has_valid_checksum으로 설정한다. 변경 사항은 복제가 일치하도록 logwr_check_page_checksum에도 반드시 반영해야 한다.
logpb_invalid_all_append_pages. append 상태를 초기화해야 할 때 (예: 부분 append 실패 후), 한 분기 (if log_Gl.append.log_pgptr != NULL)에서 커밋된 작업이 유실되지 않도록 logpb_flush_pages_direct를 통해 dirty append 페이지를 먼저 flush하고 log_pgptr을 null로 설정한다. 그런 다음 flush_mutex 아래에서 flush_info->num_toflush를 0으로 초기화하고 toflush[0] = NULL로 설정한다.
10.7 logpb_fatal_error_internal — 최후 수단의 flush 및 종료
섹션 제목: “10.7 logpb_fatal_error_internal — 최후 수단의 flush 및 종료”복구 불가능한 오류는 logpb_fatal_error → logpb_fatal_error_internal을 need_flush = true로 호출한다. flush 자체가 안전하지 않을 때는 logpb_fatal_error_exit_immediately_wo_flush가 false를 전달한다.
// logpb_fatal_error_internal -- src/transaction/log_page_buffer.cif (log_exit == true && need_flush == true && log_Gl.append.log_pgptr != NULL) { static int in_fatal = false; /* <- reentrancy guard */ if (in_fatal == false) { in_fatal = true; pgbuf_flush_checkpoint (...); /* flush only up to prev_lsa */ in_fatal = false; } }fileio_synchronize_all (thread_p); /* <- force everything to stable storage *//* then boot_server_status(DOWN); NDEBUG -> exit, debug -> abort core dump */분기 동작은 다음과 같다. flush 블록은 log_exit, need_flush, 그리고 활성 append 페이지가 모두 성립할 때만 실행된다. in_fatal 가드는 flush 자체에서 오류가 발생해 재귀 진입하는 상황을 차단한다. “현재 미완성 로그 레코드를 강제하지 않는 범위에서 최대한 flush”한다는 원칙에 따라 prev_lsa 이하의 커밋된 작업을 durable 상태로 만들고, 부분 레코드는 recovery를 위해 남겨둔다. 이후 fileio_synchronize_all을 수행하고 종료한다 (NDEBUG는 exit, debug는 abort).
10.8 상위 문서에서 이월된 미해결 질문
섹션 제목: “10.8 상위 문서에서 이월된 미해결 질문”동반 문서에서 네 가지 항목이 미해결 상태로 남아 있다. 그룹 커밋 창 (flush daemon의 깨우기 타이밍과 PRM_ID_PB_SYNC_ON_NFLUSH와의 상호작용, 10.3절). prior list의 list_size 상한이 archive/flush를 스로틀하는지 여부. TDE 배치 (암호화는 10.3절에서 지연 처리되고 logpb_archive_active_log의 직접 복사에서 생략되는데, 디스크 암호화의 단일 권위 있는 지점은 추적되지 않음). 마지막으로 LOG_DUMMY_GENERIC 불변식 (flush가 dummy를 필요로 하는 조건이 소스 주석으로만 문서화되어 있음).
10.9 장 요약 — 핵심 내용
섹션 제목: “10.9 장 요약 — 핵심 내용”- 세 가지 중첩된 header 구조체: 페이지당
LOG_HDRPAGE, 마스터 레코드로서LOG_HEADER(페이지 -9), archive당LOG_ARV_HEADER.db_creation/next_trid는 앞의 것에서 복사된다. nxarv_pageid/nxarv_phy_pageid가 archive 경계이며,logpb_archive_active_log에서 한 단위로 증가하고 flush된다 (was_active_log_reset도 여기서 초기화).- Archiving은 전체 append flush를 강제하고,
[nxarv_pageid .. prev_lsa.pageid-1]을 저장된 그대로 복사하며, I/O 오류를 치명적으로 처리한다. - 삭제는 게이팅된다:
logpb_remove_archive_logs_exceed_limit은 크래시 recovery, vacuum, CDC, flashback, HA 하한선에 대한MIN()체인으로 창을 클램핑한다. logpb_flush_header는chkpt_lsa/eof_lsa/archive 기록의 단일 durability 지점이며,LOG_CS쓰기 모드 아래flags = 0으로 동작한다.- 경계 레코드:
LOG_END_OF_LOG는append_lsa를 앞당기지 않고 제자리에 추가되고, dummy는 페이지를 패딩하며, 샘플링된 CRC32 checksum이logpb_page_check_corruption을 구동한다. - Fatal 경로:
logpb_fatal_error_internal은in_fatal가드를 사용하고,prev_lsa까지만 flush한 뒤exit/abort한다.
이 개정 시점의 위치 힌트
섹션 제목: “이 개정 시점의 위치 힌트”아래 줄 번호는 2026-06-08 기준으로 관찰된 값이다. 심볼이 정식 앵커이며 줄 번호는 시간이 지남에 따라 부정확해질 수 있는 힌트다.
| 심볼 | 파일 | 줄 |
|---|---|---|
LOG_PAGESIZE | src/storage/storage_common.h | 99 |
log_Zip_support | src/transaction/log_append.cpp | 40 |
log_Zip_min_size_to_compress | src/transaction/log_append.cpp | 41 |
log_append_info::get_nxio_lsa | src/transaction/log_append.cpp | 106 |
log_append_info::set_nxio_lsa | src/transaction/log_append.cpp | 112 |
log_prior_lsa_info::log_prior_lsa_info | src/transaction/log_append.cpp | 117 |
LOG_RESET_APPEND_LSA | src/transaction/log_append.cpp | 128 |
LOG_RESET_PREV_LSA | src/transaction/log_append.cpp | 136 |
LOG_APPEND_PTR | src/transaction/log_append.cpp | 145 |
log_append_init_zip | src/transaction/log_append.cpp | 185 |
log_append_final_zip | src/transaction/log_append.cpp | 232 |
prior_lsa_alloc_and_copy_data | src/transaction/log_append.cpp | 273 |
prior_lsa_alloc_and_copy_crumbs | src/transaction/log_append.cpp | 410 |
prior_lsa_copy_undo_data_to_node | src/transaction/log_append.cpp | 493 |
prior_lsa_copy_redo_data_to_node | src/transaction/log_append.cpp | 524 |
prior_lsa_gen_undoredo_record_from_crumbs | src/transaction/log_append.cpp | 651 |
prior_lsa_gen_record | src/transaction/log_append.cpp | 1217 |
prior_update_header_mvcc_info | src/transaction/log_append.cpp | 1320 |
prior_lsa_next_record_internal | src/transaction/log_append.cpp | 1357 |
commit_abort_lsa | src/transaction/log_append.cpp | 1485 |
prior_lsa_next_record | src/transaction/log_append.cpp | 1553 |
prior_lsa_next_record_with_lock | src/transaction/log_append.cpp | 1559 |
prior_set_tde_encrypted | src/transaction/log_append.cpp | 1565 |
prior_is_tde_encrypted | src/transaction/log_append.cpp | 1581 |
prior_lsa_start_append | src/transaction/log_append.cpp | 1593 |
prior_lsa_end_append | src/transaction/log_append.cpp | 1652 |
prior_lsa_append_data | src/transaction/log_append.cpp | 1661 |
log_append_get_zip_undo | src/transaction/log_append.cpp | 1725 |
log_append_get_zip_redo | src/transaction/log_append.cpp | 1751 |
log_prior_lsa_append_align | src/transaction/log_append.cpp | 1892 |
log_prior_lsa_append_advance_when_doesnot_fit | src/transaction/log_append.cpp | 1905 |
log_prior_lsa_append_add_align | src/transaction/log_append.cpp | 1917 |
log_crumb | src/transaction/log_append.hpp | 46 |
log_data_addr | src/transaction/log_append.hpp | 53 |
LOG_PRIOR_LSA_LOCK | src/transaction/log_append.hpp | 66 |
log_append_info | src/transaction/log_append.hpp | 73 |
log_prior_node | src/transaction/log_append.hpp | 91 |
log_prior_lsa_info | src/transaction/log_append.hpp | 112 |
log_zip_alloc | src/transaction/log_compress.c | 237 |
log_zip | src/transaction/log_compress.h | 53 |
log_global::log_global | src/transaction/log_global.c | 49 |
LOGAREA_SIZE | src/transaction/log_impl.h | 121 |
log_setdirty | src/transaction/log_impl.h | 305 |
log_flush_info | src/transaction/log_impl.h | 322 |
log_topops_addresses | src/transaction/log_impl.h | 353 |
log_topops_stack | src/transaction/log_impl.h | 362 |
log_rcv_tdes | src/transaction/log_impl.h | 458 |
log_tdes | src/transaction/log_impl.h | 475 |
log_global | src/transaction/log_impl.h | 671 |
log_lsa | src/transaction/log_lsa.hpp | 35 |
NULL_LSA | src/transaction/log_lsa.hpp | 67 |
MAX_LSA | src/transaction/log_lsa.hpp | 72 |
LSA_COPY | src/transaction/log_lsa.hpp | 80 |
LSA_AS_ARGS | src/transaction/log_lsa.hpp | 91 |
LOG_TDES_LAST_SYSOP | src/transaction/log_manager.c | 199 |
LOG_TDES_LAST_SYSOP_PARENT_LSA | src/transaction/log_manager.c | 200 |
LOG_TDES_LAST_SYSOP_POSP_LSA | src/transaction/log_manager.c | 201 |
log_Flush_daemon | src/transaction/log_manager.c | 363 |
log_create_internal | src/transaction/log_manager.c | 827 |
log_initialize_internal | src/transaction/log_manager.c | 1100 |
log_abort_by_tdes | src/transaction/log_manager.c | 1583 |
log_abort_all_active_transaction | src/transaction/log_manager.c | 1608 |
log_final | src/transaction/log_manager.c | 1720 |
log_append_undoredo_data | src/transaction/log_manager.c | 1893 |
log_append_undo_data | src/transaction/log_manager.c | 1973 |
log_append_redo_data | src/transaction/log_manager.c | 2035 |
log_append_undoredo_crumbs | src/transaction/log_manager.c | 2086 |
log_append_postpone | src/transaction/log_manager.c | 2719 |
log_append_run_postpone | src/transaction/log_manager.c | 2881 |
log_append_compensate_internal | src/transaction/log_manager.c | 3047 |
log_append_savepoint | src/transaction/log_manager.c | 3365 |
log_sysop_start | src/transaction/log_manager.c | 3599 |
log_sysop_start_atomic | src/transaction/log_manager.c | 3665 |
log_sysop_commit_internal | src/transaction/log_manager.c | 3825 |
log_sysop_commit | src/transaction/log_manager.c | 3916 |
log_sysop_end_logical_undo | src/transaction/log_manager.c | 3941 |
log_sysop_end_logical_compensate | src/transaction/log_manager.c | 3984 |
log_sysop_end_logical_run_postpone | src/transaction/log_manager.c | 4003 |
log_sysop_end_recovery_postpone | src/transaction/log_manager.c | 4024 |
log_sysop_abort | src/transaction/log_manager.c | 4038 |
log_sysop_attach_to_outer | src/transaction/log_manager.c | 4097 |
log_append_commit_postpone | src/transaction/log_manager.c | 4384 |
log_append_sysop_start_postpone | src/transaction/log_manager.c | 4455 |
log_append_repl_info_and_commit_log | src/transaction/log_manager.c | 4647 |
log_append_donetime_internal | src/transaction/log_manager.c | 4679 |
log_change_tran_as_completed | src/transaction/log_manager.c | 4722 |
log_append_commit_log | src/transaction/log_manager.c | 4779 |
log_append_commit_log_with_lock | src/transaction/log_manager.c | 4802 |
log_append_abort_log | src/transaction/log_manager.c | 4816 |
log_commit_local | src/transaction/log_manager.c | 5159 |
log_abort_local | src/transaction/log_manager.c | 5277 |
log_commit | src/transaction/log_manager.c | 5352 |
log_abort | src/transaction/log_manager.c | 5461 |
log_abort_partial | src/transaction/log_manager.c | 5558 |
log_complete | src/transaction/log_manager.c | 5653 |
log_rollback | src/transaction/log_manager.c | 7664 |
log_tran_do_postpone | src/transaction/log_manager.c | 8156 |
log_sysop_do_postpone | src/transaction/log_manager.c | 8190 |
log_do_postpone | src/transaction/log_manager.c | 8237 |
log_run_postpone_op | src/transaction/log_manager.c | 8481 |
log_wakeup_remove_log_archive_daemon | src/transaction/log_manager.c | 10099 |
log_wakeup_log_flush_daemon | src/transaction/log_manager.c | 10126 |
log_is_log_flush_daemon_available | src/transaction/log_manager.c | 10141 |
log_remove_log_archive_daemon_task | src/transaction/log_manager.c | 10185 |
log_flush_execute | src/transaction/log_manager.c | 10377 |
log_flush_daemon_init | src/transaction/log_manager.c | 10493 |
log_abort_task_execute | src/transaction/log_manager.c | 10558 |
cdc_min_log_pageid_to_keep | src/transaction/log_manager.c | 14021 |
LOG_IS_SYSTEM_OP_STARTED | src/transaction/log_manager.h | 59 |
LOGPB_HEADER_PAGE_ID | src/transaction/log_page_buffer.c | 138 |
LOG_APPEND_ALIGN | src/transaction/log_page_buffer.c | 164 |
LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT | src/transaction/log_page_buffer.c | 176 |
LOG_APPEND_ADVANCE_WHEN_DOESNOT_FIT | src/transaction/log_page_buffer.c | 177 |
LOG_APPEND_SETDIRTY_ADD_ALIGN | src/transaction/log_page_buffer.c | 184 |
log_buffer | src/transaction/log_page_buffer.c | 192 |
log_buffer | src/transaction/log_page_buffer.c | 194 |
log_pb_global_data | src/transaction/log_page_buffer.c | 244 |
logpb_get_log_buffer | src/transaction/log_page_buffer.c | 394 |
logpb_initialize_log_buffer | src/transaction/log_page_buffer.c | 425 |
logpb_compute_page_checksum | src/transaction/log_page_buffer.c | 446 |
logpb_set_page_checksum | src/transaction/log_page_buffer.c | 495 |
logpb_page_has_valid_checksum | src/transaction/log_page_buffer.c | 523 |
logpb_initialize_pool | src/transaction/log_page_buffer.c | 553 |
logpb_finalize_pool | src/transaction/log_page_buffer.c | 672 |
logpb_create_page | src/transaction/log_page_buffer.c | 783 |
logpb_locate_page | src/transaction/log_page_buffer.c | 807 |
logpb_set_dirty | src/transaction/log_page_buffer.c | 929 |
logpb_flush_header | src/transaction/log_page_buffer.c | 1676 |
logpb_fetch_start_append_page | src/transaction/log_page_buffer.c | 2504 |
logpb_fetch_start_append_page_new | src/transaction/log_page_buffer.c | 2586 |
logpb_next_append_page | src/transaction/log_page_buffer.c | 2630 |
logpb_writev_append_pages | src/transaction/log_page_buffer.c | 2780 |
logpb_write_toflush_pages_to_archive | src/transaction/log_page_buffer.c | 2868 |
logpb_append_next_record | src/transaction/log_page_buffer.c | 2981 |
logpb_append_prior_lsa_list | src/transaction/log_page_buffer.c | 3040 |
prior_lsa_remove_prior_list | src/transaction/log_page_buffer.c | 3084 |
logpb_prior_lsa_append_all_list | src/transaction/log_page_buffer.c | 3106 |
logpb_flush_all_append_pages | src/transaction/log_page_buffer.c | 3232 |
logpb_flush_pages_direct | src/transaction/log_page_buffer.c | 3952 |
logpb_flush_pages | src/transaction/log_page_buffer.c | 3980 |
logpb_force_flush_pages | src/transaction/log_page_buffer.c | 4096 |
logpb_force_flush_header_and_pages | src/transaction/log_page_buffer.c | 4104 |
logpb_invalid_all_append_pages | src/transaction/log_page_buffer.c | 4121 |
logpb_flush_log_for_wal | src/transaction/log_page_buffer.c | 4162 |
logpb_start_append | src/transaction/log_page_buffer.c | 4207 |
logpb_append_data | src/transaction/log_page_buffer.c | 4290 |
logpb_append_crumbs | src/transaction/log_page_buffer.c | 4366 |
logpb_end_append | src/transaction/log_page_buffer.c | 4455 |
logpb_archive_active_log | src/transaction/log_page_buffer.c | 5649 |
logpb_remove_archive_logs_exceed_limit | src/transaction/log_page_buffer.c | 5991 |
logpb_fatal_error | src/transaction/log_page_buffer.c | 10607 |
logpb_fatal_error_exit_immediately_wo_flush | src/transaction/log_page_buffer.c | 10618 |
logpb_fatal_error_internal | src/transaction/log_page_buffer.c | 10629 |
logpb_initialize_flush_info | src/transaction/log_page_buffer.c | 10878 |
logpb_finalize_flush_info | src/transaction/log_page_buffer.c | 10912 |
logpb_need_wal | src/transaction/log_page_buffer.c | 11229 |
logpb_page_check_corruption | src/transaction/log_page_buffer.c | 11508 |
logpb_get_tde_algorithm | src/transaction/log_page_buffer.c | 11565 |
logpb_set_tde_algorithm | src/transaction/log_page_buffer.c | 11593 |
log_rectype | src/transaction/log_record.hpp | 35 |
log_rec_header | src/transaction/log_record.hpp | 146 |
log_data | src/transaction/log_record.hpp | 157 |
log_rec_undoredo | src/transaction/log_record.hpp | 167 |
log_rec_undo | src/transaction/log_record.hpp | 176 |
log_rec_redo | src/transaction/log_record.hpp | 184 |
log_vacuum_info | src/transaction/log_record.hpp | 192 |
log_rec_mvcc_undoredo | src/transaction/log_record.hpp | 202 |
log_rec_mvcc_undo | src/transaction/log_record.hpp | 211 |
log_rec_mvcc_redo | src/transaction/log_record.hpp | 220 |
log_rec_donetime | src/transaction/log_record.hpp | 237 |
log_rec_compensate | src/transaction/log_record.hpp | 262 |
log_rec_start_postpone | src/transaction/log_record.hpp | 271 |
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_run_postpone | src/transaction/log_record.hpp | 336 |
log_rec_savept | src/transaction/log_record.hpp | 380 |
LOG_GET_LOG_RECORD_HEADER | src/transaction/log_record.hpp | 441 |
LOG_IS_MVCC_OP_RECORD_TYPE | src/transaction/log_record.hpp | 463 |
LOG_HDRPAGE_FLAG_ENCRYPTED_MASK | src/transaction/log_storage.hpp | 45 |
LOG_IS_PAGE_TDE_ENCRYPTED | src/transaction/log_storage.hpp | 47 |
LOGPB_HEADER_PAGE_ID | src/transaction/log_storage.hpp | 51 |
log_hdrpage | src/transaction/log_storage.hpp | 63 |
log_page | src/transaction/log_storage.hpp | 80 |
log_page | src/transaction/log_storage.hpp | 81 |
log_header | src/transaction/log_storage.hpp | 113 |
log_arv_header | src/transaction/log_storage.hpp | 231 |
logtb_get_new_tran_id | src/transaction/log_tran_table.c | 1741 |
LOG_IS_MVCC_OPERATION | src/transaction/mvcc.h | 261 |
cubrid-log-manager.md— 상위 수준 동반 문서.cubrid-prior-list.md(prior list 메커니즘) 및cubrid-recovery-manager.md(이 레코드들이 재생되는 방식)도 참고하라.raw/code-analysis/cubrid/storage/log_manager/아래의 원시 분석 자료.- 코드:
src/transaction/log_manager.{c,h},log_append.{cpp,hpp},log_record.hpp,log_lsa.{cpp,hpp},log_storage.hpp,log_page_buffer.c. - 방법론:
knowledge/methodology/code-analysis-detail-doc.md